From e175babc5eb334eb72a8fcd2fe0fc68671ea1189 Mon Sep 17 00:00:00 2001 From: Ibrahim Sulejmenov Date: Thu, 20 Mar 2025 01:03:39 +0100 Subject: [PATCH 01/41] Adding InvisibleControlOverlays --- .../CustomPlayer/CustomPlayer.swift | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index b9d00aa..45be44d 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -136,6 +136,7 @@ class CustomMediaPlayerViewController: UIViewController { loadSubtitleSettings() setupPlayerViewController() setupControls() + addInvisibleControlOverlays() setupSubtitleLabel() setupDismissButton() setupMenuButton() @@ -346,6 +347,45 @@ class CustomMediaPlayerViewController: UIViewController { ]) } + func addInvisibleControlOverlays() { + let playPauseOverlay = UIButton(type: .custom) + playPauseOverlay.backgroundColor = .clear + playPauseOverlay.addTarget(self, action: #selector(togglePlayPause), for: .touchUpInside) + view.addSubview(playPauseOverlay) + playPauseOverlay.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + playPauseOverlay.centerXAnchor.constraint(equalTo: playPauseButton.centerXAnchor), + playPauseOverlay.centerYAnchor.constraint(equalTo: playPauseButton.centerYAnchor), + playPauseOverlay.widthAnchor.constraint(equalTo: playPauseButton.widthAnchor, constant: 20), + playPauseOverlay.heightAnchor.constraint(equalTo: playPauseButton.heightAnchor, constant: 20) + ]) + + let backwardOverlay = UIButton(type: .custom) + backwardOverlay.backgroundColor = .clear + backwardOverlay.addTarget(self, action: #selector(seekBackward), for: .touchUpInside) + view.addSubview(backwardOverlay) + backwardOverlay.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + backwardOverlay.centerXAnchor.constraint(equalTo: backwardButton.centerXAnchor), + backwardOverlay.centerYAnchor.constraint(equalTo: backwardButton.centerYAnchor), + backwardOverlay.widthAnchor.constraint(equalTo: backwardButton.widthAnchor, constant: 20), + backwardOverlay.heightAnchor.constraint(equalTo: backwardButton.heightAnchor, constant: 20) + ]) + + let forwardOverlay = UIButton(type: .custom) + forwardOverlay.backgroundColor = .clear + forwardOverlay.addTarget(self, action: #selector(seekForward), for: .touchUpInside) + view.addSubview(forwardOverlay) + forwardOverlay.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + forwardOverlay.centerXAnchor.constraint(equalTo: forwardButton.centerXAnchor), + forwardOverlay.centerYAnchor.constraint(equalTo: forwardButton.centerYAnchor), + forwardOverlay.widthAnchor.constraint(equalTo: forwardButton.widthAnchor, constant: 20), + forwardOverlay.heightAnchor.constraint(equalTo: forwardButton.heightAnchor, constant: 20) + ]) + } + + func setupSubtitleLabel() { subtitleLabel = UILabel() subtitleLabel.textAlignment = .center @@ -735,6 +775,14 @@ class CustomMediaPlayerViewController: UIViewController { @objc func togglePlayPause() { if isPlaying { + if !isControlsVisible { + isControlsVisible = true + UIView.animate(withDuration: 0.5) { + self.controlsContainerView.alpha = 1.0 + self.skip85Button.alpha = 0.8 + self.view.layoutIfNeeded() + } + } player.pause() playPauseButton.image = UIImage(systemName: "play.fill") } else { From 09c43fa89605c49e205968724e96ae4a29f216f6 Mon Sep 17 00:00:00 2001 From: Ibrahim Sulejmenov Date: Thu, 20 Mar 2025 01:09:26 +0100 Subject: [PATCH 02/41] Fixed overlapping images in library view --- Sora/Views/LibraryView/LibraryView.swift | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/Sora/Views/LibraryView/LibraryView.swift b/Sora/Views/LibraryView/LibraryView.swift index 877f38b..ade2c46 100644 --- a/Sora/Views/LibraryView/LibraryView.swift +++ b/Sora/Views/LibraryView/LibraryView.swift @@ -24,6 +24,26 @@ struct LibraryView: View { GridItem(.adaptive(minimum: 150), spacing: 12) ] + private var columnsCount: Int { + 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 + } + } + + private var cellWidth: CGFloat { + let keyWindow = UIApplication.shared.connectedScenes + .compactMap { ($0 as? UIWindowScene)?.windows.first(where: { $0.isKeyWindow }) } + .first + let safeAreaInsets = keyWindow?.safeAreaInsets ?? .zero + let safeWidth = UIScreen.main.bounds.width - safeAreaInsets.left - safeAreaInsets.right + let totalSpacing: CGFloat = 16 * CGFloat(columnsCount + 1) + let availableWidth = safeWidth - totalSpacing + return availableWidth / CGFloat(columnsCount) + } + var body: some View { NavigationView { ScrollView { @@ -76,10 +96,6 @@ struct LibraryView: View { .frame(maxWidth: .infinity) } else { LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 12), count: columnsCount), spacing: 12) { - let totalSpacing: CGFloat = 16 * CGFloat(columnsCount + 1) - let availableWidth = UIScreen.main.bounds.width - totalSpacing - let cellWidth = availableWidth / CGFloat(columnsCount) - ForEach(libraryManager.bookmarks) { item in if let module = moduleManager.modules.first(where: { $0.id.uuidString == item.moduleId }) { NavigationLink(destination: MediaInfoView(title: item.title, imageUrl: item.imageUrl, href: item.href, module: module)) { From 1af56c719de7cf01ecaf877061e2c5a58dea03e1 Mon Sep 17 00:00:00 2001 From: Hamzenis Kryeziu Date: Thu, 20 Mar 2025 01:35:20 +0100 Subject: [PATCH 03/41] New JavaScriptCore Extension Class & more - Dedicated JavaScripteCore extension class - Seperated JS logic from JSController class - New available functions for js code: "atob()", "btoa()", "console.error()" - New fetch method "fetchv2()" that can handle ".json()" &".text()" and possible to be expanded for post method support and more - New Logger function to print in Debug mode to xcode console --- Sora/Info.plist | 2 - .../JavaScriptCore+Extensions.swift | 175 ++++++++++++++++++ Sora/Utils/JSLoader/JSController.swift | 55 +----- Sora/Utils/Logger/Logger.swift | 15 +- Sulfur.xcodeproj/project.pbxproj | 12 +- 5 files changed, 197 insertions(+), 62 deletions(-) create mode 100644 Sora/Utils/Extensions/JavaScriptCore+Extensions.swift diff --git a/Sora/Info.plist b/Sora/Info.plist index e942154..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 new file mode 100644 index 0000000..71e9f9d --- /dev/null +++ b/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift @@ -0,0 +1,175 @@ +// +// JSContext+Extensions.swift +// Sora +// +// Created by Hamzo on 19/03/25. +// + +import JavaScriptCore + +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") + } + consoleObject?.setObject(consoleErrorFunction, forKeyedSubscript: "error" as NSString) + + 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") + } + self.setObject(logFunction, forKeyedSubscript: "log" as NSString) + } + + func setupNativeFetch() { + let fetchNativeFunction: @convention(block) (String, [String: String]?, JSValue, JSValue) -> Void = { urlString, headers, resolve, reject in + guard let url = URL(string: urlString) else { + Logger.shared.log("Invalid URL", type: "Error") + reject.call(withArguments: ["Invalid URL"]) + return + } + var request = URLRequest(url: url) + if let headers = headers { + for (key, value) in headers { + request.setValue(value, forHTTPHeaderField: key) + } + } + 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]) + return + } + guard let data = data else { + Logger.shared.log("No data in response", type: "Error") + reject.call(withArguments: ["No data"]) + 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"]) + } + } + task.resume() + } + self.setObject(fetchNativeFunction, forKeyedSubscript: "fetchNative" as NSString) + + let fetchDefinition = """ + function fetch(url, headers) { + return new Promise(function(resolve, reject) { + fetchNative(url, headers, resolve, reject); + }); + } + """ + self.evaluateScript(fetchDefinition) + } + + func setupFetchV2() { + let fetchV2NativeFunction: @convention(block) (String, [String: String]?, JSValue, JSValue) -> Void = { urlString, headers, resolve, reject in + guard let url = URL(string: urlString) else { + Logger.shared.log("Invalid URL", type: "Error") + reject.call(withArguments: ["Invalid URL"]) + return + } + var request = URLRequest(url: url) + if let headers = headers { + for (key, value) in headers { + request.setValue(value, forHTTPHeaderField: key) + } + } + let task = URLSession.custom.dataTask(with: request) { data, 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 { + 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"]) + } + } + 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); + }); + } + """ + 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") + return nil + } + 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") + return nil + } + + 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() + setupFetchV2() + setupBase64Functions() + } +} diff --git a/Sora/Utils/JSLoader/JSController.swift b/Sora/Utils/JSLoader/JSController.swift index 9861b2a..b3a0f92 100644 --- a/Sora/Utils/JSLoader/JSController.swift +++ b/Sora/Utils/JSLoader/JSController.swift @@ -16,60 +16,7 @@ class JSController: ObservableObject { } private func setupContext() { - let consoleObject = JSValue(newObjectIn: context) - let consoleLogFunction: @convention(block) (String) -> Void = { message in - Logger.shared.log(message, type: "Debug") - } - consoleObject?.setObject(consoleLogFunction, forKeyedSubscript: "log" as NSString) - context.setObject(consoleObject, forKeyedSubscript: "console" as NSString) - - let logFunction: @convention(block) (String) -> Void = { message in - Logger.shared.log("JavaScript log: \(message)", type: "Debug") - } - context.setObject(logFunction, forKeyedSubscript: "log" as NSString) - - let fetchNativeFunction: @convention(block) (String, [String: String]?, JSValue, JSValue) -> Void = { urlString, headers, resolve, reject in - guard let url = URL(string: urlString) else { - Logger.shared.log("Invalid URL", type: "Error") - reject.call(withArguments: ["Invalid URL"]) - return - } - var request = URLRequest(url: url) - if let headers = headers { - for (key, value) in headers { - request.setValue(value, forHTTPHeaderField: key) - } - } - 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]) - return - } - guard let data = data else { - Logger.shared.log("No data in response", type: "Error") - reject.call(withArguments: ["No data"]) - 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"]) - } - } - task.resume() - } - context.setObject(fetchNativeFunction, forKeyedSubscript: "fetchNative" as NSString) - - let fetchDefinition = """ - function fetch(url, headers) { - return new Promise(function(resolve, reject) { - fetchNative(url, headers, resolve, reject); - }); - } - """ - context.evaluateScript(fetchDefinition) + context.setupJavaScriptEnvironment() } func loadScript(_ script: String) { diff --git a/Sora/Utils/Logger/Logger.swift b/Sora/Utils/Logger/Logger.swift index eedb87a..70ad048 100644 --- a/Sora/Utils/Logger/Logger.swift +++ b/Sora/Utils/Logger/Logger.swift @@ -31,6 +31,8 @@ class Logger { let entry = LogEntry(message: message, type: type, timestamp: Date()) logs.append(entry) saveLogToFile(entry) + + debugLog(entry) } @@ -38,7 +40,7 @@ class Logger { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "dd-MM HH:mm:ss" return logs.map { "[\(dateFormatter.string(from: $0.timestamp))] [\($0.type)] \($0.message)" } - .joined(separator: "\n----\n") + .joined(separator: "\n----\n") } func clearLogs() { @@ -64,4 +66,13 @@ class Logger { } } } -} + + /// Prints log messages to the Xcode console only in DEBUG mode + private func debugLog(_ entry: LogEntry) { + #if DEBUG + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "dd-MM HH:mm:ss" + let formattedMessage = "[\(dateFormatter.string(from: entry.timestamp))] [\(entry.type)] \(entry.message)" + print(formattedMessage) + #endif + }} diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 738eb75..7cf7cc7 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -61,6 +61,7 @@ 13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */; }; 1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */; }; 1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */; }; + 73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -118,6 +119,7 @@ 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = ""; }; 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewLoggerFilter.swift; sourceTree = ""; }; 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = ""; }; + 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JavaScriptCore+Extensions.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -309,6 +311,7 @@ 133D7C862D2BE2640075467E /* Extensions */ = { isa = PBXGroup; children = ( + 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */, 1327FBA82D758DEA00FC6689 /* UIDevice+Model.swift */, 133D7C872D2BE2640075467E /* URLSession.swift */, 1359ED132D76F49900C13034 /* finTopView.swift */, @@ -575,6 +578,7 @@ 133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */, 138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */, 1334FF4D2D786C93007E289F /* TMDB-Seasonal.swift in Sources */, + 73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */, 13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */, 1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */, 13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */, @@ -713,7 +717,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\""; - DEVELOPMENT_TEAM = 399LMK6Q2Y; + DEVELOPMENT_TEAM = 44V6G67299; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -733,7 +737,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.2.1; - PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur; + PRODUCT_BUNDLE_IDENTIFIER = me.hamzo.sulfur; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; @@ -755,7 +759,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\""; - DEVELOPMENT_TEAM = 399LMK6Q2Y; + DEVELOPMENT_TEAM = 44V6G67299; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -775,7 +779,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.2.1; - PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur; + PRODUCT_BUNDLE_IDENTIFIER = me.hamzo.sulfur; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; From d86cfe6d8a53a179688fe5bc8ce4c0c9e6cee305 Mon Sep 17 00:00:00 2001 From: Ibrahim Sulejmenov Date: Thu, 20 Mar 2025 08:13:56 +0100 Subject: [PATCH 04/41] Added double tap feature to skip + swipe down to dismiss player --- .../CustomPlayer/CustomPlayer.swift | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index 45be44d..cfafadc 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -136,6 +136,7 @@ class CustomMediaPlayerViewController: UIViewController { loadSubtitleSettings() setupPlayerViewController() setupControls() + setupSkipAndDismissGestures() addInvisibleControlOverlays() setupSubtitleLabel() setupDismissButton() @@ -385,6 +386,95 @@ class CustomMediaPlayerViewController: UIViewController { ]) } + func setupSkipAndDismissGestures() { + let doubleTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:))) + doubleTapGesture.numberOfTapsRequired = 2 + view.addGestureRecognizer(doubleTapGesture) + + if let gestures = view.gestureRecognizers { + for gesture in gestures { + if let tapGesture = gesture as? UITapGestureRecognizer, tapGesture.numberOfTapsRequired == 1 { + tapGesture.require(toFail: doubleTapGesture) + } + } + } + + let swipeDownGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipeDown(_:))) + swipeDownGesture.direction = .down + view.addGestureRecognizer(swipeDownGesture) + } + + func showSkipFeedback(direction: String) { + let diameter: CGFloat = 700.0 + + if let existingFeedback = view.viewWithTag(999) { + existingFeedback.layer.removeAllAnimations() + existingFeedback.removeFromSuperview() + } + + let circleView = UIView() + circleView.backgroundColor = UIColor.white.withAlphaComponent(0.0) + circleView.layer.cornerRadius = diameter / 2 + circleView.clipsToBounds = true + circleView.translatesAutoresizingMaskIntoConstraints = false + circleView.tag = 999 + + let iconName = (direction == "forward") ? "goforward" : "gobackward" + let imageView = UIImageView(image: UIImage(systemName: iconName)) + imageView.tintColor = .black + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.alpha = 0.8 + + circleView.addSubview(imageView) + + if direction == "forward" { + NSLayoutConstraint.activate([ + imageView.centerYAnchor.constraint(equalTo: circleView.centerYAnchor), + imageView.centerXAnchor.constraint(equalTo: circleView.leadingAnchor, constant: diameter / 4), + imageView.widthAnchor.constraint(equalToConstant: 100), + imageView.heightAnchor.constraint(equalToConstant: 100) + ]) + } else { + NSLayoutConstraint.activate([ + imageView.centerYAnchor.constraint(equalTo: circleView.centerYAnchor), + imageView.centerXAnchor.constraint(equalTo: circleView.trailingAnchor, constant: -diameter / 4), + imageView.widthAnchor.constraint(equalToConstant: 100), + imageView.heightAnchor.constraint(equalToConstant: 100) + ]) + } + + view.addSubview(circleView) + + if direction == "forward" { + NSLayoutConstraint.activate([ + circleView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + circleView.centerXAnchor.constraint(equalTo: view.trailingAnchor), + circleView.widthAnchor.constraint(equalToConstant: diameter), + circleView.heightAnchor.constraint(equalToConstant: diameter) + ]) + } else { + NSLayoutConstraint.activate([ + circleView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + circleView.centerXAnchor.constraint(equalTo: view.leadingAnchor), + circleView.widthAnchor.constraint(equalToConstant: diameter), + circleView.heightAnchor.constraint(equalToConstant: diameter) + ]) + } + + UIView.animate(withDuration: 0.2, animations: { + circleView.backgroundColor = UIColor.white.withAlphaComponent(0.5) + imageView.alpha = 0.8 + }) { _ in + UIView.animate(withDuration: 0.5, delay: 0.5, options: [], animations: { + circleView.backgroundColor = UIColor.white.withAlphaComponent(0.0) + imageView.alpha = 0.0 + }, completion: { _ in + circleView.removeFromSuperview() + imageView.removeFromSuperview() + }) + } + } func setupSubtitleLabel() { subtitleLabel = UILabel() @@ -773,6 +863,21 @@ class CustomMediaPlayerViewController: UIViewController { player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) } + @objc func handleDoubleTap(_ gesture: UITapGestureRecognizer) { + let tapLocation = gesture.location(in: view) + if tapLocation.x < view.bounds.width / 2 { + seekBackward() + showSkipFeedback(direction: "backward") + } else { + seekForward() + showSkipFeedback(direction: "forward") + } + } + + @objc func handleSwipeDown(_ gesture: UISwipeGestureRecognizer) { + dismiss(animated: true, completion: nil) + } + @objc func togglePlayPause() { if isPlaying { if !isControlsVisible { From d5d13fd407e8028e8a72dabd75c5172c26473be4 Mon Sep 17 00:00:00 2001 From: Ibrahim Sulejmenov Date: Thu, 20 Mar 2025 20:13:54 +0100 Subject: [PATCH 05/41] Added toggle for subtitles + fixed mark all previous as watched in series organized in seasons --- .../CustomPlayer/CustomPlayer.swift | 19 +++++++++++++++---- Sora/Views/MediaInfoView/MediaInfoView.swift | 2 +- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index cfafadc..66cb07d 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -42,13 +42,17 @@ class CustomMediaPlayerViewController: UIViewController { var isWatchNextVisible: Bool = false var lastDuration: Double = 0.0 var watchNextButtonAppearedAt: Double? - var subtitleForegroundColor: String = "white" var subtitleBackgroundEnabled: Bool = true var subtitleFontSize: Double = 20.0 var subtitleShadowRadius: Double = 1.0 var subtitlesLoader = VTTSubtitlesLoader() + var subtitlesEnabled: Bool = true { + didSet { + subtitleLabel.isHidden = !subtitlesEnabled + } + } var playerViewController: AVPlayerViewController! var controlsContainerView: UIView! @@ -405,7 +409,7 @@ class CustomMediaPlayerViewController: UIViewController { } func showSkipFeedback(direction: String) { - let diameter: CGFloat = 700.0 + let diameter: CGFloat = 600 if let existingFeedback = view.viewWithTag(999) { existingFeedback.layer.removeAllAnimations() @@ -417,6 +421,7 @@ class CustomMediaPlayerViewController: UIViewController { circleView.layer.cornerRadius = diameter / 2 circleView.clipsToBounds = true circleView.translatesAutoresizingMaskIntoConstraints = false + circleView.isUserInteractionEnabled = false circleView.tag = 999 let iconName = (direction == "forward") ? "goforward" : "gobackward" @@ -688,7 +693,8 @@ class CustomMediaPlayerViewController: UIViewController { UserDefaults.standard.set(self.currentTimeVal, forKey: "lastPlayedTime_\(self.fullUrl)") UserDefaults.standard.set(self.duration, forKey: "totalTime_\(self.fullUrl)") - if let currentCue = self.subtitlesLoader.cues.first(where: { self.currentTimeVal >= $0.startTime && self.currentTimeVal <= $0.endTime }) { + if self.subtitlesEnabled, + let currentCue = self.subtitlesLoader.cues.first(where: { self.currentTimeVal >= $0.startTime && self.currentTimeVal <= $0.endTime }) { self.subtitleLabel.text = currentCue.text.strippedHTML } else { self.subtitleLabel.text = "" @@ -1102,6 +1108,11 @@ class CustomMediaPlayerViewController: UIViewController { var menuElements: [UIMenuElement] = [] if let subURL = subtitlesURL, !subURL.isEmpty { + let subtitlesToggleAction = UIAction(title: "Toggle Subtitles") { [weak self] _ in + guard let self = self else { return } + self.subtitlesEnabled.toggle() + } + let foregroundActions = [ UIAction(title: "White") { _ in SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = "white" } @@ -1216,7 +1227,7 @@ class CustomMediaPlayerViewController: UIViewController { ] let paddingMenu = UIMenu(title: "Bottom Padding", children: paddingActions) - let subtitleOptionsMenu = UIMenu(title: "Subtitle Options", children: [colorMenu, fontSizeMenu, shadowMenu, backgroundMenu, paddingMenu]) + let subtitleOptionsMenu = UIMenu(title: "Subtitle Options", children: [subtitlesToggleAction, colorMenu, fontSizeMenu, shadowMenu, backgroundMenu, paddingMenu]) menuElements = [subtitleOptionsMenu] } diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 02a8499..3db0025 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -250,7 +250,7 @@ struct MediaInfoView: View { } }, onMarkAllPrevious: { - for ep2 in seasons[selectedSeason] { + for ep2 in seasons[selectedSeason] where ep2.number < ep.number { let href = ep2.href UserDefaults.standard.set(99999999.0, forKey: "lastPlayedTime_\(href)") UserDefaults.standard.set(99999999.0, forKey: "totalTime_\(href)") From d099267f6ac6267510d6f9a46893efed60640771 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Fri, 21 Mar 2025 16:31:53 +0100 Subject: [PATCH 06/41] just made the sora player support hours --- .../Components/Double+Extension.swift | 22 ++++++++++++++----- .../Components/MusicProgressSlider.swift | 4 ++-- .../CustomPlayer/CustomPlayer.swift | 1 - .../SettingsSubViews/SettingsViewPlayer.swift | 2 -- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Components/Double+Extension.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Components/Double+Extension.swift index 801cc7b..dd7d7bd 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/Components/Double+Extension.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/Components/Double+Extension.swift @@ -20,11 +20,21 @@ extension Double { } extension BinaryFloatingPoint { - func asTimeString(style: DateComponentsFormatter.UnitsStyle) -> String { - let formatter = DateComponentsFormatter() - formatter.allowedUnits = [.minute, .second] - formatter.unitsStyle = style - formatter.zeroFormattingBehavior = .pad - return formatter.string(from: TimeInterval(self)) ?? "" + func asTimeString(style: TimeStringStyle, showHours: Bool = false) -> String { + let totalSeconds = Int(self) + let hours = totalSeconds / 3600 + let minutes = (totalSeconds % 3600) / 60 + let seconds = totalSeconds % 60 + + if showHours || hours > 0 { + return String(format: "%02d:%02d:%02d", hours, minutes, seconds) + } else { + return String(format: "%02d:%02d", minutes, seconds) + } } } + +enum TimeStringStyle { + case positional + case standard +} \ No newline at end of file diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift index 30a94fc..57fb3e5 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift @@ -42,9 +42,9 @@ struct MusicProgressSlider: View { } HStack { - Text(value.asTimeString(style: .positional)) + Text(value.asTimeString(style: .positional, showHours: true)) Spacer(minLength: 0) - Text("-" + (inRange.upperBound - value).asTimeString(style: .positional)) + Text("-" + (inRange.upperBound - value).asTimeString(style: .positional, showHours: true)) } .font(.system(size: 12)) .foregroundColor(isActive ? fillColor : emptyColor) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index 66cb07d..c18dff6 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -700,7 +700,6 @@ class CustomMediaPlayerViewController: UIViewController { self.subtitleLabel.text = "" } - // ORIGINAL PROGRESS BAR CODE: DispatchQueue.main.async { self.sliderHostingController?.rootView = MusicProgressSlider( value: Binding(get: { max(0, min(self.sliderViewModel.sliderValue, self.duration)) }, diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift index c37ecd2..4f855ee 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift @@ -59,14 +59,12 @@ struct SettingsViewPlayer: View { } } Section(header: Text("Skip Settings")) { - // Normal skip HStack { Text("Tap Skip:") Spacer() Stepper("\(Int(skipIncrement))s", value: $skipIncrement, in: 5...300, step: 5) } - // Long-press skip HStack { Text("Long press Skip:") Spacer() From faa7dfbb72bc7ef3042a33063be85180c323f486 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Fri, 21 Mar 2025 16:33:08 +0100 Subject: [PATCH 07/41] Update project.pbxproj --- Sulfur.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 7cf7cc7..27a0a15 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -717,7 +717,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\""; - DEVELOPMENT_TEAM = 44V6G67299; + DEVELOPMENT_TEAM = 399LMK6Q2Y; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -737,7 +737,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.2.1; - PRODUCT_BUNDLE_IDENTIFIER = me.hamzo.sulfur; + PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; @@ -759,7 +759,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\""; - DEVELOPMENT_TEAM = 44V6G67299; + DEVELOPMENT_TEAM = 399LMK6Q2Y; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -779,7 +779,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.2.1; - PRODUCT_BUNDLE_IDENTIFIER = me.hamzo.sulfur; + PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; From 63e96588572f33ab609e015cb5a76b8b30254523 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Fri, 21 Mar 2025 16:33:28 +0100 Subject: [PATCH 08/41] Update Info.plist --- Sora/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sora/Info.plist b/Sora/Info.plist index df1d517..b085ddf 100644 --- a/Sora/Info.plist +++ b/Sora/Info.plist @@ -2,6 +2,8 @@ + NSCameraUsageDescription + Sora may requires access to your device's camera. BGTaskSchedulerPermittedIdentifiers $(PRODUCT_BUNDLE_IDENTIFIER) From d2099d65afafd9465276d1c91d7a44b3c4592688 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Fri, 21 Mar 2025 16:44:40 +0100 Subject: [PATCH 09/41] fixed crash --- Sora/Utils/JSLoader/JSController.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Sora/Utils/JSLoader/JSController.swift b/Sora/Utils/JSLoader/JSController.swift index b3a0f92..a0a220b 100644 --- a/Sora/Utils/JSLoader/JSController.swift +++ b/Sora/Utils/JSLoader/JSController.swift @@ -23,6 +23,9 @@ class JSController: ObservableObject { context = JSContext() setupContext() context.evaluateScript(script) + if let exception = context.exception { + Logger.shared.log("Error loading script: \(exception)", type: "Error") + } } func fetchSearchResults(keyword: String, module: ScrapingModule, completion: @escaping ([SearchItem]) -> Void) { @@ -196,10 +199,13 @@ class JSController: ObservableObject { let data = jsonString.data(using: .utf8) { do { if let array = try JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]] { - let resultItems = array.map { item -> SearchItem in - let title = item["title"] as? String ?? "" - let imageUrl = item["image"] as? String ?? "https://s4.anilist.co/file/anilistcdn/character/large/default.jpg" - let href = item["href"] as? String ?? "" + let resultItems = array.compactMap { item -> SearchItem? in + guard let title = item["title"] as? String, + let imageUrl = item["image"] as? String, + let href = item["href"] as? String else { + Logger.shared.log("Missing or invalid data in search result item: \(item)", type: "Error") + return nil + } return SearchItem(title: title, imageUrl: imageUrl, href: href) } From 0b3df78cddfc58dc19e8e8e0ba6ad13725cc0d7e Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Fri, 21 Mar 2025 16:52:46 +0100 Subject: [PATCH 10/41] remade the mf source selector menu --- Sora/Views/SearchView.swift | 60 +++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/Sora/Views/SearchView.swift b/Sora/Views/SearchView.swift index 85802ba..50d5528 100644 --- a/Sora/Views/SearchView.swift +++ b/Sora/Views/SearchView.swift @@ -19,7 +19,7 @@ struct SearchView: View { @AppStorage("selectedModuleId") private var selectedModuleId: String? @AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2 @AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4 - + @StateObject private var jsController = JSController() @EnvironmentObject var moduleManager: ModuleManager @Environment(\.verticalSizeClass) var verticalSizeClass @@ -30,6 +30,7 @@ struct SearchView: View { @State private var searchText = "" @State private var hasNoResults = false @State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape + @State private var isModuleSelectorPresented = false private var selectedModule: ScrapingModule? { guard let id = selectedModuleId else { return nil } @@ -63,12 +64,11 @@ struct SearchView: View { let availableWidth = safeWidth - totalSpacing return availableWidth / CGFloat(columnsCount) } - + var body: some View { NavigationView { ScrollView { let columnsCount = determineColumns() - VStack(spacing: 0) { HStack { SearchBar(text: $searchText, onSearchButtonClicked: performSearch) @@ -164,38 +164,40 @@ struct SearchView: View { .navigationBarTitleDisplayMode(.large) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { - HStack(spacing: 4) { - if let selectedModule = selectedModule { - Text(selectedModule.metadata.sourceName) - .font(.headline) - .foregroundColor(.secondary) - } - Menu { - ForEach(moduleManager.modules) { 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) - Spacer() - if module.id.uuidString == selectedModuleId { - Image(systemName: "checkmark") - } + 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) } } } - } label: { - Image(systemName: "chevron.up.chevron.down") + } + } 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) } } - .id("moduleMenuHStack") .fixedSize() } } From 4f6b7fbb13d164cdfc88a6b321f24bf466b9b9f3 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Fri, 21 Mar 2025 16:56:12 +0100 Subject: [PATCH 11/41] idk tf is this MainActor --- .../Modules/ModuleAdditionSettingsView.swift | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift index c9a38fa..b2580e2 100644 --- a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift +++ b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift @@ -146,22 +146,22 @@ struct ModuleAdditionSettingsView: View { errorMessage = nil Task { - do { - guard let url = URL(string: moduleUrl) else { - DispatchQueue.main.async { - self.errorMessage = "Invalid URL" - self.isLoading = false - } - return + guard let url = URL(string: moduleUrl) else { + await MainActor.run { + self.errorMessage = "Invalid URL" + self.isLoading = false } + return + } + do { let (data, _) = try await URLSession.custom.data(from: url) let metadata = try JSONDecoder().decode(ModuleMetadata.self, from: data) - DispatchQueue.main.async { + await MainActor.run { self.moduleMetadata = metadata self.isLoading = false } } catch { - DispatchQueue.main.async { + await MainActor.run { self.errorMessage = "Failed to fetch module: \(error.localizedDescription)" self.isLoading = false } @@ -174,13 +174,13 @@ struct ModuleAdditionSettingsView: View { Task { do { let _ = try await moduleManager.addModule(metadataUrl: moduleUrl) - DispatchQueue.main.async { + await MainActor.run { isLoading = false DropManager.shared.showDrop(title: "Module Added", subtitle: "click it to select it", duration: 2.0, icon: UIImage(systemName:"gear.badge.checkmark")) self.presentationMode.wrappedValue.dismiss() } } catch { - DispatchQueue.main.async { + await MainActor.run { isLoading = false if (error as NSError).domain == "Module already exists" { errorMessage = "Module already exists" From a5f9877474e282ee4c3435f6bc3e00ab149c2c16 Mon Sep 17 00:00:00 2001 From: Seiike <122684677+Seeike@users.noreply.github.com> Date: Sat, 22 Mar 2025 12:29:58 +0100 Subject: [PATCH 12/41] progression timer wont show hours unless media is longer than an hour --- .../CustomPlayer/Components/MusicProgressSlider.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift index 57fb3e5..e8a3ca8 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift @@ -42,10 +42,13 @@ struct MusicProgressSlider: View { } HStack { - Text(value.asTimeString(style: .positional, showHours: true)) + // Determine if we should show hours based on the total duration. + let shouldShowHours = inRange.upperBound >= 3600 + Text(value.asTimeString(style: .positional, showHours: shouldShowHours)) Spacer(minLength: 0) - Text("-" + (inRange.upperBound - value).asTimeString(style: .positional, showHours: true)) + Text("-" + (inRange.upperBound - value).asTimeString(style: .positional, showHours: shouldShowHours)) } + .font(.system(size: 12)) .foregroundColor(isActive ? fillColor : emptyColor) } From 0a526b2fe9ce10909037cdb62e661ca0dc3cb94d Mon Sep 17 00:00:00 2001 From: Seiike <122684677+Seeike@users.noreply.github.com> Date: Sat, 22 Mar 2025 13:12:05 +0100 Subject: [PATCH 13/41] adjusted the positions of the buttons for symmetry --- .../CustomPlayer/CustomPlayer.swift | 57 ++++++++----------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index c18dff6..cb8cfdb 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -144,9 +144,9 @@ class CustomMediaPlayerViewController: UIViewController { addInvisibleControlOverlays() setupSubtitleLabel() setupDismissButton() + setupQualityButton() setupMenuButton() setupSpeedButton() - setupQualityButton() setupSkip85Button() setupWatchNextButton() addTimeObserver() @@ -527,20 +527,20 @@ class CustomMediaPlayerViewController: UIViewController { menuButton = UIButton(type: .system) menuButton.setImage(UIImage(systemName: "text.bubble"), for: .normal) menuButton.tintColor = .white - + if let subtitlesURL = subtitlesURL, !subtitlesURL.isEmpty { menuButton.showsMenuAsPrimaryAction = true menuButton.menu = buildOptionsMenu() } else { menuButton.isHidden = true } - + controlsContainerView.addSubview(menuButton) menuButton.translatesAutoresizingMaskIntoConstraints = false - guard let sliderView = sliderHostingController?.view else { return } + NSLayoutConstraint.activate([ - menuButton.bottomAnchor.constraint(equalTo: sliderView.topAnchor), - menuButton.trailingAnchor.constraint(equalTo: sliderView.trailingAnchor), + menuButton.topAnchor.constraint(equalTo: controlsContainerView.topAnchor, constant: 20), + menuButton.trailingAnchor.constraint(equalTo: qualityButton.leadingAnchor, constant: -20), menuButton.widthAnchor.constraint(equalToConstant: 40), menuButton.heightAnchor.constraint(equalToConstant: 40) ]) @@ -550,30 +550,20 @@ class CustomMediaPlayerViewController: UIViewController { speedButton = UIButton(type: .system) speedButton.setImage(UIImage(systemName: "speedometer"), for: .normal) speedButton.tintColor = .white - + speedButton.showsMenuAsPrimaryAction = true - speedButton.menu = speedChangerMenu() - + speedButton.menu = speedChangerMenu() // If you want the speed menu + controlsContainerView.addSubview(speedButton) speedButton.translatesAutoresizingMaskIntoConstraints = false - - guard let sliderView = sliderHostingController?.view else { return } - - if menuButton.isHidden { - NSLayoutConstraint.activate([ - speedButton.bottomAnchor.constraint(equalTo: controlsContainerView.bottomAnchor, constant: -50), - speedButton.trailingAnchor.constraint(equalTo: sliderView.trailingAnchor), - speedButton.widthAnchor.constraint(equalToConstant: 40), - speedButton.heightAnchor.constraint(equalToConstant: 40) - ]) - } else { - NSLayoutConstraint.activate([ - speedButton.bottomAnchor.constraint(equalTo: controlsContainerView.bottomAnchor, constant: -50), - speedButton.trailingAnchor.constraint(equalTo: menuButton.leadingAnchor), - speedButton.widthAnchor.constraint(equalToConstant: 40), - speedButton.heightAnchor.constraint(equalToConstant: 40) - ]) - } + + // Pin it at the top, to the left of the subtitles (menu) button. + NSLayoutConstraint.activate([ + speedButton.topAnchor.constraint(equalTo: controlsContainerView.topAnchor, constant: 20), + speedButton.trailingAnchor.constraint(equalTo: menuButton.leadingAnchor, constant: -20), + speedButton.widthAnchor.constraint(equalToConstant: 40), + speedButton.heightAnchor.constraint(equalToConstant: 40) + ]) } func setupWatchNextButton() { @@ -599,8 +589,8 @@ class CustomMediaPlayerViewController: UIViewController { ] watchNextButtonControlsConstraints = [ - watchNextButton.trailingAnchor.constraint(equalTo: qualityButton.leadingAnchor), - watchNextButton.bottomAnchor.constraint(equalTo: skip85Button.bottomAnchor), + watchNextButton.trailingAnchor.constraint(equalTo: sliderHostingController!.view.trailingAnchor), + watchNextButton.bottomAnchor.constraint(equalTo: sliderHostingController!.view.topAnchor, constant: -5), watchNextButton.heightAnchor.constraint(equalToConstant: 50), watchNextButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 120) ] @@ -637,17 +627,18 @@ class CustomMediaPlayerViewController: UIViewController { qualityButton.showsMenuAsPrimaryAction = true qualityButton.menu = qualitySelectionMenu() qualityButton.isHidden = true - + controlsContainerView.addSubview(qualityButton) qualityButton.translatesAutoresizingMaskIntoConstraints = false - + NSLayoutConstraint.activate([ - qualityButton.bottomAnchor.constraint(equalTo: controlsContainerView.bottomAnchor, constant: -50), - qualityButton.trailingAnchor.constraint(equalTo: speedButton.leadingAnchor), + qualityButton.topAnchor.constraint(equalTo: controlsContainerView.topAnchor, constant: 20), + qualityButton.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor, constant: -20), qualityButton.widthAnchor.constraint(equalToConstant: 40), qualityButton.heightAnchor.constraint(equalToConstant: 40) ]) } + func updateSubtitleLabelAppearance() { subtitleLabel.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize)) From 607e8a9677e66239f6b2afe5908aeb7b3d13b15d Mon Sep 17 00:00:00 2001 From: Seiike <122684677+Seeike@users.noreply.github.com> Date: Sat, 22 Mar 2025 13:24:12 +0100 Subject: [PATCH 14/41] =?UTF-8?q?problem=20solved=20=F0=9F=8E=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index cb8cfdb..1acb788 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -145,8 +145,8 @@ class CustomMediaPlayerViewController: UIViewController { setupSubtitleLabel() setupDismissButton() setupQualityButton() - setupMenuButton() setupSpeedButton() + setupMenuButton() setupSkip85Button() setupWatchNextButton() addTimeObserver() @@ -540,7 +540,7 @@ class CustomMediaPlayerViewController: UIViewController { NSLayoutConstraint.activate([ menuButton.topAnchor.constraint(equalTo: controlsContainerView.topAnchor, constant: 20), - menuButton.trailingAnchor.constraint(equalTo: qualityButton.leadingAnchor, constant: -20), + menuButton.trailingAnchor.constraint(equalTo: speedButton.leadingAnchor, constant: -20), menuButton.widthAnchor.constraint(equalToConstant: 40), menuButton.heightAnchor.constraint(equalToConstant: 40) ]) @@ -550,17 +550,16 @@ class CustomMediaPlayerViewController: UIViewController { speedButton = UIButton(type: .system) speedButton.setImage(UIImage(systemName: "speedometer"), for: .normal) speedButton.tintColor = .white - speedButton.showsMenuAsPrimaryAction = true - speedButton.menu = speedChangerMenu() // If you want the speed menu + speedButton.menu = speedChangerMenu() controlsContainerView.addSubview(speedButton) speedButton.translatesAutoresizingMaskIntoConstraints = false - // Pin it at the top, to the left of the subtitles (menu) button. NSLayoutConstraint.activate([ + // Middle speedButton.topAnchor.constraint(equalTo: controlsContainerView.topAnchor, constant: 20), - speedButton.trailingAnchor.constraint(equalTo: menuButton.leadingAnchor, constant: -20), + speedButton.trailingAnchor.constraint(equalTo: qualityButton.leadingAnchor, constant: -20), speedButton.widthAnchor.constraint(equalToConstant: 40), speedButton.heightAnchor.constraint(equalToConstant: 40) ]) From 6d08ce08cadaaee29347701e78c4f5e9d06a7de6 Mon Sep 17 00:00:00 2001 From: Seiike <122684677+Seeike@users.noreply.github.com> Date: Sat, 22 Mar 2025 13:32:53 +0100 Subject: [PATCH 15/41] info about double tapping the screen in settings --- .../SettingsView/SettingsSubViews/SettingsViewPlayer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift index 4f855ee..2792468 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift @@ -58,7 +58,7 @@ struct SettingsViewPlayer: View { } } } - Section(header: Text("Skip Settings")) { + 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() From 748cc4e999a4efb5a5550fe2aa324090b5425a3d Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Sun, 23 Mar 2025 10:25:53 +0100 Subject: [PATCH 16/41] added auth exchange --- Sora/SoraApp.swift | 22 +- .../AniList/Auth/Anilist-Login.swift | 34 ++ .../AniList/Auth/Anilist-Token.swift | 88 +++++ .../AniList/HomePage/AniList-Seasonal.swift | 117 ------- .../AniList/HomePage/AniList-Trending.swift | 93 ------ .../DetailsView/AniList-DetailsView.swift | 316 ------------------ .../AniList/MediaInfo/AniList-MediaInfo.swift | 135 -------- Sora/Utils/Extensions/URL.swift | 20 ++ Sulfur.xcodeproj/project.pbxproj | 60 ++-- 9 files changed, 183 insertions(+), 702 deletions(-) create mode 100644 Sora/Tracking Services/AniList/Auth/Anilist-Login.swift create mode 100644 Sora/Tracking Services/AniList/Auth/Anilist-Token.swift delete mode 100644 Sora/Tracking Services/AniList/HomePage/AniList-Seasonal.swift delete mode 100644 Sora/Tracking Services/AniList/HomePage/AniList-Trending.swift delete mode 100644 Sora/Tracking Services/AniList/HomePage/DetailsView/AniList-DetailsView.swift delete mode 100644 Sora/Tracking Services/AniList/MediaInfo/AniList-MediaInfo.swift create mode 100644 Sora/Utils/Extensions/URL.swift diff --git a/Sora/SoraApp.swift b/Sora/SoraApp.swift index a945639..4073e20 100644 --- a/Sora/SoraApp.swift +++ b/Sora/SoraApp.swift @@ -29,7 +29,11 @@ struct SoraApp: App { } } .onOpenURL { url in - handleURL(url) + if let params = url.queryParameters, params["code"] != nil { + Self.handleRedirect(url: url) + } else { + handleURL(url) + } } } } @@ -52,4 +56,20 @@ struct SoraApp: App { Logger.shared.log("Failed to present module addition view: No window scene found", type: "Error") } } + + static func handleRedirect(url: URL) { + guard let params = url.queryParameters, + let code = params["code"] else { + Logger.shared.log("Failed to extract authorization code") + return + } + + AniListToken.exchangeAuthorizationCodeForToken(code: code) { success in + if success { + Logger.shared.log("Token exchange successful") + } else { + Logger.shared.log("Token exchange failed", type: "Error") + } + } + } } diff --git a/Sora/Tracking Services/AniList/Auth/Anilist-Login.swift b/Sora/Tracking Services/AniList/Auth/Anilist-Login.swift new file mode 100644 index 0000000..de958b1 --- /dev/null +++ b/Sora/Tracking Services/AniList/Auth/Anilist-Login.swift @@ -0,0 +1,34 @@ +// +// Login.swift +// Ryu +// +// Created by Francesco on 08/08/24. +// + +import UIKit + +class AniListLogin { + static let clientID = "19551" + static let redirectURI = "sora://anilist" + + static let authorizationEndpoint = "https://anilist.co/api/v2/oauth/authorize" + + static func authenticate() { + let urlString = "\(authorizationEndpoint)?client_id=\(clientID)&redirect_uri=\(redirectURI)&response_type=code" + guard let url = URL(string: urlString) else { + return + } + + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:]) { success in + if success { + Logger.shared.log("Safari opened successfully", type: "Debug") + } else { + Logger.shared.log("Failed to open Safari", type: "Error") + } + } + } else { + Logger.shared.log("Cannot open URL", type: "Error") + } + } +} diff --git a/Sora/Tracking Services/AniList/Auth/Anilist-Token.swift b/Sora/Tracking Services/AniList/Auth/Anilist-Token.swift new file mode 100644 index 0000000..68bc232 --- /dev/null +++ b/Sora/Tracking Services/AniList/Auth/Anilist-Token.swift @@ -0,0 +1,88 @@ +// +// Token.swift +// Ryu +// +// Created by Francesco on 08/08/24. +// + +import UIKit +import Security + +class AniListToken { + static let clientID = "19551" + static let clientSecret = "fk8EgkyFbXk95TbPwLYQLaiMaNIryMpDBwJsPXoX" + static let redirectURI = "sora://anilist" + + static let tokenEndpoint = "https://anilist.co/api/v2/oauth/token" + static let serviceName = "me.cranci.sora.AniListToken" + static let accountName = "AniListAccessToken" + + static func saveTokenToKeychain(token: String) -> Bool { + let tokenData = token.data(using: .utf8)! + + let deleteQuery: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: serviceName, + kSecAttrAccount as String: accountName + ] + SecItemDelete(deleteQuery as CFDictionary) + + let addQuery: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: serviceName, + kSecAttrAccount as String: accountName, + kSecValueData as String: tokenData + ] + + let status = SecItemAdd(addQuery as CFDictionary, nil) + return status == errSecSuccess + } + + static func exchangeAuthorizationCodeForToken(code: String, completion: @escaping (Bool) -> Void) { + Logger.shared.log("Exchanging authorization code for access token...") + + guard let url = URL(string: tokenEndpoint) else { + Logger.shared.log("Invalid token endpoint URL", type: "Error") + completion(false) + return + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + + let bodyString = "grant_type=authorization_code&client_id=\(clientID)&client_secret=\(clientSecret)&redirect_uri=\(redirectURI)&code=\(code)" + request.httpBody = bodyString.data(using: .utf8) + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + Logger.shared.log("Error: \(error.localizedDescription)", type: "Error") + completion(false) + return + } + + guard let data = data else { + Logger.shared.log("No data received", type: "Error") + completion(false) + return + } + + do { + if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] { + if let accessToken = json["access_token"] as? String { + let success = saveTokenToKeychain(token: accessToken) + completion(success) + } else { + Logger.shared.log("Unexpected response: \(json)", type: "Error") + completion(false) + } + } + } catch { + Logger.shared.log("Failed to parse JSON: \(error.localizedDescription)", type: "Error") + completion(false) + } + } + + task.resume() + } +} diff --git a/Sora/Tracking Services/AniList/HomePage/AniList-Seasonal.swift b/Sora/Tracking Services/AniList/HomePage/AniList-Seasonal.swift deleted file mode 100644 index 341615f..0000000 --- a/Sora/Tracking Services/AniList/HomePage/AniList-Seasonal.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// AniList-Seasonal.swift -// Sora -// -// Created by Francesco on 09/02/25. -// - -import Foundation - -class AnilistServiceSeasonalAnime { - func fetchSeasonalAnime(completion: @escaping ([AniListItem]?) -> Void) { - let currentDate = Date() - let calendar = Calendar.current - let year = calendar.component(.year, from: currentDate) - let month = calendar.component(.month, from: currentDate) - - let season: String - switch month { - case 1...3: - season = "WINTER" - case 4...6: - season = "SPRING" - case 7...9: - season = "SUMMER" - default: - season = "FALL" - } - - let query = """ - query { - Page(page: 1, perPage: 100) { - media(season: \(season), seasonYear: \(year), type: ANIME, isAdult: false) { - id - title { - romaji - english - native - } - coverImage { - large - } - } - } - } - """ - - guard let url = URL(string: "https://graphql.anilist.co") else { - print("Invalid URL") - completion(nil) - return - } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - - let parameters: [String: Any] = ["query": query] - do { - request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: []) - } catch { - print("Error encoding JSON: \(error.localizedDescription)") - completion(nil) - return - } - - let task = URLSession.custom.dataTask(with: request) { data, response, error in - DispatchQueue.main.async { - if let error = error { - print("Error fetching seasonal anime: \(error.localizedDescription)") - completion(nil) - return - } - - guard let data = data else { - print("No data returned") - completion(nil) - return - } - - do { - if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], - let dataObject = json["data"] as? [String: Any], - let page = dataObject["Page"] as? [String: Any], - let media = page["media"] as? [[String: Any]] { - - let seasonalAnime: [AniListItem] = media.compactMap { item -> AniListItem? in - guard let id = item["id"] as? Int, - let titleData = item["title"] as? [String: Any], - let romaji = titleData["romaji"] as? String, - let english = titleData["english"] as? String?, - let native = titleData["native"] as? String?, - let coverImageData = item["coverImage"] as? [String: Any], - let largeImageUrl = coverImageData["large"] as? String, - URL(string: largeImageUrl) != nil else { - return nil - } - - return AniListItem( - id: id, - title: AniListTitle(romaji: romaji, english: english, native: native), - coverImage: AniListCoverImage(large: largeImageUrl) - ) - } - completion(seasonalAnime) - } else { - print("Error parsing JSON or missing expected fields") - completion(nil) - } - } catch { - print("Error decoding JSON: \(error.localizedDescription)") - completion(nil) - } - } - } - task.resume() - } -} diff --git a/Sora/Tracking Services/AniList/HomePage/AniList-Trending.swift b/Sora/Tracking Services/AniList/HomePage/AniList-Trending.swift deleted file mode 100644 index b02e970..0000000 --- a/Sora/Tracking Services/AniList/HomePage/AniList-Trending.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// AniList-Trending.swift -// Sora -// -// Created by Francesco on 09/02/25. -// - -import Foundation - -class AnilistServiceTrendingAnime { - func fetchTrendingAnime(completion: @escaping ([AniListItem]?) -> Void) { - let query = """ - query { - Page(page: 1, perPage: 100) { - media(sort: TRENDING_DESC, type: ANIME, isAdult: false) { - id - title { - romaji - english - native - } - coverImage { - large - } - } - } - } - """ - guard let url = URL(string: "https://graphql.anilist.co") else { - print("Invalid URL") - completion(nil) - return - } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - let parameters: [String: Any] = ["query": query] - do { - request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: []) - } catch { - print("Error encoding JSON: \(error.localizedDescription)") - completion(nil) - return - } - - let task = URLSession.custom.dataTask(with: request) { data, response, error in - DispatchQueue.main.async { - if let error = error { - print("Error fetching trending anime: \(error.localizedDescription)") - completion(nil) - return - } - guard let data = data else { - print("No data returned") - completion(nil) - return - } - do { - if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], - let dataObject = json["data"] as? [String: Any], - let page = dataObject["Page"] as? [String: Any], - let media = page["media"] as? [[String: Any]] { - - let trendingAnime: [AniListItem] = media.compactMap { item in - guard let id = item["id"] as? Int, - let titleData = item["title"] as? [String: Any], - let romaji = titleData["romaji"] as? String, - let coverImageData = item["coverImage"] as? [String: Any], - let largeImageUrl = coverImageData["large"] as? String else { - return nil - } - - return AniListItem( - id: id, - title: AniListTitle(romaji: romaji, english: titleData["english"] as? String, native: titleData["native"] as? String), - coverImage: AniListCoverImage(large: largeImageUrl) - ) - } - completion(trendingAnime) - } else { - print("Error parsing JSON or missing expected fields") - completion(nil) - } - } catch { - print("Error decoding JSON: \(error.localizedDescription)") - completion(nil) - } - } - } - task.resume() - } -} diff --git a/Sora/Tracking Services/AniList/HomePage/DetailsView/AniList-DetailsView.swift b/Sora/Tracking Services/AniList/HomePage/DetailsView/AniList-DetailsView.swift deleted file mode 100644 index 4f7a9d4..0000000 --- a/Sora/Tracking Services/AniList/HomePage/DetailsView/AniList-DetailsView.swift +++ /dev/null @@ -1,316 +0,0 @@ -// -// AniList-DetailsView.swift -// Sora -// -// Created by Francesco on 11/02/25. -// - -import SwiftUI -import Kingfisher - -struct AniListDetailsView: View { - let animeID: Int - @StateObject private var viewModel: AniListDetailsViewModel - - init(animeID: Int) { - self.animeID = animeID - _viewModel = StateObject(wrappedValue: AniListDetailsViewModel(animeID: animeID)) - } - - var body: some View { - ScrollView { - VStack(spacing: 16) { - if viewModel.isLoading { - ProgressView() - .padding() - } else if let media = viewModel.mediaInfo { - MediaHeaderView(media: media) - Divider() - MediaDetailsScrollView(media: media) - Divider() - SynopsisView(synopsis: media["description"] as? String) - Divider() - CharactersView(characters: media["characters"] as? [String: Any]) - Divider() - ScoreDistributionView(stats: media["stats"] as? [String: Any]) - } else { - Text("Failed to load media details.") - .padding() - } - } - } - .navigationBarTitle("", displayMode: .inline) - .navigationViewStyle(StackNavigationViewStyle()) - .onAppear { - viewModel.fetchDetails() - } - } -} - -class AniListDetailsViewModel: ObservableObject { - @Published var mediaInfo: [String: AnyHashable]? - @Published var isLoading: Bool = true - - let animeID: Int - - init(animeID: Int) { - self.animeID = animeID - } - - func fetchDetails() { - AnilistServiceMediaInfo.fetchAnimeDetails(animeID: animeID) { result in - DispatchQueue.main.async { - switch result { - case .success(let media): - var convertedMedia: [String: AnyHashable] = [:] - for (key, value) in media { - if let value = value as? AnyHashable { - convertedMedia[key] = value - } - } - self.mediaInfo = convertedMedia - case .failure(let error): - print("Error: \(error)") - } - self.isLoading = false - } - } - } -} - -struct MediaHeaderView: View { - let media: [String: Any] - - var body: some View { - HStack(alignment: .top, spacing: 16) { - if let coverDict = media["coverImage"] as? [String: Any], - let posterURLString = coverDict["extraLarge"] as? String, - let posterURL = URL(string: posterURLString) { - KFImage(posterURL) - .placeholder { - RoundedRectangle(cornerRadius: 10) - .fill(Color.gray.opacity(0.3)) - .frame(width: 150, height: 225) - .shimmering() - } - .resizable() - .aspectRatio(2/3, contentMode: .fill) - .cornerRadius(10) - .frame(width: 150, height: 225) - } - - VStack(alignment: .leading) { - if let titleDict = media["title"] as? [String: Any], - let userPreferred = titleDict["english"] as? String { - Text(userPreferred) - .font(.system(size: 17)) - .fontWeight(.bold) - .onLongPressGesture { - UIPasteboard.general.string = userPreferred - DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill")) - } - } - - if let titleDict = media["title"] as? [String: Any], - let userPreferred = titleDict["romaji"] as? String { - Text(userPreferred) - .font(.system(size: 13)) - .foregroundColor(.secondary) - } - - if let titleDict = media["title"] as? [String: Any], - let userPreferred = titleDict["native"] as? String { - Text(userPreferred) - .font(.system(size: 13)) - .foregroundColor(.secondary) - } - } - - Spacer() - } - .padding() - } -} - -struct MediaDetailsScrollView: View { - let media: [String: Any] - - var body: some View { - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 4) { - if let type = media["type"] as? String { - MediaDetailItem(title: "Type", value: type) - Divider() - } - if let episodes = media["episodes"] as? Int { - MediaDetailItem(title: "Episodes", value: "\(episodes)") - Divider() - } - if let duration = media["duration"] as? Int { - MediaDetailItem(title: "Length", value: "\(duration) mins") - Divider() - } - if let format = media["format"] as? String { - MediaDetailItem(title: "Format", value: format) - Divider() - } - if let status = media["status"] as? String { - MediaDetailItem(title: "Status", value: status) - Divider() - } - if let season = media["season"] as? String { - MediaDetailItem(title: "Season", value: season) - Divider() - } - if let startDate = media["startDate"] as? [String: Any], - let year = startDate["year"] as? Int, - let month = startDate["month"] as? Int, - let day = startDate["day"] as? Int { - MediaDetailItem(title: "Start Date", value: "\(year)-\(month)-\(day)") - Divider() - } - if let endDate = media["endDate"] as? [String: Any], - let year = endDate["year"] as? Int, - let month = endDate["month"] as? Int, - let day = endDate["day"] as? Int { - MediaDetailItem(title: "End Date", value: "\(year)-\(month)-\(day)") - } - } - } - } -} - -struct SynopsisView: View { - let synopsis: String? - - var body: some View { - if let synopsis = synopsis { - Text(synopsis.strippedHTML) - .padding(.horizontal) - .foregroundColor(.secondary) - .font(.system(size: 14)) - } else { - EmptyView() - } - } -} - -struct CharactersView: View { - let characters: [String: Any]? - - var body: some View { - if let charactersDict = characters, - let edges = charactersDict["edges"] as? [[String: Any]] { - VStack(alignment: .leading, spacing: 8) { - Text("Characters") - .font(.headline) - .padding(.horizontal) - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 8) { - ForEach(Array(edges.prefix(15).enumerated()), id: \.offset) { _, edge in - if let node = edge["node"] as? [String: Any], - let nameDict = node["name"] as? [String: Any], - let fullName = nameDict["full"] as? String, - let imageDict = node["image"] as? [String: Any], - let imageUrlStr = imageDict["large"] as? String, - let imageUrl = URL(string: imageUrlStr) { - CharacterItemView(imageUrl: imageUrl, name: fullName) - } - } - } - .padding(.horizontal) - } - } - } else { - EmptyView() - } - } -} - -struct CharacterItemView: View { - let imageUrl: URL - let name: String - - var body: some View { - VStack { - KFImage(imageUrl) - .placeholder { - Circle() - .fill(Color.gray.opacity(0.3)) - .frame(width: 90, height: 90) - .shimmering() - } - .resizable() - .scaledToFill() - .frame(width: 90, height: 90) - .clipShape(Circle()) - Text(name) - .font(.caption) - .lineLimit(1) - } - .frame(width: 105, height: 110) - } -} - -struct ScoreDistributionView: View { - let stats: [String: Any]? - - @State private var barHeights: [CGFloat] = [] - - var body: some View { - if let stats = stats, - let scoreDistribution = stats["scoreDistribution"] as? [[String: AnyHashable]] { - - let maxValue: Int = scoreDistribution.compactMap { $0["amount"] as? Int }.max() ?? 1 - - let calculatedHeights = scoreDistribution.map { dataPoint -> CGFloat in - guard let amount = dataPoint["amount"] as? Int else { return 0 } - return CGFloat(amount) / CGFloat(maxValue) * 100 - } - - VStack { - Text("Score Distribution") - .font(.headline) - HStack(alignment: .bottom) { - ForEach(Array(scoreDistribution.enumerated()), id: \.offset) { index, dataPoint in - if let score = dataPoint["score"] as? Int { - VStack { - Rectangle() - .fill(Color.accentColor) - .frame(width: 20, height: calculatedHeights[index]) - Text("\(score)") - .font(.caption) - } - } - } - } - } - .frame(maxWidth: .infinity) - .padding(.horizontal) - .onAppear { - barHeights = calculatedHeights - } - .onChange(of: scoreDistribution) { _ in - barHeights = calculatedHeights - } - } else { - EmptyView() - } - } -} - -struct MediaDetailItem: View { - var title: String - var value: String - - var body: some View { - VStack { - Text(value) - .font(.system(size: 17)) - Text(title) - .font(.system(size: 13)) - .foregroundColor(.secondary) - } - .padding(.horizontal) - } -} diff --git a/Sora/Tracking Services/AniList/MediaInfo/AniList-MediaInfo.swift b/Sora/Tracking Services/AniList/MediaInfo/AniList-MediaInfo.swift deleted file mode 100644 index 92b34b5..0000000 --- a/Sora/Tracking Services/AniList/MediaInfo/AniList-MediaInfo.swift +++ /dev/null @@ -1,135 +0,0 @@ -// -// AniList-MediaInfo.swift -// Sora -// -// Created by Francesco on 11/02/25. -// - -import Foundation - -class AnilistServiceMediaInfo { - static func fetchAnimeDetails(animeID: Int, completion: @escaping (Result<[String: Any], Error>) -> Void) { - let query = """ - query { - Media(id: \(animeID), type: ANIME) { - id - idMal - title { - romaji - english - native - userPreferred - } - type - format - status - description - startDate { - year - month - day - } - endDate { - year - month - day - } - season - episodes - duration - countryOfOrigin - isLicensed - source - hashtag - trailer { - id - site - } - updatedAt - coverImage { - extraLarge - } - bannerImage - genres - popularity - tags { - id - name - } - relations { - nodes { - id - coverImage { extraLarge } - title { userPreferred }, - mediaListEntry { status } - } - } - characters { - edges { - node { - name { - full - } - image { - large - } - } - role - voiceActors { - name { - first - last - native - } - } - } - } - siteUrl - stats { - scoreDistribution { - score - amount - } - } - airingSchedule(notYetAired: true) { - nodes { - airingAt - episode - } - } - } - } - """ - - let apiUrl = URL(string: "https://graphql.anilist.co")! - - var request = URLRequest(url: apiUrl) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.httpBody = try? JSONSerialization.data(withJSONObject: ["query": query], options: []) - - URLSession.custom.dataTask(with: request) { data, response, error in - if let error = error { - completion(.failure(error)) - return - } - - guard let data = data else { - completion(.failure(NSError(domain: "AnimeService", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data received"]))) - return - } - - do { - if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], - let data = json["data"] as? [String: Any], - let media = data["Media"] as? [String: Any] { - completion(.success(media)) - } else { - completion(.failure(NSError(domain: "AnimeService", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid response format"]))) - } - } catch { - completion(.failure(error)) - } - }.resume() - } -} diff --git a/Sora/Utils/Extensions/URL.swift b/Sora/Utils/Extensions/URL.swift new file mode 100644 index 0000000..3dc88f2 --- /dev/null +++ b/Sora/Utils/Extensions/URL.swift @@ -0,0 +1,20 @@ +// +// URL.swift +// Sulfur +// +// Created by Francesco on 23/03/25. +// + +import Foundation + +extension URL { + var queryParameters: [String: String]? { + guard let components = URLComponents(url: self, resolvingAgainstBaseURL: true), + let queryItems = components.queryItems else { return nil } + var params = [String: String]() + for queryItem in queryItems { + params[queryItem.name] = queryItem.value + } + return params + } +} diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 27a0a15..fe85c2f 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -9,8 +9,6 @@ /* 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 */; }; - 13103E842D589D8B000F0673 /* AniList-Seasonal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E832D589D8B000F0673 /* AniList-Seasonal.swift */; }; - 13103E862D58A328000F0673 /* AniList-Trending.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E852D58A328000F0673 /* AniList-Trending.swift */; }; 13103E892D58A39A000F0673 /* AniListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E882D58A39A000F0673 /* AniListItem.swift */; }; 13103E8B2D58E028000F0673 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E8A2D58E028000F0673 /* View.swift */; }; 13103E8E2D58E04A000F0673 /* SkeletonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E8D2D58E04A000F0673 /* SkeletonCell.swift */; }; @@ -38,8 +36,6 @@ 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 */; }; - 136F21B92D5B8DD8006409AC /* AniList-MediaInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136F21B82D5B8DD8006409AC /* AniList-MediaInfo.swift */; }; - 136F21BC2D5B8F29006409AC /* AniList-DetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136F21BB2D5B8F29006409AC /* AniList-DetailsView.swift */; }; 138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */; }; 138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */; }; 139935662D468C450065CEFF /* ModuleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 139935652D468C450065CEFF /* ModuleManager.swift */; }; @@ -52,6 +48,9 @@ 13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13CBEFD92D5F7D1200D011EE /* String.swift */; }; 13D842552D45267500EBBFA6 /* DropManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13D842542D45267500EBBFA6 /* DropManager.swift */; }; 13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13D99CF62D4E73C300250A86 /* ModuleAdditionSettingsView.swift */; }; + 13DB468D2D90093A008CBC03 /* Anilist-Login.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB468B2D900939008CBC03 /* Anilist-Login.swift */; }; + 13DB468E2D90093A008CBC03 /* Anilist-Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB468C2D90093A008CBC03 /* Anilist-Token.swift */; }; + 13DB46902D900A38008CBC03 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB468F2D900A38008CBC03 /* URL.swift */; }; 13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */; }; 13DB7CC62D7DC7D2004371D3 /* FFmpeg-iOS-Lame in Frameworks */ = {isa = PBXBuildFile; productRef = 13DB7CC52D7DC7D2004371D3 /* FFmpeg-iOS-Lame */; }; 13DB7CEC2D7DED5D004371D3 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */; }; @@ -68,8 +67,6 @@ 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 = ""; }; - 13103E832D589D8B000F0673 /* AniList-Seasonal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AniList-Seasonal.swift"; sourceTree = ""; }; - 13103E852D58A328000F0673 /* AniList-Trending.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AniList-Trending.swift"; sourceTree = ""; }; 13103E882D58A39A000F0673 /* AniListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AniListItem.swift; sourceTree = ""; }; 13103E8A2D58E028000F0673 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; 13103E8D2D58E04A000F0673 /* SkeletonCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkeletonCell.swift; sourceTree = ""; }; @@ -96,8 +93,6 @@ 133F55BA2D33B55100E08EEA /* LibraryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryManager.swift; sourceTree = ""; }; 1359ED132D76F49900C13034 /* finTopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = finTopView.swift; sourceTree = ""; }; 135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewPlayer.swift; sourceTree = ""; }; - 136F21B82D5B8DD8006409AC /* AniList-MediaInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AniList-MediaInfo.swift"; sourceTree = ""; }; - 136F21BB2D5B8F29006409AC /* AniList-DetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AniList-DetailsView.swift"; sourceTree = ""; }; 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeCell.swift; sourceTree = ""; }; 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.swift; sourceTree = ""; }; 139935652D468C450065CEFF /* ModuleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleManager.swift; sourceTree = ""; }; @@ -110,6 +105,9 @@ 13CBEFD92D5F7D1200D011EE /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 13D842542D45267500EBBFA6 /* DropManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropManager.swift; sourceTree = ""; }; 13D99CF62D4E73C300250A86 /* ModuleAdditionSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleAdditionSettingsView.swift; sourceTree = ""; }; + 13DB468B2D900939008CBC03 /* Anilist-Login.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Anilist-Login.swift"; sourceTree = ""; }; + 13DB468C2D90093A008CBC03 /* Anilist-Token.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Anilist-Token.swift"; sourceTree = ""; }; + 13DB468F2D900A38008CBC03 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.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 = ""; }; @@ -148,23 +146,12 @@ 13103E812D589D77000F0673 /* AniList */ = { isa = PBXGroup; children = ( - 136F21B72D5B8DAC006409AC /* MediaInfo */, + 13DB468A2D900919008CBC03 /* Auth */, 13103E872D58A392000F0673 /* Struct */, - 13103E822D589D7D000F0673 /* HomePage */, ); path = AniList; sourceTree = ""; }; - 13103E822D589D7D000F0673 /* HomePage */ = { - isa = PBXGroup; - children = ( - 136F21BA2D5B8F17006409AC /* DetailsView */, - 13103E832D589D8B000F0673 /* AniList-Seasonal.swift */, - 13103E852D58A328000F0673 /* AniList-Trending.swift */, - ); - path = HomePage; - sourceTree = ""; - }; 13103E872D58A392000F0673 /* Struct */ = { isa = PBXGroup; children = ( @@ -317,6 +304,7 @@ 1359ED132D76F49900C13034 /* finTopView.swift */, 13CBEFD92D5F7D1200D011EE /* String.swift */, 13103E8A2D58E028000F0673 /* View.swift */, + 13DB468F2D900A38008CBC03 /* URL.swift */, ); path = Extensions; sourceTree = ""; @@ -348,22 +336,6 @@ path = LibraryView; sourceTree = ""; }; - 136F21B72D5B8DAC006409AC /* MediaInfo */ = { - isa = PBXGroup; - children = ( - 136F21B82D5B8DD8006409AC /* AniList-MediaInfo.swift */, - ); - path = MediaInfo; - sourceTree = ""; - }; - 136F21BA2D5B8F17006409AC /* DetailsView */ = { - isa = PBXGroup; - children = ( - 136F21BB2D5B8F29006409AC /* AniList-DetailsView.swift */, - ); - path = DetailsView; - sourceTree = ""; - }; 1384DCDF2D89BE870094797A /* Helpers */ = { isa = PBXGroup; children = ( @@ -416,6 +388,15 @@ path = Drops; sourceTree = ""; }; + 13DB468A2D900919008CBC03 /* Auth */ = { + isa = PBXGroup; + children = ( + 13DB468B2D900939008CBC03 /* Anilist-Login.swift */, + 13DB468C2D90093A008CBC03 /* Anilist-Token.swift */, + ); + path = Auth; + sourceTree = ""; + }; 13DB7CEA2D7DED50004371D3 /* DownloadManager */ = { isa = PBXGroup; children = ( @@ -542,12 +523,12 @@ 133D7C902D2BE2640075467E /* SettingsView.swift in Sources */, 1334FF4F2D786C9E007E289F /* TMDB-Trending.swift in Sources */, 13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */, + 13DB46902D900A38008CBC03 /* URL.swift in Sources */, 130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */, 1334FF542D787217007E289F /* TMDBRequest.swift in Sources */, 1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */, 13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */, 133D7C932D2BE2640075467E /* Modules.swift in Sources */, - 136F21B92D5B8DD8006409AC /* AniList-MediaInfo.swift in Sources */, 13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */, 133D7C702D2BE2500075467E /* ContentView.swift in Sources */, 13DB7CEC2D7DED5D004371D3 /* DownloadManager.swift in Sources */, @@ -555,7 +536,6 @@ 13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */, 13C0E5EC2D5F85F800E7F619 /* ContinueWatchingItem.swift in Sources */, 13CBA0882D60F19C00EFE70A /* VTTSubtitlesLoader.swift in Sources */, - 13103E862D58A328000F0673 /* AniList-Trending.swift in Sources */, 13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */, 133D7C8F2D2BE2640075467E /* MediaInfoView.swift in Sources */, 13103E892D58A39A000F0673 /* AniListItem.swift in Sources */, @@ -566,18 +546,18 @@ 1327FBA92D758DEA00FC6689 /* UIDevice+Model.swift in Sources */, 138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */, 133D7C8C2D2BE2640075467E /* SearchView.swift in Sources */, + 13DB468E2D90093A008CBC03 /* Anilist-Token.swift in Sources */, 1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */, 133D7C942D2BE2640075467E /* JSController.swift in Sources */, 133D7C922D2BE2640075467E /* URLSession.swift in Sources */, 133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */, - 136F21BC2D5B8F29006409AC /* AniList-DetailsView.swift in Sources */, 130217CC2D81C55E0011EFF5 /* DownloadView.swift in Sources */, 133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */, - 13103E842D589D8B000F0673 /* AniList-Seasonal.swift in Sources */, 133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */, 133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */, 138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */, 1334FF4D2D786C93007E289F /* TMDB-Seasonal.swift in Sources */, + 13DB468D2D90093A008CBC03 /* Anilist-Login.swift in Sources */, 73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */, 13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */, 1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */, From 59b5f84077ed375af70aef04cf9eb8530c737f9c Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Sun, 23 Mar 2025 10:58:32 +0100 Subject: [PATCH 17/41] trackers page --- .../SettingsViewTrackers.swift | 194 ++++++++++++++++++ Sora/Views/SettingsView/SettingsView.swift | 3 + Sulfur.xcodeproj/project.pbxproj | 4 + 3 files changed, 201 insertions(+) create mode 100644 Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift new file mode 100644 index 0000000..ec75678 --- /dev/null +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift @@ -0,0 +1,194 @@ +// +// SettingsViewTrackers.swift +// Sora +// +// Created by Francesco on 23/03/25. +// + +import SwiftUI +import Security +import Kingfisher + +struct SettingsViewTrackers: View { + @State private var status: String = "You are not logged in" + @State private var isLoggedIn: Bool = false + @State private var username: String = "" + @State private var isLoading: Bool = false + @State private var profileColor: Color = .primary + + var body: some View { + Form { + Section(header: Text("AniList"), footer: Text("Sora and cranci1 are not affiliated with AniList in any way.")) { + HStack() { + KFImage(URL(string: "https://raw.githubusercontent.com/cranci1/Ryu/2f10226aa087154974a70c1ec78aa83a47daced9/Ryu/Assets.xcassets/Listing/Anilist.imageset/anilist.png")) + .resizable() + .frame(width: 80, height: 80) + .clipShape(Rectangle()) + Text("AniList.co") + .font(.title2) + } + if isLoading { + ProgressView() + } else { + if isLoggedIn { + HStack(spacing: 0) { + Text("Logged in as ") + Text(username) + .foregroundColor(profileColor) + .fontWeight(.semibold) + } + } else { + Text(status) + .multilineTextAlignment(.center) + } + } + Button(isLoggedIn ? "Log Out from AniList.co" : "Log In with AniList.co") { + if isLoggedIn { + logout() + } else { + login() + } + } + } + } + .navigationTitle("Trackers") + .onAppear { + updateStatus() + } + } + + func login() { + status = "Starting authentication..." + AniListLogin.authenticate() + } + + func logout() { + removeTokenFromKeychain() + status = "You are not logged in" + isLoggedIn = false + username = "" + profileColor = .primary + } + + func updateStatus() { + if let token = getTokenFromKeychain() { + isLoggedIn = true + fetchUserInfo(token: token) + } else { + isLoggedIn = false + status = "You are not logged in" + } + } + + func fetchUserInfo(token: String) { + isLoading = true + let userInfoURL = URL(string: "https://graphql.anilist.co")! + var request = URLRequest(url: userInfoURL) + request.httpMethod = "POST" + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let query = """ + { + Viewer { + id + name + options { + profileColor + } + } + } + """ + let body: [String: Any] = ["query": query] + + do { + request.httpBody = try JSONSerialization.data(withJSONObject: body, options: []) + } catch { + status = "Failed to serialize request" + Logger.shared.log("Failed to serialize request", type: "Error") + isLoading = false + return + } + + URLSession.shared.dataTask(with: request) { data, response, error in + DispatchQueue.main.async { + isLoading = false + if let error = error { + status = "Error: \(error.localizedDescription)" + Logger.shared.log("Error: \(error.localizedDescription)", type: "Error") + return + } + guard let data = data else { + status = "No data received" + Logger.shared.log("No data received", type: "Error") + return + } + do { + if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], + let dataDict = json["data"] as? [String: Any], + let viewer = dataDict["Viewer"] as? [String: Any], + let name = viewer["name"] as? String, + let options = viewer["options"] as? [String: Any], + let colorName = options["profileColor"] as? String { + + username = name + profileColor = colorFromName(colorName) + status = "Logged in as \(name)" + } else { + status = "Unexpected response format!" + Logger.shared.log("Unexpected response format!", type: "Error") + } + } catch { + status = "Failed to parse response: \(error.localizedDescription)" + Logger.shared.log("Failed to parse response: \(error.localizedDescription)", type: "Error") + } + } + }.resume() + } + + func colorFromName(_ name: String) -> Color { + switch name.lowercased() { + case "blue": + return .blue + case "purple": + return .purple + case "green": + return .green + case "orange": + return .orange + case "red": + return .red + case "pink": + return .pink + case "gray": + return .gray + default: + return .accentColor + } + } + + func getTokenFromKeychain() -> String? { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: "me.cranci.sora.AniListToken", + kSecAttrAccount as String: "AniListAccessToken", + kSecReturnData as String: kCFBooleanTrue!, + kSecMatchLimit as String: kSecMatchLimitOne + ] + var item: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &item) + guard status == errSecSuccess, let tokenData = item as? Data else { + return nil + } + return String(data: tokenData, encoding: .utf8) + } + + func removeTokenFromKeychain() { + let deleteQuery: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: "me.cranci.sora.AniListToken", + kSecAttrAccount as String: "AniListAccessToken" + ] + SecItemDelete(deleteQuery as CFDictionary) + } +} diff --git a/Sora/Views/SettingsView/SettingsView.swift b/Sora/Views/SettingsView/SettingsView.swift index ee43405..f7f6ec0 100644 --- a/Sora/Views/SettingsView/SettingsView.swift +++ b/Sora/Views/SettingsView/SettingsView.swift @@ -21,6 +21,9 @@ struct SettingsView: View { NavigationLink(destination: SettingsViewModule()) { Text("Modules") } + NavigationLink(destination: SettingsViewTrackers()) { + Text("Trackers") + } } Section(header: Text("Info")) { diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index fe85c2f..c38b27b 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -51,6 +51,7 @@ 13DB468D2D90093A008CBC03 /* Anilist-Login.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB468B2D900939008CBC03 /* Anilist-Login.swift */; }; 13DB468E2D90093A008CBC03 /* Anilist-Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB468C2D90093A008CBC03 /* Anilist-Token.swift */; }; 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 */; }; 13DB7CC62D7DC7D2004371D3 /* FFmpeg-iOS-Lame in Frameworks */ = {isa = PBXBuildFile; productRef = 13DB7CC52D7DC7D2004371D3 /* FFmpeg-iOS-Lame */; }; 13DB7CEC2D7DED5D004371D3 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */; }; @@ -108,6 +109,7 @@ 13DB468B2D900939008CBC03 /* Anilist-Login.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Anilist-Login.swift"; sourceTree = ""; }; 13DB468C2D90093A008CBC03 /* Anilist-Token.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Anilist-Token.swift"; sourceTree = ""; }; 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 = ""; }; @@ -274,6 +276,7 @@ 131845F82D47C62D00CA7A54 /* SettingsViewGeneral.swift */, 135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */, 130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */, + 13DB46912D900BCE008CBC03 /* SettingsViewTrackers.swift */, ); path = SettingsSubViews; sourceTree = ""; @@ -561,6 +564,7 @@ 73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */, 13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */, 1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */, + 13DB46922D900BCE008CBC03 /* SettingsViewTrackers.swift in Sources */, 13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; From 3661e601e59d0b171a26cfa0976014642d67aeb1 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Sun, 23 Mar 2025 18:01:01 +0100 Subject: [PATCH 18/41] Update SettingsViewTrackers.swift --- .../SettingsSubViews/SettingsViewTrackers.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift index ec75678..6727051 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift @@ -14,16 +14,23 @@ struct SettingsViewTrackers: View { @State private var isLoggedIn: Bool = false @State private var username: String = "" @State private var isLoading: Bool = false - @State private var profileColor: Color = .primary + @State private var profileColor: Color = .accentColor var body: some View { Form { Section(header: Text("AniList"), footer: Text("Sora and cranci1 are not affiliated with AniList in any way.")) { HStack() { KFImage(URL(string: "https://raw.githubusercontent.com/cranci1/Ryu/2f10226aa087154974a70c1ec78aa83a47daced9/Ryu/Assets.xcassets/Listing/Anilist.imageset/anilist.png")) + .placeholder { + RoundedRectangle(cornerRadius: 10) + .fill(Color.gray.opacity(0.3)) + .frame(width: 80, height: 80) + .shimmering() + } .resizable() .frame(width: 80, height: 80) .clipShape(Rectangle()) + .cornerRadius(10) Text("AniList.co") .font(.title2) } @@ -35,6 +42,7 @@ struct SettingsViewTrackers: View { Text("Logged in as ") Text(username) .foregroundColor(profileColor) + .font(.body) .fontWeight(.semibold) } } else { @@ -49,6 +57,7 @@ struct SettingsViewTrackers: View { login() } } + .font(.body) } } .navigationTitle("Trackers") From f030f65ce0713014ba5c216d71bb711975bc6130 Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Mon, 24 Mar 2025 16:31:01 +0100 Subject: [PATCH 19/41] =?UTF-8?q?idk=20if=20this=20uses=201.1.1.1=20DNS=20?= =?UTF-8?q?=F0=9F=98=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions/JavaScriptCore+Extensions.swift | 4 ++-- Sora/Utils/Extensions/URLSession.swift | 17 +++++++++++++++++ Sora/Utils/JSLoader/JSController.swift | 8 ++++---- .../Helpers/VTTSubtitlesLoader.swift | 2 +- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift b/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift index 71e9f9d..553a7e7 100644 --- a/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift +++ b/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift @@ -45,7 +45,7 @@ extension JSContext { request.setValue(value, forHTTPHeaderField: key) } } - let task = URLSession.custom.dataTask(with: request) { data, _, error in + let task = URLSession.cloudflareCustom.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]) @@ -90,7 +90,7 @@ extension JSContext { request.setValue(value, forHTTPHeaderField: key) } } - let task = URLSession.custom.dataTask(with: request) { data, response, error in + let task = URLSession.cloudflareCustom.dataTask(with: request) { data, response, error in if let error = error { Logger.shared.log("Network error in fetchV2NativeFunction: \(error.localizedDescription)", type: "Error") reject.call(withArguments: [error.localizedDescription]) diff --git a/Sora/Utils/Extensions/URLSession.swift b/Sora/Utils/Extensions/URLSession.swift index cb1862d..9144c9f 100644 --- a/Sora/Utils/Extensions/URLSession.swift +++ b/Sora/Utils/Extensions/URLSession.swift @@ -6,6 +6,7 @@ // import Foundation +import Network extension URLSession { static let userAgents = [ @@ -45,4 +46,20 @@ extension URLSession { ] 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 + return URLSession(configuration: configuration) + }() } diff --git a/Sora/Utils/JSLoader/JSController.swift b/Sora/Utils/JSLoader/JSController.swift index a0a220b..6cf5e82 100644 --- a/Sora/Utils/JSLoader/JSController.swift +++ b/Sora/Utils/JSLoader/JSController.swift @@ -36,7 +36,7 @@ class JSController: ObservableObject { return } - URLSession.custom.dataTask(with: url) { [weak self] data, _, error in + URLSession.cloudflareCustom.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.custom.dataTask(with: url) { [weak self] data, _, error in + URLSession.cloudflareCustom.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.custom.dataTask(with: url) { [weak self] data, _, error in + URLSession.cloudflareCustom.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.custom.dataTask(with: url) { data, response, error in + let task = URLSession.cloudflareCustom.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/Helpers/VTTSubtitlesLoader.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Helpers/VTTSubtitlesLoader.swift index 37b98d4..944f2de 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.custom.dataTask(with: url) { data, _, error in + URLSession.cloudflareCustom.dataTask(with: url) { data, _, error in guard let data = data, let content = String(data: data, encoding: .utf8), error == nil else { return } From a5c71c674376483ffe4d24e86e3c31d6ba65c93b Mon Sep 17 00:00:00 2001 From: Hamzenis Kryeziu Date: Tue, 25 Mar 2025 01:10:59 +0100 Subject: [PATCH 20/41] Added POST, PUT, PATCH Support - "fetchv2" javascript function now supports proper methods, header and body support - Backwards compatibility with previous implementation - Defaults to GET if only url is provided --- .../JavaScriptCore+Extensions.swift | 124 +++++++++++++----- 1 file changed, 93 insertions(+), 31 deletions(-) diff --git a/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift b/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift index 553a7e7..947350c 100644 --- a/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift +++ b/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift @@ -78,65 +78,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") + + // Ensure no body for GET requests + 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 + } + + // Set the body for non-GET requests + if httpMethod != "GET", let body = body, !body.isEmpty, body != "null", body != "undefined" { + request.httpBody = body.data(using: .utf8) + } + + + // Set headers 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.cloudflareCustom.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) + + // Check response size before processing + if data.count > 10_000_000 { // Example: 10MB limit + 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) } From 5826f89866103583a8aad1b5708979c2046451dd Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Tue, 25 Mar 2025 16:04:26 +0100 Subject: [PATCH 21/41] yeah idk tf is this --- .../AccentColor.colorset/Contents.json | 12 +- Sora/ContentView.swift | 32 +- Sora/Views/OnBoardingView.swift | 295 ++++++++++++++++++ .../SettingsViewGeneral.swift | 1 - Sulfur.xcodeproj/project.pbxproj | 4 + 5 files changed, 329 insertions(+), 15 deletions(-) create mode 100644 Sora/Views/OnBoardingView.swift diff --git a/Sora/Assets.xcassets/AccentColor.colorset/Contents.json b/Sora/Assets.xcassets/AccentColor.colorset/Contents.json index bb57667..929c9c5 100644 --- a/Sora/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Sora/Assets.xcassets/AccentColor.colorset/Contents.json @@ -2,8 +2,13 @@ "colors" : [ { "color" : { - "platform" : "universal", - "reference" : "systemMintColor" + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.231", + "green" : "0.620", + "red" : "0.976" + } }, "idiom" : "universal" } @@ -11,5 +16,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "localizable" : true } } diff --git a/Sora/ContentView.swift b/Sora/ContentView.swift index afbd4ea..6f7b2f2 100644 --- a/Sora/ContentView.swift +++ b/Sora/ContentView.swift @@ -9,20 +9,28 @@ import SwiftUI import Kingfisher struct ContentView: View { + @AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding: Bool = false + var body: some View { - TabView { - LibraryView() - .tabItem { - Label("Library", systemImage: "books.vertical") - } - SearchView() - .tabItem { - Label("Search", systemImage: "magnifyingglass") - } - SettingsView() - .tabItem { - Label("Settings", systemImage: "gear") + Group { + if !hasCompletedOnboarding { + OnboardingView(hasCompletedOnboarding: $hasCompletedOnboarding) + } else { + TabView { + LibraryView() + .tabItem { + Label("Library", systemImage: "books.vertical") + } + SearchView() + .tabItem { + Label("Search", systemImage: "magnifyingglass") + } + SettingsView() + .tabItem { + Label("Settings", systemImage: "gear") + } } + } } } } diff --git a/Sora/Views/OnBoardingView.swift b/Sora/Views/OnBoardingView.swift new file mode 100644 index 0000000..243b825 --- /dev/null +++ b/Sora/Views/OnBoardingView.swift @@ -0,0 +1,295 @@ +// +// OnBoardingView.swift +// Sulfur +// +// Created by Francesco on 25/03/25. +// + +import SwiftUI + +struct OnboardingView: View { + @Binding var hasCompletedOnboarding: Bool + @State private var currentPage = 0 + + @EnvironmentObject var settings: Settings + + private var totalPages: Int { + onboardingScreens.count + 1 + } + + var body: some View { + ZStack { + Color(.systemBackground) + .edgesIgnoringSafeArea(.all) + + VStack { + HStack { + Spacer() + Button(action: { + withAnimation { + hasCompletedOnboarding = true + UserDefaults.standard.set(true, forKey: "hasCompletedOnboarding") + } + }) { + Text("Skip") + .foregroundColor(.accentColor) + .padding() + } + } + + TabView(selection: $currentPage) { + ForEach(0.. 0 { + Button(action: { + withAnimation { currentPage -= 1 } + }) { + Text("Back") + } + .buttonStyle(PlainButtonStyle()) + .padding() + } + Spacer() + Button(action: { + withAnimation { + if currentPage == totalPages - 1 { + hasCompletedOnboarding = true + UserDefaults.standard.set(true, forKey: "hasCompletedOnboarding") + } else { + currentPage += 1 + } + } + }) { + Text(currentPage == totalPages - 1 ? "Get Started" : "Continue") + } + .buttonStyle(FilledButtonStyle()) + } + .padding(.horizontal) + } + } + } +} + +struct PageIndicatorView: View { + let currentPage: Int + let totalPages: Int + + var body: some View { + HStack(spacing: 8) { + ForEach(0.. some View { + configuration.label + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding() + .background(Color.accentColor) + .cornerRadius(12) + .scaleEffect(configuration.isPressed ? 0.95 : 1.0) + .animation(.spring(), value: configuration.isPressed) + } +} + +struct OnboardingPage { + let imageName: String + let title: String + let description: String +} + +let onboardingScreens = [ + OnboardingPage( + imageName: "puzzlepiece.fill", + title: "Modular Web Scraping", + description: "Sora is a powerful, open-source web scraping app that works exclusively with custom modules." + ), + OnboardingPage( + imageName: "display", + title: "Multi-Platform Support", + description: "Enjoy Sora on iOS, iPadOS 15.0+ and macOS 12.0+. A flexible app for all your devices." + ), + OnboardingPage( + imageName: "play.circle.fill", + title: "Diverse Media Playback", + description: "Stream content from Jellyfin/Plex servers or any module and play media in external players like VLC, Infuse, and nPlayer or directly with the Sora or iOS player" + ), + OnboardingPage( + imageName: "lock.shield.fill", + title: "Privacy First", + description: "No subscriptions, no logins, no data collection. Sora prioritizes your privacy and will always be free and open source under the GPLv3.0 License." + ) +] + +struct OnboardingCustomizeAppearanceView: View { + @EnvironmentObject var settings: Settings + + @AppStorage("alwaysLandscape") private var isAlwaysLandscape = false + + @AppStorage("externalPlayer") private var externalPlayer: String = "Sora" + + @AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2 + @AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4 + + private let mediaPlayers = ["Default", "VLC", "OutPlayer", "Infuse", "nPlayer", "Sora"] + + var body: some View { + VStack { + Text("Customize Sora") + .font(.largeTitle) + .fontWeight(.bold) + .multilineTextAlignment(.center) + .padding(.top, 20) + + VStack(spacing: 20) { + SettingsSection(title: "Theme") { + ColorPicker("Accent Color", selection: $settings.accentColor) + + Picker("Appearance", selection: $settings.selectedAppearance) { + Text("System").tag(Appearance.system) + Text("Light").tag(Appearance.light) + Text("Dark").tag(Appearance.dark) + } + .pickerStyle(SegmentedPickerStyle()) + } + + SettingsSection(title: "Media Player") { + HStack { + Text("Media Player") + Spacer() + Menu(externalPlayer) { + ForEach(mediaPlayers, id: \.self) { provider in + Button(action: { + externalPlayer = provider + }) { + Text(provider) + } + } + } + } + + Toggle("Force Landscape", isOn: $isAlwaysLandscape) + .tint(.accentColor) + } + + SettingsSection(title: "Grid Layout") { + HStack { + Text("Portrait Columns") + Spacer() + Picker("", selection: $mediaColumnsPortrait) { + if UIDevice.current.userInterfaceIdiom == .pad { + ForEach(1..<6) { i in + Text("\(i)").tag(i) + } + } else { + ForEach(1..<5) { i in + Text("\(i)").tag(i) + } + } + } + .pickerStyle(MenuPickerStyle()) + .labelsHidden() + } + + HStack { + Text("Landscape Columns") + Spacer() + Picker("", selection: $mediaColumnsLandscape) { + if UIDevice.current.userInterfaceIdiom == .pad { + ForEach(2..<9) { i in + Text("\(i)").tag(i) + } + } else { + ForEach(2..<6) { i in + Text("\(i)").tag(i) + } + } + } + .pickerStyle(MenuPickerStyle()) + .labelsHidden() + } + } + } + .padding() + .background( + RoundedRectangle(cornerRadius: 20, style: .continuous) + .fill(Color(UIColor.secondarySystemBackground)) + .shadow(color: Color.black.opacity(0.1), radius: 10, x: 0, y: 5) + ) + .padding(.horizontal) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +struct SettingsSection: View { + let title: String + let content: Content + + init(title: String, @ViewBuilder content: () -> Content) { + self.title = title + self.content = content() + } + + var body: some View { + VStack(alignment: .center, spacing: 10) { + Text(title + ":") + .font(.headline) + + content + + Divider() + } + } +} diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index ed0f83e..b3b38f4 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -70,7 +70,6 @@ struct SettingsViewGeneral: View { } } } - } } diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index c38b27b..92dd81e 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -41,6 +41,7 @@ 139935662D468C450065CEFF /* ModuleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 139935652D468C450065CEFF /* ModuleManager.swift */; }; 1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1399FAD32D3AB38C00E97C31 /* SettingsViewLogger.swift */; }; 1399FAD62D3AB3DB00E97C31 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1399FAD52D3AB3DB00E97C31 /* Logger.swift */; }; + 13ADAE022D92ED46007DCE7D /* OnBoardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13ADAE012D92ED46007DCE7D /* OnBoardingView.swift */; }; 13B7F4C12D58FFDD0045714A /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13B7F4C02D58FFDD0045714A /* Shimmer.swift */; }; 13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13C0E5E92D5F85EA00E7F619 /* ContinueWatchingManager.swift */; }; 13C0E5EC2D5F85F800E7F619 /* ContinueWatchingItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13C0E5EB2D5F85F800E7F619 /* ContinueWatchingItem.swift */; }; @@ -99,6 +100,7 @@ 139935652D468C450065CEFF /* ModuleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleManager.swift; sourceTree = ""; }; 1399FAD32D3AB38C00E97C31 /* SettingsViewLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewLogger.swift; sourceTree = ""; }; 1399FAD52D3AB3DB00E97C31 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + 13ADAE012D92ED46007DCE7D /* OnBoardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnBoardingView.swift; sourceTree = ""; }; 13B7F4C02D58FFDD0045714A /* Shimmer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shimmer.swift; sourceTree = ""; }; 13C0E5E92D5F85EA00E7F619 /* ContinueWatchingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingManager.swift; sourceTree = ""; }; 13C0E5EB2D5F85F800E7F619 /* ContinueWatchingItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingItem.swift; sourceTree = ""; }; @@ -254,6 +256,7 @@ 133F55B92D33B53E00E08EEA /* LibraryView */, 133D7C7C2D2BE2630075467E /* SearchView.swift */, 130217CB2D81C55E0011EFF5 /* DownloadView.swift */, + 13ADAE012D92ED46007DCE7D /* OnBoardingView.swift */, ); path = Views; sourceTree = ""; @@ -525,6 +528,7 @@ 139935662D468C450065CEFF /* ModuleManager.swift in Sources */, 133D7C902D2BE2640075467E /* SettingsView.swift in Sources */, 1334FF4F2D786C9E007E289F /* TMDB-Trending.swift in Sources */, + 13ADAE022D92ED46007DCE7D /* OnBoardingView.swift in Sources */, 13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */, 13DB46902D900A38008CBC03 /* URL.swift in Sources */, 130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */, From 43c0509b9339a10efadf837e6b236a9779855e21 Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Tue, 25 Mar 2025 16:30:41 +0100 Subject: [PATCH 22/41] less stuffs --- Sora/Views/OnBoardingView.swift | 42 --------------------------------- 1 file changed, 42 deletions(-) diff --git a/Sora/Views/OnBoardingView.swift b/Sora/Views/OnBoardingView.swift index 243b825..e07b7dd 100644 --- a/Sora/Views/OnBoardingView.swift +++ b/Sora/Views/OnBoardingView.swift @@ -178,10 +178,6 @@ struct OnboardingCustomizeAppearanceView: View { @AppStorage("alwaysLandscape") private var isAlwaysLandscape = false @AppStorage("externalPlayer") private var externalPlayer: String = "Sora" - - @AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2 - @AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4 - private let mediaPlayers = ["Default", "VLC", "OutPlayer", "Infuse", "nPlayer", "Sora"] var body: some View { @@ -222,44 +218,6 @@ struct OnboardingCustomizeAppearanceView: View { Toggle("Force Landscape", isOn: $isAlwaysLandscape) .tint(.accentColor) } - - SettingsSection(title: "Grid Layout") { - HStack { - Text("Portrait Columns") - Spacer() - Picker("", selection: $mediaColumnsPortrait) { - if UIDevice.current.userInterfaceIdiom == .pad { - ForEach(1..<6) { i in - Text("\(i)").tag(i) - } - } else { - ForEach(1..<5) { i in - Text("\(i)").tag(i) - } - } - } - .pickerStyle(MenuPickerStyle()) - .labelsHidden() - } - - HStack { - Text("Landscape Columns") - Spacer() - Picker("", selection: $mediaColumnsLandscape) { - if UIDevice.current.userInterfaceIdiom == .pad { - ForEach(2..<9) { i in - Text("\(i)").tag(i) - } - } else { - ForEach(2..<6) { i in - Text("\(i)").tag(i) - } - } - } - .pickerStyle(MenuPickerStyle()) - .labelsHidden() - } - } } .padding() .background( From e06751d458f9bfcb991bcaa5b4de1ec7597750e9 Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Wed, 26 Mar 2025 15:06:07 +0100 Subject: [PATCH 23/41] Revert "less stuffs" This reverts commit 43c0509b9339a10efadf837e6b236a9779855e21. --- Sora/Views/OnBoardingView.swift | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/Sora/Views/OnBoardingView.swift b/Sora/Views/OnBoardingView.swift index e07b7dd..243b825 100644 --- a/Sora/Views/OnBoardingView.swift +++ b/Sora/Views/OnBoardingView.swift @@ -178,6 +178,10 @@ struct OnboardingCustomizeAppearanceView: View { @AppStorage("alwaysLandscape") private var isAlwaysLandscape = false @AppStorage("externalPlayer") private var externalPlayer: String = "Sora" + + @AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2 + @AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4 + private let mediaPlayers = ["Default", "VLC", "OutPlayer", "Infuse", "nPlayer", "Sora"] var body: some View { @@ -218,6 +222,44 @@ struct OnboardingCustomizeAppearanceView: View { Toggle("Force Landscape", isOn: $isAlwaysLandscape) .tint(.accentColor) } + + SettingsSection(title: "Grid Layout") { + HStack { + Text("Portrait Columns") + Spacer() + Picker("", selection: $mediaColumnsPortrait) { + if UIDevice.current.userInterfaceIdiom == .pad { + ForEach(1..<6) { i in + Text("\(i)").tag(i) + } + } else { + ForEach(1..<5) { i in + Text("\(i)").tag(i) + } + } + } + .pickerStyle(MenuPickerStyle()) + .labelsHidden() + } + + HStack { + Text("Landscape Columns") + Spacer() + Picker("", selection: $mediaColumnsLandscape) { + if UIDevice.current.userInterfaceIdiom == .pad { + ForEach(2..<9) { i in + Text("\(i)").tag(i) + } + } else { + ForEach(2..<6) { i in + Text("\(i)").tag(i) + } + } + } + .pickerStyle(MenuPickerStyle()) + .labelsHidden() + } + } } .padding() .background( From 0b9dae6b98b04e600bd049de2e4c344833b8d2d7 Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Wed, 26 Mar 2025 15:06:10 +0100 Subject: [PATCH 24/41] Revert "yeah idk tf is this" This reverts commit 5826f89866103583a8aad1b5708979c2046451dd. --- .../AccentColor.colorset/Contents.json | 12 +- Sora/ContentView.swift | 32 +- Sora/Views/OnBoardingView.swift | 295 ------------------ .../SettingsViewGeneral.swift | 1 + Sulfur.xcodeproj/project.pbxproj | 4 - 5 files changed, 15 insertions(+), 329 deletions(-) delete mode 100644 Sora/Views/OnBoardingView.swift diff --git a/Sora/Assets.xcassets/AccentColor.colorset/Contents.json b/Sora/Assets.xcassets/AccentColor.colorset/Contents.json index 929c9c5..bb57667 100644 --- a/Sora/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Sora/Assets.xcassets/AccentColor.colorset/Contents.json @@ -2,13 +2,8 @@ "colors" : [ { "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.231", - "green" : "0.620", - "red" : "0.976" - } + "platform" : "universal", + "reference" : "systemMintColor" }, "idiom" : "universal" } @@ -16,8 +11,5 @@ "info" : { "author" : "xcode", "version" : 1 - }, - "properties" : { - "localizable" : true } } diff --git a/Sora/ContentView.swift b/Sora/ContentView.swift index 6f7b2f2..afbd4ea 100644 --- a/Sora/ContentView.swift +++ b/Sora/ContentView.swift @@ -9,28 +9,20 @@ import SwiftUI import Kingfisher struct ContentView: View { - @AppStorage("hasCompletedOnboarding") private var hasCompletedOnboarding: Bool = false - var body: some View { - Group { - if !hasCompletedOnboarding { - OnboardingView(hasCompletedOnboarding: $hasCompletedOnboarding) - } else { - TabView { - LibraryView() - .tabItem { - Label("Library", systemImage: "books.vertical") - } - SearchView() - .tabItem { - Label("Search", systemImage: "magnifyingglass") - } - SettingsView() - .tabItem { - Label("Settings", systemImage: "gear") - } + TabView { + LibraryView() + .tabItem { + Label("Library", systemImage: "books.vertical") + } + SearchView() + .tabItem { + Label("Search", systemImage: "magnifyingglass") + } + SettingsView() + .tabItem { + Label("Settings", systemImage: "gear") } - } } } } diff --git a/Sora/Views/OnBoardingView.swift b/Sora/Views/OnBoardingView.swift deleted file mode 100644 index 243b825..0000000 --- a/Sora/Views/OnBoardingView.swift +++ /dev/null @@ -1,295 +0,0 @@ -// -// OnBoardingView.swift -// Sulfur -// -// Created by Francesco on 25/03/25. -// - -import SwiftUI - -struct OnboardingView: View { - @Binding var hasCompletedOnboarding: Bool - @State private var currentPage = 0 - - @EnvironmentObject var settings: Settings - - private var totalPages: Int { - onboardingScreens.count + 1 - } - - var body: some View { - ZStack { - Color(.systemBackground) - .edgesIgnoringSafeArea(.all) - - VStack { - HStack { - Spacer() - Button(action: { - withAnimation { - hasCompletedOnboarding = true - UserDefaults.standard.set(true, forKey: "hasCompletedOnboarding") - } - }) { - Text("Skip") - .foregroundColor(.accentColor) - .padding() - } - } - - TabView(selection: $currentPage) { - ForEach(0.. 0 { - Button(action: { - withAnimation { currentPage -= 1 } - }) { - Text("Back") - } - .buttonStyle(PlainButtonStyle()) - .padding() - } - Spacer() - Button(action: { - withAnimation { - if currentPage == totalPages - 1 { - hasCompletedOnboarding = true - UserDefaults.standard.set(true, forKey: "hasCompletedOnboarding") - } else { - currentPage += 1 - } - } - }) { - Text(currentPage == totalPages - 1 ? "Get Started" : "Continue") - } - .buttonStyle(FilledButtonStyle()) - } - .padding(.horizontal) - } - } - } -} - -struct PageIndicatorView: View { - let currentPage: Int - let totalPages: Int - - var body: some View { - HStack(spacing: 8) { - ForEach(0.. some View { - configuration.label - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .padding() - .background(Color.accentColor) - .cornerRadius(12) - .scaleEffect(configuration.isPressed ? 0.95 : 1.0) - .animation(.spring(), value: configuration.isPressed) - } -} - -struct OnboardingPage { - let imageName: String - let title: String - let description: String -} - -let onboardingScreens = [ - OnboardingPage( - imageName: "puzzlepiece.fill", - title: "Modular Web Scraping", - description: "Sora is a powerful, open-source web scraping app that works exclusively with custom modules." - ), - OnboardingPage( - imageName: "display", - title: "Multi-Platform Support", - description: "Enjoy Sora on iOS, iPadOS 15.0+ and macOS 12.0+. A flexible app for all your devices." - ), - OnboardingPage( - imageName: "play.circle.fill", - title: "Diverse Media Playback", - description: "Stream content from Jellyfin/Plex servers or any module and play media in external players like VLC, Infuse, and nPlayer or directly with the Sora or iOS player" - ), - OnboardingPage( - imageName: "lock.shield.fill", - title: "Privacy First", - description: "No subscriptions, no logins, no data collection. Sora prioritizes your privacy and will always be free and open source under the GPLv3.0 License." - ) -] - -struct OnboardingCustomizeAppearanceView: View { - @EnvironmentObject var settings: Settings - - @AppStorage("alwaysLandscape") private var isAlwaysLandscape = false - - @AppStorage("externalPlayer") private var externalPlayer: String = "Sora" - - @AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2 - @AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4 - - private let mediaPlayers = ["Default", "VLC", "OutPlayer", "Infuse", "nPlayer", "Sora"] - - var body: some View { - VStack { - Text("Customize Sora") - .font(.largeTitle) - .fontWeight(.bold) - .multilineTextAlignment(.center) - .padding(.top, 20) - - VStack(spacing: 20) { - SettingsSection(title: "Theme") { - ColorPicker("Accent Color", selection: $settings.accentColor) - - Picker("Appearance", selection: $settings.selectedAppearance) { - Text("System").tag(Appearance.system) - Text("Light").tag(Appearance.light) - Text("Dark").tag(Appearance.dark) - } - .pickerStyle(SegmentedPickerStyle()) - } - - SettingsSection(title: "Media Player") { - HStack { - Text("Media Player") - Spacer() - Menu(externalPlayer) { - ForEach(mediaPlayers, id: \.self) { provider in - Button(action: { - externalPlayer = provider - }) { - Text(provider) - } - } - } - } - - Toggle("Force Landscape", isOn: $isAlwaysLandscape) - .tint(.accentColor) - } - - SettingsSection(title: "Grid Layout") { - HStack { - Text("Portrait Columns") - Spacer() - Picker("", selection: $mediaColumnsPortrait) { - if UIDevice.current.userInterfaceIdiom == .pad { - ForEach(1..<6) { i in - Text("\(i)").tag(i) - } - } else { - ForEach(1..<5) { i in - Text("\(i)").tag(i) - } - } - } - .pickerStyle(MenuPickerStyle()) - .labelsHidden() - } - - HStack { - Text("Landscape Columns") - Spacer() - Picker("", selection: $mediaColumnsLandscape) { - if UIDevice.current.userInterfaceIdiom == .pad { - ForEach(2..<9) { i in - Text("\(i)").tag(i) - } - } else { - ForEach(2..<6) { i in - Text("\(i)").tag(i) - } - } - } - .pickerStyle(MenuPickerStyle()) - .labelsHidden() - } - } - } - .padding() - .background( - RoundedRectangle(cornerRadius: 20, style: .continuous) - .fill(Color(UIColor.secondarySystemBackground)) - .shadow(color: Color.black.opacity(0.1), radius: 10, x: 0, y: 5) - ) - .padding(.horizontal) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } -} - -struct SettingsSection: View { - let title: String - let content: Content - - init(title: String, @ViewBuilder content: () -> Content) { - self.title = title - self.content = content() - } - - var body: some View { - VStack(alignment: .center, spacing: 10) { - Text(title + ":") - .font(.headline) - - content - - Divider() - } - } -} diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index b3b38f4..ed0f83e 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -70,6 +70,7 @@ struct SettingsViewGeneral: View { } } } + } } diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 92dd81e..c38b27b 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -41,7 +41,6 @@ 139935662D468C450065CEFF /* ModuleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 139935652D468C450065CEFF /* ModuleManager.swift */; }; 1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1399FAD32D3AB38C00E97C31 /* SettingsViewLogger.swift */; }; 1399FAD62D3AB3DB00E97C31 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1399FAD52D3AB3DB00E97C31 /* Logger.swift */; }; - 13ADAE022D92ED46007DCE7D /* OnBoardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13ADAE012D92ED46007DCE7D /* OnBoardingView.swift */; }; 13B7F4C12D58FFDD0045714A /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13B7F4C02D58FFDD0045714A /* Shimmer.swift */; }; 13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13C0E5E92D5F85EA00E7F619 /* ContinueWatchingManager.swift */; }; 13C0E5EC2D5F85F800E7F619 /* ContinueWatchingItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13C0E5EB2D5F85F800E7F619 /* ContinueWatchingItem.swift */; }; @@ -100,7 +99,6 @@ 139935652D468C450065CEFF /* ModuleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleManager.swift; sourceTree = ""; }; 1399FAD32D3AB38C00E97C31 /* SettingsViewLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewLogger.swift; sourceTree = ""; }; 1399FAD52D3AB3DB00E97C31 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; - 13ADAE012D92ED46007DCE7D /* OnBoardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnBoardingView.swift; sourceTree = ""; }; 13B7F4C02D58FFDD0045714A /* Shimmer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shimmer.swift; sourceTree = ""; }; 13C0E5E92D5F85EA00E7F619 /* ContinueWatchingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingManager.swift; sourceTree = ""; }; 13C0E5EB2D5F85F800E7F619 /* ContinueWatchingItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingItem.swift; sourceTree = ""; }; @@ -256,7 +254,6 @@ 133F55B92D33B53E00E08EEA /* LibraryView */, 133D7C7C2D2BE2630075467E /* SearchView.swift */, 130217CB2D81C55E0011EFF5 /* DownloadView.swift */, - 13ADAE012D92ED46007DCE7D /* OnBoardingView.swift */, ); path = Views; sourceTree = ""; @@ -528,7 +525,6 @@ 139935662D468C450065CEFF /* ModuleManager.swift in Sources */, 133D7C902D2BE2640075467E /* SettingsView.swift in Sources */, 1334FF4F2D786C9E007E289F /* TMDB-Trending.swift in Sources */, - 13ADAE022D92ED46007DCE7D /* OnBoardingView.swift in Sources */, 13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */, 13DB46902D900A38008CBC03 /* URL.swift in Sources */, 130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */, From 37425a2e55c1ef08d40abbfc32d160ae0b8f9d05 Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Wed, 26 Mar 2025 15:39:49 +0100 Subject: [PATCH 25/41] idk maybe i've added DNS services? Yeah it should --- Sora/Utils/Extensions/URLSession.swift | 63 ++++++++++++++++--- .../Modules/ModuleAdditionSettingsView.swift | 2 +- Sora/Utils/Modules/ModuleManager.swift | 8 +-- .../SettingsViewGeneral.swift | 19 +++++- 4 files changed, 77 insertions(+), 15 deletions(-) diff --git a/Sora/Utils/Extensions/URLSession.swift b/Sora/Utils/Extensions/URLSession.swift index 9144c9f..d51eb38 100644 --- a/Sora/Utils/Extensions/URLSession.swift +++ b/Sora/Utils/Extensions/URLSession.swift @@ -5,10 +5,55 @@ // Created by Francesco on 05/01/25. // -import Foundation import Network +import Foundation + +enum DNSProvider: String, CaseIterable, Hashable { + case cloudflare = "Cloudflare" + case google = "Google" + case openDNS = "OpenDNS" + case quad9 = "Quad9" + case adGuard = "AdGuard" + case cleanbrowsing = "CleanBrowsing" + case controld = "ControlD" + + var servers: [String] { + switch self { + case .cloudflare: + return ["1.1.1.1", "1.0.0.1"] + case .google: + return ["8.8.8.8", "8.8.4.4"] + case .openDNS: + return ["208.67.222.222", "208.67.220.220"] + case .quad9: + return ["9.9.9.9", "149.112.112.112"] + case .adGuard: + return ["94.140.14.14", "94.140.15.15"] + case .cleanbrowsing: + return ["185.228.168.168", "185.228.169.168"] + case .controld: + return ["76.76.2.0", "76.76.10.0"] + } + } +} extension URLSession { + private static let dnsSelectorKey = "CustomDNSProvider" + + static var currentDNSProvider: DNSProvider { + get { + guard let savedProviderRawValue = UserDefaults.standard.string(forKey: dnsSelectorKey) else { + UserDefaults.standard.set(DNSProvider.cloudflare.rawValue, forKey: dnsSelectorKey) + return .cloudflare + } + + return DNSProvider(rawValue: savedProviderRawValue) ?? .cloudflare + } + set { + UserDefaults.standard.set(newValue.rawValue, forKey: dnsSelectorKey) + } + } + 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", @@ -35,31 +80,33 @@ extension URLSession { "Mozilla/5.0 (Android 13; Mobile; rv:122.0) Gecko/122.0 Firefox/122.0" ] - static let randomUserAgent: String = { + static var randomUserAgent: String { userAgents.randomElement() ?? userAgents[0] - }() + } - static let custom: URLSession = { + static var custom: URLSession { let configuration = URLSessionConfiguration.default configuration.httpAdditionalHeaders = [ "User-Agent": randomUserAgent ] return URLSession(configuration: configuration) - }() + } - static let cloudflareCustom: URLSession = { + static var cloudflareCustom: URLSession { let configuration = URLSessionConfiguration.default configuration.httpAdditionalHeaders = [ "User-Agent": randomUserAgent ] + let dnsServers = currentDNSProvider.servers + let dnsSettings: [AnyHashable: Any] = [ "DNSSettings": [ - "ServerAddresses": ["1.1.1.1", "1.0.0.1"] + "ServerAddresses": dnsServers ] ] configuration.connectionProxyDictionary = dnsSettings return URLSession(configuration: configuration) - }() + } } diff --git a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift index b2580e2..143cd94 100644 --- a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift +++ b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift @@ -154,7 +154,7 @@ struct ModuleAdditionSettingsView: View { return } do { - let (data, _) = try await URLSession.custom.data(from: url) + let (data, _) = try await URLSession.cloudflareCustom.data(from: url) let metadata = try JSONDecoder().decode(ModuleMetadata.self, from: data) await MainActor.run { self.moduleMetadata = metadata diff --git a/Sora/Utils/Modules/ModuleManager.swift b/Sora/Utils/Modules/ModuleManager.swift index d13d8cf..ada264c 100644 --- a/Sora/Utils/Modules/ModuleManager.swift +++ b/Sora/Utils/Modules/ModuleManager.swift @@ -46,14 +46,14 @@ class ModuleManager: ObservableObject { throw NSError(domain: "Module already exists", code: -1) } - let (metadataData, _) = try await URLSession.custom.data(from: url) + let (metadataData, _) = try await URLSession.cloudflareCustom.data(from: url) let metadata = try JSONDecoder().decode(ModuleMetadata.self, from: metadataData) guard let scriptUrl = URL(string: metadata.scriptUrl) else { throw NSError(domain: "Invalid script URL", code: -1) } - let (scriptData, _) = try await URLSession.custom.data(from: scriptUrl) + let (scriptData, _) = try await URLSession.cloudflareCustom.data(from: scriptUrl) guard let jsContent = String(data: scriptData, encoding: .utf8) else { throw NSError(domain: "Invalid script encoding", code: -1) } @@ -94,7 +94,7 @@ class ModuleManager: ObservableObject { func refreshModules() async { for (index, module) in modules.enumerated() { do { - let (metadataData, _) = try await URLSession.custom.data(from: URL(string: module.metadataUrl)!) + let (metadataData, _) = try await URLSession.cloudflareCustom.data(from: URL(string: module.metadataUrl)!) let newMetadata = try JSONDecoder().decode(ModuleMetadata.self, from: metadataData) if newMetadata.version != module.metadata.version { @@ -102,7 +102,7 @@ class ModuleManager: ObservableObject { throw NSError(domain: "Invalid script URL", code: -1) } - let (scriptData, _) = try await URLSession.custom.data(from: scriptUrl) + let (scriptData, _) = try await URLSession.cloudflareCustom.data(from: scriptUrl) guard let jsContent = String(data: scriptData, encoding: .utf8) else { throw NSError(domain: "Invalid script encoding", code: -1) } diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index ed0f83e..310e5f3 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -14,9 +14,11 @@ 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("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2 @AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4 + private let customDNSProviderList = ["Cloudflare", "Google", "OpenDNS", "Quad9", "AdGuard", "CleanBrowsing", "ControlD"] private let metadataProvidersList = ["AniList"] @EnvironmentObject var settings: Settings @@ -70,7 +72,6 @@ struct SettingsViewGeneral: View { } } } - } } @@ -121,9 +122,23 @@ 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("Thanks to this 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")) { Toggle("Enable Analytics", isOn: $analyticsEnabled) .tint(.accentColor) + + HStack { + Text("DNS service") + Spacer() + Menu(customDNSProvider) { + ForEach(customDNSProviderList, id: \.self) { provider in + Button(action: { + customDNSProvider = provider + }) { + Text(provider) + } + } + } + } } } .navigationTitle("General") From 4ace715a8fbfa60381d9a93ba6f435937a8eeebd Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Wed, 26 Mar 2025 15:40:22 +0100 Subject: [PATCH 26/41] Update SettingsView.swift --- Sora/Views/SettingsView/SettingsView.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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")) { From ec6c8c94eb7b95ad03ad945ffb66f0b5754819b2 Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Wed, 26 Mar 2025 15:47:35 +0100 Subject: [PATCH 27/41] fixed stream crash --- Sora/Views/MediaInfoView/MediaInfoView.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 3db0025..1a1d27d 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -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")) } } } From 11bcb663d266d81bf224a4711321532605803344 Mon Sep 17 00:00:00 2001 From: Seiike <122684677+Seeike@users.noreply.github.com> Date: Wed, 26 Mar 2025 20:23:12 +0100 Subject: [PATCH 28/41] dns settings now actually work --- Sora/Info.plist | 2 - Sora/SoraApp.swift | 4 + .../JavaScriptCore+Extensions.swift | 4 +- Sora/Utils/Extensions/URLSession.swift | 82 +----- Sora/Utils/JSLoader/JSController.swift | 8 +- .../Helpers/VTTSubtitlesLoader.swift | 2 +- .../Modules/ModuleAdditionSettingsView.swift | 2 +- Sora/Utils/Modules/ModuleManager.swift | 8 +- Sora/Utils/NetworkDns/CustomDNS.swift | 248 ++++++++++++++++++ Sulfur.xcodeproj/project.pbxproj | 11 +- .../xcshareddata/swiftpm/Package.resolved | 79 +++--- 11 files changed, 325 insertions(+), 125 deletions(-) create mode 100644 Sora/Utils/NetworkDns/CustomDNS.swift 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/SoraApp.swift b/Sora/SoraApp.swift index 4073e20..34ababb 100644 --- a/Sora/SoraApp.swift +++ b/Sora/SoraApp.swift @@ -13,6 +13,10 @@ struct SoraApp: App { @StateObject private var moduleManager = ModuleManager() @StateObject private var librarykManager = LibraryManager() + init() { + registerCustomDNSGlobally() + } + var body: some Scene { WindowGroup { ContentView() diff --git a/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift b/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift index 947350c..d30569b 100644 --- a/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift +++ b/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift @@ -45,7 +45,7 @@ extension JSContext { request.setValue(value, forHTTPHeaderField: key) } } - let task = URLSession.cloudflareCustom.dataTask(with: request) { data, _, error in + let task = URLSession.customDNS.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]) @@ -111,7 +111,7 @@ extension JSContext { } } - let task = URLSession.cloudflareCustom.downloadTask(with: request) { tempFileURL, response, error in + let task = URLSession.customDNS.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]) diff --git a/Sora/Utils/Extensions/URLSession.swift b/Sora/Utils/Extensions/URLSession.swift index d51eb38..187128c 100644 --- a/Sora/Utils/Extensions/URLSession.swift +++ b/Sora/Utils/Extensions/URLSession.swift @@ -5,55 +5,9 @@ // Created by Francesco on 05/01/25. // -import Network import Foundation -enum DNSProvider: String, CaseIterable, Hashable { - case cloudflare = "Cloudflare" - case google = "Google" - case openDNS = "OpenDNS" - case quad9 = "Quad9" - case adGuard = "AdGuard" - case cleanbrowsing = "CleanBrowsing" - case controld = "ControlD" - - var servers: [String] { - switch self { - case .cloudflare: - return ["1.1.1.1", "1.0.0.1"] - case .google: - return ["8.8.8.8", "8.8.4.4"] - case .openDNS: - return ["208.67.222.222", "208.67.220.220"] - case .quad9: - return ["9.9.9.9", "149.112.112.112"] - case .adGuard: - return ["94.140.14.14", "94.140.15.15"] - case .cleanbrowsing: - return ["185.228.168.168", "185.228.169.168"] - case .controld: - return ["76.76.2.0", "76.76.10.0"] - } - } -} - extension URLSession { - private static let dnsSelectorKey = "CustomDNSProvider" - - static var currentDNSProvider: DNSProvider { - get { - guard let savedProviderRawValue = UserDefaults.standard.string(forKey: dnsSelectorKey) else { - UserDefaults.standard.set(DNSProvider.cloudflare.rawValue, forKey: dnsSelectorKey) - return .cloudflare - } - - return DNSProvider(rawValue: savedProviderRawValue) ?? .cloudflare - } - set { - UserDefaults.standard.set(newValue.rawValue, forKey: dnsSelectorKey) - } - } - 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", @@ -80,33 +34,21 @@ extension URLSession { "Mozilla/5.0 (Android 13; Mobile; rv:122.0) Gecko/122.0 Firefox/122.0" ] - static var randomUserAgent: String { + static let randomUserAgent: String = { userAgents.randomElement() ?? userAgents[0] - } + }() - static var custom: URLSession { + static let custom: URLSession = { let configuration = URLSessionConfiguration.default - configuration.httpAdditionalHeaders = [ - "User-Agent": randomUserAgent - ] + configuration.httpAdditionalHeaders = ["User-Agent": randomUserAgent] return URLSession(configuration: configuration) - } + }() - static var cloudflareCustom: URLSession { - let configuration = URLSessionConfiguration.default - configuration.httpAdditionalHeaders = [ - "User-Agent": randomUserAgent - ] - - let dnsServers = currentDNSProvider.servers - - let dnsSettings: [AnyHashable: Any] = [ - "DNSSettings": [ - "ServerAddresses": dnsServers - ] - ] - - configuration.connectionProxyDictionary = dnsSettings - return URLSession(configuration: configuration) - } + static let customDNS: URLSession = { + let config = URLSessionConfiguration.default + var protocols = config.protocolClasses ?? [] + protocols.insert(CustomURLProtocol.self, at: 0) + config.protocolClasses = protocols + return URLSession(configuration: config, delegate: InsecureSessionDelegate(), delegateQueue: nil) + }() } diff --git a/Sora/Utils/JSLoader/JSController.swift b/Sora/Utils/JSLoader/JSController.swift index 6cf5e82..8edbda2 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.customDNS.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.customDNS.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.customDNS.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.customDNS.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/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/Utils/Modules/ModuleAdditionSettingsView.swift b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift index 143cd94..c7f6d96 100644 --- a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift +++ b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift @@ -154,7 +154,7 @@ struct ModuleAdditionSettingsView: View { return } do { - let (data, _) = try await URLSession.cloudflareCustom.data(from: url) + let (data, _) = try await URLSession.customDNS.data(from: url) let metadata = try JSONDecoder().decode(ModuleMetadata.self, from: data) await MainActor.run { self.moduleMetadata = metadata diff --git a/Sora/Utils/Modules/ModuleManager.swift b/Sora/Utils/Modules/ModuleManager.swift index ada264c..9bf815c 100644 --- a/Sora/Utils/Modules/ModuleManager.swift +++ b/Sora/Utils/Modules/ModuleManager.swift @@ -46,14 +46,14 @@ class ModuleManager: ObservableObject { throw NSError(domain: "Module already exists", code: -1) } - let (metadataData, _) = try await URLSession.cloudflareCustom.data(from: url) + let (metadataData, _) = try await URLSession.customDNS.data(from: url) let metadata = try JSONDecoder().decode(ModuleMetadata.self, from: metadataData) guard let scriptUrl = URL(string: metadata.scriptUrl) else { throw NSError(domain: "Invalid script URL", code: -1) } - let (scriptData, _) = try await URLSession.cloudflareCustom.data(from: scriptUrl) + let (scriptData, _) = try await URLSession.customDNS.data(from: scriptUrl) guard let jsContent = String(data: scriptData, encoding: .utf8) else { throw NSError(domain: "Invalid script encoding", code: -1) } @@ -94,7 +94,7 @@ class ModuleManager: ObservableObject { func refreshModules() async { for (index, module) in modules.enumerated() { do { - let (metadataData, _) = try await URLSession.cloudflareCustom.data(from: URL(string: module.metadataUrl)!) + let (metadataData, _) = try await URLSession.customDNS.data(from: URL(string: module.metadataUrl)!) let newMetadata = try JSONDecoder().decode(ModuleMetadata.self, from: metadataData) if newMetadata.version != module.metadata.version { @@ -102,7 +102,7 @@ class ModuleManager: ObservableObject { throw NSError(domain: "Invalid script URL", code: -1) } - let (scriptData, _) = try await URLSession.cloudflareCustom.data(from: scriptUrl) + let (scriptData, _) = try await URLSession.customDNS.data(from: scriptUrl) guard let jsContent = String(data: scriptData, encoding: .utf8) else { throw NSError(domain: "Invalid script encoding", code: -1) } diff --git a/Sora/Utils/NetworkDns/CustomDNS.swift b/Sora/Utils/NetworkDns/CustomDNS.swift new file mode 100644 index 0000000..ee41506 --- /dev/null +++ b/Sora/Utils/NetworkDns/CustomDNS.swift @@ -0,0 +1,248 @@ +// +// CustomDNS.swift +// Sora +// +// Created by Seiike on 26/03/25. +// + +import Foundation +import Network + +enum DNSProvider: String, CaseIterable, Hashable { + case cloudflare = "Cloudflare" + case google = "Google" + case openDNS = "OpenDNS" + case quad9 = "Quad9" + case adGuard = "AdGuard" + case cleanbrowsing = "CleanBrowsing" + case controld = "ControlD" + + var servers: [String] { + switch self { + case .cloudflare: + return ["1.1.1.1", "1.0.0.1"] + case .google: + return ["8.8.8.8", "8.8.4.4"] + case .openDNS: + return ["208.67.222.222", "208.67.220.220"] + case .quad9: + return ["9.9.9.9", "149.112.112.112"] + case .adGuard: + return ["94.140.14.14", "94.140.15.15"] + case .cleanbrowsing: + return ["185.228.168.168", "185.228.169.168"] + case .controld: + return ["76.76.2.0", "76.76.10.0"] + } + } + + static var current: DNSProvider { + get { + let raw = UserDefaults.standard.string(forKey: "SelectedDNSProvider") ?? DNSProvider.cloudflare.rawValue + return DNSProvider(rawValue: raw) ?? .cloudflare + } + set { + UserDefaults.standard.setValue(newValue.rawValue, forKey: "SelectedDNSProvider") + } + } +} + +class CustomDNSResolver { + var dnsServerIP: String { + return DNSProvider.current.servers.first ?? "1.1.1.1" + } + + func buildDNSQuery(for host: String) -> (Data, UInt16) { + var data = Data() + let queryID = UInt16.random(in: 0...UInt16.max) + data.append(UInt8(queryID >> 8)) + data.append(UInt8(queryID & 0xFF)) + data.append(contentsOf: [0x01, 0x00]) + data.append(contentsOf: [0x00, 0x01]) + data.append(contentsOf: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + let labels = host.split(separator: ".") + for label in labels { + if let labelData = label.data(using: .utf8) { + data.append(UInt8(labelData.count)) + data.append(labelData) + } + } + data.append(0) + data.append(contentsOf: [0x00, 0x01]) + data.append(contentsOf: [0x00, 0x01]) + return (data, queryID) + } + + func parseDNSResponse(_ data: Data, queryID: UInt16) -> [String] { + var ips = [String]() + var offset = 0 + func readUInt16() -> UInt16? { + guard offset + 2 <= data.count else { return nil } + let value = (UInt16(data[offset]) << 8) | UInt16(data[offset+1]) + offset += 2 + return value + } + func readUInt32() -> UInt32? { + guard offset + 4 <= data.count else { return nil } + let value = (UInt32(data[offset]) << 24) | (UInt32(data[offset+1]) << 16) | (UInt32(data[offset+2]) << 8) | UInt32(data[offset+3]) + offset += 4 + return value + } + guard data.count >= 12 else { return [] } + let responseID = (UInt16(data[0]) << 8) | UInt16(data[1]) + if responseID != queryID { return [] } + offset = 2 + offset += 2 + guard let qdCount = readUInt16() else { return [] } + guard let anCount = readUInt16() else { return [] } + offset += 4 + for _ in 0..) -> Void) { + let dnsServer = self.dnsServerIP + guard let port = NWEndpoint.Port(rawValue: 53) else { + completion(.failure(NSError(domain: "CustomDNS", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid port"]))) + return + } + let connection = NWConnection(host: NWEndpoint.Host(dnsServer), port: port, using: .udp) + connection.stateUpdateHandler = { newState in + switch newState { + case .ready: + let (queryData, queryID) = self.buildDNSQuery(for: host) + connection.send(content: queryData, completion: .contentProcessed({ error in + if let error = error { + completion(.failure(error)) + connection.cancel() + } else { + connection.receive(minimumIncompleteLength: 1, maximumLength: 512) { content, _, _, error in + if let error = error { + completion(.failure(error)) + } else if let content = content { + let ips = self.parseDNSResponse(content, queryID: queryID) + if !ips.isEmpty { + completion(.success(ips)) + } else { + completion(.failure(NSError(domain: "CustomDNS", code: 2, userInfo: [NSLocalizedDescriptionKey: "No A records found"]))) + } + } + connection.cancel() + } + } + })) + case .failed(let error): + completion(.failure(error)) + connection.cancel() + default: + break + } + } + connection.start(queue: DispatchQueue.global()) + } +} + +class CustomURLProtocol: URLProtocol { + static let resolver = CustomDNSResolver() + override class func canInit(with request: URLRequest) -> Bool { + return URLProtocol.property(forKey: "Handled", in: request) == nil + } + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + override func startLoading() { + guard let url = request.url, let host = url.host else { + client?.urlProtocol(self, didFailWithError: NSError(domain: "CustomDNS", code: -1, userInfo: nil)) + return + } + CustomURLProtocol.resolver.resolve(host: host) { result in + switch result { + case .success(let ips): + guard let ip = ips.first, + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + self.client?.urlProtocol(self, didFailWithError: NSError(domain: "CustomDNS", code: -2, userInfo: nil)) + return + } + components.host = ip + guard let ipURL = components.url else { + self.client?.urlProtocol(self, didFailWithError: NSError(domain: "CustomDNS", code: -3, userInfo: nil)) + return + } + guard let mutableRequest = (self.request as NSURLRequest).mutableCopy() as? NSMutableURLRequest else { + self.client?.urlProtocol(self, didFailWithError: NSError(domain: "CustomDNS", code: -4, userInfo: nil)) + return + } + mutableRequest.url = ipURL + mutableRequest.setValue(host, forHTTPHeaderField: "Host") + URLProtocol.setProperty(true, forKey: "Handled", in: mutableRequest) + let finalRequest = mutableRequest as URLRequest + let session = URLSession.customDNS + let task = session.dataTask(with: finalRequest) { data, response, error in + if let data = data, let response = response { + self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + self.client?.urlProtocol(self, didLoad: data) + self.client?.urlProtocolDidFinishLoading(self) + } else if let error = error { + self.client?.urlProtocol(self, didFailWithError: error) + } + } + task.resume() + case .failure(let error): + self.client?.urlProtocol(self, didFailWithError: error) + } + } + } + override func stopLoading() {} +} + +class InsecureSessionDelegate: NSObject, URLSessionDelegate { + func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, let serverTrust = challenge.protectionSpace.serverTrust { + let credential = URLCredential(trust: serverTrust) + completionHandler(.useCredential, credential) + } else { + completionHandler(.performDefaultHandling, nil) + } + } +} + +func registerCustomDNSGlobally() { + let config = URLSessionConfiguration.default + var protocols = config.protocolClasses ?? [] + protocols.insert(CustomURLProtocol.self, at: 0) + config.protocolClasses = protocols + URLSessionConfiguration.default.protocolClasses = protocols + URLSessionConfiguration.ephemeral.protocolClasses = protocols + URLSessionConfiguration.background(withIdentifier: "CustomDNSBackground").protocolClasses = protocols +} diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index c38b27b..ac91e90 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -122,6 +122,10 @@ 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JavaScriptCore+Extensions.swift"; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 1EBA87D92D94653B00CABC28 /* NetworkDns */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = NetworkDns; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 133D7C672D2BE2500075467E /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -294,6 +298,7 @@ 133D7C882D2BE2640075467E /* Modules */, 1399FAD12D3AB33D00E97C31 /* Logger */, 13D842532D45266900EBBFA6 /* Drops */, + 1EBA87D92D94653B00CABC28 /* NetworkDns */, ); path = Utils; sourceTree = ""; @@ -452,6 +457,9 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + 1EBA87D92D94653B00CABC28 /* NetworkDns */, + ); name = Sulfur; packageProductDependencies = ( 133D7C962D2BE2AF0075467E /* Kingfisher */, @@ -701,13 +709,13 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\""; - DEVELOPMENT_TEAM = 399LMK6Q2Y; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Sora/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Sora; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; + INFOPLIST_KEY_NSCameraUsageDescription = "Sora may requires access to your device's camera."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -750,6 +758,7 @@ INFOPLIST_FILE = Sora/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Sora; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; + INFOPLIST_KEY_NSCameraUsageDescription = "Sora may requires access to your device's camera."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; diff --git a/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9784b35..fe8d570 100644 --- a/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,43 +1,42 @@ { - "object": { - "pins": [ - { - "package": "Drops", - "repositoryURL": "https://github.com/omaralbeik/Drops.git", - "state": { - "branch": "main", - "revision": "5824681795286c36bdc4a493081a63e64e2a064e", - "version": null - } - }, - { - "package": "FFmpeg-iOS-Lame", - "repositoryURL": "https://github.com/kewlbear/FFmpeg-iOS-Lame", - "state": { - "branch": "main", - "revision": "1808fa5a1263c5e216646cd8421fc7dcb70520cc", - "version": null - } - }, - { - "package": "FFmpeg-iOS-Support", - "repositoryURL": "https://github.com/kewlbear/FFmpeg-iOS-Support", - "state": { - "branch": null, - "revision": "be3bd9149ac53760e8725652eee99c405b2be47a", - "version": "0.0.2" - } - }, - { - "package": "Kingfisher", - "repositoryURL": "https://github.com/onevcat/Kingfisher.git", - "state": { - "branch": null, - "revision": "b6f62758f21a8c03cd64f4009c037cfa580a256e", - "version": "7.9.1" - } + "originHash" : "28f2c123747ea3d0aee96430294eb72e7254bafd504c83303b2a2e02f270f26f", + "pins" : [ + { + "identity" : "drops", + "kind" : "remoteSourceControl", + "location" : "https://github.com/omaralbeik/Drops.git", + "state" : { + "branch" : "main", + "revision" : "5824681795286c36bdc4a493081a63e64e2a064e" } - ] - }, - "version": 1 + }, + { + "identity" : "ffmpeg-ios-lame", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kewlbear/FFmpeg-iOS-Lame", + "state" : { + "branch" : "main", + "revision" : "1808fa5a1263c5e216646cd8421fc7dcb70520cc" + } + }, + { + "identity" : "ffmpeg-ios-support", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kewlbear/FFmpeg-iOS-Support", + "state" : { + "revision" : "be3bd9149ac53760e8725652eee99c405b2be47a", + "version" : "0.0.2" + } + }, + { + "identity" : "kingfisher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/onevcat/Kingfisher.git", + "state" : { + "revision" : "b6f62758f21a8c03cd64f4009c037cfa580a256e", + "version" : "7.9.1" + } + } + ], + "version" : 3 } From fff3449174a51bffad2c3be8342cc61e2dd57310 Mon Sep 17 00:00:00 2001 From: Seiike <122684677+Seeike@users.noreply.github.com> Date: Wed, 26 Mar 2025 20:52:12 +0100 Subject: [PATCH 29/41] prob very works --- Sora/Utils/NetworkDns/CustomDNS.swift | 20 ++++++- .../SettingsViewGeneral.swift | 59 +++++++------------ 2 files changed, 40 insertions(+), 39 deletions(-) diff --git a/Sora/Utils/NetworkDns/CustomDNS.swift b/Sora/Utils/NetworkDns/CustomDNS.swift index ee41506..14e7f5d 100644 --- a/Sora/Utils/NetworkDns/CustomDNS.swift +++ b/Sora/Utils/NetworkDns/CustomDNS.swift @@ -48,8 +48,24 @@ enum DNSProvider: String, CaseIterable, Hashable { } class CustomDNSResolver { + // Use custom DNS servers if "Custom" is selected; otherwise, fall back to the default provider's servers. + var dnsServers: [String] { + if let provider = UserDefaults.standard.string(forKey: "CustomDNSProvider"), + provider == "Custom" { + let primary = UserDefaults.standard.string(forKey: "customPrimaryDNS") ?? "" + let secondary = UserDefaults.standard.string(forKey: "customSecondaryDNS") ?? "" + var servers = [String]() + if !primary.isEmpty { servers.append(primary) } + if !secondary.isEmpty { servers.append(secondary) } + if !servers.isEmpty { + return servers + } + } + return DNSProvider.current.servers + } + var dnsServerIP: String { - return DNSProvider.current.servers.first ?? "1.1.1.1" + return dnsServers.first ?? "1.1.1.1" } func buildDNSQuery(for host: String) -> (Data, UInt16) { @@ -74,6 +90,7 @@ class CustomDNSResolver { } func parseDNSResponse(_ data: Data, queryID: UInt16) -> [String] { + // Existing implementation remains unchanged. var ips = [String]() var offset = 0 func readUInt16() -> UInt16? { @@ -173,6 +190,7 @@ class CustomDNSResolver { } } + class CustomURLProtocol: URLProtocol { static let resolver = CustomDNSResolver() override class func canInit(with request: URLRequest) -> Bool { diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index 310e5f3..fe08109 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -15,10 +15,12 @@ struct SettingsViewGeneral: View { @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"] + private let customDNSProviderList = ["Cloudflare", "Google", "OpenDNS", "Quad9", "AdGuard", "CleanBrowsing", "ControlD", "Custom"] private let metadataProvidersList = ["AniList"] @EnvironmentObject var settings: Settings @@ -26,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) @@ -42,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)") } @@ -65,9 +59,7 @@ struct SettingsViewGeneral: View { Spacer() Menu(metadataProviders) { ForEach(metadataProvidersList, id: \.self) { provider in - Button(action: { - metadataProviders = provider - }) { + Button(action: { metadataProviders = provider }) { Text(provider) } } @@ -75,25 +67,16 @@ struct SettingsViewGeneral: View { } } - //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()) } @@ -101,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()) } @@ -122,7 +101,7 @@ struct SettingsViewGeneral: View { .tint(.accentColor) } - Section(header: Text("Advanced"), footer: Text("Thanks to this 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) @@ -131,14 +110,18 @@ struct SettingsViewGeneral: View { Spacer() Menu(customDNSProvider) { ForEach(customDNSProviderList, id: \.self) { provider in - Button(action: { - customDNSProvider = provider - }) { + Button(action: { customDNSProvider = provider }) { Text(provider) } } } } + if customDNSProvider == "Custom" { + TextField("Primary DNS", text: $customPrimaryDNS) + .keyboardType(.numbersAndPunctuation) + TextField("Secondary DNS", text: $customSecondaryDNS) + .keyboardType(.numbersAndPunctuation) + } } } .navigationTitle("General") From 284bc11ad789ddbb2aa0f1c25f4557248ebde21a Mon Sep 17 00:00:00 2001 From: Seiike <122684677+Seeike@users.noreply.github.com> Date: Wed, 26 Mar 2025 21:01:35 +0100 Subject: [PATCH 30/41] =?UTF-8?q?fuck=20region=20restricions=20?= =?UTF-8?q?=F0=9F=93=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sora/Utils/NetworkDns/CustomDNS.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sora/Utils/NetworkDns/CustomDNS.swift b/Sora/Utils/NetworkDns/CustomDNS.swift index 14e7f5d..1b90529 100644 --- a/Sora/Utils/NetworkDns/CustomDNS.swift +++ b/Sora/Utils/NetworkDns/CustomDNS.swift @@ -4,6 +4,7 @@ // // Created by Seiike on 26/03/25. // +// fuck region restrictions import Foundation import Network From 4da986eaa32ded3299657d650b6555174ffc6796 Mon Sep 17 00:00:00 2001 From: Seiike <122684677+Seeike@users.noreply.github.com> Date: Wed, 26 Mar 2025 21:10:19 +0100 Subject: [PATCH 31/41] fuck xcodeproj --- Sulfur.xcodeproj/project.pbxproj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index ac91e90..bf4fa14 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -709,13 +709,13 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\""; + DEVELOPMENT_TEAM = 399LMK6Q2Y; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Sora/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Sora; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; - INFOPLIST_KEY_NSCameraUsageDescription = "Sora may requires access to your device's camera."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -758,7 +758,6 @@ INFOPLIST_FILE = Sora/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Sora; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; - INFOPLIST_KEY_NSCameraUsageDescription = "Sora may requires access to your device's camera."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; From fe874f3692bf37870e629c1e6b48ef1bdde052f0 Mon Sep 17 00:00:00 2001 From: Seiike <122684677+Seeike@users.noreply.github.com> Date: Wed, 26 Mar 2025 21:29:45 +0100 Subject: [PATCH 32/41] ta fucking da --- .../CustomDNS.swift | 0 Sulfur.xcodeproj/project.pbxproj | 20 +++++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) rename Sora/Utils/{NetworkDns => NetworkDNS}/CustomDNS.swift (100%) diff --git a/Sora/Utils/NetworkDns/CustomDNS.swift b/Sora/Utils/NetworkDNS/CustomDNS.swift similarity index 100% rename from Sora/Utils/NetworkDns/CustomDNS.swift rename to Sora/Utils/NetworkDNS/CustomDNS.swift diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index bf4fa14..ec7d983 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -61,6 +61,7 @@ 13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */; }; 1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */; }; 1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */; }; + 1ED156202D949B1A00C11BCD /* CustomDNS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED1561F2D949B1A00C11BCD /* CustomDNS.swift */; }; 73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */; }; /* End PBXBuildFile section */ @@ -119,13 +120,10 @@ 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = ""; }; 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewLoggerFilter.swift; sourceTree = ""; }; 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = ""; }; + 1ED1561F2D949B1A00C11BCD /* CustomDNS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDNS.swift; sourceTree = ""; }; 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JavaScriptCore+Extensions.swift"; sourceTree = ""; }; /* End PBXFileReference section */ -/* Begin PBXFileSystemSynchronizedRootGroup section */ - 1EBA87D92D94653B00CABC28 /* NetworkDns */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = NetworkDns; sourceTree = ""; }; -/* End PBXFileSystemSynchronizedRootGroup section */ - /* Begin PBXFrameworksBuildPhase section */ 133D7C672D2BE2500075467E /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -288,6 +286,7 @@ 133D7C852D2BE2640075467E /* Utils */ = { isa = PBXGroup; children = ( + 1ED1561E2D949AFB00C11BCD /* NetworkDNS */, 13DB7CEA2D7DED50004371D3 /* DownloadManager */, 13C0E5E82D5F85DD00E7F619 /* ContinueWatching */, 13103E8C2D58E037000F0673 /* SkeletonCells */, @@ -298,7 +297,6 @@ 133D7C882D2BE2640075467E /* Modules */, 1399FAD12D3AB33D00E97C31 /* Logger */, 13D842532D45266900EBBFA6 /* Drops */, - 1EBA87D92D94653B00CABC28 /* NetworkDns */, ); path = Utils; sourceTree = ""; @@ -442,6 +440,14 @@ path = Components; sourceTree = ""; }; + 1ED1561E2D949AFB00C11BCD /* NetworkDNS */ = { + isa = PBXGroup; + children = ( + 1ED1561F2D949B1A00C11BCD /* CustomDNS.swift */, + ); + path = NetworkDNS; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -457,9 +463,6 @@ ); dependencies = ( ); - fileSystemSynchronizedGroups = ( - 1EBA87D92D94653B00CABC28 /* NetworkDns */, - ); name = Sulfur; packageProductDependencies = ( 133D7C962D2BE2AF0075467E /* Kingfisher */, @@ -570,6 +573,7 @@ 1334FF4D2D786C93007E289F /* TMDB-Seasonal.swift in Sources */, 13DB468D2D90093A008CBC03 /* Anilist-Login.swift in Sources */, 73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */, + 1ED156202D949B1A00C11BCD /* CustomDNS.swift in Sources */, 13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */, 1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */, 13DB46922D900BCE008CBC03 /* SettingsViewTrackers.swift in Sources */, From ad96b5fdaad421338ee619be8f1ebe4e58b456e6 Mon Sep 17 00:00:00 2001 From: Seiike <122684677+Seeike@users.noreply.github.com> Date: Wed, 26 Mar 2025 21:54:16 +0100 Subject: [PATCH 33/41] changed it to series https://discord.com/channels/1293430817841741899/1318240587886891029/1354558523824144504 --- Sora/Views/MediaInfoView/MediaInfoView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 1a1d27d..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) From b1ce1e86dd2fb0ab4056befe5d9d8fa4cfd74096 Mon Sep 17 00:00:00 2001 From: Seiike <122684677+Seeike@users.noreply.github.com> Date: Wed, 26 Mar 2025 22:04:40 +0100 Subject: [PATCH 34/41] as cranci wanted --- .../SettingsSubViews/SettingsViewGeneral.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index fe08109..a3414dc 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -101,10 +101,7 @@ struct SettingsViewGeneral: View { .tint(.accentColor) } - 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) - + Section(header: Text("Network"), footer: Text("Try between some of the providers in case something is not loading if it should be, as it might be the fault of your ISP.")){ HStack { Text("DNS service") Spacer() @@ -123,6 +120,11 @@ struct SettingsViewGeneral: View { .keyboardType(.numbersAndPunctuation) } } + + 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) + } } .navigationTitle("General") } From d29d340d84909ff9e1ca4733d81f1caef4acc105 Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Thu, 27 Mar 2025 15:50:16 +0100 Subject: [PATCH 35/41] d --- Sora/Utils/NetworkDNS/CustomDNS.swift | 8 ++-- Sulfur.xcodeproj/project.pbxproj | 59 +-------------------------- 2 files changed, 4 insertions(+), 63 deletions(-) diff --git a/Sora/Utils/NetworkDNS/CustomDNS.swift b/Sora/Utils/NetworkDNS/CustomDNS.swift index 1b90529..98c7bc4 100644 --- a/Sora/Utils/NetworkDNS/CustomDNS.swift +++ b/Sora/Utils/NetworkDNS/CustomDNS.swift @@ -49,7 +49,6 @@ enum DNSProvider: String, CaseIterable, Hashable { } class CustomDNSResolver { - // Use custom DNS servers if "Custom" is selected; otherwise, fall back to the default provider's servers. var dnsServers: [String] { if let provider = UserDefaults.standard.string(forKey: "CustomDNSProvider"), provider == "Custom" { @@ -91,7 +90,6 @@ class CustomDNSResolver { } func parseDNSResponse(_ data: Data, queryID: UInt16) -> [String] { - // Existing implementation remains unchanged. var ips = [String]() var offset = 0 func readUInt16() -> UInt16? { @@ -210,9 +208,9 @@ class CustomURLProtocol: URLProtocol { case .success(let ips): guard let ip = ips.first, var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { - self.client?.urlProtocol(self, didFailWithError: NSError(domain: "CustomDNS", code: -2, userInfo: nil)) - return - } + self.client?.urlProtocol(self, didFailWithError: NSError(domain: "CustomDNS", code: -2, userInfo: nil)) + return + } components.host = ip guard let ipURL = components.url else { self.client?.urlProtocol(self, didFailWithError: NSError(domain: "CustomDNS", code: -3, userInfo: nil)) diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index ec7d983..fcf3d0a 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -31,10 +31,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 */; }; @@ -53,7 +51,6 @@ 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 */; }; - 13DB7CC62D7DC7D2004371D3 /* FFmpeg-iOS-Lame in Frameworks */ = {isa = PBXBuildFile; productRef = 13DB7CC52D7DC7D2004371D3 /* FFmpeg-iOS-Lame */; }; 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 */; }; @@ -129,9 +126,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 13DB7CC62D7DC7D2004371D3 /* FFmpeg-iOS-Lame in Frameworks */, - 1359ED1A2D76FA7D00C13034 /* Drops in Frameworks */, - 133D7C972D2BE2AF0075467E /* Kingfisher in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -286,10 +280,10 @@ 133D7C852D2BE2640075467E /* Utils */ = { isa = PBXGroup; children = ( - 1ED1561E2D949AFB00C11BCD /* NetworkDNS */, 13DB7CEA2D7DED50004371D3 /* DownloadManager */, 13C0E5E82D5F85DD00E7F619 /* ContinueWatching */, 13103E8C2D58E037000F0673 /* SkeletonCells */, + 1ED1561E2D949AFB00C11BCD /* NetworkDNS */, 13DC0C442D302C6A00D0F966 /* MediaPlayer */, 133D7C862D2BE2640075467E /* Extensions */, 1327FBA52D758CEA00FC6689 /* Analytics */, @@ -465,9 +459,6 @@ ); name = Sulfur; packageProductDependencies = ( - 133D7C962D2BE2AF0075467E /* Kingfisher */, - 1359ED192D76FA7D00C13034 /* Drops */, - 13DB7CC52D7DC7D2004371D3 /* FFmpeg-iOS-Lame */, ); productName = Sora; productReference = 133D7C6A2D2BE2500075467E /* Sulfur.app */; @@ -497,9 +488,6 @@ ); mainGroup = 133D7C612D2BE2500075467E; packageReferences = ( - 133D7C952D2BE2AF0075467E /* XCRemoteSwiftPackageReference "Kingfisher" */, - 1359ED182D76FA7D00C13034 /* XCRemoteSwiftPackageReference "Drops" */, - 13DB7CC42D7DC7D2004371D3 /* XCRemoteSwiftPackageReference "FFmpeg-iOS-Lame" */, ); productRefGroup = 133D7C6B2D2BE2500075467E /* Products */; projectDirPath = ""; @@ -808,51 +796,6 @@ defaultConfigurationName = Release; }; /* 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" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/omaralbeik/Drops.git"; - requirement = { - branch = main; - kind = branch; - }; - }; - 13DB7CC42D7DC7D2004371D3 /* XCRemoteSwiftPackageReference "FFmpeg-iOS-Lame" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/kewlbear/FFmpeg-iOS-Lame"; - requirement = { - branch = main; - kind = branch; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - 133D7C962D2BE2AF0075467E /* Kingfisher */ = { - isa = XCSwiftPackageProductDependency; - package = 133D7C952D2BE2AF0075467E /* XCRemoteSwiftPackageReference "Kingfisher" */; - productName = Kingfisher; - }; - 1359ED192D76FA7D00C13034 /* Drops */ = { - isa = XCSwiftPackageProductDependency; - package = 1359ED182D76FA7D00C13034 /* XCRemoteSwiftPackageReference "Drops" */; - productName = Drops; - }; - 13DB7CC52D7DC7D2004371D3 /* FFmpeg-iOS-Lame */ = { - isa = XCSwiftPackageProductDependency; - package = 13DB7CC42D7DC7D2004371D3 /* XCRemoteSwiftPackageReference "FFmpeg-iOS-Lame" */; - productName = "FFmpeg-iOS-Lame"; - }; -/* End XCSwiftPackageProductDependency section */ }; rootObject = 133D7C622D2BE2500075467E /* Project object */; } From 4764e16ccd0218c987b190d47cad2c0c640c24cc Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Thu, 27 Mar 2025 15:55:25 +0100 Subject: [PATCH 36/41] =?UTF-8?q?fuck=20@Seeike=20=F0=9F=98=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sulfur.xcodeproj/project.pbxproj | 57 +++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 79 ++++++++++--------- 2 files changed, 97 insertions(+), 39 deletions(-) diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index fcf3d0a..d9c74ca 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -15,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 */; }; @@ -126,6 +129,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 132E35232D959E410007800E /* Kingfisher in Frameworks */, + 132E35202D959E1D0007800E /* FFmpeg-iOS-Lame in Frameworks */, + 132E351D2D959DDB0007800E /* Drops in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -459,6 +465,9 @@ ); name = Sulfur; packageProductDependencies = ( + 132E351C2D959DDB0007800E /* Drops */, + 132E351F2D959E1D0007800E /* FFmpeg-iOS-Lame */, + 132E35222D959E410007800E /* Kingfisher */, ); productName = Sora; productReference = 133D7C6A2D2BE2500075467E /* Sulfur.app */; @@ -488,6 +497,9 @@ ); mainGroup = 133D7C612D2BE2500075467E; packageReferences = ( + 132E351B2D959DDB0007800E /* XCRemoteSwiftPackageReference "Drops" */, + 132E351E2D959E1D0007800E /* XCRemoteSwiftPackageReference "FFmpeg-iOS-Lame" */, + 132E35212D959E410007800E /* XCRemoteSwiftPackageReference "Kingfisher" */, ); productRefGroup = 133D7C6B2D2BE2500075467E /* Products */; projectDirPath = ""; @@ -796,6 +808,51 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 132E351B2D959DDB0007800E /* XCRemoteSwiftPackageReference "Drops" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/omaralbeik/Drops.git"; + requirement = { + branch = main; + 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 */ + 132E351C2D959DDB0007800E /* Drops */ = { + isa = XCSwiftPackageProductDependency; + 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 */; } diff --git a/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index fe8d570..9784b35 100644 --- a/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,42 +1,43 @@ { - "originHash" : "28f2c123747ea3d0aee96430294eb72e7254bafd504c83303b2a2e02f270f26f", - "pins" : [ - { - "identity" : "drops", - "kind" : "remoteSourceControl", - "location" : "https://github.com/omaralbeik/Drops.git", - "state" : { - "branch" : "main", - "revision" : "5824681795286c36bdc4a493081a63e64e2a064e" + "object": { + "pins": [ + { + "package": "Drops", + "repositoryURL": "https://github.com/omaralbeik/Drops.git", + "state": { + "branch": "main", + "revision": "5824681795286c36bdc4a493081a63e64e2a064e", + "version": null + } + }, + { + "package": "FFmpeg-iOS-Lame", + "repositoryURL": "https://github.com/kewlbear/FFmpeg-iOS-Lame", + "state": { + "branch": "main", + "revision": "1808fa5a1263c5e216646cd8421fc7dcb70520cc", + "version": null + } + }, + { + "package": "FFmpeg-iOS-Support", + "repositoryURL": "https://github.com/kewlbear/FFmpeg-iOS-Support", + "state": { + "branch": null, + "revision": "be3bd9149ac53760e8725652eee99c405b2be47a", + "version": "0.0.2" + } + }, + { + "package": "Kingfisher", + "repositoryURL": "https://github.com/onevcat/Kingfisher.git", + "state": { + "branch": null, + "revision": "b6f62758f21a8c03cd64f4009c037cfa580a256e", + "version": "7.9.1" + } } - }, - { - "identity" : "ffmpeg-ios-lame", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kewlbear/FFmpeg-iOS-Lame", - "state" : { - "branch" : "main", - "revision" : "1808fa5a1263c5e216646cd8421fc7dcb70520cc" - } - }, - { - "identity" : "ffmpeg-ios-support", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kewlbear/FFmpeg-iOS-Support", - "state" : { - "revision" : "be3bd9149ac53760e8725652eee99c405b2be47a", - "version" : "0.0.2" - } - }, - { - "identity" : "kingfisher", - "kind" : "remoteSourceControl", - "location" : "https://github.com/onevcat/Kingfisher.git", - "state" : { - "revision" : "b6f62758f21a8c03cd64f4009c037cfa580a256e", - "version" : "7.9.1" - } - } - ], - "version" : 3 + ] + }, + "version": 1 } From 9bd4d6e74d42ebbbc11dd12658b22a07424137b2 Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Thu, 27 Mar 2025 15:59:34 +0100 Subject: [PATCH 37/41] fixed randomness again --- Sora/Utils/Extensions/URLSession.swift | 50 +++++++++++++------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/Sora/Utils/Extensions/URLSession.swift b/Sora/Utils/Extensions/URLSession.swift index 187128c..50a32e1 100644 --- a/Sora/Utils/Extensions/URLSession.swift +++ b/Sora/Utils/Extensions/URLSession.swift @@ -9,32 +9,32 @@ import Foundation 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] }() @@ -51,4 +51,4 @@ extension URLSession { config.protocolClasses = protocols return URLSession(configuration: config, delegate: InsecureSessionDelegate(), delegateQueue: nil) }() -} +} \ No newline at end of file From 07b4a0280e898f0c4968679291b5a310caf26990 Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Thu, 27 Mar 2025 16:03:47 +0100 Subject: [PATCH 38/41] =?UTF-8?q?does=20ts=20even=20work=3F=20=F0=9F=98=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sora/Views/SearchView.swift | 46 ++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/Sora/Views/SearchView.swift b/Sora/Views/SearchView.swift index 50d5528..0b978b8 100644 --- a/Sora/Views/SearchView.swift +++ b/Sora/Views/SearchView.swift @@ -165,20 +165,38 @@ 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) + let modulesByLanguage = Dictionary(grouping: moduleManager.modules) { module in + guard let language = module.metadata.language else { return "Unknown" } + + let cleanLanguage = language.replacingOccurrences( + of: "\\s*\\([^\\)]*\\)", + with: "", + options: .regularExpression + ).trimmingCharacters(in: .whitespaces) + + return cleanLanguage.isEmpty ? "Unknown" : cleanLanguage + } + + let sortedLanguages = modulesByLanguage.keys.sorted() + + ForEach(sortedLanguages, id: \.self) { language in + Menu(language) { + ForEach(modulesByLanguage[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) + } + } } } } From 6989fbfe8733046c75b69e1bdf2dee1995956eee Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Thu, 27 Mar 2025 16:09:19 +0100 Subject: [PATCH 39/41] fixed maybe? at least on my machine --- .../CustomPlayer/CustomPlayer.swift | 12 ----- Sora/Views/SearchView.swift | 53 +++++++++++++------ 2 files changed, 37 insertions(+), 28 deletions(-) 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/Views/SearchView.swift b/Sora/Views/SearchView.swift index 0b978b8..cfd739d 100644 --- a/Sora/Views/SearchView.swift +++ b/Sora/Views/SearchView.swift @@ -165,23 +165,9 @@ struct SearchView: View { .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Menu { - let modulesByLanguage = Dictionary(grouping: moduleManager.modules) { module in - guard let language = module.metadata.language else { return "Unknown" } - - let cleanLanguage = language.replacingOccurrences( - of: "\\s*\\([^\\)]*\\)", - with: "", - options: .regularExpression - ).trimmingCharacters(in: .whitespaces) - - return cleanLanguage.isEmpty ? "Unknown" : cleanLanguage - } - - let sortedLanguages = modulesByLanguage.keys.sorted() - - ForEach(sortedLanguages, id: \.self) { language in + ForEach(getModuleLanguageGroups(), id: \.self) { language in Menu(language) { - ForEach(modulesByLanguage[language] ?? [], id: \.id) { module in + ForEach(getModulesForLanguage(language), id: \.id) { module in Button { selectedModuleId = module.id.uuidString } label: { @@ -287,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 { From 2eb07aebf19e5b7ff3b9e84a8ee7b2379e4a2e09 Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:23:55 +0100 Subject: [PATCH 40/41] =?UTF-8?q?yeah=20fuck=20@Seeike=20not=20the=20ISPs?= =?UTF-8?q?=20=F0=9F=98=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sora/SoraApp.swift | 4 - .../JavaScriptCore+Extensions.swift | 17 +- Sora/Utils/Extensions/URLSession.swift | 10 +- Sora/Utils/JSLoader/JSController.swift | 8 +- .../Modules/ModuleAdditionSettingsView.swift | 2 +- Sora/Utils/Modules/ModuleManager.swift | 8 +- Sora/Utils/NetworkDNS/CustomDNS.swift | 265 ------------------ .../SettingsViewGeneral.swift | 20 -- Sulfur.xcodeproj/project.pbxproj | 12 - 9 files changed, 13 insertions(+), 333 deletions(-) delete mode 100644 Sora/Utils/NetworkDNS/CustomDNS.swift diff --git a/Sora/SoraApp.swift b/Sora/SoraApp.swift index 34ababb..4073e20 100644 --- a/Sora/SoraApp.swift +++ b/Sora/SoraApp.swift @@ -13,10 +13,6 @@ struct SoraApp: App { @StateObject private var moduleManager = ModuleManager() @StateObject private var librarykManager = LibraryManager() - init() { - registerCustomDNSGlobally() - } - var body: some Scene { WindowGroup { ContentView() diff --git a/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift b/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift index d30569b..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.customDNS.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]) @@ -91,27 +88,24 @@ extension JSContext { Logger.shared.log("FetchV2 Request: URL=\(url), Method=\(httpMethod), Body=\(body ?? "nil")", type: "Debug") - // Ensure no body for GET requests 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 } - // Set the body for non-GET requests if httpMethod != "GET", let body = body, !body.isEmpty, body != "null", body != "undefined" { request.httpBody = body.data(using: .utf8) } - // Set headers if let headers = headers { for (key, value) in headers { request.setValue(value, forHTTPHeaderField: key) } } - let task = URLSession.customDNS.downloadTask(with: request) { tempFileURL, 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]) @@ -127,8 +121,7 @@ extension JSContext { do { let data = try Data(contentsOf: tempFileURL) - // Check response size before processing - if data.count > 10_000_000 { // Example: 10MB limit + if data.count > 10_000_000 { Logger.shared.log("Response exceeds maximum size", type: "Error") reject.call(withArguments: ["Response exceeds maximum size"]) return @@ -203,7 +196,6 @@ extension JSContext { } 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") @@ -212,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") @@ -222,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 50a32e1..544b084 100644 --- a/Sora/Utils/Extensions/URLSession.swift +++ b/Sora/Utils/Extensions/URLSession.swift @@ -43,12 +43,4 @@ extension URLSession { configuration.httpAdditionalHeaders = ["User-Agent": randomUserAgent] return URLSession(configuration: configuration) }() - - static let customDNS: URLSession = { - let config = URLSessionConfiguration.default - var protocols = config.protocolClasses ?? [] - protocols.insert(CustomURLProtocol.self, at: 0) - config.protocolClasses = protocols - return URLSession(configuration: config, delegate: InsecureSessionDelegate(), delegateQueue: nil) - }() -} \ No newline at end of file +} diff --git a/Sora/Utils/JSLoader/JSController.swift b/Sora/Utils/JSLoader/JSController.swift index 8edbda2..a0a220b 100644 --- a/Sora/Utils/JSLoader/JSController.swift +++ b/Sora/Utils/JSLoader/JSController.swift @@ -36,7 +36,7 @@ class JSController: ObservableObject { return } - URLSession.customDNS.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.customDNS.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.customDNS.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.customDNS.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/Modules/ModuleAdditionSettingsView.swift b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift index c7f6d96..b2580e2 100644 --- a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift +++ b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift @@ -154,7 +154,7 @@ struct ModuleAdditionSettingsView: View { return } do { - let (data, _) = try await URLSession.customDNS.data(from: url) + let (data, _) = try await URLSession.custom.data(from: url) let metadata = try JSONDecoder().decode(ModuleMetadata.self, from: data) await MainActor.run { self.moduleMetadata = metadata diff --git a/Sora/Utils/Modules/ModuleManager.swift b/Sora/Utils/Modules/ModuleManager.swift index 9bf815c..d13d8cf 100644 --- a/Sora/Utils/Modules/ModuleManager.swift +++ b/Sora/Utils/Modules/ModuleManager.swift @@ -46,14 +46,14 @@ class ModuleManager: ObservableObject { throw NSError(domain: "Module already exists", code: -1) } - let (metadataData, _) = try await URLSession.customDNS.data(from: url) + let (metadataData, _) = try await URLSession.custom.data(from: url) let metadata = try JSONDecoder().decode(ModuleMetadata.self, from: metadataData) guard let scriptUrl = URL(string: metadata.scriptUrl) else { throw NSError(domain: "Invalid script URL", code: -1) } - let (scriptData, _) = try await URLSession.customDNS.data(from: scriptUrl) + let (scriptData, _) = try await URLSession.custom.data(from: scriptUrl) guard let jsContent = String(data: scriptData, encoding: .utf8) else { throw NSError(domain: "Invalid script encoding", code: -1) } @@ -94,7 +94,7 @@ class ModuleManager: ObservableObject { func refreshModules() async { for (index, module) in modules.enumerated() { do { - let (metadataData, _) = try await URLSession.customDNS.data(from: URL(string: module.metadataUrl)!) + let (metadataData, _) = try await URLSession.custom.data(from: URL(string: module.metadataUrl)!) let newMetadata = try JSONDecoder().decode(ModuleMetadata.self, from: metadataData) if newMetadata.version != module.metadata.version { @@ -102,7 +102,7 @@ class ModuleManager: ObservableObject { throw NSError(domain: "Invalid script URL", code: -1) } - let (scriptData, _) = try await URLSession.customDNS.data(from: scriptUrl) + let (scriptData, _) = try await URLSession.custom.data(from: scriptUrl) guard let jsContent = String(data: scriptData, encoding: .utf8) else { throw NSError(domain: "Invalid script encoding", code: -1) } diff --git a/Sora/Utils/NetworkDNS/CustomDNS.swift b/Sora/Utils/NetworkDNS/CustomDNS.swift deleted file mode 100644 index 98c7bc4..0000000 --- a/Sora/Utils/NetworkDNS/CustomDNS.swift +++ /dev/null @@ -1,265 +0,0 @@ -// -// CustomDNS.swift -// Sora -// -// Created by Seiike on 26/03/25. -// -// fuck region restrictions - -import Foundation -import Network - -enum DNSProvider: String, CaseIterable, Hashable { - case cloudflare = "Cloudflare" - case google = "Google" - case openDNS = "OpenDNS" - case quad9 = "Quad9" - case adGuard = "AdGuard" - case cleanbrowsing = "CleanBrowsing" - case controld = "ControlD" - - var servers: [String] { - switch self { - case .cloudflare: - return ["1.1.1.1", "1.0.0.1"] - case .google: - return ["8.8.8.8", "8.8.4.4"] - case .openDNS: - return ["208.67.222.222", "208.67.220.220"] - case .quad9: - return ["9.9.9.9", "149.112.112.112"] - case .adGuard: - return ["94.140.14.14", "94.140.15.15"] - case .cleanbrowsing: - return ["185.228.168.168", "185.228.169.168"] - case .controld: - return ["76.76.2.0", "76.76.10.0"] - } - } - - static var current: DNSProvider { - get { - let raw = UserDefaults.standard.string(forKey: "SelectedDNSProvider") ?? DNSProvider.cloudflare.rawValue - return DNSProvider(rawValue: raw) ?? .cloudflare - } - set { - UserDefaults.standard.setValue(newValue.rawValue, forKey: "SelectedDNSProvider") - } - } -} - -class CustomDNSResolver { - var dnsServers: [String] { - if let provider = UserDefaults.standard.string(forKey: "CustomDNSProvider"), - provider == "Custom" { - let primary = UserDefaults.standard.string(forKey: "customPrimaryDNS") ?? "" - let secondary = UserDefaults.standard.string(forKey: "customSecondaryDNS") ?? "" - var servers = [String]() - if !primary.isEmpty { servers.append(primary) } - if !secondary.isEmpty { servers.append(secondary) } - if !servers.isEmpty { - return servers - } - } - return DNSProvider.current.servers - } - - var dnsServerIP: String { - return dnsServers.first ?? "1.1.1.1" - } - - func buildDNSQuery(for host: String) -> (Data, UInt16) { - var data = Data() - let queryID = UInt16.random(in: 0...UInt16.max) - data.append(UInt8(queryID >> 8)) - data.append(UInt8(queryID & 0xFF)) - data.append(contentsOf: [0x01, 0x00]) - data.append(contentsOf: [0x00, 0x01]) - data.append(contentsOf: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) - let labels = host.split(separator: ".") - for label in labels { - if let labelData = label.data(using: .utf8) { - data.append(UInt8(labelData.count)) - data.append(labelData) - } - } - data.append(0) - data.append(contentsOf: [0x00, 0x01]) - data.append(contentsOf: [0x00, 0x01]) - return (data, queryID) - } - - func parseDNSResponse(_ data: Data, queryID: UInt16) -> [String] { - var ips = [String]() - var offset = 0 - func readUInt16() -> UInt16? { - guard offset + 2 <= data.count else { return nil } - let value = (UInt16(data[offset]) << 8) | UInt16(data[offset+1]) - offset += 2 - return value - } - func readUInt32() -> UInt32? { - guard offset + 4 <= data.count else { return nil } - let value = (UInt32(data[offset]) << 24) | (UInt32(data[offset+1]) << 16) | (UInt32(data[offset+2]) << 8) | UInt32(data[offset+3]) - offset += 4 - return value - } - guard data.count >= 12 else { return [] } - let responseID = (UInt16(data[0]) << 8) | UInt16(data[1]) - if responseID != queryID { return [] } - offset = 2 - offset += 2 - guard let qdCount = readUInt16() else { return [] } - guard let anCount = readUInt16() else { return [] } - offset += 4 - for _ in 0..) -> Void) { - let dnsServer = self.dnsServerIP - guard let port = NWEndpoint.Port(rawValue: 53) else { - completion(.failure(NSError(domain: "CustomDNS", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid port"]))) - return - } - let connection = NWConnection(host: NWEndpoint.Host(dnsServer), port: port, using: .udp) - connection.stateUpdateHandler = { newState in - switch newState { - case .ready: - let (queryData, queryID) = self.buildDNSQuery(for: host) - connection.send(content: queryData, completion: .contentProcessed({ error in - if let error = error { - completion(.failure(error)) - connection.cancel() - } else { - connection.receive(minimumIncompleteLength: 1, maximumLength: 512) { content, _, _, error in - if let error = error { - completion(.failure(error)) - } else if let content = content { - let ips = self.parseDNSResponse(content, queryID: queryID) - if !ips.isEmpty { - completion(.success(ips)) - } else { - completion(.failure(NSError(domain: "CustomDNS", code: 2, userInfo: [NSLocalizedDescriptionKey: "No A records found"]))) - } - } - connection.cancel() - } - } - })) - case .failed(let error): - completion(.failure(error)) - connection.cancel() - default: - break - } - } - connection.start(queue: DispatchQueue.global()) - } -} - - -class CustomURLProtocol: URLProtocol { - static let resolver = CustomDNSResolver() - override class func canInit(with request: URLRequest) -> Bool { - return URLProtocol.property(forKey: "Handled", in: request) == nil - } - override class func canonicalRequest(for request: URLRequest) -> URLRequest { - return request - } - override func startLoading() { - guard let url = request.url, let host = url.host else { - client?.urlProtocol(self, didFailWithError: NSError(domain: "CustomDNS", code: -1, userInfo: nil)) - return - } - CustomURLProtocol.resolver.resolve(host: host) { result in - switch result { - case .success(let ips): - guard let ip = ips.first, - var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { - self.client?.urlProtocol(self, didFailWithError: NSError(domain: "CustomDNS", code: -2, userInfo: nil)) - return - } - components.host = ip - guard let ipURL = components.url else { - self.client?.urlProtocol(self, didFailWithError: NSError(domain: "CustomDNS", code: -3, userInfo: nil)) - return - } - guard let mutableRequest = (self.request as NSURLRequest).mutableCopy() as? NSMutableURLRequest else { - self.client?.urlProtocol(self, didFailWithError: NSError(domain: "CustomDNS", code: -4, userInfo: nil)) - return - } - mutableRequest.url = ipURL - mutableRequest.setValue(host, forHTTPHeaderField: "Host") - URLProtocol.setProperty(true, forKey: "Handled", in: mutableRequest) - let finalRequest = mutableRequest as URLRequest - let session = URLSession.customDNS - let task = session.dataTask(with: finalRequest) { data, response, error in - if let data = data, let response = response { - self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) - self.client?.urlProtocol(self, didLoad: data) - self.client?.urlProtocolDidFinishLoading(self) - } else if let error = error { - self.client?.urlProtocol(self, didFailWithError: error) - } - } - task.resume() - case .failure(let error): - self.client?.urlProtocol(self, didFailWithError: error) - } - } - } - override func stopLoading() {} -} - -class InsecureSessionDelegate: NSObject, URLSessionDelegate { - func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, let serverTrust = challenge.protectionSpace.serverTrust { - let credential = URLCredential(trust: serverTrust) - completionHandler(.useCredential, credential) - } else { - completionHandler(.performDefaultHandling, nil) - } - } -} - -func registerCustomDNSGlobally() { - let config = URLSessionConfiguration.default - var protocols = config.protocolClasses ?? [] - protocols.insert(CustomURLProtocol.self, at: 0) - config.protocolClasses = protocols - URLSessionConfiguration.default.protocolClasses = protocols - URLSessionConfiguration.ephemeral.protocolClasses = protocols - URLSessionConfiguration.background(withIdentifier: "CustomDNSBackground").protocolClasses = protocols -} diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index a3414dc..154d1d2 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -101,26 +101,6 @@ struct SettingsViewGeneral: View { .tint(.accentColor) } - Section(header: Text("Network"), footer: Text("Try between some of the providers in case something is not loading if it should be, as it might be the fault of your ISP.")){ - HStack { - Text("DNS service") - Spacer() - Menu(customDNSProvider) { - ForEach(customDNSProviderList, id: \.self) { provider in - Button(action: { customDNSProvider = provider }) { - Text(provider) - } - } - } - } - if customDNSProvider == "Custom" { - TextField("Primary DNS", text: $customPrimaryDNS) - .keyboardType(.numbersAndPunctuation) - TextField("Secondary DNS", text: $customSecondaryDNS) - .keyboardType(.numbersAndPunctuation) - } - } - 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/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index d9c74ca..ad75420 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -61,7 +61,6 @@ 13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */; }; 1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */; }; 1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */; }; - 1ED156202D949B1A00C11BCD /* CustomDNS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED1561F2D949B1A00C11BCD /* CustomDNS.swift */; }; 73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */; }; /* End PBXBuildFile section */ @@ -120,7 +119,6 @@ 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = ""; }; 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewLoggerFilter.swift; sourceTree = ""; }; 1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = ""; }; - 1ED1561F2D949B1A00C11BCD /* CustomDNS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDNS.swift; sourceTree = ""; }; 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JavaScriptCore+Extensions.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -289,7 +287,6 @@ 13DB7CEA2D7DED50004371D3 /* DownloadManager */, 13C0E5E82D5F85DD00E7F619 /* ContinueWatching */, 13103E8C2D58E037000F0673 /* SkeletonCells */, - 1ED1561E2D949AFB00C11BCD /* NetworkDNS */, 13DC0C442D302C6A00D0F966 /* MediaPlayer */, 133D7C862D2BE2640075467E /* Extensions */, 1327FBA52D758CEA00FC6689 /* Analytics */, @@ -440,14 +437,6 @@ path = Components; sourceTree = ""; }; - 1ED1561E2D949AFB00C11BCD /* NetworkDNS */ = { - isa = PBXGroup; - children = ( - 1ED1561F2D949B1A00C11BCD /* CustomDNS.swift */, - ); - path = NetworkDNS; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -573,7 +562,6 @@ 1334FF4D2D786C93007E289F /* TMDB-Seasonal.swift in Sources */, 13DB468D2D90093A008CBC03 /* Anilist-Login.swift in Sources */, 73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */, - 1ED156202D949B1A00C11BCD /* CustomDNS.swift in Sources */, 13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */, 1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */, 13DB46922D900BCE008CBC03 /* SettingsViewTrackers.swift in Sources */, From fe19ffda1f27c0e79675408b0ff1861accae9a66 Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:36:47 +0100 Subject: [PATCH 41/41] Update ContinueWatchingManager.swift --- Sora/Utils/ContinueWatching/ContinueWatchingManager.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sora/Utils/ContinueWatching/ContinueWatchingManager.swift b/Sora/Utils/ContinueWatching/ContinueWatchingManager.swift index 0c6cc0e..f8b6452 100644 --- a/Sora/Utils/ContinueWatching/ContinueWatchingManager.swift +++ b/Sora/Utils/ContinueWatching/ContinueWatchingManager.swift @@ -14,6 +14,11 @@ class ContinueWatchingManager { private init() {} func save(item: ContinueWatchingItem) { + if item.progress >= 0.9 { + remove(item: item) + return + } + var items = fetchItems() if let index = items.firstIndex(where: { $0.streamUrl == item.streamUrl && $0.episodeNumber == item.episodeNumber }) { items[index] = item