diff --git a/Sora/Info.plist b/Sora/Info.plist index a8da192..6a6654d 100644 --- a/Sora/Info.plist +++ b/Sora/Info.plist @@ -2,10 +2,10 @@ - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + diff --git a/Sora/Utils/Loaders/JSController.swift b/Sora/Utils/Loaders/JSController.swift index 4737f61..33479fb 100644 --- a/Sora/Utils/Loaders/JSController.swift +++ b/Sora/Utils/Loaders/JSController.swift @@ -20,6 +20,46 @@ class JSController: ObservableObject { print("JavaScript log: \(message)") } context.setObject(logFunction, forKeyedSubscript: "log" as NSString) + + // Because fetch isnt available in JSContext, it simulates the fetch api for Javascript. + // Performs network calls with URLSession + let fetchNativeFunction: @convention(block) (String, JSValue, JSValue) -> Void = { urlString, resolve, reject in + guard let url = URL(string: urlString) else { + print("Invalid URL") + reject.call(withArguments: ["Invalid URL"]) + return + } + let task = URLSession.shared.dataTask(with: url) { data, _, error in + if let error = error { + print("Network error in fetchNativeFunction: \(error.localizedDescription)") + reject.call(withArguments: [error.localizedDescription]) + return + } + guard let data = data else { + print("No data in response") + reject.call(withArguments: ["No data"]) + return + } + if let text = String(data: data, encoding: .utf8) { + resolve.call(withArguments: [text]) + } else { + print("Unable to decode data to text") + reject.call(withArguments: ["Unable to decode data"]) + } + } + task.resume() + } + context.setObject(fetchNativeFunction, forKeyedSubscript: "fetchNative" as NSString) + + // Define fetch for JavaScript + let fetchDefinition = """ + function fetch(url) { + return new Promise(function(resolve, reject) { + fetchNative(url, resolve, reject); + }); + } + """ + context.evaluateScript(fetchDefinition) } func loadScript(_ script: String) { @@ -154,4 +194,81 @@ class JSController: ObservableObject { } }.resume() } + + /// Use Javascript to fetch search results + func fetchJsSearchResults(keyword: String, module: ScrapingModule, completion: @escaping ([SearchItem]) -> Void) { + if let exception = context.exception { + print("JavaScript exception: \(exception)") + completion([]) + return + } + + guard let searchResultsFunction = context.objectForKeyedSubscript("searchResults") else { + print("No JavaScript function searchResults found") + completion([]) + return + } + + // Call the JavaScript function, passing in the parameter + let promiseValue = searchResultsFunction.call(withArguments: [keyword]) + guard let promise = promiseValue else { + print("searchResults did not return a Promise") + completion([]) + return + } + + // Handles successful promise resolution. + let thenBlock: @convention(block) (JSValue) -> Void = { result in + + if let jsonString = result.toString(), + 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 ?? "" + return SearchItem(title: title, imageUrl: imageUrl, href: href) + } + + DispatchQueue.main.async { + completion(resultItems) + } + + } else { + print("Failed to parse JSON") + DispatchQueue.main.async { + completion([]) + } + } + } catch { + print("JSON parsing error: \(error)") + DispatchQueue.main.async { + completion([]) + } + } + } else { + print("Result is not a string") + DispatchQueue.main.async { + completion([]) + } + } + } + + // Handles promise rejection. + let catchBlock: @convention(block) (JSValue) -> Void = { error in + print("Promise rejected: \(String(describing: error.toString()))") + DispatchQueue.main.async { + completion([]) + } + } + + // Wrap the Swift blocks into JSValue functions + let thenFunction = JSValue(object: thenBlock, in: context) + let catchFunction = JSValue(object: catchBlock, in: context) + + // Attach the 'then' and 'catch' callbacks to the Promise + promise.invokeMethod("then", withArguments: [thenFunction]) + promise.invokeMethod("catch", withArguments: [catchFunction]) + } } diff --git a/Sora/Utils/Modules/Modules.swift b/Sora/Utils/Modules/Modules.swift index f33a1c0..d22a985 100644 --- a/Sora/Utils/Modules/Modules.swift +++ b/Sora/Utils/Modules/Modules.swift @@ -16,6 +16,7 @@ struct ModuleMetadata: Codable, Hashable { let baseUrl: String let searchBaseUrl: String let scriptUrl: String + let asyncJS: Bool? } struct ScrapingModule: Codable, Identifiable, Hashable { diff --git a/Sora/Views/SearchView.swift b/Sora/Views/SearchView.swift index bf82305..8a0280d 100644 --- a/Sora/Views/SearchView.swift +++ b/Sora/Views/SearchView.swift @@ -154,10 +154,18 @@ struct SearchView: View { do { let jsContent = try moduleManager.getModuleContent(module) jsController.loadScript(jsContent) - jsController.fetchSearchResults(keyword: searchText, module: module) { items in - searchItems = items - hasNoResults = items.isEmpty - isSearching = false + if(module.metadata.asyncJS == false || module.metadata.asyncJS == nil) { + jsController.fetchSearchResults(keyword: searchText, module: module) { items in + searchItems = items + hasNoResults = items.isEmpty + isSearching = false + } + } else { + jsController.fetchJsSearchResults(keyword: searchText, module: module) { items in + searchItems = items + hasNoResults = items.isEmpty + isSearching = false + } } } catch { print("Error loading module: \(error)")