diff --git a/Sora/Utils/Drops/DropManager.swift b/Sora/Utils/Drops/DropManager.swift index 8a56eb3..e5980ee 100644 --- a/Sora/Utils/Drops/DropManager.swift +++ b/Sora/Utils/Drops/DropManager.swift @@ -14,6 +14,7 @@ class DropManager { private init() {} func showDrop(title: String, subtitle: String, duration: TimeInterval, icon: UIImage?) { + #if !os(tvOS) let position: Drop.Position = .top let drop = Drop( @@ -24,5 +25,6 @@ class DropManager { duration: .seconds(duration) ) Drops.show(drop) + #endif } } diff --git a/Sora/Utils/Extensions/UIDevice+Model.swift b/Sora/Utils/Extensions/UIDevice+Model.swift index e07970d..83479c7 100644 --- a/Sora/Utils/Extensions/UIDevice+Model.swift +++ b/Sora/Utils/Extensions/UIDevice+Model.swift @@ -19,7 +19,7 @@ public extension UIDevice { } func mapToDevice(identifier: String) -> String { // swiftlint:disable:this cyclomatic_complexity -#if os(iOS) +#if !os(tvOS) switch identifier { case "iPod5,1": return "iPod touch (5th generation)" diff --git a/Sora/Utils/Logger/Logger.swift b/Sora/Utils/Logger/Logger.swift index 70ad048..cda535b 100644 --- a/Sora/Utils/Logger/Logger.swift +++ b/Sora/Utils/Logger/Logger.swift @@ -21,7 +21,12 @@ class Logger { private let logFilterViewModel = LogFilterViewModel.shared 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") } diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift index 5c97e1d..805fe01 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift @@ -94,6 +94,7 @@ struct MusicProgressSlider: View { } .frame(width: bounds.size.width, height: bounds.size.height, alignment: .center) .contentShape(Rectangle()) + #if !os(tvOS) .gesture( DragGesture(minimumDistance: 0, coordinateSpace: .local) .updating($isActive) { _, state, _ in @@ -108,6 +109,7 @@ struct MusicProgressSlider: View { localTempProgress = 0 } ) + #endif .onChange(of: isActive) { newValue in value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound) onEditingChanged(newValue) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Components/VolumeSlider.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Components/VolumeSlider.swift index 15437c6..a96cbf7 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/Components/VolumeSlider.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/Components/VolumeSlider.swift @@ -55,6 +55,7 @@ struct VolumeSlider: View { .animation(animation, value: isActive) } .frame(width: bounds.size.width, height: bounds.size.height) + #if !os(tvOS) .gesture( DragGesture(minimumDistance: 0, coordinateSpace: .local) .updating($isActive) { _, state, _ in state = true } @@ -68,6 +69,7 @@ struct VolumeSlider: View { localTempProgress = 0 } ) + #endif .onChange(of: isActive) { newValue in if !newValue { value = sliderValueInRange() diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index 34521f3..174087a 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -162,7 +162,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele private var volumeObserver: NSKeyValueObservation? private var audioSession = AVAudioSession.sharedInstance() private var hiddenVolumeView = MPVolumeView(frame: .zero) +#if !os(tvOS) private var systemVolumeSlider: UISlider? +#endif private var volumeValue: Double = 0.0 private var volumeViewModel = VolumeViewModel() var volumeSliderHostingView: UIView? @@ -281,14 +283,16 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele } } +#if !os(tvOS) if #available(iOS 16.0, *) { playerViewController.allowsVideoFrameAnalysis = false } +#endif if let url = subtitlesURL, !url.isEmpty { subtitlesLoader.load(from: url) } - + DispatchQueue.main.async { self.isControlsVisible = true NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) @@ -297,7 +301,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele self.view.layoutIfNeeded() } +#if !os(tvOS) hiddenVolumeView.showsRouteButton = false +#endif hiddenVolumeView.isHidden = true view.addSubview(hiddenVolumeView) @@ -307,9 +313,11 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele hiddenVolumeView.leadingAnchor.constraint(equalTo: view.leadingAnchor).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 { systemVolumeSlider = slider } +#endif } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { @@ -397,10 +405,17 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele } private func getSegmentsColor() -> Color { +#if !os(tvOS) 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) } +#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 } @@ -596,7 +611,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele func holdForPause() { let holdForPauseGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleHoldForPause(_:))) holdForPauseGesture.minimumPressDuration = 1 +#if !os(tvOS) holdForPauseGesture.numberOfTouchesRequired = 2 +#endif view.addGestureRecognizer(holdForPauseGesture) } @@ -832,9 +849,11 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele func volumeSlider() { let container = VolumeSliderContainer(volumeVM: self.volumeViewModel) { newVal in +#if !os(tvOS) if let sysSlider = self.systemVolumeSlider { sysSlider.value = Float(newVal) } +#endif } let hostingController = UIHostingController(rootView: container) @@ -1042,7 +1061,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele skipIntroButton.setImage(introImage, for: .normal) 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) +#endif skipIntroButton.tintColor = .white skipIntroButton.setTitleColor(.white, for: .normal) skipIntroButton.layer.cornerRadius = 21 @@ -1074,7 +1095,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele skipOutroButton.setImage(outroImage, for: .normal) 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) +#endif skipOutroButton.tintColor = .white skipOutroButton.setTitleColor(.white, for: .normal) skipOutroButton.layer.cornerRadius = 21 @@ -1248,7 +1271,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele skip85Button.setImage(image, for: .normal) 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) +#endif skip85Button.tintColor = .white skip85Button.setTitleColor(.white, for: .normal) skip85Button.layer.cornerRadius = 21 @@ -2092,7 +2117,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele self.subtitleBackgroundEnabled = settings.backgroundEnabled self.subtitleBottomPadding = settings.bottomPadding } - + +#if !os(tvOS) override var supportedInterfaceOrientations: UIInterfaceOrientationMask { if UserDefaults.standard.bool(forKey: "alwaysLandscape") { return .landscape @@ -2108,13 +2134,14 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele override var prefersStatusBarHidden: Bool { return true } +#endif func setupAudioSession() { do { let audioSession = AVAudioSession.sharedInstance() try audioSession.setCategory(.playback, mode: .moviePlayback, options: .mixWithOthers) try audioSession.setActive(true) - try audioSession.overrideOutputAudioPort(.speaker) +// try audioSession.overrideOutputAudioPort(.speaker) } catch { Logger.shared.log("Didn't set up AVAudioSession: \(error)", type: "Debug") } diff --git a/Sora/Utils/MediaPlayer/NormalPlayer.swift b/Sora/Utils/MediaPlayer/NormalPlayer.swift index dd8a5f6..68020e4 100644 --- a/Sora/Utils/MediaPlayer/NormalPlayer.swift +++ b/Sora/Utils/MediaPlayer/NormalPlayer.swift @@ -14,7 +14,9 @@ class NormalPlayer: AVPlayerViewController { override func viewDidLoad() { super.viewDidLoad() setupHoldGesture() - setupAudioSession() +#if !os(tvOS) + setupAudioSession() +#endif } private func setupHoldGesture() { @@ -52,8 +54,9 @@ class NormalPlayer: AVPlayerViewController { let audioSession = AVAudioSession.sharedInstance() try audioSession.setCategory(.playback, mode: .moviePlayback, options: .mixWithOthers) try audioSession.setActive(true) - +#if !os(tvOS) try audioSession.overrideOutputAudioPort(.speaker) +#endif } catch { Logger.shared.log("Didn't set up AVAudioSession: \(error)", type: "Debug") } diff --git a/Sora/Utils/MediaPlayer/VideoPlayer.swift b/Sora/Utils/MediaPlayer/VideoPlayer.swift index 122c466..1c8a1a9 100644 --- a/Sora/Utils/MediaPlayer/VideoPlayer.swift +++ b/Sora/Utils/MediaPlayer/VideoPlayer.swift @@ -147,7 +147,7 @@ class VideoPlayerViewController: UIViewController { } } } - +#if !os(tvOS) override var supportedInterfaceOrientations: UIInterfaceOrientationMask { if UserDefaults.standard.bool(forKey: "alwaysLandscape") { return .landscape @@ -163,7 +163,7 @@ class VideoPlayerViewController: UIViewController { override var prefersStatusBarHidden: Bool { return true } - +#endif deinit { player?.pause() if let timeObserverToken = timeObserverToken { diff --git a/Sora/Utils/Modules/CommunityLib.swift b/Sora/Utils/Modules/CommunityLib.swift index 10ae24f..c5f6b52 100644 --- a/Sora/Utils/Modules/CommunityLib.swift +++ b/Sora/Utils/Modules/CommunityLib.swift @@ -6,7 +6,9 @@ // import SwiftUI +#if !os(tvOS) import WebKit +#endif private struct ModuleLink: Identifiable { let id = UUID() @@ -28,7 +30,7 @@ struct CommunityLibraryView: View { .foregroundColor(.red) .padding(.horizontal) } - +#if !os(tvOS) WebView(url: webURL) { linkURL in if let comps = URLComponents(url: linkURL, resolvingAgainstBaseURL: false), @@ -37,6 +39,7 @@ struct CommunityLibraryView: View { } } .ignoresSafeArea(edges: .top) +#endif } .onAppear(perform: loadURL) .sheet(item: $moduleLinkToAdd) { link in @@ -61,6 +64,7 @@ struct CommunityLibraryView: View { } } +#if !os(tvOS) struct WebView: UIViewRepresentable { let url: URL? let onCustomScheme: (URL) -> Void @@ -102,3 +106,4 @@ struct WebView: UIViewRepresentable { } } } +#endif diff --git a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift index fb49ef0..cc49463 100644 --- a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift +++ b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift @@ -67,15 +67,19 @@ struct ModuleAdditionSettingsView: View { InfoRow(title: "Quality", value: metadata.quality) InfoRow(title: "Stream Typed", value: metadata.streamType) InfoRow(title: "Base URL", value: metadata.baseUrl) +#if !os(tvOS) .onLongPressGesture { UIPasteboard.general.string = metadata.baseUrl 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) +#if !os(tvOS) .onLongPressGesture { UIPasteboard.general.string = metadata.scriptUrl DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill")) } +#endif } .padding(.horizontal) } diff --git a/Sora/Utils/Modules/ModuleManager.swift b/Sora/Utils/Modules/ModuleManager.swift index e8ae65c..cea135c 100644 --- a/Sora/Utils/Modules/ModuleManager.swift +++ b/Sora/Utils/Modules/ModuleManager.swift @@ -7,7 +7,7 @@ import Foundation -class ModuleManager: ObservableObject { +class ModuleManager: ObservableObject, @unchecked Sendable { @Published var modules: [ScrapingModule] = [] private let fileManager = FileManager.default @@ -59,7 +59,12 @@ class ModuleManager: ObservableObject { } 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 { diff --git a/Sora/Views/LibraryView/LibraryView.swift b/Sora/Views/LibraryView/LibraryView.swift index 0f7a233..7ee9c7f 100644 --- a/Sora/Views/LibraryView/LibraryView.swift +++ b/Sora/Views/LibraryView/LibraryView.swift @@ -21,8 +21,9 @@ struct LibraryView: View { @State private var isDetailActive: Bool = false @State private var continueWatchingItems: [ContinueWatchingItem] = [] +#if !os(tvOS) @State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape - +#endif private let columns = [ GridItem(.adaptive(minimum: 150), spacing: 12) ] @@ -166,9 +167,11 @@ struct LibraryView: View { .onAppear { updateOrientation() } +#if !os(tvOS) .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in updateOrientation() } +#endif } } .padding(.vertical, 20) @@ -201,13 +204,19 @@ struct LibraryView: View { private func updateOrientation() { DispatchQueue.main.async { +#if !os(tvOS) isLandscape = UIDevice.current.orientation.isLandscape +#endif } } private func determineColumns() -> Int { if UIDevice.current.userInterfaceIdiom == .pad { +#if !os(tvOS) return isLandscape ? mediaColumnsLandscape : mediaColumnsPortrait +#elseif os(tvOS) + return mediaColumnsLandscape +#endif } else { return verticalSizeClass == .compact ? mediaColumnsLandscape : mediaColumnsPortrait } diff --git a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift index bf41b8b..faea421 100644 --- a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift +++ b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift @@ -51,6 +51,7 @@ struct EpisodeCell: View { } var body: some View { + #if !os(tvOS) HStack { ZStack { KFImage(URL(string: episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl)) @@ -58,13 +59,7 @@ struct EpisodeCell: View { .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)) @@ -73,44 +68,102 @@ struct EpisodeCell: View { .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") + 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) { _ 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 { - updateProgress() - fetchEpisodeDetails() - } - .onChange(of: progress) { _ in - updateProgress() - } - .onTapGesture { + #elseif os(tvOS) + Button{ let imageUrl = episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl 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() { diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 709e3c2..846d975 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -7,7 +7,9 @@ import SwiftUI import Kingfisher +#if !os(tvOS) import SafariServices +#endif struct MediaItem: Identifiable { let id = UUID() @@ -92,8 +94,10 @@ struct MediaInfoView: View { .font(.system(size: 17)) .fontWeight(.bold) .onLongPressGesture { +#if !os(tvOS) UIPasteboard.general.string = title 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" { @@ -122,7 +126,9 @@ struct MediaInfoView: View { HStack(alignment: .center, spacing: 12) { Button(action: { +#if !os(tvOS) openSafariViewController(with: href) +#endif }) { HStack(spacing: 4) { Text(module.metadata.sourceName) @@ -164,9 +170,11 @@ struct MediaInfoView: View { if let id = itemID ?? customAniListID { Button(action: { +#if !os(tvOS) if let url = URL(string: "https://anilist.co/anime/\(id)") { openSafariViewController(with: url.absoluteString) } +#endif }) { Label("Open in AniList", systemImage: "link") } @@ -418,7 +426,9 @@ struct MediaInfoView: View { } } .padding() +#if !os(tvOS) .navigationBarTitleDisplayMode(.inline) +#endif .navigationBarTitle("") .navigationViewStyle(StackNavigationViewStyle()) } @@ -452,10 +462,11 @@ struct MediaInfoView: View { } selectedRange = 0.. String { guard let title = title else { return "Unknown" } diff --git a/Sora/Views/SearchView.swift b/Sora/Views/SearchView.swift index 856e2a4..9ba3175 100644 --- a/Sora/Views/SearchView.swift +++ b/Sora/Views/SearchView.swift @@ -30,7 +30,9 @@ struct SearchView: View { @State private var isSearching = false @State private var searchText = "" @State private var hasNoResults = false +#if !os(tvOS) @State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape +#endif @State private var isModuleSelectorPresented = false private var selectedModule: ScrapingModule? { @@ -47,12 +49,16 @@ struct SearchView: View { ] private var columnsCount: Int { +#if !os(tvOS) if UIDevice.current.userInterfaceIdiom == .pad { let isLandscape = UIScreen.main.bounds.width > UIScreen.main.bounds.height return isLandscape ? mediaColumnsLandscape : mediaColumnsPortrait } else { return verticalSizeClass == .compact ? mediaColumnsLandscape : mediaColumnsPortrait } +#elseif os(tvOS) + return mediaColumnsLandscape +#endif } private var cellWidth: CGFloat { @@ -61,11 +67,16 @@ struct SearchView: View { .first let safeAreaInsets = keyWindow?.safeAreaInsets ?? .zero let safeWidth = UIScreen.main.bounds.width - safeAreaInsets.left - safeAreaInsets.right +#if !os(tvOS) let totalSpacing: CGFloat = 16 * CGFloat(columnsCount + 1) +#elseif os(tvOS) + let totalSpacing: CGFloat = 32 * CGFloat(columnsCount + 1) +#endif let availableWidth = safeWidth - totalSpacing return availableWidth / CGFloat(columnsCount) } - + +#if !os(tvOS) var body: some View { NavigationView { 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.. Int { +#if !os(tvOS) if UIDevice.current.userInterfaceIdiom == .pad { return isLandscape ? mediaColumnsLandscape : mediaColumnsPortrait } else { return verticalSizeClass == .compact ? mediaColumnsLandscape : mediaColumnsPortrait } +#elseif os(tvOS) + return mediaColumnsLandscape +#endif } private func cleanLanguageName(_ language: String?) -> String { @@ -321,9 +491,11 @@ struct SearchBar: View { TextField("Search...", text: $text, onCommit: onSearchButtonClicked) .padding(7) .padding(.horizontal, 25) +#if !os(tvOS) .background(Color(.systemGray6)) +#endif .cornerRadius(8) - .onChange(of: text){newValue in + .onChange(of: text){ newValue in debounceTimer?.invalidate() // Start a new timer to wait before performing the action debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift index bfbf687..1814ba5 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift @@ -83,7 +83,12 @@ struct SettingsViewData: View { func removeAllFilesInDocuments() { 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 { let fileURLs = try fileManager.contentsOfDirectory(at: documentsURL, includingPropertiesForKeys: nil) for fileURL in fileURLs { diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index 79bbbff..c8e0c93 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -25,7 +25,9 @@ struct SettingsViewGeneral: View { var body: some View { Form { Section(header: Text("Interface")) { +#if !os(tvOS) ColorPicker("Accent Color", selection: $settings.accentColor) +#endif HStack { Text("Appearance") Picker("Appearance", selection: $settings.selectedAppearance) { diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLogger.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLogger.swift index 0d5c9c5..93131a5 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLogger.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLogger.swift @@ -19,7 +19,9 @@ struct SettingsViewLogger: View { .foregroundColor(.secondary) .frame(maxWidth: .infinity, alignment: .leading) .padding() +#if !os(tvOS) .textSelection(.enabled) +#endif } .navigationTitle("Logs") .onAppear { @@ -30,12 +32,14 @@ struct SettingsViewLogger: View { ToolbarItem(placement: .navigationBarTrailing) { HStack { Menu { +#if !os(tvOS) Button(action: { UIPasteboard.general.string = logs 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") } +#endif Button(role: .destructive, action: { Logger.shared.clearLogs() logs = Logger.shared.getLogs() diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift index b75b02b..73e3fa4 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift @@ -88,12 +88,14 @@ struct SettingsViewModule: View { selectedModuleId = module.id.uuidString } .contextMenu { +#if !os(tvOS) Button(action: { UIPasteboard.general.string = module.metadataUrl 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") } +#endif Button(role: .destructive) { if selectedModuleId != module.id.uuidString { moduleManager.deleteModule(module) @@ -104,6 +106,7 @@ struct SettingsViewModule: View { } .disabled(selectedModuleId == module.id.uuidString) } +#if !os(tvOS) .swipeActions { if selectedModuleId != module.id.uuidString { Button(role: .destructive) { @@ -114,6 +117,7 @@ struct SettingsViewModule: View { } } } +#endif } } } @@ -180,7 +184,11 @@ struct SettingsViewModule: View { } func showAddModuleAlert() { +#if !os(tvOS) let pasteboardString = UIPasteboard.general.string ?? "" +#elseif os(tvOS) + let pasteboardString = "" +#endif if !pasteboardString.isEmpty { let clipboardAlert = UIAlertController( diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift index d07110d..1ff49fd 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift @@ -52,6 +52,7 @@ struct SettingsViewPlayer: View { HStack { Text("Hold Speed:") Spacer() +#if !os(tvOS) Stepper( value: $holdSpeedPlayer, in: 0.25...2.5, @@ -59,9 +60,10 @@ struct SettingsViewPlayer: View { ) { Text(String(format: "%.2f", holdSpeedPlayer)) } +#endif } } - +#if !os(tvOS) Section(header: Text("Progress bar Marker Color")) { ColorPicker("Segments Color", selection: Binding( 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.")) { HStack { Text("Tap Skip:") Spacer() +#if !os(tvOS) Stepper("\(Int(skipIncrement))s", value: $skipIncrement, in: 5...300, step: 5) +#endif } HStack { Text("Long press Skip:") Spacer() +#if !os(tvOS) Stepper("\(Int(skipIncrementHold))s", value: $skipIncrementHold, in: 5...300, step: 5) +#endif } Toggle("Double Tap to Seek", isOn: $doubleTapSeekEnabled) @@ -159,32 +166,38 @@ struct SubtitleSettingsSection: View { Toggle("Background Enabled", isOn: $backgroundEnabled) .tint(.accentColor) +#if !os(tvOS) .onChange(of: backgroundEnabled) { newValue in SubtitleSettingsManager.shared.update { settings in settings.backgroundEnabled = newValue } } +#endif HStack { Text("Font Size:") Spacer() +#if !os(tvOS) Stepper("\(Int(fontSize))", value: $fontSize, in: 12...36, step: 1) .onChange(of: fontSize) { newValue in SubtitleSettingsManager.shared.update { settings in settings.fontSize = newValue } } +#endif } HStack { Text("Bottom Padding:") Spacer() +#if !os(tvOS) Stepper("\(Int(bottomPadding))", value: $bottomPadding, in: 0...50, step: 1) .onChange(of: bottomPadding) { newValue in SubtitleSettingsManager.shared.update { settings in settings.bottomPadding = newValue } } +#endif } } } diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 89079d9..0da109a 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -474,7 +474,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1320; - LastUpgradeCheck = 1320; + LastUpgradeCheck = 1630; TargetAttributes = { 133D7C692D2BE2500075467E = { CreatedOnToolsVersion = 13.2.1; @@ -487,6 +487,7 @@ hasScannedForEncodings = 0; knownRegions = ( en, + Base, ); mainGroup = 133D7C612D2BE2500075467E; packageReferences = ( @@ -615,8 +616,10 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 399LMK6Q2Y; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -677,8 +680,10 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 399LMK6Q2Y; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -709,7 +714,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\""; - DEVELOPMENT_TEAM = 399LMK6Q2Y; + DEVELOPMENT_TEAM = ""; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -734,10 +739,11 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3"; }; name = Debug; }; @@ -752,7 +758,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\""; - DEVELOPMENT_TEAM = 399LMK6Q2Y; + DEVELOPMENT_TEAM = ""; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -777,10 +783,11 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = "1,2,3"; }; name = Release; };