* First tvOS commit

Barely usable on tvOS:
- changed documentDirectory to cachesDirectory because is the only writeable folder
- changed default player on default because custom has too many issues to fix on tvOS
- commented a lot of lines because that property or function does not exist on tvOS
- to add modules copy the link from Iphone

* add conditional compilation

Only searchview need a separation of a class due to compilation timeout issue

* Fix incompatibility on tvOS

---------

Co-authored-by: K <kimiko88@users.noreply.github.com>
This commit is contained in:
cranci 2025-04-27 08:25:07 +02:00 committed by GitHub
parent 713046ce64
commit 28533651f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 408 additions and 66 deletions

View file

@ -14,6 +14,7 @@ class DropManager {
private init() {} private init() {}
func showDrop(title: String, subtitle: String, duration: TimeInterval, icon: UIImage?) { func showDrop(title: String, subtitle: String, duration: TimeInterval, icon: UIImage?) {
#if !os(tvOS)
let position: Drop.Position = .top let position: Drop.Position = .top
let drop = Drop( let drop = Drop(
@ -24,5 +25,6 @@ class DropManager {
duration: .seconds(duration) duration: .seconds(duration)
) )
Drops.show(drop) Drops.show(drop)
#endif
} }
} }

View file

@ -19,7 +19,7 @@ public extension UIDevice {
} }
func mapToDevice(identifier: String) -> String { // swiftlint:disable:this cyclomatic_complexity func mapToDevice(identifier: String) -> String { // swiftlint:disable:this cyclomatic_complexity
#if os(iOS) #if !os(tvOS)
switch identifier { switch identifier {
case "iPod5,1": case "iPod5,1":
return "iPod touch (5th generation)" return "iPod touch (5th generation)"

View file

@ -21,7 +21,12 @@ class Logger {
private let logFilterViewModel = LogFilterViewModel.shared private let logFilterViewModel = LogFilterViewModel.shared
private init() { private init() {
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! #if !os(tvOS)
let directory = FileManager.SearchPathDirectory.documentDirectory
#elseif os(tvOS)
let directory = FileManager.SearchPathDirectory.cachesDirectory
#endif
let documentDirectory = FileManager.default.urls(for: directory, in: .userDomainMask).first!
logFileURL = documentDirectory.appendingPathComponent("logs.txt") logFileURL = documentDirectory.appendingPathComponent("logs.txt")
} }

View file

@ -94,6 +94,7 @@ struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
} }
.frame(width: bounds.size.width, height: bounds.size.height, alignment: .center) .frame(width: bounds.size.width, height: bounds.size.height, alignment: .center)
.contentShape(Rectangle()) .contentShape(Rectangle())
#if !os(tvOS)
.gesture( .gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .local) DragGesture(minimumDistance: 0, coordinateSpace: .local)
.updating($isActive) { _, state, _ in .updating($isActive) { _, state, _ in
@ -108,6 +109,7 @@ struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
localTempProgress = 0 localTempProgress = 0
} }
) )
#endif
.onChange(of: isActive) { newValue in .onChange(of: isActive) { newValue in
value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound) value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound)
onEditingChanged(newValue) onEditingChanged(newValue)

View file

@ -55,6 +55,7 @@ struct VolumeSlider<T: BinaryFloatingPoint>: View {
.animation(animation, value: isActive) .animation(animation, value: isActive)
} }
.frame(width: bounds.size.width, height: bounds.size.height) .frame(width: bounds.size.width, height: bounds.size.height)
#if !os(tvOS)
.gesture( .gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .local) DragGesture(minimumDistance: 0, coordinateSpace: .local)
.updating($isActive) { _, state, _ in state = true } .updating($isActive) { _, state, _ in state = true }
@ -68,6 +69,7 @@ struct VolumeSlider<T: BinaryFloatingPoint>: View {
localTempProgress = 0 localTempProgress = 0
} }
) )
#endif
.onChange(of: isActive) { newValue in .onChange(of: isActive) { newValue in
if !newValue { if !newValue {
value = sliderValueInRange() value = sliderValueInRange()

View file

@ -162,7 +162,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
private var volumeObserver: NSKeyValueObservation? private var volumeObserver: NSKeyValueObservation?
private var audioSession = AVAudioSession.sharedInstance() private var audioSession = AVAudioSession.sharedInstance()
private var hiddenVolumeView = MPVolumeView(frame: .zero) private var hiddenVolumeView = MPVolumeView(frame: .zero)
#if !os(tvOS)
private var systemVolumeSlider: UISlider? private var systemVolumeSlider: UISlider?
#endif
private var volumeValue: Double = 0.0 private var volumeValue: Double = 0.0
private var volumeViewModel = VolumeViewModel() private var volumeViewModel = VolumeViewModel()
var volumeSliderHostingView: UIView? var volumeSliderHostingView: UIView?
@ -281,14 +283,16 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
} }
} }
#if !os(tvOS)
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
playerViewController.allowsVideoFrameAnalysis = false playerViewController.allowsVideoFrameAnalysis = false
} }
#endif
if let url = subtitlesURL, !url.isEmpty { if let url = subtitlesURL, !url.isEmpty {
subtitlesLoader.load(from: url) subtitlesLoader.load(from: url)
} }
DispatchQueue.main.async { DispatchQueue.main.async {
self.isControlsVisible = true self.isControlsVisible = true
NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints)
@ -297,7 +301,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
self.view.layoutIfNeeded() self.view.layoutIfNeeded()
} }
#if !os(tvOS)
hiddenVolumeView.showsRouteButton = false hiddenVolumeView.showsRouteButton = false
#endif
hiddenVolumeView.isHidden = true hiddenVolumeView.isHidden = true
view.addSubview(hiddenVolumeView) view.addSubview(hiddenVolumeView)
@ -307,9 +313,11 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
hiddenVolumeView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true hiddenVolumeView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
hiddenVolumeView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true hiddenVolumeView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
#if !os(tvOS)
if let slider = hiddenVolumeView.subviews.first(where: { $0 is UISlider }) as? UISlider { if let slider = hiddenVolumeView.subviews.first(where: { $0 is UISlider }) as? UISlider {
systemVolumeSlider = slider systemVolumeSlider = slider
} }
#endif
} }
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
@ -397,10 +405,17 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
} }
private func getSegmentsColor() -> Color { private func getSegmentsColor() -> Color {
#if !os(tvOS)
if let data = UserDefaults.standard.data(forKey: "segmentsColorData"), if let data = UserDefaults.standard.data(forKey: "segmentsColorData"),
let uiColor = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? UIColor { let uiColor = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? UIColor {
return Color(uiColor) return Color(uiColor)
} }
#elseif os(tvOS)
if let data = UserDefaults.standard.data(forKey: "segmentsColorData"),
let uiColor = try? NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: data) {
return Color(uiColor)
}
#endif
return .yellow return .yellow
} }
@ -596,7 +611,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
func holdForPause() { func holdForPause() {
let holdForPauseGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleHoldForPause(_:))) let holdForPauseGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleHoldForPause(_:)))
holdForPauseGesture.minimumPressDuration = 1 holdForPauseGesture.minimumPressDuration = 1
#if !os(tvOS)
holdForPauseGesture.numberOfTouchesRequired = 2 holdForPauseGesture.numberOfTouchesRequired = 2
#endif
view.addGestureRecognizer(holdForPauseGesture) view.addGestureRecognizer(holdForPauseGesture)
} }
@ -832,9 +849,11 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
func volumeSlider() { func volumeSlider() {
let container = VolumeSliderContainer(volumeVM: self.volumeViewModel) { newVal in let container = VolumeSliderContainer(volumeVM: self.volumeViewModel) { newVal in
#if !os(tvOS)
if let sysSlider = self.systemVolumeSlider { if let sysSlider = self.systemVolumeSlider {
sysSlider.value = Float(newVal) sysSlider.value = Float(newVal)
} }
#endif
} }
let hostingController = UIHostingController(rootView: container) let hostingController = UIHostingController(rootView: container)
@ -1042,7 +1061,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
skipIntroButton.setImage(introImage, for: .normal) skipIntroButton.setImage(introImage, for: .normal)
skipIntroButton.backgroundColor = UIColor(red: 51/255.0, green: 51/255.0, blue: 51/255.0, alpha: 0.8) skipIntroButton.backgroundColor = UIColor(red: 51/255.0, green: 51/255.0, blue: 51/255.0, alpha: 0.8)
#if !os(tvOS)
skipIntroButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10) skipIntroButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10)
#endif
skipIntroButton.tintColor = .white skipIntroButton.tintColor = .white
skipIntroButton.setTitleColor(.white, for: .normal) skipIntroButton.setTitleColor(.white, for: .normal)
skipIntroButton.layer.cornerRadius = 21 skipIntroButton.layer.cornerRadius = 21
@ -1074,7 +1095,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
skipOutroButton.setImage(outroImage, for: .normal) skipOutroButton.setImage(outroImage, for: .normal)
skipOutroButton.backgroundColor = UIColor(red: 51/255.0, green: 51/255.0, blue: 51/255.0, alpha: 0.8) skipOutroButton.backgroundColor = UIColor(red: 51/255.0, green: 51/255.0, blue: 51/255.0, alpha: 0.8)
#if !os(tvOS)
skipOutroButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10) skipOutroButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10)
#endif
skipOutroButton.tintColor = .white skipOutroButton.tintColor = .white
skipOutroButton.setTitleColor(.white, for: .normal) skipOutroButton.setTitleColor(.white, for: .normal)
skipOutroButton.layer.cornerRadius = 21 skipOutroButton.layer.cornerRadius = 21
@ -1248,7 +1271,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
skip85Button.setImage(image, for: .normal) skip85Button.setImage(image, for: .normal)
skip85Button.backgroundColor = UIColor(red: 51/255.0, green: 51/255.0, blue: 51/255.0, alpha: 0.8) skip85Button.backgroundColor = UIColor(red: 51/255.0, green: 51/255.0, blue: 51/255.0, alpha: 0.8)
#if !os(tvOS)
skip85Button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10) skip85Button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10)
#endif
skip85Button.tintColor = .white skip85Button.tintColor = .white
skip85Button.setTitleColor(.white, for: .normal) skip85Button.setTitleColor(.white, for: .normal)
skip85Button.layer.cornerRadius = 21 skip85Button.layer.cornerRadius = 21
@ -2092,7 +2117,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
self.subtitleBackgroundEnabled = settings.backgroundEnabled self.subtitleBackgroundEnabled = settings.backgroundEnabled
self.subtitleBottomPadding = settings.bottomPadding self.subtitleBottomPadding = settings.bottomPadding
} }
#if !os(tvOS)
override var supportedInterfaceOrientations: UIInterfaceOrientationMask { override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UserDefaults.standard.bool(forKey: "alwaysLandscape") { if UserDefaults.standard.bool(forKey: "alwaysLandscape") {
return .landscape return .landscape
@ -2108,13 +2134,14 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
override var prefersStatusBarHidden: Bool { override var prefersStatusBarHidden: Bool {
return true return true
} }
#endif
func setupAudioSession() { func setupAudioSession() {
do { do {
let audioSession = AVAudioSession.sharedInstance() let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .moviePlayback, options: .mixWithOthers) try audioSession.setCategory(.playback, mode: .moviePlayback, options: .mixWithOthers)
try audioSession.setActive(true) try audioSession.setActive(true)
try audioSession.overrideOutputAudioPort(.speaker) // try audioSession.overrideOutputAudioPort(.speaker)
} catch { } catch {
Logger.shared.log("Didn't set up AVAudioSession: \(error)", type: "Debug") Logger.shared.log("Didn't set up AVAudioSession: \(error)", type: "Debug")
} }

View file

@ -14,7 +14,9 @@ class NormalPlayer: AVPlayerViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
setupHoldGesture() setupHoldGesture()
setupAudioSession() #if !os(tvOS)
setupAudioSession()
#endif
} }
private func setupHoldGesture() { private func setupHoldGesture() {
@ -52,8 +54,9 @@ class NormalPlayer: AVPlayerViewController {
let audioSession = AVAudioSession.sharedInstance() let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playback, mode: .moviePlayback, options: .mixWithOthers) try audioSession.setCategory(.playback, mode: .moviePlayback, options: .mixWithOthers)
try audioSession.setActive(true) try audioSession.setActive(true)
#if !os(tvOS)
try audioSession.overrideOutputAudioPort(.speaker) try audioSession.overrideOutputAudioPort(.speaker)
#endif
} catch { } catch {
Logger.shared.log("Didn't set up AVAudioSession: \(error)", type: "Debug") Logger.shared.log("Didn't set up AVAudioSession: \(error)", type: "Debug")
} }

View file

@ -147,7 +147,7 @@ class VideoPlayerViewController: UIViewController {
} }
} }
} }
#if !os(tvOS)
override var supportedInterfaceOrientations: UIInterfaceOrientationMask { override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UserDefaults.standard.bool(forKey: "alwaysLandscape") { if UserDefaults.standard.bool(forKey: "alwaysLandscape") {
return .landscape return .landscape
@ -163,7 +163,7 @@ class VideoPlayerViewController: UIViewController {
override var prefersStatusBarHidden: Bool { override var prefersStatusBarHidden: Bool {
return true return true
} }
#endif
deinit { deinit {
player?.pause() player?.pause()
if let timeObserverToken = timeObserverToken { if let timeObserverToken = timeObserverToken {

View file

@ -6,7 +6,9 @@
// //
import SwiftUI import SwiftUI
#if !os(tvOS)
import WebKit import WebKit
#endif
private struct ModuleLink: Identifiable { private struct ModuleLink: Identifiable {
let id = UUID() let id = UUID()
@ -28,7 +30,7 @@ struct CommunityLibraryView: View {
.foregroundColor(.red) .foregroundColor(.red)
.padding(.horizontal) .padding(.horizontal)
} }
#if !os(tvOS)
WebView(url: webURL) { linkURL in WebView(url: webURL) { linkURL in
if let comps = URLComponents(url: linkURL, resolvingAgainstBaseURL: false), if let comps = URLComponents(url: linkURL, resolvingAgainstBaseURL: false),
@ -37,6 +39,7 @@ struct CommunityLibraryView: View {
} }
} }
.ignoresSafeArea(edges: .top) .ignoresSafeArea(edges: .top)
#endif
} }
.onAppear(perform: loadURL) .onAppear(perform: loadURL)
.sheet(item: $moduleLinkToAdd) { link in .sheet(item: $moduleLinkToAdd) { link in
@ -61,6 +64,7 @@ struct CommunityLibraryView: View {
} }
} }
#if !os(tvOS)
struct WebView: UIViewRepresentable { struct WebView: UIViewRepresentable {
let url: URL? let url: URL?
let onCustomScheme: (URL) -> Void let onCustomScheme: (URL) -> Void
@ -102,3 +106,4 @@ struct WebView: UIViewRepresentable {
} }
} }
} }
#endif

View file

@ -67,15 +67,19 @@ struct ModuleAdditionSettingsView: View {
InfoRow(title: "Quality", value: metadata.quality) InfoRow(title: "Quality", value: metadata.quality)
InfoRow(title: "Stream Typed", value: metadata.streamType) InfoRow(title: "Stream Typed", value: metadata.streamType)
InfoRow(title: "Base URL", value: metadata.baseUrl) InfoRow(title: "Base URL", value: metadata.baseUrl)
#if !os(tvOS)
.onLongPressGesture { .onLongPressGesture {
UIPasteboard.general.string = metadata.baseUrl UIPasteboard.general.string = metadata.baseUrl
DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill")) DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill"))
} }
#endif
InfoRow(title: "Script URL", value: metadata.scriptUrl) InfoRow(title: "Script URL", value: metadata.scriptUrl)
#if !os(tvOS)
.onLongPressGesture { .onLongPressGesture {
UIPasteboard.general.string = metadata.scriptUrl UIPasteboard.general.string = metadata.scriptUrl
DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill")) DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill"))
} }
#endif
} }
.padding(.horizontal) .padding(.horizontal)
} }

View file

@ -7,7 +7,7 @@
import Foundation import Foundation
class ModuleManager: ObservableObject { class ModuleManager: ObservableObject, @unchecked Sendable {
@Published var modules: [ScrapingModule] = [] @Published var modules: [ScrapingModule] = []
private let fileManager = FileManager.default private let fileManager = FileManager.default
@ -59,7 +59,12 @@ class ModuleManager: ObservableObject {
} }
private func getDocumentsDirectory() -> URL { private func getDocumentsDirectory() -> URL {
fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0] #if !os(tvOS)
let directory = FileManager.SearchPathDirectory.documentDirectory
#elseif os(tvOS)
let directory = FileManager.SearchPathDirectory.cachesDirectory
#endif
return fileManager.urls(for: directory, in: .userDomainMask)[0]
} }
private func getModulesFilePath() -> URL { private func getModulesFilePath() -> URL {

View file

@ -21,8 +21,9 @@ struct LibraryView: View {
@State private var isDetailActive: Bool = false @State private var isDetailActive: Bool = false
@State private var continueWatchingItems: [ContinueWatchingItem] = [] @State private var continueWatchingItems: [ContinueWatchingItem] = []
#if !os(tvOS)
@State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape @State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape
#endif
private let columns = [ private let columns = [
GridItem(.adaptive(minimum: 150), spacing: 12) GridItem(.adaptive(minimum: 150), spacing: 12)
] ]
@ -166,9 +167,11 @@ struct LibraryView: View {
.onAppear { .onAppear {
updateOrientation() updateOrientation()
} }
#if !os(tvOS)
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
updateOrientation() updateOrientation()
} }
#endif
} }
} }
.padding(.vertical, 20) .padding(.vertical, 20)
@ -201,13 +204,19 @@ struct LibraryView: View {
private func updateOrientation() { private func updateOrientation() {
DispatchQueue.main.async { DispatchQueue.main.async {
#if !os(tvOS)
isLandscape = UIDevice.current.orientation.isLandscape isLandscape = UIDevice.current.orientation.isLandscape
#endif
} }
} }
private func determineColumns() -> Int { private func determineColumns() -> Int {
if UIDevice.current.userInterfaceIdiom == .pad { if UIDevice.current.userInterfaceIdiom == .pad {
#if !os(tvOS)
return isLandscape ? mediaColumnsLandscape : mediaColumnsPortrait return isLandscape ? mediaColumnsLandscape : mediaColumnsPortrait
#elseif os(tvOS)
return mediaColumnsLandscape
#endif
} else { } else {
return verticalSizeClass == .compact ? mediaColumnsLandscape : mediaColumnsPortrait return verticalSizeClass == .compact ? mediaColumnsLandscape : mediaColumnsPortrait
} }

View file

@ -51,6 +51,7 @@ struct EpisodeCell: View {
} }
var body: some View { var body: some View {
#if !os(tvOS)
HStack { HStack {
ZStack { ZStack {
KFImage(URL(string: episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl)) KFImage(URL(string: episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl))
@ -58,13 +59,7 @@ struct EpisodeCell: View {
.aspectRatio(16/9, contentMode: .fill) .aspectRatio(16/9, contentMode: .fill)
.frame(width: 100, height: 56) .frame(width: 100, height: 56)
.cornerRadius(8) .cornerRadius(8)
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
}
} }
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Episode \(episodeID + 1)") Text("Episode \(episodeID + 1)")
.font(.system(size: 15)) .font(.system(size: 15))
@ -73,44 +68,102 @@ struct EpisodeCell: View {
.font(.system(size: 13)) .font(.system(size: 13))
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
} Spacer()
CircularProgressBar(progress: currentProgress)
Spacer() .frame(width: 40, height: 40)
}.contentShape(Rectangle())
CircularProgressBar(progress: currentProgress) .contextMenu {
.frame(width: 40, height: 40) if progress <= 0.9 {
} Button(action: markAsWatched) {
.contentShape(Rectangle()) Label("Mark as Watched", systemImage: "checkmark.circle")
.contextMenu { }
if progress <= 0.9 { if progress != 0 {
Button(action: markAsWatched) { Button(action: resetProgress) {
Label("Mark as Watched", systemImage: "checkmark.circle") Label("Reset Progress", systemImage: "arrow.counterclockwise")
if episodeIndex > 0 {
Button(action: onMarkAllPrevious) {
Label("Mark All Previous Watched", systemImage: "checkmark.circle.fill")
}
.onAppear {
updateProgress()
fetchEpisodeDetails()
}
.onChange(of: progress) { _ in
updateProgress()
}
.onTapGesture {
let imageUrl = episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl
onTap(imageUrl)
}
}
}
}
}
} }
}
if progress != 0 {
Button(action: resetProgress) {
Label("Reset Progress", systemImage: "arrow.counterclockwise")
}
}
if episodeIndex > 0 {
Button(action: onMarkAllPrevious) {
Label("Mark All Previous Watched", systemImage: "checkmark.circle.fill")
}
}
} }
.onAppear { #elseif os(tvOS)
updateProgress() Button{
fetchEpisodeDetails()
}
.onChange(of: progress) { _ in
updateProgress()
}
.onTapGesture {
let imageUrl = episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl let imageUrl = episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl
onTap(imageUrl) onTap(imageUrl)
} label: {
HStack {
ZStack {
KFImage(URL(string: episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl))
.resizable()
.aspectRatio(16/9, contentMode: .fill)
.frame(width: 100, height: 56)
.cornerRadius(8)
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
}
}
VStack(alignment: .leading) {
Text("Episode \(episodeID + 1)")
.font(.system(size: 15))
if !episodeTitle.isEmpty {
Text(episodeTitle)
.font(.system(size: 13))
.foregroundColor(.secondary)
}
}
Spacer()
CircularProgressBar(progress: currentProgress)
.frame(width: 40, height: 40)
}
.contentShape(Rectangle())
.contextMenu {
if progress <= 0.9 {
Button(action: markAsWatched) {
Label("Mark as Watched", systemImage: "checkmark.circle")
}
}
if progress != 0 {
Button(action: resetProgress) {
Label("Reset Progress", systemImage: "arrow.counterclockwise")
}
}
if episodeIndex > 0 {
Button(action: onMarkAllPrevious) {
Label("Mark All Previous Watched", systemImage: "checkmark.circle.fill")
}
}
}
.onAppear {
updateProgress()
fetchEpisodeDetails()
}
.onChange(of: progress) { oldValue, _ in
updateProgress()
}
} }
#endif
} }
private func markAsWatched() { private func markAsWatched() {

View file

@ -7,7 +7,9 @@
import SwiftUI import SwiftUI
import Kingfisher import Kingfisher
#if !os(tvOS)
import SafariServices import SafariServices
#endif
struct MediaItem: Identifiable { struct MediaItem: Identifiable {
let id = UUID() let id = UUID()
@ -92,8 +94,10 @@ struct MediaInfoView: View {
.font(.system(size: 17)) .font(.system(size: 17))
.fontWeight(.bold) .fontWeight(.bold)
.onLongPressGesture { .onLongPressGesture {
#if !os(tvOS)
UIPasteboard.general.string = title UIPasteboard.general.string = title
DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill")) DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill"))
#endif
} }
if !aliases.isEmpty && aliases != title && aliases != "N/A" && aliases != "No Data" { if !aliases.isEmpty && aliases != title && aliases != "N/A" && aliases != "No Data" {
@ -122,7 +126,9 @@ struct MediaInfoView: View {
HStack(alignment: .center, spacing: 12) { HStack(alignment: .center, spacing: 12) {
Button(action: { Button(action: {
#if !os(tvOS)
openSafariViewController(with: href) openSafariViewController(with: href)
#endif
}) { }) {
HStack(spacing: 4) { HStack(spacing: 4) {
Text(module.metadata.sourceName) Text(module.metadata.sourceName)
@ -164,9 +170,11 @@ struct MediaInfoView: View {
if let id = itemID ?? customAniListID { if let id = itemID ?? customAniListID {
Button(action: { Button(action: {
#if !os(tvOS)
if let url = URL(string: "https://anilist.co/anime/\(id)") { if let url = URL(string: "https://anilist.co/anime/\(id)") {
openSafariViewController(with: url.absoluteString) openSafariViewController(with: url.absoluteString)
} }
#endif
}) { }) {
Label("Open in AniList", systemImage: "link") Label("Open in AniList", systemImage: "link")
} }
@ -418,7 +426,9 @@ struct MediaInfoView: View {
} }
} }
.padding() .padding()
#if !os(tvOS)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
#endif
.navigationBarTitle("") .navigationBarTitle("")
.navigationViewStyle(StackNavigationViewStyle()) .navigationViewStyle(StackNavigationViewStyle())
} }
@ -452,10 +462,11 @@ struct MediaInfoView: View {
} }
selectedRange = 0..<episodeChunkSize selectedRange = 0..<episodeChunkSize
} }
#if !os(tvOS)
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
orientationChanged.toggle() orientationChanged.toggle()
} }
#endif
if showStreamLoadingView { if showStreamLoadingView {
VStack(spacing: 16) { VStack(spacing: 16) {
Text("Loading \(currentStreamTitle)") Text("Loading \(currentStreamTitle)")
@ -774,8 +785,9 @@ struct MediaInfoView: View {
AnalyticsManager.shared.sendEvent(event: "error", additionalData: ["error": error, "message": "Failed to fetch stream"]) AnalyticsManager.shared.sendEvent(event: "error", additionalData: ["error": error, "message": "Failed to fetch stream"])
} }
DropManager.shared.showDrop(title: "Stream not Found", subtitle: "", duration: 0.5, icon: UIImage(systemName: "xmark")) DropManager.shared.showDrop(title: "Stream not Found", subtitle: "", duration: 0.5, icon: UIImage(systemName: "xmark"))
#if !os(tvOS)
UINotificationFeedbackGenerator().notificationOccurred(.error) UINotificationFeedbackGenerator().notificationOccurred(.error)
#endif
self.isLoading = false self.isLoading = false
} }
@ -847,7 +859,7 @@ struct MediaInfoView: View {
self.isFetchingEpisode = false self.isFetchingEpisode = false
self.showStreamLoadingView = false self.showStreamLoadingView = false
DispatchQueue.main.async { DispatchQueue.main.async {
let externalPlayer = UserDefaults.standard.string(forKey: "externalPlayer") ?? "Sora" let externalPlayer = UserDefaults.standard.string(forKey: "externalPlayer") ?? "Default"
var scheme: String? var scheme: String?
switch externalPlayer { switch externalPlayer {
@ -929,6 +941,7 @@ struct MediaInfoView: View {
DropManager.shared.showDrop(title: "Fetching Next Episode", subtitle: "", duration: 0.5, icon: UIImage(systemName: "arrow.triangle.2.circlepath")) DropManager.shared.showDrop(title: "Fetching Next Episode", subtitle: "", duration: 0.5, icon: UIImage(systemName: "arrow.triangle.2.circlepath"))
} }
#if !os(tvOS)
private func openSafariViewController(with urlString: String) { private func openSafariViewController(with urlString: String) {
guard let url = URL(string: urlString), UIApplication.shared.canOpenURL(url) else { guard let url = URL(string: urlString), UIApplication.shared.canOpenURL(url) else {
Logger.shared.log("Unable to open the webpage", type: "Error") Logger.shared.log("Unable to open the webpage", type: "Error")
@ -940,6 +953,7 @@ struct MediaInfoView: View {
rootVC.present(safariViewController, animated: true, completion: nil) rootVC.present(safariViewController, animated: true, completion: nil)
} }
} }
#endif
private func cleanTitle(_ title: String?) -> String { private func cleanTitle(_ title: String?) -> String {
guard let title = title else { return "Unknown" } guard let title = title else { return "Unknown" }

View file

@ -30,7 +30,9 @@ struct SearchView: View {
@State private var isSearching = false @State private var isSearching = false
@State private var searchText = "" @State private var searchText = ""
@State private var hasNoResults = false @State private var hasNoResults = false
#if !os(tvOS)
@State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape @State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape
#endif
@State private var isModuleSelectorPresented = false @State private var isModuleSelectorPresented = false
private var selectedModule: ScrapingModule? { private var selectedModule: ScrapingModule? {
@ -47,12 +49,16 @@ struct SearchView: View {
] ]
private var columnsCount: Int { private var columnsCount: Int {
#if !os(tvOS)
if UIDevice.current.userInterfaceIdiom == .pad { if UIDevice.current.userInterfaceIdiom == .pad {
let isLandscape = UIScreen.main.bounds.width > UIScreen.main.bounds.height let isLandscape = UIScreen.main.bounds.width > UIScreen.main.bounds.height
return isLandscape ? mediaColumnsLandscape : mediaColumnsPortrait return isLandscape ? mediaColumnsLandscape : mediaColumnsPortrait
} else { } else {
return verticalSizeClass == .compact ? mediaColumnsLandscape : mediaColumnsPortrait return verticalSizeClass == .compact ? mediaColumnsLandscape : mediaColumnsPortrait
} }
#elseif os(tvOS)
return mediaColumnsLandscape
#endif
} }
private var cellWidth: CGFloat { private var cellWidth: CGFloat {
@ -61,11 +67,16 @@ struct SearchView: View {
.first .first
let safeAreaInsets = keyWindow?.safeAreaInsets ?? .zero let safeAreaInsets = keyWindow?.safeAreaInsets ?? .zero
let safeWidth = UIScreen.main.bounds.width - safeAreaInsets.left - safeAreaInsets.right let safeWidth = UIScreen.main.bounds.width - safeAreaInsets.left - safeAreaInsets.right
#if !os(tvOS)
let totalSpacing: CGFloat = 16 * CGFloat(columnsCount + 1) let totalSpacing: CGFloat = 16 * CGFloat(columnsCount + 1)
#elseif os(tvOS)
let totalSpacing: CGFloat = 32 * CGFloat(columnsCount + 1)
#endif
let availableWidth = safeWidth - totalSpacing let availableWidth = safeWidth - totalSpacing
return availableWidth / CGFloat(columnsCount) return availableWidth / CGFloat(columnsCount)
} }
#if !os(tvOS)
var body: some View { var body: some View {
NavigationView { NavigationView {
ScrollView { ScrollView {
@ -222,6 +233,159 @@ struct SearchView: View {
} }
} }
#elseif os(tvOS)
var body: some View {
NavigationView {
ScrollView {
let columnsCount = determineColumns()
VStack(spacing: 0) {
HStack {
SearchBar(text: $searchText, onSearchButtonClicked: performSearch)
.padding(.leading)
.padding(.trailing, searchText.isEmpty ? 16 : 0)
.disabled(selectedModule == nil)
.padding(.top)
if !searchText.isEmpty {
Button("Cancel") {
searchText = ""
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
.padding(.trailing)
.padding(.top)
}
}
if selectedModule == nil {
VStack(spacing: 8) {
Image(systemName: "questionmark.app")
.font(.largeTitle)
.foregroundColor(.secondary)
Text("No Module Selected")
.font(.headline)
Text("Please select a module from settings")
.font(.caption)
.foregroundColor(.secondary)
}
.padding()
.frame(maxWidth: .infinity)
.shadow(color: Color.black.opacity(0.1), radius: 2, y: 1)
}
if !searchText.isEmpty {
if isSearching {
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 16), count: columnsCount), spacing: 16) {
ForEach(0..<columnsCount*4, id: \.self) { _ in
SearchSkeletonCell(cellWidth: cellWidth)
}
}
.padding(.top)
.padding()
} else if hasNoResults {
VStack(spacing: 8) {
Image(systemName: "magnifyingglass")
.font(.largeTitle)
.foregroundColor(.secondary)
Text("No Results Found")
.font(.headline)
Text("Try different keywords")
.font(.caption)
.foregroundColor(.secondary)
}
.padding()
.frame(maxWidth: .infinity)
.padding(.top)
} else {
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 16), count: columnsCount), spacing: 16) {
ForEach(searchItems) { item in
NavigationLink(destination: MediaInfoView(title: item.title, imageUrl: item.imageUrl, href: item.href, module: selectedModule!)) {
VStack {
KFImage(URL(string: item.imageUrl))
.resizable()
.aspectRatio(contentMode: .fill)
.frame(height: cellWidth * 3 / 2)
.frame(maxWidth: cellWidth - 60)
.cornerRadius(10)
.clipped()
Text(item.title)
.font(.subheadline)
.foregroundColor(Color.primary)
.padding([.leading, .bottom], 8)
.lineLimit(1)
}
}
}
}
.padding(.top)
.padding()
}
}
}
}
.navigationTitle("Search")
.toolbar {
ToolbarItem(placement: .principal) {
Menu {
ForEach(getModuleLanguageGroups(), id: \.self) { language in
Menu {
ForEach(getModulesForLanguage(language), id: \.id) { module in
Button {
selectedModuleId = module.id.uuidString
} label: {
HStack {
KFImage(URL(string: module.metadata.iconUrl))
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
.cornerRadius(4)
Text(module.metadata.sourceName)
if module.id.uuidString == selectedModuleId {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}
}
}
}
label: {
Text(language)
}
}
} label: {
HStack(spacing: 4) {
if let selectedModule = selectedModule {
Text(selectedModule.metadata.sourceName)
.font(.headline)
.foregroundColor(.secondary)
} else {
Text("Select Module")
.font(.headline)
.foregroundColor(.accentColor)
}
Image(systemName: "chevron.down")
.foregroundColor(.secondary)
}
}
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
.onChange(of: selectedModuleId) { oldValue, _ in
if !searchText.isEmpty {
performSearch()
}
}
.onChange(of: searchText) { oldValue, newValue in
if newValue.isEmpty {
searchItems = []
hasNoResults = false
isSearching = false
}
}
}
#endif
private func performSearch() { private func performSearch() {
Logger.shared.log("Searching for: \(searchText)", type: "General") Logger.shared.log("Searching for: \(searchText)", type: "General")
guard !searchText.isEmpty, let module = selectedModule else { guard !searchText.isEmpty, let module = selectedModule else {
@ -260,19 +424,25 @@ struct SearchView: View {
} }
} }
} }
#if !os(tvOS)
private func updateOrientation() { private func updateOrientation() {
DispatchQueue.main.async { DispatchQueue.main.async {
isLandscape = UIDevice.current.orientation.isLandscape isLandscape = UIDevice.current.orientation.isLandscape
} }
} }
#endif
private func determineColumns() -> Int { private func determineColumns() -> Int {
#if !os(tvOS)
if UIDevice.current.userInterfaceIdiom == .pad { if UIDevice.current.userInterfaceIdiom == .pad {
return isLandscape ? mediaColumnsLandscape : mediaColumnsPortrait return isLandscape ? mediaColumnsLandscape : mediaColumnsPortrait
} else { } else {
return verticalSizeClass == .compact ? mediaColumnsLandscape : mediaColumnsPortrait return verticalSizeClass == .compact ? mediaColumnsLandscape : mediaColumnsPortrait
} }
#elseif os(tvOS)
return mediaColumnsLandscape
#endif
} }
private func cleanLanguageName(_ language: String?) -> String { private func cleanLanguageName(_ language: String?) -> String {
@ -321,9 +491,11 @@ struct SearchBar: View {
TextField("Search...", text: $text, onCommit: onSearchButtonClicked) TextField("Search...", text: $text, onCommit: onSearchButtonClicked)
.padding(7) .padding(7)
.padding(.horizontal, 25) .padding(.horizontal, 25)
#if !os(tvOS)
.background(Color(.systemGray6)) .background(Color(.systemGray6))
#endif
.cornerRadius(8) .cornerRadius(8)
.onChange(of: text){newValue in .onChange(of: text){ newValue in
debounceTimer?.invalidate() debounceTimer?.invalidate()
// Start a new timer to wait before performing the action // Start a new timer to wait before performing the action
debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in

View file

@ -83,7 +83,12 @@ struct SettingsViewData: View {
func removeAllFilesInDocuments() { func removeAllFilesInDocuments() {
let fileManager = FileManager.default let fileManager = FileManager.default
if let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first { #if !os(tvOS)
let directory = FileManager.SearchPathDirectory.documentDirectory
#elseif os(tvOS)
let directory = FileManager.SearchPathDirectory.cachesDirectory
#endif
if let documentsURL = fileManager.urls(for: directory, in: .userDomainMask).first {
do { do {
let fileURLs = try fileManager.contentsOfDirectory(at: documentsURL, includingPropertiesForKeys: nil) let fileURLs = try fileManager.contentsOfDirectory(at: documentsURL, includingPropertiesForKeys: nil)
for fileURL in fileURLs { for fileURL in fileURLs {

View file

@ -25,7 +25,9 @@ struct SettingsViewGeneral: View {
var body: some View { var body: some View {
Form { Form {
Section(header: Text("Interface")) { Section(header: Text("Interface")) {
#if !os(tvOS)
ColorPicker("Accent Color", selection: $settings.accentColor) ColorPicker("Accent Color", selection: $settings.accentColor)
#endif
HStack { HStack {
Text("Appearance") Text("Appearance")
Picker("Appearance", selection: $settings.selectedAppearance) { Picker("Appearance", selection: $settings.selectedAppearance) {

View file

@ -19,7 +19,9 @@ struct SettingsViewLogger: View {
.foregroundColor(.secondary) .foregroundColor(.secondary)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding() .padding()
#if !os(tvOS)
.textSelection(.enabled) .textSelection(.enabled)
#endif
} }
.navigationTitle("Logs") .navigationTitle("Logs")
.onAppear { .onAppear {
@ -30,12 +32,14 @@ struct SettingsViewLogger: View {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
HStack { HStack {
Menu { Menu {
#if !os(tvOS)
Button(action: { Button(action: {
UIPasteboard.general.string = logs UIPasteboard.general.string = logs
DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill")) DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill"))
}) { }) {
Label("Copy to Clipboard", systemImage: "doc.on.doc") Label("Copy to Clipboard", systemImage: "doc.on.doc")
} }
#endif
Button(role: .destructive, action: { Button(role: .destructive, action: {
Logger.shared.clearLogs() Logger.shared.clearLogs()
logs = Logger.shared.getLogs() logs = Logger.shared.getLogs()

View file

@ -88,12 +88,14 @@ struct SettingsViewModule: View {
selectedModuleId = module.id.uuidString selectedModuleId = module.id.uuidString
} }
.contextMenu { .contextMenu {
#if !os(tvOS)
Button(action: { Button(action: {
UIPasteboard.general.string = module.metadataUrl UIPasteboard.general.string = module.metadataUrl
DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill")) DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill"))
}) { }) {
Label("Copy URL", systemImage: "doc.on.doc") Label("Copy URL", systemImage: "doc.on.doc")
} }
#endif
Button(role: .destructive) { Button(role: .destructive) {
if selectedModuleId != module.id.uuidString { if selectedModuleId != module.id.uuidString {
moduleManager.deleteModule(module) moduleManager.deleteModule(module)
@ -104,6 +106,7 @@ struct SettingsViewModule: View {
} }
.disabled(selectedModuleId == module.id.uuidString) .disabled(selectedModuleId == module.id.uuidString)
} }
#if !os(tvOS)
.swipeActions { .swipeActions {
if selectedModuleId != module.id.uuidString { if selectedModuleId != module.id.uuidString {
Button(role: .destructive) { Button(role: .destructive) {
@ -114,6 +117,7 @@ struct SettingsViewModule: View {
} }
} }
} }
#endif
} }
} }
} }
@ -180,7 +184,11 @@ struct SettingsViewModule: View {
} }
func showAddModuleAlert() { func showAddModuleAlert() {
#if !os(tvOS)
let pasteboardString = UIPasteboard.general.string ?? "" let pasteboardString = UIPasteboard.general.string ?? ""
#elseif os(tvOS)
let pasteboardString = ""
#endif
if !pasteboardString.isEmpty { if !pasteboardString.isEmpty {
let clipboardAlert = UIAlertController( let clipboardAlert = UIAlertController(

View file

@ -52,6 +52,7 @@ struct SettingsViewPlayer: View {
HStack { HStack {
Text("Hold Speed:") Text("Hold Speed:")
Spacer() Spacer()
#if !os(tvOS)
Stepper( Stepper(
value: $holdSpeedPlayer, value: $holdSpeedPlayer,
in: 0.25...2.5, in: 0.25...2.5,
@ -59,9 +60,10 @@ struct SettingsViewPlayer: View {
) { ) {
Text(String(format: "%.2f", holdSpeedPlayer)) Text(String(format: "%.2f", holdSpeedPlayer))
} }
#endif
} }
} }
#if !os(tvOS)
Section(header: Text("Progress bar Marker Color")) { Section(header: Text("Progress bar Marker Color")) {
ColorPicker("Segments Color", selection: Binding( ColorPicker("Segments Color", selection: Binding(
get: { get: {
@ -82,18 +84,23 @@ struct SettingsViewPlayer: View {
} }
)) ))
} }
#endif
Section(header: Text("Skip Settings"), footer : Text("Double tapping the screen on it's sides will skip with the short tap setting.")) { Section(header: Text("Skip Settings"), footer : Text("Double tapping the screen on it's sides will skip with the short tap setting.")) {
HStack { HStack {
Text("Tap Skip:") Text("Tap Skip:")
Spacer() Spacer()
#if !os(tvOS)
Stepper("\(Int(skipIncrement))s", value: $skipIncrement, in: 5...300, step: 5) Stepper("\(Int(skipIncrement))s", value: $skipIncrement, in: 5...300, step: 5)
#endif
} }
HStack { HStack {
Text("Long press Skip:") Text("Long press Skip:")
Spacer() Spacer()
#if !os(tvOS)
Stepper("\(Int(skipIncrementHold))s", value: $skipIncrementHold, in: 5...300, step: 5) Stepper("\(Int(skipIncrementHold))s", value: $skipIncrementHold, in: 5...300, step: 5)
#endif
} }
Toggle("Double Tap to Seek", isOn: $doubleTapSeekEnabled) Toggle("Double Tap to Seek", isOn: $doubleTapSeekEnabled)
@ -159,32 +166,38 @@ struct SubtitleSettingsSection: View {
Toggle("Background Enabled", isOn: $backgroundEnabled) Toggle("Background Enabled", isOn: $backgroundEnabled)
.tint(.accentColor) .tint(.accentColor)
#if !os(tvOS)
.onChange(of: backgroundEnabled) { newValue in .onChange(of: backgroundEnabled) { newValue in
SubtitleSettingsManager.shared.update { settings in SubtitleSettingsManager.shared.update { settings in
settings.backgroundEnabled = newValue settings.backgroundEnabled = newValue
} }
} }
#endif
HStack { HStack {
Text("Font Size:") Text("Font Size:")
Spacer() Spacer()
#if !os(tvOS)
Stepper("\(Int(fontSize))", value: $fontSize, in: 12...36, step: 1) Stepper("\(Int(fontSize))", value: $fontSize, in: 12...36, step: 1)
.onChange(of: fontSize) { newValue in .onChange(of: fontSize) { newValue in
SubtitleSettingsManager.shared.update { settings in SubtitleSettingsManager.shared.update { settings in
settings.fontSize = newValue settings.fontSize = newValue
} }
} }
#endif
} }
HStack { HStack {
Text("Bottom Padding:") Text("Bottom Padding:")
Spacer() Spacer()
#if !os(tvOS)
Stepper("\(Int(bottomPadding))", value: $bottomPadding, in: 0...50, step: 1) Stepper("\(Int(bottomPadding))", value: $bottomPadding, in: 0...50, step: 1)
.onChange(of: bottomPadding) { newValue in .onChange(of: bottomPadding) { newValue in
SubtitleSettingsManager.shared.update { settings in SubtitleSettingsManager.shared.update { settings in
settings.bottomPadding = newValue settings.bottomPadding = newValue
} }
} }
#endif
} }
} }
} }

View file

@ -474,7 +474,7 @@
attributes = { attributes = {
BuildIndependentTargetsInParallel = 1; BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1320; LastSwiftUpdateCheck = 1320;
LastUpgradeCheck = 1320; LastUpgradeCheck = 1630;
TargetAttributes = { TargetAttributes = {
133D7C692D2BE2500075467E = { 133D7C692D2BE2500075467E = {
CreatedOnToolsVersion = 13.2.1; CreatedOnToolsVersion = 13.2.1;
@ -487,6 +487,7 @@
hasScannedForEncodings = 0; hasScannedForEncodings = 0;
knownRegions = ( knownRegions = (
en, en,
Base,
); );
mainGroup = 133D7C612D2BE2500075467E; mainGroup = 133D7C612D2BE2500075467E;
packageReferences = ( packageReferences = (
@ -615,8 +616,10 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 399LMK6Q2Y;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO; GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES; GCC_NO_COMMON_BLOCKS = YES;
@ -677,8 +680,10 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 399LMK6Q2Y;
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES; GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@ -709,7 +714,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\"";
DEVELOPMENT_TEAM = 399LMK6Q2Y; DEVELOPMENT_TEAM = "";
"ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -734,10 +739,11 @@
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "";
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2,3";
}; };
name = Debug; name = Debug;
}; };
@ -752,7 +758,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\"";
DEVELOPMENT_TEAM = 399LMK6Q2Y; DEVELOPMENT_TEAM = "";
"ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -777,10 +783,11 @@
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "";
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2,3";
}; };
name = Release; name = Release;
}; };