-
Notifications
You must be signed in to change notification settings - Fork 0
fix: pre-warm wiki index on service worker startup; fix iOS UI navigation delegate (v2.3.0) #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f654093
c36df15
548b3f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) { | ||
|
Comment on lines
+3
to
9
|
||
| if (err) return | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
safeJSONis computed but never used, which will produce an unused-variable warning and is misleading. Either remove it, or use a correct escaping approach for injecting JSON intoevaluateJavaScript(the current replacement only touches\\\"and doesn’t affect howjsonis embedded).