* 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() {}
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
}
}

View file

@ -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)"

View file

@ -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")
}

View file

@ -94,6 +94,7 @@ struct MusicProgressSlider<T: BinaryFloatingPoint>: 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<T: BinaryFloatingPoint>: View {
localTempProgress = 0
}
)
#endif
.onChange(of: isActive) { newValue in
value = max(min(getPrgValue(), inRange.upperBound), inRange.lowerBound)
onEditingChanged(newValue)

View file

@ -55,6 +55,7 @@ struct VolumeSlider<T: BinaryFloatingPoint>: 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<T: BinaryFloatingPoint>: View {
localTempProgress = 0
}
)
#endif
.onChange(of: isActive) { newValue in
if !newValue {
value = sliderValueInRange()

View file

@ -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")
}

View file

@ -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")
}

View file

@ -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 {

View file

@ -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

View file

@ -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)
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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() {

View file

@ -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..<episodeChunkSize
}
#if !os(tvOS)
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
orientationChanged.toggle()
}
#endif
if showStreamLoadingView {
VStack(spacing: 16) {
Text("Loading \(currentStreamTitle)")
@ -774,8 +785,9 @@ struct MediaInfoView: View {
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"))
#if !os(tvOS)
UINotificationFeedbackGenerator().notificationOccurred(.error)
#endif
self.isLoading = false
}
@ -847,7 +859,7 @@ struct MediaInfoView: View {
self.isFetchingEpisode = false
self.showStreamLoadingView = false
DispatchQueue.main.async {
let externalPlayer = UserDefaults.standard.string(forKey: "externalPlayer") ?? "Sora"
let externalPlayer = UserDefaults.standard.string(forKey: "externalPlayer") ?? "Default"
var scheme: String?
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"))
}
#if !os(tvOS)
private func openSafariViewController(with urlString: String) {
guard let url = URL(string: urlString), UIApplication.shared.canOpenURL(url) else {
Logger.shared.log("Unable to open the webpage", type: "Error")
@ -940,6 +953,7 @@ struct MediaInfoView: View {
rootVC.present(safariViewController, animated: true, completion: nil)
}
}
#endif
private func cleanTitle(_ title: String?) -> String {
guard let title = title else { return "Unknown" }

View file

@ -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..<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() {
Logger.shared.log("Searching for: \(searchText)", type: "General")
guard !searchText.isEmpty, let module = selectedModule else {
@ -260,19 +424,25 @@ struct SearchView: View {
}
}
}
#if !os(tvOS)
private func updateOrientation() {
DispatchQueue.main.async {
isLandscape = UIDevice.current.orientation.isLandscape
}
}
#endif
private func determineColumns() -> 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

View file

@ -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 {

View file

@ -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) {

View file

@ -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()

View file

@ -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(

View file

@ -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
}
}
}

View file

@ -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;
};