From f63a1fed21e6cd1227272c754edae9524aaf3db1 Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Thu, 8 May 2025 16:34:02 +0200 Subject: [PATCH] frrfrfr --- Sora/Info.plist | 1 + Sora/Views/MediaInfoView/MediaInfoView.swift | 752 +++++++++--------- .../SettingsSubViews/SettingsViewPlayer.swift | 26 +- 3 files changed, 381 insertions(+), 398 deletions(-) diff --git a/Sora/Info.plist b/Sora/Info.plist index 6e00c4a..269b520 100644 --- a/Sora/Info.plist +++ b/Sora/Info.plist @@ -21,6 +21,7 @@ LSApplicationQueriesSchemes + iina outplayer infuse vlc diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index dfbb8f7..7f8b92b 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -59,293 +59,243 @@ struct MediaInfoView: View { @Environment(\.dismiss) private var dismiss @State private var orientationChanged: Bool = false + @State private var showLoadingAlert: Bool = false private var isGroupedBySeasons: Bool { return groupedEpisodes().count > 1 } var body: some View { - ZStack { - Group { - if isLoading { - ProgressView() - .padding() - } else { - ScrollView { - VStack(alignment: .leading, spacing: 16) { - HStack(alignment: .top, spacing: 10) { - KFImage(URL(string: imageUrl)) - .placeholder { - RoundedRectangle(cornerRadius: 10) - .fill(Color.gray.opacity(0.3)) - .frame(width: 150, height: 225) - .shimmering() + Group { + if isLoading { + ProgressView() + .padding() + } else { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + HStack(alignment: .top, spacing: 10) { + KFImage(URL(string: imageUrl)) + .placeholder { + RoundedRectangle(cornerRadius: 10) + .fill(Color.gray.opacity(0.3)) + .frame(width: 150, height: 225) + .shimmering() + } + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 150, height: 225) + .clipped() + .cornerRadius(10) + + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.system(size: 17)) + .fontWeight(.bold) + .onLongPressGesture { + UIPasteboard.general.string = title + DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill")) } - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 150, height: 225) - .clipped() - .cornerRadius(10) - VStack(alignment: .leading, spacing: 4) { - Text(title) - .font(.system(size: 17)) - .fontWeight(.bold) - .onLongPressGesture { - UIPasteboard.general.string = title - DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill")) - } - - if !aliases.isEmpty && aliases != title && aliases != "N/A" && aliases != "No Data" { - Text(aliases) - .font(.system(size: 13)) - .foregroundColor(.secondary) - } - - Spacer() - - if !airdate.isEmpty && airdate != "N/A" && airdate != "No Data" { - HStack(alignment: .center, spacing: 12) { - HStack(spacing: 4) { - Image(systemName: "calendar") - .resizable() - .frame(width: 15, height: 15) - .foregroundColor(.secondary) - - Text(airdate) - .font(.system(size: 12)) - .foregroundColor(.secondary) - } - .padding(4) - } - } - + if !aliases.isEmpty && aliases != title && aliases != "N/A" && aliases != "No Data" { + Text(aliases) + .font(.system(size: 13)) + .foregroundColor(.secondary) + } + + Spacer() + + if !airdate.isEmpty && airdate != "N/A" && airdate != "No Data" { HStack(alignment: .center, spacing: 12) { - Button(action: { - openSafariViewController(with: href) - }) { - HStack(spacing: 4) { - Text(module.metadata.sourceName) - .font(.system(size: 13)) - .foregroundColor(.primary) - - Image(systemName: "safari") - .resizable() - .frame(width: 20, height: 20) - .foregroundColor(.primary) - } - .padding(4) - .background(Capsule().fill(Color.accentColor.opacity(0.4))) + HStack(spacing: 4) { + Image(systemName: "calendar") + .resizable() + .frame(width: 15, height: 15) + .foregroundColor(.secondary) + + Text(airdate) + .font(.system(size: 12)) + .foregroundColor(.secondary) } - - Menu { - Button(action: { - showCustomIDAlert() - }) { - Label("Set Custom AniList ID", systemImage: "number") - } + .padding(4) + } + } + + HStack(alignment: .center, spacing: 12) { + Button(action: { + openSafariViewController(with: href) + }) { + HStack(spacing: 4) { + Text(module.metadata.sourceName) + .font(.system(size: 13)) + .foregroundColor(.primary) - if let customID = customAniListID { - Button(action: { - customAniListID = nil - itemID = nil - fetchItemID(byTitle: cleanTitle(title)) { result in - switch result { - case .success(let id): - itemID = id - case .failure(let error): - Logger.shared.log("Failed to fetch AniList ID: \(error)") - } - } - }) { - Label("Reset AniList ID", systemImage: "arrow.clockwise") - } - } - - if let id = itemID ?? customAniListID { - Button(action: { - if let url = URL(string: "https://anilist.co/anime/\(id)") { - openSafariViewController(with: url.absoluteString) - } - }) { - Label("Open in AniList", systemImage: "link") - } - } - - Divider() - - Button(action: { - Logger.shared.log("Debug Info:\nTitle: \(title)\nHref: \(href)\nModule: \(module.metadata.sourceName)\nAniList ID: \(itemID ?? -1)\nCustom ID: \(customAniListID ?? -1)", type: "Debug") - DropManager.shared.showDrop(title: "Debug Info Logged", subtitle: "", duration: 1.0, icon: UIImage(systemName: "terminal")) - }) { - Label("Log Debug Info", systemImage: "terminal") - } - } label: { - Image(systemName: "ellipsis.circle") + Image(systemName: "safari") .resizable() .frame(width: 20, height: 20) .foregroundColor(.primary) } - } - } - } - - if !synopsis.isEmpty { - VStack(alignment: .leading, spacing: 2) { - HStack(alignment: .center) { - Text("Synopsis") - .font(.system(size: 18)) - .fontWeight(.bold) - - Spacer() - - Button(action: { - showFullSynopsis.toggle() - }) { - Text(showFullSynopsis ? "Less" : "More") - .font(.system(size: 14)) - } + .padding(4) + .background(Capsule().fill(Color.accentColor.opacity(0.4))) } - Text(synopsis) - .lineLimit(showFullSynopsis ? nil : 4) - .font(.system(size: 14)) - } - } - - HStack { - Button(action: { - playFirstUnwatchedEpisode() - }) { - HStack { - Image(systemName: "play.fill") - .foregroundColor(.primary) - Text(startWatchingText) - .font(.headline) + Menu { + Button(action: { + showCustomIDAlert() + }) { + Label("Set Custom AniList ID", systemImage: "number") + } + + if let customID = customAniListID { + Button(action: { + customAniListID = nil + itemID = nil + fetchItemID(byTitle: cleanTitle(title)) { result in + switch result { + case .success(let id): + itemID = id + case .failure(let error): + Logger.shared.log("Failed to fetch AniList ID: \(error)") + } + } + }) { + Label("Reset AniList ID", systemImage: "arrow.clockwise") + } + } + + if let id = itemID ?? customAniListID { + Button(action: { + if let url = URL(string: "https://anilist.co/anime/\(id)") { + openSafariViewController(with: url.absoluteString) + } + }) { + Label("Open in AniList", systemImage: "link") + } + } + + Divider() + + Button(action: { + Logger.shared.log("Debug Info:\nTitle: \(title)\nHref: \(href)\nModule: \(module.metadata.sourceName)\nAniList ID: \(itemID ?? -1)\nCustom ID: \(customAniListID ?? -1)", type: "Debug") + DropManager.shared.showDrop(title: "Debug Info Logged", subtitle: "", duration: 1.0, icon: UIImage(systemName: "terminal")) + }) { + Label("Log Debug Info", systemImage: "terminal") + } + } label: { + Image(systemName: "ellipsis.circle") + .resizable() + .frame(width: 20, height: 20) .foregroundColor(.primary) } - .padding() - .frame(maxWidth: .infinity) - .background(Color.accentColor) - .cornerRadius(10) - } - .disabled(isFetchingEpisode) - .id(buttonRefreshTrigger) - - Button(action: { - libraryManager.toggleBookmark( - title: title, - imageUrl: imageUrl, - href: href, - moduleId: module.id.uuidString, - moduleName: module.metadata.sourceName - ) - }) { - Image(systemName: libraryManager.isBookmarked(href: href, moduleName: module.metadata.sourceName) ? "bookmark.fill" : "bookmark") - .resizable() - .frame(width: 20, height: 27) - .foregroundColor(Color.accentColor) } } + } + + if !synopsis.isEmpty { + VStack(alignment: .leading, spacing: 2) { + HStack(alignment: .center) { + Text("Synopsis") + .font(.system(size: 18)) + .fontWeight(.bold) + + Spacer() + + Button(action: { + showFullSynopsis.toggle() + }) { + Text(showFullSynopsis ? "Less" : "More") + .font(.system(size: 14)) + } + } + + Text(synopsis) + .lineLimit(showFullSynopsis ? nil : 4) + .font(.system(size: 14)) + } + } + + HStack { + Button(action: { + playFirstUnwatchedEpisode() + }) { + HStack { + Image(systemName: "play.fill") + .foregroundColor(.primary) + Text(startWatchingText) + .font(.headline) + .foregroundColor(.primary) + } + .padding() + .frame(maxWidth: .infinity) + .background(Color.accentColor) + .cornerRadius(10) + } + .disabled(isFetchingEpisode) + .id(buttonRefreshTrigger) - if !episodeLinks.isEmpty { - VStack(alignment: .leading, spacing: 10) { - HStack { - Text("Episodes") - .font(.system(size: 18)) - .fontWeight(.bold) - - Spacer() - - if !isGroupedBySeasons, episodeLinks.count > episodeChunkSize { + Button(action: { + libraryManager.toggleBookmark( + title: title, + imageUrl: imageUrl, + href: href, + moduleId: module.id.uuidString, + moduleName: module.metadata.sourceName + ) + }) { + Image(systemName: libraryManager.isBookmarked(href: href, moduleName: module.metadata.sourceName) ? "bookmark.fill" : "bookmark") + .resizable() + .frame(width: 20, height: 27) + .foregroundColor(Color.accentColor) + } + } + + if !episodeLinks.isEmpty { + VStack(alignment: .leading, spacing: 10) { + HStack { + Text("Episodes") + .font(.system(size: 18)) + .fontWeight(.bold) + + Spacer() + + if !isGroupedBySeasons, episodeLinks.count > episodeChunkSize { + Menu { + ForEach(generateRanges(), id: \.self) { range in + Button(action: { selectedRange = range }) { + Text("\(range.lowerBound + 1)-\(range.upperBound)") + } + } + } label: { + Text("\(selectedRange.lowerBound + 1)-\(selectedRange.upperBound)") + .font(.system(size: 14)) + .foregroundColor(.accentColor) + } + } else if isGroupedBySeasons { + let seasons = groupedEpisodes() + if seasons.count > 1 { Menu { - ForEach(generateRanges(), id: \.self) { range in - Button(action: { selectedRange = range }) { - Text("\(range.lowerBound + 1)-\(range.upperBound)") + ForEach(0.. 1 { - Menu { - ForEach(0.. 0 ? lastPlayedTime / totalTime : 0 - - EpisodeCell( - episodeIndex: selectedSeason, - episode: ep.href, - episodeID: ep.number - 1, - progress: progress, - itemID: itemID ?? 0, - onTap: { imageUrl in - if !isFetchingEpisode { - selectedEpisodeNumber = ep.number - selectedEpisodeImage = imageUrl - fetchStream(href: ep.href) - AnalyticsManager.shared.sendEvent( - event: "watch", - additionalData: ["title": title, "episode": ep.number] - ) - } - }, - onMarkAllPrevious: { - let userDefaults = UserDefaults.standard - var updates = [String: Double]() - - for ep2 in seasons[selectedSeason] where ep2.number < ep.number { - let href = ep2.href - updates["lastPlayedTime_\(href)"] = 99999999.0 - updates["totalTime_\(href)"] = 99999999.0 - } - - for (key, value) in updates { - userDefaults.set(value, forKey: key) - } - - userDefaults.synchronize() - - refreshTrigger.toggle() - Logger.shared.log("Marked episodes watched within season \(selectedSeason + 1) of \"\(title)\".", type: "General") - } - ) - .id(refreshTrigger) - .disabled(isFetchingEpisode) - } - } else { - Text("No episodes available") - } - } else { - ForEach(episodeLinks.indices.filter { selectedRange.contains($0) }, id: \.self) { i in - let ep = episodeLinks[i] + } + if isGroupedBySeasons { + let seasons = groupedEpisodes() + if !seasons.isEmpty, selectedSeason < seasons.count { + ForEach(seasons[selectedSeason]) { ep in let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)") let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)") let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0 EpisodeCell( - episodeIndex: i, + episodeIndex: selectedSeason, episode: ep.href, episodeID: ep.number - 1, progress: progress, @@ -365,150 +315,163 @@ struct MediaInfoView: View { let userDefaults = UserDefaults.standard var updates = [String: Double]() - for idx in 0.. 0 ? lastPlayedTime / totalTime : 0 + + EpisodeCell( + episodeIndex: i, + episode: ep.href, + episodeID: ep.number - 1, + progress: progress, + itemID: itemID ?? 0, + onTap: { imageUrl in + if !isFetchingEpisode { + selectedEpisodeNumber = ep.number + selectedEpisodeImage = imageUrl + fetchStream(href: ep.href) + AnalyticsManager.shared.sendEvent( + event: "watch", + additionalData: ["title": title, "episode": ep.number] + ) + } + }, + onMarkAllPrevious: { + let userDefaults = UserDefaults.standard + var updates = [String: Double]() + + for idx in 0.. Void = { result in guard self.activeFetchID == fetchID else { return } - if let streams = result.sources, !streams.isEmpty{ - if streams.count > 1 { - self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) - } else { - self.playStream(url: streams[0]["streamUrl"] as? String ?? "", fullURL: href, subtitles: streams[0]["subtitle"] as? String ?? "",headers: streams[0]["headers"] as! [String : String]) + self.showLoadingAlert = false + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + if let streams = result.sources, !streams.isEmpty { + if streams.count > 1 { + self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) + } else { + self.playStream(url: streams[0]["streamUrl"] as? String ?? "", fullURL: href, subtitles: streams[0]["subtitle"] as? String ?? "", headers: streams[0]["headers"] as! [String : String]) + } } - } - else if let streams = result.streams, !streams.isEmpty { - if streams.count > 1 { - self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) + else if let streams = result.streams, !streams.isEmpty { + if streams.count > 1 { + self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) + } else { + self.playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first) + } } else { - self.playStream(url: streams[0], fullURL: href, subtitles: result.subtitles?.first) + self.handleStreamFailure(error: nil) + } + + DispatchQueue.main.async { + self.isFetchingEpisode = false } - } else { - self.handleStreamFailure(error: nil) - } - DispatchQueue.main.async { - self.isFetchingEpisode = false } } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { @@ -690,7 +658,7 @@ struct MediaInfoView: View { func handleStreamFailure(error: Error? = nil) { self.isFetchingEpisode = false - self.showStreamLoadingView = false + self.showLoadingAlert = false if let error = error { Logger.shared.log("Error loading module: \(error)", type: "Error") AnalyticsManager.shared.sendEvent(event: "error", additionalData: ["error": error, "message": "Failed to fetch stream"]) @@ -703,7 +671,7 @@ struct MediaInfoView: View { func showStreamSelectionAlert(streams: [Any], fullURL: String, subtitles: String? = nil) { self.isFetchingEpisode = false - self.showStreamLoadingView = false + self.showLoadingAlert = false print("MULTIPLE STREAMS \(streams)") DispatchQueue.main.async { let alert = UIAlertController(title: "Select Server", message: "Choose a server to play from", preferredStyle: .actionSheet) @@ -785,10 +753,11 @@ struct MediaInfoView: View { } } - func playStream(url: String, fullURL: String, subtitles: String? = nil,headers: [String:String]? = nil) { + func playStream(url: String, fullURL: String, subtitles: String? = nil, headers: [String:String]? = nil) { self.isFetchingEpisode = false - self.showStreamLoadingView = false - DispatchQueue.main.async { + self.showLoadingAlert = false + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { let externalPlayer = UserDefaults.standard.string(forKey: "externalPlayer") ?? "Sora" var scheme: String? @@ -802,7 +771,9 @@ struct MediaInfoView: View { case "nPlayer": scheme = "nplayer-\(url)" case "SenPlayer": - scheme = "SenPlayer://x-callback-url/play?url=\(url)" + scheme = "senplayer://x-callback-url/play?url=\(url)" + case "IINA": + scheme = "iina://weblink?url=\(url)" case "Default": let videoPlayerViewController = VideoPlayerViewController(module: module) videoPlayerViewController.headers = headers @@ -835,7 +806,6 @@ struct MediaInfoView: View { } let customMediaPlayer = CustomMediaPlayerViewController( - module: module, urlString: url.absoluteString, fullUrl: fullURL, diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift index 28b6d77..7bce329 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift @@ -19,16 +19,27 @@ struct SettingsViewPlayer: View { @AppStorage("doubleTapSeekEnabled") private var doubleTapSeekEnabled: Bool = false @AppStorage("skipIntroOutroVisible") private var skipIntroOutroVisible: Bool = true - private let mediaPlayers = ["Default", "VLC", "OutPlayer", "Infuse", "nPlayer", "SenPlayer", "Sora"] + private let mediaPlayers = ["Default", "Sora", "VLC", "OutPlayer", "Infuse", "nPlayer", "SenPlayer", "IINA"] var body: some View { Form { - Section(header: Text("Media Player"), footer: Text("Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.")) { - HStack { - Text("Media Player") - Spacer() - Menu(externalPlayer) { - ForEach(mediaPlayers, id: \.self) { player in + Section(header: Text("Media Player"), footer: Text("Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.")) { + HStack { + Text("Media Player") + Spacer() + Menu(externalPlayer) { + Menu("In-App Players") { + ForEach(mediaPlayers.prefix(2), id: \.self) { player in + Button(action: { + externalPlayer = player + }) { + Text(player) + } + } + } + + Menu("External Players") { + ForEach(mediaPlayers.dropFirst(2), id: \.self) { player in Button(action: { externalPlayer = player }) { @@ -37,6 +48,7 @@ struct SettingsViewPlayer: View { } } } + } Toggle("Force Landscape", isOn: $isAlwaysLandscape) .tint(.accentColor)