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)