diff --git a/Sora/Info.plist b/Sora/Info.plist index b085ddf..df1d517 100644 --- a/Sora/Info.plist +++ b/Sora/Info.plist @@ -2,8 +2,6 @@ - NSCameraUsageDescription - Sora may requires access to your device's camera. BGTaskSchedulerPermittedIdentifiers $(PRODUCT_BUNDLE_IDENTIFIER) diff --git a/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift b/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift index 553a7e7..eda6f2a 100644 --- a/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift +++ b/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift @@ -11,13 +11,11 @@ extension JSContext { func setupConsoleLogging() { let consoleObject = JSValue(newObjectIn: self) - // Set up console.log let consoleLogFunction: @convention(block) (String) -> Void = { message in Logger.shared.log(message, type: "Debug") } consoleObject?.setObject(consoleLogFunction, forKeyedSubscript: "log" as NSString) - // Set up console.error let consoleErrorFunction: @convention(block) (String) -> Void = { message in Logger.shared.log(message, type: "Error") } @@ -25,7 +23,6 @@ extension JSContext { self.setObject(consoleObject, forKeyedSubscript: "console" as NSString) - // Global log function let logFunction: @convention(block) (String) -> Void = { message in Logger.shared.log("JavaScript log: \(message)", type: "Debug") } @@ -45,7 +42,7 @@ extension JSContext { request.setValue(value, forHTTPHeaderField: key) } } - let task = URLSession.cloudflareCustom.dataTask(with: request) { data, _, error in + let task = URLSession.custom.dataTask(with: request) { data, _, error in if let error = error { Logger.shared.log("Network error in fetchNativeFunction: \(error.localizedDescription)", type: "Error") reject.call(withArguments: [error.localizedDescription]) @@ -78,70 +75,127 @@ extension JSContext { } func setupFetchV2() { - let fetchV2NativeFunction: @convention(block) (String, [String: String]?, JSValue, JSValue) -> Void = { urlString, headers, resolve, reject in + let fetchV2NativeFunction: @convention(block) (String, [String: String]?, String?, String?, JSValue, JSValue) -> Void = { urlString, headers, method, body, resolve, reject in guard let url = URL(string: urlString) else { Logger.shared.log("Invalid URL", type: "Error") reject.call(withArguments: ["Invalid URL"]) return } + + let httpMethod = method ?? "GET" var request = URLRequest(url: url) + request.httpMethod = httpMethod + + Logger.shared.log("FetchV2 Request: URL=\(url), Method=\(httpMethod), Body=\(body ?? "nil")", type: "Debug") + + if httpMethod == "GET", let body = body, !body.isEmpty, body != "null", body != "undefined" { + Logger.shared.log("GET request must not have a body", type: "Error") + reject.call(withArguments: ["GET request must not have a body"]) + return + } + + if httpMethod != "GET", let body = body, !body.isEmpty, body != "null", body != "undefined" { + request.httpBody = body.data(using: .utf8) + } + + if let headers = headers { for (key, value) in headers { request.setValue(value, forHTTPHeaderField: key) } } - let task = URLSession.cloudflareCustom.dataTask(with: request) { data, response, error in + + let task = URLSession.custom.downloadTask(with: request) { tempFileURL, response, error in if let error = error { Logger.shared.log("Network error in fetchV2NativeFunction: \(error.localizedDescription)", type: "Error") reject.call(withArguments: [error.localizedDescription]) return } - guard let data = data else { + + guard let tempFileURL = tempFileURL else { Logger.shared.log("No data in response", type: "Error") reject.call(withArguments: ["No data"]) return } - // Just pass the raw data string and let JavaScript handle it - if let text = String(data: data, encoding: .utf8) { - resolve.call(withArguments: [text]) - } else { - Logger.shared.log("Unable to decode data to text", type: "Error") - reject.call(withArguments: ["Unable to decode data"]) + do { + let data = try Data(contentsOf: tempFileURL) + + if data.count > 10_000_000 { + Logger.shared.log("Response exceeds maximum size", type: "Error") + reject.call(withArguments: ["Response exceeds maximum size"]) + return + } + + if let text = String(data: data, encoding: .utf8) { + resolve.call(withArguments: [text]) + } else { + Logger.shared.log("Unable to decode data to text", type: "Error") + reject.call(withArguments: ["Unable to decode data"]) + } + + } catch { + Logger.shared.log("Error reading downloaded file: \(error.localizedDescription)", type: "Error") + reject.call(withArguments: ["Error reading downloaded file"]) } } task.resume() } + + self.setObject(fetchV2NativeFunction, forKeyedSubscript: "fetchV2Native" as NSString) - // Simpler fetchv2 implementation with text() and json() methods let fetchv2Definition = """ - function fetchv2(url, headers) { - return new Promise(function(resolve, reject) { - fetchV2Native(url, headers, function(rawText) { - const responseObj = { - _data: rawText, - text: function() { - return Promise.resolve(this._data); - }, - json: function() { - try { - return Promise.resolve(JSON.parse(this._data)); - } catch (e) { - return Promise.reject("JSON parse error: " + e.message); - } - } - }; - resolve(responseObj); - }, reject); - }); - } - """ + function fetchv2(url, headers = {}, method = "GET", body = null) { + if (method === "GET") { + return new Promise(function(resolve, reject) { + fetchV2Native(url, headers, method, null, function(rawText) { // Pass `null` explicitly + const responseObj = { + _data: rawText, + text: function() { + return Promise.resolve(this._data); + }, + json: function() { + try { + return Promise.resolve(JSON.parse(this._data)); + } catch (e) { + return Promise.reject("JSON parse error: " + e.message); + } + } + }; + resolve(responseObj); + }, reject); + }); + } + + // Ensure body is properly serialized + const processedBody = body ? JSON.stringify(body) : null; + + return new Promise(function(resolve, reject) { + fetchV2Native(url, headers, method, processedBody, function(rawText) { + const responseObj = { + _data: rawText, + text: function() { + return Promise.resolve(this._data); + }, + json: function() { + try { + return Promise.resolve(JSON.parse(this._data)); + } catch (e) { + return Promise.reject("JSON parse error: " + e.message); + } + } + }; + resolve(responseObj); + }, reject); + }); + } + + """ self.evaluateScript(fetchv2Definition) } func setupBase64Functions() { - // btoa function: converts binary string to base64-encoded ASCII string let btoaFunction: @convention(block) (String) -> String? = { data in guard let data = data.data(using: .utf8) else { Logger.shared.log("btoa: Failed to encode input as UTF-8", type: "Error") @@ -150,7 +204,6 @@ extension JSContext { return data.base64EncodedString() } - // atob function: decodes base64-encoded ASCII string to binary string let atobFunction: @convention(block) (String) -> String? = { base64String in guard let data = Data(base64Encoded: base64String) else { Logger.shared.log("atob: Invalid base64 input", type: "Error") @@ -160,12 +213,10 @@ extension JSContext { return String(data: data, encoding: .utf8) } - // Add the functions to the JavaScript context self.setObject(btoaFunction, forKeyedSubscript: "btoa" as NSString) self.setObject(atobFunction, forKeyedSubscript: "atob" as NSString) } - // Helper method to set up all JavaScript functionality func setupJavaScriptEnvironment() { setupConsoleLogging() setupNativeFetch() diff --git a/Sora/Utils/Extensions/URLSession.swift b/Sora/Utils/Extensions/URLSession.swift index 9144c9f..544b084 100644 --- a/Sora/Utils/Extensions/URLSession.swift +++ b/Sora/Utils/Extensions/URLSession.swift @@ -6,60 +6,41 @@ // import Foundation -import Network extension URLSession { static let userAgents = [ - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.2365.92", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.2277.128", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_3_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.3; rv:123.0) Gecko/20100101 Firefox/123.0", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.2; rv:122.0) Gecko/20100101 Firefox/122.0", - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36", - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", - "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0", - "Mozilla/5.0 (X11; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0", - "Mozilla/5.0 (Linux; Android 14; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.105 Mobile Safari/537.36", - "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.105 Mobile Safari/537.36", - "Mozilla/5.0 (iPhone; CPU iPhone OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1", - "Mozilla/5.0 (iPad; CPU OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1", - "Mozilla/5.0 (Android 14; Mobile; rv:123.0) Gecko/123.0 Firefox/123.0", - "Mozilla/5.0 (Android 13; Mobile; rv:122.0) Gecko/122.0 Firefox/122.0" + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.2569.45", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.2478.89", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 15_1_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/19.1 Safari/605.1.15", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 15_0_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/19.0 Safari/605.1.15", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 15.1; rv:128.0) Gecko/20100101 Firefox/128.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 15.0; rv:127.0) Gecko/20100101 Firefox/127.0", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0", + "Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0", + "Mozilla/5.0 (Linux; Android 15; SM-S928B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6782.112 Mobile Safari/537.36", + "Mozilla/5.0 (Linux; Android 15; Pixel 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6782.112 Mobile Safari/537.36", + "Mozilla/5.0 (iPhone; CPU iPhone OS 18_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/19.3 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (iPad; CPU OS 18_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/19.3 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (Android 15; Mobile; rv:128.0) Gecko/128.0 Firefox/128.0", + "Mozilla/5.0 (Android 15; Mobile; rv:127.0) Gecko/127.0 Firefox/127.0" ] - static let randomUserAgent: String = { + static var randomUserAgent: String = { userAgents.randomElement() ?? userAgents[0] }() static let custom: URLSession = { let configuration = URLSessionConfiguration.default - configuration.httpAdditionalHeaders = [ - "User-Agent": randomUserAgent - ] - return URLSession(configuration: configuration) - }() - - static let cloudflareCustom: URLSession = { - let configuration = URLSessionConfiguration.default - configuration.httpAdditionalHeaders = [ - "User-Agent": randomUserAgent - ] - - let dnsSettings: [AnyHashable: Any] = [ - "DNSSettings": [ - "ServerAddresses": ["1.1.1.1", "1.0.0.1"] - ] - ] - - configuration.connectionProxyDictionary = dnsSettings + configuration.httpAdditionalHeaders = ["User-Agent": randomUserAgent] return URLSession(configuration: configuration) }() } diff --git a/Sora/Utils/JSLoader/JSController.swift b/Sora/Utils/JSLoader/JSController.swift index 6cf5e82..a0a220b 100644 --- a/Sora/Utils/JSLoader/JSController.swift +++ b/Sora/Utils/JSLoader/JSController.swift @@ -36,7 +36,7 @@ class JSController: ObservableObject { return } - URLSession.cloudflareCustom.dataTask(with: url) { [weak self] data, _, error in + URLSession.custom.dataTask(with: url) { [weak self] data, _, error in guard let self = self else { return } if let error = error { @@ -77,7 +77,7 @@ class JSController: ObservableObject { return } - URLSession.cloudflareCustom.dataTask(with: url) { [weak self] data, _, error in + URLSession.custom.dataTask(with: url) { [weak self] data, _, error in guard let self = self else { return } if let error = error { @@ -130,7 +130,7 @@ class JSController: ObservableObject { return } - URLSession.cloudflareCustom.dataTask(with: url) { [weak self] data, _, error in + URLSession.custom.dataTask(with: url) { [weak self] data, _, error in guard let self = self else { return } if let error = error { @@ -430,7 +430,7 @@ class JSController: ObservableObject { func fetchStreamUrlJSSecond(episodeUrl: String, softsub: Bool = false, completion: @escaping ((stream: String?, subtitles: String?)) -> Void) { let url = URL(string: episodeUrl)! - let task = URLSession.cloudflareCustom.dataTask(with: url) { data, response, error in + let task = URLSession.custom.dataTask(with: url) { data, response, error in if let error = error { Logger.shared.log("URLSession error: \(error.localizedDescription)", type: "Error") DispatchQueue.main.async { completion((nil, nil)) } diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index 1acb788..f6e1f71 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -709,20 +709,16 @@ class CustomMediaPlayerViewController: UIViewController { ) } - // Watch Next Button Logic: - let hideNext = UserDefaults.standard.bool(forKey: "hideNextButton") let isNearEnd = (self.duration - self.currentTimeVal) <= (self.duration * 0.10) && self.currentTimeVal != self.duration && self.showWatchNextButton && self.duration != 0 if isNearEnd { - // First appearance: show the button in its normal position. if !self.isWatchNextVisible { self.isWatchNextVisible = true self.watchNextButtonAppearedAt = self.currentTimeVal - // Choose constraints based on current controls visibility. if self.isControlsVisible { NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) @@ -730,7 +726,6 @@ class CustomMediaPlayerViewController: UIViewController { NSLayoutConstraint.deactivate(self.watchNextButtonControlsConstraints) NSLayoutConstraint.activate(self.watchNextButtonNormalConstraints) } - // Soft fade-in. self.watchNextButton.isHidden = false self.watchNextButton.alpha = 0.0 UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: { @@ -738,23 +733,19 @@ class CustomMediaPlayerViewController: UIViewController { }, completion: nil) } - // When 5 seconds have elapsed from when the button first appeared: if let appearedAt = self.watchNextButtonAppearedAt, (self.currentTimeVal - appearedAt) >= 5, !self.isWatchNextRepositioned { - // Fade out the button first (even if controls are visible). UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: { self.watchNextButton.alpha = 0.0 }, completion: { _ in self.watchNextButton.isHidden = true - // Then lock it to the controls-attached constraints. NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) self.isWatchNextRepositioned = true }) } } else { - // Not near end: reset the watch-next button state. self.watchNextButtonAppearedAt = nil self.isWatchNextVisible = false self.isWatchNextRepositioned = false @@ -771,7 +762,6 @@ class CustomMediaPlayerViewController: UIViewController { func repositionWatchNextButton() { self.isWatchNextRepositioned = true - // Update constraints so the button is now attached next to the playback controls. UIView.animate(withDuration: 0.3, animations: { NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) @@ -799,7 +789,6 @@ class CustomMediaPlayerViewController: UIViewController { self.skip85Button.alpha = self.isControlsVisible ? 0.8 : 0 if self.isControlsVisible { - // Always use the controls-attached constraints. NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints) NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints) if self.isWatchNextRepositioned || self.isWatchNextVisible { @@ -809,7 +798,6 @@ class CustomMediaPlayerViewController: UIViewController { }) } } else { - // When controls are hidden: if !self.isWatchNextRepositioned && self.isWatchNextVisible { NSLayoutConstraint.deactivate(self.watchNextButtonControlsConstraints) NSLayoutConstraint.activate(self.watchNextButtonNormalConstraints) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Helpers/VTTSubtitlesLoader.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Helpers/VTTSubtitlesLoader.swift index 944f2de..d457d7a 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/Helpers/VTTSubtitlesLoader.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/Helpers/VTTSubtitlesLoader.swift @@ -29,7 +29,7 @@ class VTTSubtitlesLoader: ObservableObject { let format = determineSubtitleFormat(from: url) - URLSession.cloudflareCustom.dataTask(with: url) { data, _, error in + URLSession.shared.dataTask(with: url) { data, _, error in guard let data = data, let content = String(data: data, encoding: .utf8), error == nil else { return } diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 3db0025..6e1af48 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -296,7 +296,7 @@ struct MediaInfoView: View { UserDefaults.standard.set(99999999.0, forKey: "totalTime_\(href)") } refreshTrigger.toggle() - Logger.shared.log("Marked \(ep.number - 1) episodes watched within anime \"\(title)\".", type: "General") + Logger.shared.log("Marked \(ep.number - 1) episodes watched within series \"\(title)\".", type: "General") } ) .id(refreshTrigger) @@ -626,9 +626,15 @@ struct MediaInfoView: View { UIApplication.shared.open(url, options: [:], completionHandler: nil) Logger.shared.log("Opening external app with scheme: \(url)", type: "General") } else { + guard let url = URL(string: url) else { + Logger.shared.log("Invalid stream URL: \(url)", type: "Error") + DropManager.shared.showDrop(title: "Error", subtitle: "Invalid stream URL", duration: 2.0, icon: UIImage(systemName: "xmark.circle")) + return + } + let customMediaPlayer = CustomMediaPlayerViewController( module: module, - urlString: url, + urlString: url.absoluteString, fullUrl: fullURL, title: title, episodeNumber: selectedEpisodeNumber, @@ -644,6 +650,9 @@ struct MediaInfoView: View { if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let rootVC = windowScene.windows.first?.rootViewController { findTopViewController.findViewController(rootVC).present(customMediaPlayer, animated: true, completion: nil) + } else { + Logger.shared.log("Failed to find root view controller", type: "Error") + DropManager.shared.showDrop(title: "Error", subtitle: "Failed to present player", duration: 2.0, icon: UIImage(systemName: "xmark.circle")) } } } diff --git a/Sora/Views/SearchView.swift b/Sora/Views/SearchView.swift index 50d5528..cfd739d 100644 --- a/Sora/Views/SearchView.swift +++ b/Sora/Views/SearchView.swift @@ -165,20 +165,24 @@ struct SearchView: View { .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Menu { - ForEach(moduleManager.modules, 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) + ForEach(getModuleLanguageGroups(), id: \.self) { language in + Menu(language) { + 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) + } + } } } } @@ -269,6 +273,41 @@ struct SearchView: View { return verticalSizeClass == .compact ? mediaColumnsLandscape : mediaColumnsPortrait } } + + private func cleanLanguageName(_ language: String?) -> String { + guard let language = language else { return "Unknown" } + + let cleaned = language.replacingOccurrences( + of: "\\s*\\([^\\)]*\\)", + with: "", + options: .regularExpression + ).trimmingCharacters(in: .whitespaces) + + return cleaned.isEmpty ? "Unknown" : cleaned + } + + private func getModulesByLanguage() -> [String: [ScrapingModule]] { + var result = [String: [ScrapingModule]]() + + for module in moduleManager.modules { + let language = cleanLanguageName(module.metadata.language) + if result[language] == nil { + result[language] = [module] + } else { + result[language]?.append(module) + } + } + + return result + } + + private func getModuleLanguageGroups() -> [String] { + return getModulesByLanguage().keys.sorted() + } + + private func getModulesForLanguage(_ language: String) -> [ScrapingModule] { + return getModulesByLanguage()[language] ?? [] + } } struct SearchBar: View { diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index ed0f83e..154d1d2 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -14,9 +14,13 @@ struct SettingsViewGeneral: View { @AppStorage("analyticsEnabled") private var analyticsEnabled: Bool = false @AppStorage("multiThreads") private var multiThreadsEnabled: Bool = false @AppStorage("metadataProviders") private var metadataProviders: String = "AniList" + @AppStorage("CustomDNSProvider") private var customDNSProvider: String = "Cloudflare" + @AppStorage("customPrimaryDNS") private var customPrimaryDNS: String = "" + @AppStorage("customSecondaryDNS") private var customSecondaryDNS: String = "" @AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2 @AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4 + private let customDNSProviderList = ["Cloudflare", "Google", "OpenDNS", "Quad9", "AdGuard", "CleanBrowsing", "ControlD", "Custom"] private let metadataProvidersList = ["AniList"] @EnvironmentObject var settings: Settings @@ -24,7 +28,7 @@ struct SettingsViewGeneral: View { Form { Section(header: Text("Interface")) { ColorPicker("Accent Color", selection: $settings.accentColor) - HStack() { + HStack { Text("Appearance") Picker("Appearance", selection: $settings.selectedAppearance) { Text("System").tag(Appearance.system) @@ -40,18 +44,10 @@ struct SettingsViewGeneral: View { Text("Episodes Range") Spacer() Menu { - Button(action: { episodeChunkSize = 25 }) { - Text("25") - } - Button(action: { episodeChunkSize = 50 }) { - Text("50") - } - Button(action: { episodeChunkSize = 75 }) { - Text("75") - } - Button(action: { episodeChunkSize = 100 }) { - Text("100") - } + Button(action: { episodeChunkSize = 25 }) { Text("25") } + Button(action: { episodeChunkSize = 50 }) { Text("50") } + Button(action: { episodeChunkSize = 75 }) { Text("75") } + Button(action: { episodeChunkSize = 100 }) { Text("100") } } label: { Text("\(episodeChunkSize)") } @@ -63,36 +59,24 @@ struct SettingsViewGeneral: View { Spacer() Menu(metadataProviders) { ForEach(metadataProvidersList, id: \.self) { provider in - Button(action: { - metadataProviders = provider - }) { + Button(action: { metadataProviders = provider }) { Text(provider) } } } - } } - //Section(header: Text("Downloads"), footer: Text("Note that the modules will be replaced only if there is a different version string inside the JSON file.")) { - // Toggle("Multi Threads conversion", isOn: $multiThreadsEnabled) - // .tint(.accentColor) - //} - Section(header: Text("Media Grid Layout"), footer: Text("Adjust the number of media items per row in portrait and landscape modes.")) { HStack { if UIDevice.current.userInterfaceIdiom == .pad { Picker("Portrait Columns", selection: $mediaColumnsPortrait) { - ForEach(1..<6) { i in - Text("\(i)").tag(i) - } + ForEach(1..<6) { i in Text("\(i)").tag(i) } } .pickerStyle(MenuPickerStyle()) } else { Picker("Portrait Columns", selection: $mediaColumnsPortrait) { - ForEach(1..<5) { i in - Text("\(i)").tag(i) - } + ForEach(1..<5) { i in Text("\(i)").tag(i) } } .pickerStyle(MenuPickerStyle()) } @@ -100,16 +84,12 @@ struct SettingsViewGeneral: View { HStack { if UIDevice.current.userInterfaceIdiom == .pad { Picker("Landscape Columns", selection: $mediaColumnsLandscape) { - ForEach(2..<9) { i in - Text("\(i)").tag(i) - } + ForEach(2..<9) { i in Text("\(i)").tag(i) } } .pickerStyle(MenuPickerStyle()) } else { Picker("Landscape Columns", selection: $mediaColumnsLandscape) { - ForEach(2..<6) { i in - Text("\(i)").tag(i) - } + ForEach(2..<6) { i in Text("\(i)").tag(i) } } .pickerStyle(MenuPickerStyle()) } @@ -121,7 +101,7 @@ struct SettingsViewGeneral: View { .tint(.accentColor) } - Section(header: Text("Analytics"), footer: Text("Allow Sora to collect anonymous data to improve the app. No personal information is collected. This can be disabled at any time.\n\n Information collected: \n- App version\n- Device model\n- Module Name/Version\n- Error Messages\n- Title of Watched Content")) { + Section(header: Text("Advanced"), footer: Text("Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time.")) { Toggle("Enable Analytics", isOn: $analyticsEnabled) .tint(.accentColor) } diff --git a/Sora/Views/SettingsView/SettingsView.swift b/Sora/Views/SettingsView/SettingsView.swift index f7f6ec0..eacb628 100644 --- a/Sora/Views/SettingsView/SettingsView.swift +++ b/Sora/Views/SettingsView/SettingsView.swift @@ -11,9 +11,9 @@ struct SettingsView: View { var body: some View { NavigationView { Form { - Section(header: Text("Main Settings")) { + Section(header: Text("Main")) { NavigationLink(destination: SettingsViewGeneral()) { - Text("General Settings") + Text("General Preferences") } NavigationLink(destination: SettingsViewPlayer()) { Text("Media Player") @@ -21,9 +21,9 @@ struct SettingsView: View { NavigationLink(destination: SettingsViewModule()) { Text("Modules") } - NavigationLink(destination: SettingsViewTrackers()) { - Text("Trackers") - } + //NavigationLink(destination: SettingsViewTrackers()) { + // Text("Trackers") + //} } Section(header: Text("Info")) { diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index e72ea8f..ad75420 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 130217CC2D81C55E0011EFF5 /* DownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130217CB2D81C55E0011EFF5 /* DownloadView.swift */; }; 130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */; }; 13103E892D58A39A000F0673 /* AniListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E882D58A39A000F0673 /* AniListItem.swift */; }; 13103E8B2D58E028000F0673 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E8A2D58E028000F0673 /* View.swift */; }; @@ -14,6 +15,9 @@ 131845F92D47C62D00CA7A54 /* SettingsViewGeneral.swift in Sources */ = {isa = PBXBuildFile; fileRef = 131845F82D47C62D00CA7A54 /* SettingsViewGeneral.swift */; }; 1327FBA72D758CEA00FC6689 /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1327FBA62D758CEA00FC6689 /* Analytics.swift */; }; 1327FBA92D758DEA00FC6689 /* UIDevice+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1327FBA82D758DEA00FC6689 /* UIDevice+Model.swift */; }; + 132E351D2D959DDB0007800E /* Drops in Frameworks */ = {isa = PBXBuildFile; productRef = 132E351C2D959DDB0007800E /* Drops */; }; + 132E35202D959E1D0007800E /* FFmpeg-iOS-Lame in Frameworks */ = {isa = PBXBuildFile; productRef = 132E351F2D959E1D0007800E /* FFmpeg-iOS-Lame */; }; + 132E35232D959E410007800E /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 132E35222D959E410007800E /* Kingfisher */; }; 1334FF4D2D786C93007E289F /* TMDB-Seasonal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1334FF4C2D786C93007E289F /* TMDB-Seasonal.swift */; }; 1334FF4F2D786C9E007E289F /* TMDB-Trending.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1334FF4E2D786C9E007E289F /* TMDB-Trending.swift */; }; 1334FF522D7871B7007E289F /* TMDBItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1334FF512D7871B7007E289F /* TMDBItem.swift */; }; @@ -30,10 +34,8 @@ 133D7C922D2BE2640075467E /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C872D2BE2640075467E /* URLSession.swift */; }; 133D7C932D2BE2640075467E /* Modules.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C892D2BE2640075467E /* Modules.swift */; }; 133D7C942D2BE2640075467E /* JSController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C8B2D2BE2640075467E /* JSController.swift */; }; - 133D7C972D2BE2AF0075467E /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 133D7C962D2BE2AF0075467E /* Kingfisher */; }; 133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133F55BA2D33B55100E08EEA /* LibraryManager.swift */; }; 1359ED142D76F49900C13034 /* finTopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1359ED132D76F49900C13034 /* finTopView.swift */; }; - 1359ED1A2D76FA7D00C13034 /* Drops in Frameworks */ = {isa = PBXBuildFile; productRef = 1359ED192D76FA7D00C13034 /* Drops */; }; 135CCBE22D4D1138008B9C0E /* SettingsViewPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */; }; 138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */; }; 138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */; }; @@ -52,6 +54,7 @@ 13DB46902D900A38008CBC03 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB468F2D900A38008CBC03 /* URL.swift */; }; 13DB46922D900BCE008CBC03 /* SettingsViewTrackers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB46912D900BCE008CBC03 /* SettingsViewTrackers.swift */; }; 13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */; }; + 13DB7CEC2D7DED5D004371D3 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */; }; 13DC0C462D302C7500D0F966 /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DC0C452D302C7500D0F966 /* VideoPlayer.swift */; }; 13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */; }; 13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */; }; @@ -62,6 +65,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 130217CB2D81C55E0011EFF5 /* DownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadView.swift; sourceTree = ""; }; 130C6BF82D53A4C200DC1432 /* Sora.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Sora.entitlements; sourceTree = ""; }; 130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewData.swift; sourceTree = ""; }; 13103E882D58A39A000F0673 /* AniListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AniListItem.swift; sourceTree = ""; }; @@ -107,6 +111,7 @@ 13DB468F2D900A38008CBC03 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; 13DB46912D900BCE008CBC03 /* SettingsViewTrackers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewTrackers.swift; sourceTree = ""; }; 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleSettingsManager.swift; sourceTree = ""; }; + 13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = ""; }; 13DC0C412D2EC9BA00D0F966 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 13DC0C452D302C7500D0F966 /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = ""; }; 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPlayer.swift; sourceTree = ""; }; @@ -122,8 +127,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 1359ED1A2D76FA7D00C13034 /* Drops in Frameworks */, - 133D7C972D2BE2AF0075467E /* Kingfisher in Frameworks */, + 132E35232D959E410007800E /* Kingfisher in Frameworks */, + 132E35202D959E1D0007800E /* FFmpeg-iOS-Lame in Frameworks */, + 132E351D2D959DDB0007800E /* Drops in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -247,6 +253,7 @@ 1399FAD22D3AB34F00E97C31 /* SettingsView */, 133F55B92D33B53E00E08EEA /* LibraryView */, 133D7C7C2D2BE2630075467E /* SearchView.swift */, + 130217CB2D81C55E0011EFF5 /* DownloadView.swift */, ); path = Views; sourceTree = ""; @@ -277,6 +284,7 @@ 133D7C852D2BE2640075467E /* Utils */ = { isa = PBXGroup; children = ( + 13DB7CEA2D7DED50004371D3 /* DownloadManager */, 13C0E5E82D5F85DD00E7F619 /* ContinueWatching */, 13103E8C2D58E037000F0673 /* SkeletonCells */, 13DC0C442D302C6A00D0F966 /* MediaPlayer */, @@ -392,6 +400,14 @@ path = Auth; sourceTree = ""; }; + 13DB7CEA2D7DED50004371D3 /* DownloadManager */ = { + isa = PBXGroup; + children = ( + 13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */, + ); + path = DownloadManager; + sourceTree = ""; + }; 13DC0C442D302C6A00D0F966 /* MediaPlayer */ = { isa = PBXGroup; children = ( @@ -438,8 +454,9 @@ ); name = Sulfur; packageProductDependencies = ( - 133D7C962D2BE2AF0075467E /* Kingfisher */, - 1359ED192D76FA7D00C13034 /* Drops */, + 132E351C2D959DDB0007800E /* Drops */, + 132E351F2D959E1D0007800E /* FFmpeg-iOS-Lame */, + 132E35222D959E410007800E /* Kingfisher */, ); productName = Sora; productReference = 133D7C6A2D2BE2500075467E /* Sulfur.app */; @@ -469,8 +486,9 @@ ); mainGroup = 133D7C612D2BE2500075467E; packageReferences = ( - 133D7C952D2BE2AF0075467E /* XCRemoteSwiftPackageReference "Kingfisher" */, - 1359ED182D76FA7D00C13034 /* XCRemoteSwiftPackageReference "Drops" */, + 132E351B2D959DDB0007800E /* XCRemoteSwiftPackageReference "Drops" */, + 132E351E2D959E1D0007800E /* XCRemoteSwiftPackageReference "FFmpeg-iOS-Lame" */, + 132E35212D959E410007800E /* XCRemoteSwiftPackageReference "Kingfisher" */, ); productRefGroup = 133D7C6B2D2BE2500075467E /* Products */; projectDirPath = ""; @@ -516,6 +534,7 @@ 133D7C932D2BE2640075467E /* Modules.swift in Sources */, 13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */, 133D7C702D2BE2500075467E /* ContentView.swift in Sources */, + 13DB7CEC2D7DED5D004371D3 /* DownloadManager.swift in Sources */, 1334FF522D7871B7007E289F /* TMDBItem.swift in Sources */, 13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */, 13C0E5EC2D5F85F800E7F619 /* ContinueWatchingItem.swift in Sources */, @@ -535,6 +554,7 @@ 133D7C942D2BE2640075467E /* JSController.swift in Sources */, 133D7C922D2BE2640075467E /* URLSession.swift in Sources */, 133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */, + 130217CC2D81C55E0011EFF5 /* DownloadView.swift in Sources */, 133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */, 133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */, 133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */, @@ -778,15 +798,7 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 133D7C952D2BE2AF0075467E /* XCRemoteSwiftPackageReference "Kingfisher" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/onevcat/Kingfisher.git"; - requirement = { - kind = exactVersion; - version = 7.9.1; - }; - }; - 1359ED182D76FA7D00C13034 /* XCRemoteSwiftPackageReference "Drops" */ = { + 132E351B2D959DDB0007800E /* XCRemoteSwiftPackageReference "Drops" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/omaralbeik/Drops.git"; requirement = { @@ -794,19 +806,40 @@ kind = branch; }; }; + 132E351E2D959E1D0007800E /* XCRemoteSwiftPackageReference "FFmpeg-iOS-Lame" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/kewlbear/FFmpeg-iOS-Lame"; + requirement = { + branch = main; + kind = branch; + }; + }; + 132E35212D959E410007800E /* XCRemoteSwiftPackageReference "Kingfisher" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/onevcat/Kingfisher.git"; + requirement = { + kind = exactVersion; + version = 7.9.1; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 133D7C962D2BE2AF0075467E /* Kingfisher */ = { + 132E351C2D959DDB0007800E /* Drops */ = { isa = XCSwiftPackageProductDependency; - package = 133D7C952D2BE2AF0075467E /* XCRemoteSwiftPackageReference "Kingfisher" */; - productName = Kingfisher; - }; - 1359ED192D76FA7D00C13034 /* Drops */ = { - isa = XCSwiftPackageProductDependency; - package = 1359ED182D76FA7D00C13034 /* XCRemoteSwiftPackageReference "Drops" */; + package = 132E351B2D959DDB0007800E /* XCRemoteSwiftPackageReference "Drops" */; productName = Drops; }; + 132E351F2D959E1D0007800E /* FFmpeg-iOS-Lame */ = { + isa = XCSwiftPackageProductDependency; + package = 132E351E2D959E1D0007800E /* XCRemoteSwiftPackageReference "FFmpeg-iOS-Lame" */; + productName = "FFmpeg-iOS-Lame"; + }; + 132E35222D959E410007800E /* Kingfisher */ = { + isa = XCSwiftPackageProductDependency; + package = 132E35212D959E410007800E /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = Kingfisher; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 133D7C622D2BE2500075467E /* Project object */;