mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-11 17:45:37 +00:00
New JavaScriptCore Extension Class & more (#54)
Some checks are pending
Build and Release IPA / Build IPA (push) Waiting to run
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:
commit
341c512ef9
5 changed files with 197 additions and 62 deletions
|
|
@ -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>
|
||||
|
|
|
|||
175
Sora/Utils/Extensions/JavaScriptCore+Extensions.swift
Normal file
175
Sora/Utils/Extensions/JavaScriptCore+Extensions.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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*]" = "";
|
||||
|
|
|
|||
Loading…
Reference in a new issue