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*]" = "";