New JavaScriptCore Extension Class & more (#54)
Some checks are pending
Build and Release IPA / Build IPA (push) Waiting to run

- 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 function
for backwards compatibility)
- New Logger function to print in Debug mode to xcode console
This commit is contained in:
cranci 2025-03-20 15:13:56 +01:00 committed by GitHub
commit 341c512ef9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 197 additions and 62 deletions

View file

@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>Sora may requires access to your device's camera.</string>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>

View file

@ -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()
}
}

View file

@ -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) {

View file

@ -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
}}

View file

@ -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 = "<group>"; };
1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewLoggerFilter.swift; sourceTree = "<group>"; };
1EAC7A312D888BC50083984D /* MusicProgressSlider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = "<group>"; };
73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JavaScriptCore+Extensions.swift"; sourceTree = "<group>"; };
/* 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*]" = "";