diff --git a/ios/Flean.xcodeproj/project.pbxproj b/ios/Flean.xcodeproj/project.pbxproj index ad03046..ede1fac 100644 --- a/ios/Flean.xcodeproj/project.pbxproj +++ b/ios/Flean.xcodeproj/project.pbxproj @@ -394,7 +394,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.3.0; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -430,7 +430,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.3.0; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -588,7 +588,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.3.0; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -624,7 +624,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.3.0; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -651,7 +651,7 @@ DEVELOPMENT_TEAM = PWL627GZ4Y; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.3.0; PRODUCT_BUNDLE_IDENTIFIER = slf.FleanTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -669,7 +669,7 @@ DEVELOPMENT_TEAM = PWL627GZ4Y; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.3.0; PRODUCT_BUNDLE_IDENTIFIER = slf.FleanTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -685,7 +685,7 @@ CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = PWL627GZ4Y; GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.3.0; PRODUCT_BUNDLE_IDENTIFIER = slf.FleanUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -701,7 +701,7 @@ CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = PWL627GZ4Y; GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.3.0; PRODUCT_BUNDLE_IDENTIFIER = slf.FleanUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; diff --git a/ios/Flean/iOS/WebViewContainer.swift b/ios/Flean/iOS/WebViewContainer.swift index 6330ceb..04a8733 100644 --- a/ios/Flean/iOS/WebViewContainer.swift +++ b/ios/Flean/iOS/WebViewContainer.swift @@ -1,151 +1,172 @@ // iOS-only web view container. Guard compilation so macOS builds don't attempt to // compile UIKit/SwiftUI iOS-only APIs. #if os(iOS) -import SwiftUI -import WebKit -import UIKit + import SwiftUI + import WebKit + import UIKit -struct WebViewContainer: UIViewRepresentable { + struct WebViewContainer: UIViewRepresentable { func makeUIView(context: Context) -> WKWebView { - let webCfg = WKWebViewConfiguration() - let web = WKWebView(frame: .zero, configuration: webCfg) - - // Add script message handler compatible with the macOS app's controller name - web.configuration.userContentController.add(context.coordinator, name: "controller") - - // Give the coordinator a reference to the web view so it can inject - // settings back into the page when requested. - context.coordinator.webView = web - - // Load bundled Main.html from the app bundle resources - if let url = Bundle.main.url(forResource: "Main", withExtension: "html", subdirectory: "Base.lproj") { - web.loadFileURL(url, allowingReadAccessTo: Bundle.main.resourceURL!) - } - - return web + let webCfg = WKWebViewConfiguration() + let web = WKWebView(frame: .zero, configuration: webCfg) + + // Add script message handler compatible with the macOS app's controller name + web.configuration.userContentController.add(context.coordinator, name: "controller") + web.navigationDelegate = context.coordinator + + // Give the coordinator a reference to the web view so it can inject + // settings back into the page when requested. + context.coordinator.webView = web + + // Load bundled Main.html from the app bundle resources + if let url = Bundle.main.url( + forResource: "Main", withExtension: "html", subdirectory: "Base.lproj"), + let resourceURL = Bundle.main.resourceURL + { + web.loadFileURL(url, allowingReadAccessTo: resourceURL) + } + + return web } func updateUIView(_ uiView: WKWebView, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator() } - class Coordinator: NSObject, WKScriptMessageHandler { - // Weak reference to avoid retain cycles - weak var webView: WKWebView? - - override init() { - super.init() - NotificationCenter.default.addObserver(self, selector: #selector(settingsChanged(_:)), name: Notification.Name("FleanSettingsDidChange"), object: nil) + class Coordinator: NSObject, WKScriptMessageHandler, WKNavigationDelegate { + // Weak reference to avoid retain cycles + weak var webView: WKWebView? + + override init() { + super.init() + NotificationCenter.default.addObserver( + self, selector: #selector(settingsChanged(_:)), + name: Notification.Name("FleanSettingsDidChange"), object: nil) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: - Settings storage in app container + private func settingsFileURL() -> URL? { + let fm = FileManager.default + do { + let appSupport = try fm.url( + for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, + create: true) + let dir = appSupport.appendingPathComponent("Flean", isDirectory: true) + if !fm.fileExists(atPath: dir.path) { + try fm.createDirectory(at: dir, withIntermediateDirectories: true, attributes: nil) + } + return dir.appendingPathComponent("settings.json") + } catch { + NSLog("Flean: failed to get application support directory: %s", String(describing: error)) + return nil } + } - deinit { - NotificationCenter.default.removeObserver(self) + private func loadSettings() -> [String: Any] { + guard let url = settingsFileURL(), FileManager.default.fileExists(atPath: url.path) else { + return [:] } - - // MARK: - Settings storage in app container - private func settingsFileURL() -> URL? { - let fm = FileManager.default - do { - let appSupport = try fm.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) - let dir = appSupport.appendingPathComponent("Flean", isDirectory: true) - if !fm.fileExists(atPath: dir.path) { - try fm.createDirectory(at: dir, withIntermediateDirectories: true, attributes: nil) - } - return dir.appendingPathComponent("settings.json") - } catch { - NSLog("Flean: failed to get application support directory: %s", String(describing: error)) - return nil - } + do { + let data = try Data(contentsOf: url) + if let obj = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { + return obj + } + } catch { + NSLog("Flean: failed to load settings: %s", String(describing: error)) } - - private func loadSettings() -> [String: Any] { - guard let url = settingsFileURL(), FileManager.default.fileExists(atPath: url.path) else { return [:] } - do { - let data = try Data(contentsOf: url) - if let obj = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { - return obj - } - } catch { - NSLog("Flean: failed to load settings: %s", String(describing: error)) - } - return [:] + return [:] + } + + private func saveSettings(_ dict: [String: Any]) -> Bool { + guard let url = settingsFileURL() else { return false } + do { + let data = try JSONSerialization.data(withJSONObject: dict, options: [.prettyPrinted]) + try data.write(to: url, options: [.atomic]) + return true + } catch { + NSLog("Flean: failed to save settings: %s", String(describing: error)) + return false } - - private func saveSettings(_ dict: [String: Any]) -> Bool { - guard let url = settingsFileURL() else { return false } - do { - let data = try JSONSerialization.data(withJSONObject: dict, options: [.prettyPrinted]) - try data.write(to: url, options: [.atomic]) - return true - } catch { - NSLog("Flean: failed to save settings: %s", String(describing: error)) - return false - } + } + + func userContentController( + _ userContentController: WKUserContentController, didReceive message: WKScriptMessage + ) { + // Expect either a string command or a dictionary with { action: "getSettings" | "setSettings", data: {...} } + if let bodyStr = message.body as? String { + if bodyStr == "open-preferences" { + NSLog("Flean: open-preferences requested (iOS) — manual enable in Settings required") + return + } + // legacy: handle simple 'get-settings' / 'set-settings' encoded strings + if bodyStr == "get-settings" { + sendSettingsToPage() + return + } } - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - // Expect either a string command or a dictionary with { action: "getSettings" | "setSettings", data: {...} } - if let bodyStr = message.body as? String { - if bodyStr == "open-preferences" { - NSLog("Flean: open-preferences requested (iOS) — manual enable in Settings required") - return - } - // legacy: handle simple 'get-settings' / 'set-settings' encoded strings - if bodyStr == "get-settings" { - sendSettingsToPage() - return - } - } - - if let body = message.body as? [String: Any], let action = body["action"] as? String { - switch action { - case "getSettings": - sendSettingsToPage() - case "setSettings": - if let data = body["data"] as? [String: Any] { - let ok = saveSettings(data) - // Acknowledge back to the page - let ack = ["action": "setSettingsAck", "success": ok] as [String : Any] - sendJSONToPage(ack) - } - default: - break - } + if let body = message.body as? [String: Any], let action = body["action"] as? String { + switch action { + case "getSettings": + sendSettingsToPage() + case "setSettings": + if let data = body["data"] as? [String: Any] { + let ok = saveSettings(data) + // Acknowledge back to the page + let ack = ["action": "setSettingsAck", "success": ok] as [String: Any] + sendJSONToPage(ack) } + default: + break + } } - - private func sendJSONToPage(_ obj: Any) { - guard let web = webView else { return } - do { - let data = try JSONSerialization.data(withJSONObject: obj, options: []) - if let json = String(data: data, encoding: .utf8) { - // We dispatch a CustomEvent 'flean:message' with the JSON as detail - let safeJSON = json.replacingOccurrences(of: "\\\"", with: "\\\\\"") - let js = "window.dispatchEvent(new CustomEvent('flean:message', { detail: \(json) }));" - web.evaluateJavaScript(js, completionHandler: nil) - } - } catch { - NSLog("Flean: failed to serialize message to page: %s", String(describing: error)) - } + } + + private func sendJSONToPage(_ obj: Any) { + guard let web = webView else { return } + do { + let data = try JSONSerialization.data(withJSONObject: obj, options: []) + if let json = String(data: data, encoding: .utf8) { + // We dispatch a CustomEvent 'flean:message' with the JSON as detail + let safeJSON = json.replacingOccurrences(of: "\\\"", with: "\\\\\"") + let js = "window.dispatchEvent(new CustomEvent('flean:message', { detail: \(json) }));" + web.evaluateJavaScript(js, completionHandler: nil) + } + } catch { + NSLog("Flean: failed to serialize message to page: %s", String(describing: error)) } + } + + private func sendSettingsToPage() { + let settings = loadSettings() + sendJSONToPage(["action": "settings", "data": settings]) + } + + @objc private func settingsChanged(_ note: Notification) { + // Push updated settings into the web view when the SettingsStore saves. + sendSettingsToPage() + } + + // MARK: - WKNavigationDelegate + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation?) { + // Update the UI to use iOS-appropriate text. Extension state cannot be + // queried programmatically on iOS (no SFSafariExtensionManager equivalent), + // so we pass null for the enabled state (shows "unknown") and true for + // useSettingsInsteadOfPreferences so the correct iOS wording is displayed. + webView.evaluateJavaScript("show(null, true)", completionHandler: nil) + } - private func sendSettingsToPage() { - let settings = loadSettings() - sendJSONToPage(["action": "settings", "data": settings]) - } - - @objc private func settingsChanged(_ note: Notification) { - // Push updated settings into the web view when the SettingsStore saves. - sendSettingsToPage() - } - } -} + } -struct WebViewContainer_Previews: PreviewProvider { + struct WebViewContainer_Previews: PreviewProvider { static var previews: some View { - WebViewContainer() + WebViewContainer() } -} + } #endif diff --git a/ios/extention/Resources/background.js b/ios/extention/Resources/background.js index 3e6d85e..5cec806 100644 --- a/ios/extention/Resources/background.js +++ b/ios/extention/Resources/background.js @@ -1,9 +1,11 @@ -import { getWikiData, fetchWikiData, findMatchingWiki, invalidateIndex } from './scripts/wiki-data-manager.js' +import { fetchWikiData, findMatchingWiki, invalidateIndex, warmIndex } from './scripts/wiki-data-manager.js' -// Initialise wiki data on extension startup (loads from cache or fetches fresh) +// Initialise wiki data on extension startup and pre-warm the lookup index so the +// first findWiki message from content.js is answered without needing to build the +// index inside the 500ms race window. async function initWikiData () { try { - await getWikiData() + await warmIndex() } catch (err) { if (err) return } diff --git a/ios/extention/Resources/manifest.json b/ios/extention/Resources/manifest.json index cac74e8..d7f412c 100644 --- a/ios/extention/Resources/manifest.json +++ b/ios/extention/Resources/manifest.json @@ -4,7 +4,7 @@ "name": "Flean Extension", "description": "Redirect Fandom wiki pages to independent mirrors.", - "version": "2.2.0", + "version": "2.3.0", "icons": { "48": "images/icon-48.png", diff --git a/ios/extention/Resources/scripts/wiki-data-manager.js b/ios/extention/Resources/scripts/wiki-data-manager.js index 2d69ed3..acf1d44 100644 --- a/ios/extention/Resources/scripts/wiki-data-manager.js +++ b/ios/extention/Resources/scripts/wiki-data-manager.js @@ -134,6 +134,15 @@ export function invalidateIndex () { _wikiIndex = null } +/** + * Pre-build the in-memory wiki lookup index. + * Call this during extension startup so the first findMatchingWiki request + * does not stall waiting for index construction within the content-script timeout. + */ +export function warmIndex () { + return ensureIndex() +} + /** * Given a URL string, find the best matching independent wiki destination. * Returns { destinationUrl: string, wikiName: string } or null if no match found. diff --git a/mos/Flean Extension/Resources/background.js b/mos/Flean Extension/Resources/background.js index 3e6d85e..5cec806 100644 --- a/mos/Flean Extension/Resources/background.js +++ b/mos/Flean Extension/Resources/background.js @@ -1,9 +1,11 @@ -import { getWikiData, fetchWikiData, findMatchingWiki, invalidateIndex } from './scripts/wiki-data-manager.js' +import { fetchWikiData, findMatchingWiki, invalidateIndex, warmIndex } from './scripts/wiki-data-manager.js' -// Initialise wiki data on extension startup (loads from cache or fetches fresh) +// Initialise wiki data on extension startup and pre-warm the lookup index so the +// first findWiki message from content.js is answered without needing to build the +// index inside the 500ms race window. async function initWikiData () { try { - await getWikiData() + await warmIndex() } catch (err) { if (err) return } diff --git a/mos/Flean Extension/Resources/manifest.json b/mos/Flean Extension/Resources/manifest.json index 98d9ed7..17c2576 100644 --- a/mos/Flean Extension/Resources/manifest.json +++ b/mos/Flean Extension/Resources/manifest.json @@ -4,7 +4,7 @@ "name": "__MSG_extension_name__", "description": "__MSG_extension_description__", - "version": "2.2.0", + "version": "2.3.0", "icons": { "48": "images/icon-48.png", diff --git a/mos/Flean Extension/Resources/scripts/wiki-data-manager.js b/mos/Flean Extension/Resources/scripts/wiki-data-manager.js index 2d69ed3..acf1d44 100644 --- a/mos/Flean Extension/Resources/scripts/wiki-data-manager.js +++ b/mos/Flean Extension/Resources/scripts/wiki-data-manager.js @@ -134,6 +134,15 @@ export function invalidateIndex () { _wikiIndex = null } +/** + * Pre-build the in-memory wiki lookup index. + * Call this during extension startup so the first findMatchingWiki request + * does not stall waiting for index construction within the content-script timeout. + */ +export function warmIndex () { + return ensureIndex() +} + /** * Given a URL string, find the best matching independent wiki destination. * Returns { destinationUrl: string, wikiName: string } or null if no match found. diff --git a/mos/Flean.xcodeproj/project.pbxproj b/mos/Flean.xcodeproj/project.pbxproj index 2587a7d..44f911b 100644 --- a/mos/Flean.xcodeproj/project.pbxproj +++ b/mos/Flean.xcodeproj/project.pbxproj @@ -392,7 +392,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.3.0; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -423,7 +423,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.3.0; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -577,7 +577,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.3.0; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -616,7 +616,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.3.0; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -643,7 +643,7 @@ DEVELOPMENT_TEAM = PWL627GZ4Y; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.3.0; PRODUCT_BUNDLE_IDENTIFIER = slf.FleanTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -661,7 +661,7 @@ DEVELOPMENT_TEAM = PWL627GZ4Y; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.3.0; PRODUCT_BUNDLE_IDENTIFIER = slf.FleanTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -677,7 +677,7 @@ CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = PWL627GZ4Y; GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.3.0; PRODUCT_BUNDLE_IDENTIFIER = slf.FleanUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -693,7 +693,7 @@ CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = PWL627GZ4Y; GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.3.0; PRODUCT_BUNDLE_IDENTIFIER = slf.FleanUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO;