Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions ios/Flean.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.0;
MARKETING_VERSION = 2.3.0;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
Expand Down Expand Up @@ -430,7 +430,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.0;
MARKETING_VERSION = 2.3.0;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
Expand Down Expand Up @@ -588,7 +588,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.2.0;
MARKETING_VERSION = 2.3.0;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
Expand Down Expand Up @@ -624,7 +624,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.2.0;
MARKETING_VERSION = 2.3.0;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
267 changes: 144 additions & 123 deletions ios/Flean/iOS/WebViewContainer.swift
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))
}
}
Comment on lines +124 to +127
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

safeJSON is 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 into evaluateJavaScript (the current replacement only touches \\\" and doesn’t affect how json is embedded).

Copilot uses AI. Check for mistakes.

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
8 changes: 5 additions & 3 deletions ios/extention/Resources/background.js
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
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initWikiData() starts warmIndex() but nothing waits for it before handling findWiki messages. Since the service worker can be started by the first sendMessage from content.js, the first findMatchingWiki() call can still end up awaiting index construction and hit the 500ms content-script timeout. Consider either persisting a prebuilt index (so cold starts don’t require decompress+build), or increasing the content-script timeout, or adding a fast-path response when the index isn’t ready (with a retry/backoff) to avoid falling back to the mirror unnecessarily.

Copilot uses AI. Check for mistakes.
if (err) return
}
Expand Down
2 changes: 1 addition & 1 deletion ios/extention/Resources/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading
Loading