From 56587c8c517a80fb11dda45f14e34725977a5e14 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sat, 30 Aug 2025 13:25:20 +0000 Subject: [PATCH 01/14] refactor: add once option to the listeners --- src/ext/content-scripts/entry-userscripts.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/ext/content-scripts/entry-userscripts.js b/src/ext/content-scripts/entry-userscripts.js index 5721dd90..e160d7e3 100644 --- a/src/ext/content-scripts/entry-userscripts.js +++ b/src/ext/content-scripts/entry-userscripts.js @@ -25,19 +25,23 @@ function triageJS(userscript) { if (document.readyState !== "loading") { injectJS(userscript); } else { - document.addEventListener("DOMContentLoaded", () => { - injectJS(userscript); - }); + document.addEventListener( + "DOMContentLoaded", + () => injectJS(userscript), + { once: true }, + ); } } else if (runAt === "document-idle") { if (document.readyState === "complete") { injectJS(userscript); } else { - document.addEventListener("readystatechange", () => { + const handle = () => { if (document.readyState === "complete") { injectJS(userscript); + document.removeEventListener("readystatechange", handle); } - }); + }; + document.addEventListener("readystatechange", handle); } } } @@ -233,6 +237,10 @@ async function injection() { } function listeners() { + /** listen for CSP violations */ + document.addEventListener("securitypolicyviolation", cspFallback, { + once: true, + }); // listens for messages from background, popup, etc... browser.runtime.onMessage.addListener((request) => { const name = request.name; @@ -255,8 +263,6 @@ function listeners() { console.error(`Couldn't find ${filename} code!`); } }); - // listen for CSP violations - document.addEventListener("securitypolicyviolation", cspFallback); } async function initialize() { From 7e9301b00048b0eeac6266a727e07e7f4266bedc Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sat, 30 Aug 2025 13:33:59 +0000 Subject: [PATCH 02/14] fix: dynamically remove listeners when tab unload `browser.runtime.onMessage` listeners seems to prevent content scripts recycle from causing memory leaks --- .../content-scripts/entry-script-market.js | 23 +++++++++++++++---- src/ext/content-scripts/entry-userscripts.js | 23 +++++++++++++++---- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/ext/content-scripts/entry-script-market.js b/src/ext/content-scripts/entry-script-market.js index 67022e47..45ba17d6 100644 --- a/src/ext/content-scripts/entry-script-market.js +++ b/src/ext/content-scripts/entry-script-market.js @@ -9,10 +9,10 @@ async function injection() { ); } for (const link of links) { + if (link["href"]) url = link["href"]; link.addEventListener( "click", (e) => { - url = link["href"]; e.stopImmediatePropagation(); e.preventDefault(); browser.runtime.sendMessage({ name: "WEB_USERJS_POPUP" }); @@ -23,14 +23,27 @@ async function injection() { } async function listeners() { - browser.runtime.onMessage.addListener(async (message) => { + /** + * handle messages from background, popup, etc... + * @type {import("webextension-polyfill").Runtime.OnMessageListener} + */ + const handleMessage = async (message) => { if (import.meta.env.MODE === "development") { console.debug(message, url); } if (message === "TAB_CLICK_USERJS") { - const response = url; - url = undefined; // respond only once of click event - return response; + return url; + } + }; + /** Dynamically remove listeners to avoid memory leaks */ + if (document.visibilityState === "visible") { + browser.runtime.onMessage.addListener(handleMessage); + } + document.addEventListener("visibilitychange", () => { + if (document.hidden) { + browser.runtime.onMessage.removeListener(handleMessage); + } else { + browser.runtime.onMessage.addListener(handleMessage); } }); } diff --git a/src/ext/content-scripts/entry-userscripts.js b/src/ext/content-scripts/entry-userscripts.js index e160d7e3..362bfacb 100644 --- a/src/ext/content-scripts/entry-userscripts.js +++ b/src/ext/content-scripts/entry-userscripts.js @@ -241,17 +241,19 @@ function listeners() { document.addEventListener("securitypolicyviolation", cspFallback, { once: true, }); - // listens for messages from background, popup, etc... - browser.runtime.onMessage.addListener((request) => { - const name = request.name; + /** + * listens for messages from background, popup, etc... + * @type {import("webextension-polyfill").Runtime.OnMessageListener} + */ + const handleMessage = (message) => { + const name = message.name; if (name === "CONTEXT_RUN") { // from bg script when context-menu item is clicked // double check to ensure context-menu scripts only run in top windows if (window !== window.top) return; - // loop through context-menu scripts saved to data object and find match // if no match found, nothing will execute and error will log - const filename = request.menuItemId; + const filename = message.menuItemId; for (let i = 0; i < data.files.menu.length; i++) { const item = data.files.menu[i]; if (item.scriptObject.filename === filename) { @@ -262,6 +264,17 @@ function listeners() { } console.error(`Couldn't find ${filename} code!`); } + }; + /** Dynamically remove listeners to avoid memory leaks */ + if (document.visibilityState === "visible") { + browser.runtime.onMessage.addListener(handleMessage); + } + document.addEventListener("visibilitychange", () => { + if (document.hidden) { + browser.runtime.onMessage.removeListener(handleMessage); + } else { + browser.runtime.onMessage.addListener(handleMessage); + } }); } From f8683a962eb88e286aa28f50b6d0c1b87d55daf8 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sat, 30 Aug 2025 14:04:38 +0000 Subject: [PATCH 03/14] build(gem): update dependencies --- fastlane/Gemfile.lock | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/fastlane/Gemfile.lock b/fastlane/Gemfile.lock index a8ba4db2..8cbf6d55 100644 --- a/fastlane/Gemfile.lock +++ b/fastlane/Gemfile.lock @@ -10,25 +10,27 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1125.0) - aws-sdk-core (3.226.2) + aws-partitions (1.1153.0) + aws-sdk-core (3.232.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) base64 + bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.106.0) - aws-sdk-core (~> 3, >= 3.225.0) + aws-sdk-kms (1.112.0) + aws-sdk-core (~> 3, >= 3.231.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.192.0) - aws-sdk-core (~> 3, >= 3.225.0) + aws-sdk-s3 (1.198.0) + aws-sdk-core (~> 3, >= 3.231.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.3.0) + bigdecimal (3.2.2) claide (1.1.0) colored (1.2) colored2 (3.1.2) @@ -157,13 +159,13 @@ GEM httpclient (2.9.0) mutex_m jmespath (1.6.2) - json (2.12.2) + json (2.13.2) jwt (2.10.2) base64 logger (1.7.0) mini_magick (4.13.2) mini_mime (1.1.5) - multi_json (1.15.0) + multi_json (1.17.0) multipart-post (2.4.1) mutex_m (0.3.0) nanaimo (0.4.0) @@ -179,15 +181,15 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.4.1) + rexml (3.4.2) rouge (3.28.0) ruby2_keywords (0.0.5) rubyzip (2.4.1) security (0.1.5) - signet (0.20.0) + signet (0.21.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 3.0) + jwt (>= 1.5, < 4.0) multi_json (~> 1.10) simctl (1.6.10) CFPropertyList From bcf0e9c02678ec74a1905e3c76cb051d4ae51b84 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sat, 30 Aug 2025 14:05:24 +0000 Subject: [PATCH 04/14] ci: update xcode version --- .github/workflows/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deployment.yaml b/.github/workflows/deployment.yaml index 86855b3f..e66a4544 100644 --- a/.github/workflows/deployment.yaml +++ b/.github/workflows/deployment.yaml @@ -38,7 +38,7 @@ jobs: - run: npm run build:${{ matrix.platform }}-safari-15 if: ${{ ! github.event.release.prerelease }} - name: Set xcode version - run: sudo xcode-select -s "/Applications/Xcode_26_beta.app" # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md#xcode + run: sudo xcode-select -s "/Applications/Xcode_26_beta_6.app" # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md#xcode - run: xcodebuild -downloadPlatform iOS # Temporary fix for Xcode_26_beta - name: Run fastlane id: fastlane From 77d5237e5fb7509302bb6dc18dbc1094c6314817 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Tue, 16 Sep 2025 06:06:29 +0000 Subject: [PATCH 05/14] ci: limit matrix platform for ios 26 sdk --- .github/workflows/deployment.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deployment.yaml b/.github/workflows/deployment.yaml index e66a4544..f1a99642 100644 --- a/.github/workflows/deployment.yaml +++ b/.github/workflows/deployment.yaml @@ -40,6 +40,7 @@ jobs: - name: Set xcode version run: sudo xcode-select -s "/Applications/Xcode_26_beta_6.app" # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md#xcode - run: xcodebuild -downloadPlatform iOS # Temporary fix for Xcode_26_beta + if: matrix.platform == 'ios' - name: Run fastlane id: fastlane working-directory: ./fastlane From 77a0ec828637e760e4e212ebc5a2ce3e7f09fa39 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sat, 29 Nov 2025 08:49:32 +0000 Subject: [PATCH 06/14] chore: do not use localized description for logger --- xcode/App-Mac/ViewController.swift | 2 +- xcode/App-Shared/ViewController.swift | 6 +++--- xcode/Ext-Safari/Functions.swift | 14 +++++++------- xcode/Shared/Preferences.swift | 8 ++++---- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/xcode/App-Mac/ViewController.swift b/xcode/App-Mac/ViewController.swift index fc7598ee..e09d909c 100644 --- a/xcode/App-Mac/ViewController.swift +++ b/xcode/App-Mac/ViewController.swift @@ -36,7 +36,7 @@ class ViewController: NSViewController { guard let state = state else { self.enabledText.stringValue = "Safari Extension State Unknown" if let error = error { - logger?.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") + logger?.error("\(#function, privacy: .public) - \(error, privacy: .public)") } else { logger?.error("\(#function, privacy: .public) - couldn't get safari extension state in containing app") } diff --git a/xcode/App-Shared/ViewController.swift b/xcode/App-Shared/ViewController.swift index db573177..83e0040d 100644 --- a/xcode/App-Shared/ViewController.swift +++ b/xcode/App-Shared/ViewController.swift @@ -149,7 +149,7 @@ class ViewController: PlatformViewController, WKNavigationDelegate, WKScriptMess self.extensionState = state.isEnabled ? .on : .off } catch { self.extensionState = .error - logger?.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") + logger?.error("\(#function, privacy: .public) - \(error, privacy: .public)") } if #available(macOS 13, *) { useSettingsInsteadOfPreferences = true @@ -193,7 +193,7 @@ class ViewController: PlatformViewController, WKNavigationDelegate, WKScriptMess do { try await SFSafariApplication.showPreferencesForExtension(withIdentifier: extIdentifier) } catch { - logger?.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") + logger?.error("\(#function, privacy: .public) - \(error, privacy: .public)") return (nil, error.localizedDescription) } break @@ -344,7 +344,7 @@ extension ViewController { } else { status = .error if let error = error { - logger?.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") + logger?.error("\(#function, privacy: .public) - \(error, privacy: .public)") } else { logger?.error("\(#function, privacy: .public) - couldn't get safari extension state in containing app") } diff --git a/xcode/Ext-Safari/Functions.swift b/xcode/Ext-Safari/Functions.swift index 7163c9eb..582aef0b 100644 --- a/xcode/Ext-Safari/Functions.swift +++ b/xcode/Ext-Safari/Functions.swift @@ -220,7 +220,7 @@ func updateManifest(with data: Manifest) -> Bool { try fileContent.write(to: url, atomically: false, encoding: .utf8) return true } catch { - logger?.error("\(#function, privacy: .public) - failed to update manifest: \(error.localizedDescription, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed to update manifest: \(error, privacy: .public)") return false } } @@ -742,7 +742,7 @@ func getRequiredCode(_ filename: String, _ resources: [String], _ fileType: Stri do { try FileManager.default.removeItem(at: directory) } catch { - logger?.error("\(#function, privacy: .public) - failed to remove directory: \(error.localizedDescription, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed to remove directory: \(error, privacy: .public)") } } return true @@ -796,11 +796,11 @@ func getRequiredCode(_ filename: String, _ resources: [String], _ fileType: Stri try FileManager.default.removeItem(at: abandonFileUrl) logger?.info("\(#function, privacy: .public) - cleanup abandoned resource: \(unsanitize(abandonFileUrl.lastPathComponent), privacy: .public)") } catch { - logger?.error("\(#function, privacy: .public) - failed to remove abandoned resource: \(error.localizedDescription, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed to remove abandoned resource: \(error, privacy: .public)") } } } catch { - logger?.error("\(#function, privacy: .public) - failed to cleanup resources: \(error.localizedDescription, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed to cleanup resources: \(error, privacy: .public)") } return true } @@ -877,7 +877,7 @@ func getRemoteFileContents(_ url: String) -> String? { } } if let error = error { - logger?.error("\(#function, privacy: .public) - task error: \(error.localizedDescription, privacy: .public) (\(url, privacy: .public))") + logger?.error("\(#function, privacy: .public) - task error: \(error, privacy: .public) (\(url, privacy: .public))") } semaphore.signal() } @@ -984,7 +984,7 @@ func checkDefaultDirectories() -> Bool { try FileManager.default.createDirectory(at: url, withIntermediateDirectories: false) } catch { // could not create the save location directory, show error - logger?.error("\(#function, privacy: .public) - failed at (1) - \(url, privacy: .public) - \(error.localizedDescription, privacy: .public)") + logger?.error("\(#function, privacy: .public) - failed at (1) - \(url, privacy: .public) - \(error, privacy: .public)") return false } } @@ -1763,7 +1763,7 @@ func trashFile(_ item: [String: Any]) -> Bool { do { try FileManager.default.trashItem(at: url, resultingItemURL: nil) } catch { - logger?.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") + logger?.error("\(#function, privacy: .public) - \(error, privacy: .public)") return false } } diff --git a/xcode/Shared/Preferences.swift b/xcode/Shared/Preferences.swift index d26e10ed..45c5a5fe 100644 --- a/xcode/Shared/Preferences.swift +++ b/xcode/Shared/Preferences.swift @@ -60,7 +60,7 @@ private struct SecurityScopedBookmark { relativeTo: nil ) } catch { - logger?.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") + logger?.error("\(#function, privacy: .public) - \(error, privacy: .public)") } return nil } @@ -74,7 +74,7 @@ private struct SecurityScopedBookmark { bookmarkDataIsStale: &isStale ) } catch { - logger?.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") + logger?.error("\(#function, privacy: .public) - \(error, privacy: .public)") } return nil } @@ -202,7 +202,7 @@ private struct SecurityScopedBookmark { do { return try url.bookmarkData(options: .minimalBookmark, includingResourceValuesForKeys: nil, relativeTo: nil) } catch { - logger?.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") + logger?.error("\(#function, privacy: .public) - \(error, privacy: .public)") } return nil } @@ -211,7 +211,7 @@ private struct SecurityScopedBookmark { do { return try URL(resolvingBookmarkData: data, bookmarkDataIsStale: &isStale) } catch { - logger?.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") + logger?.error("\(#function, privacy: .public) - \(error, privacy: .public)") } return nil } From 57467c9a27f1ed03d5e237513f6f394a81a5ed01 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sat, 29 Nov 2025 09:02:52 +0000 Subject: [PATCH 07/14] fix(ios): not remove bookmark while resolve fails The failure may be temporary, and not removing the bookmark may alleviate the need for user interaction and frustration, but it does not solve the underlying problem, and the error will still occur silently. --- xcode/Shared/Preferences.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/xcode/Shared/Preferences.swift b/xcode/Shared/Preferences.swift index 45c5a5fe..20f0b60e 100644 --- a/xcode/Shared/Preferences.swift +++ b/xcode/Shared/Preferences.swift @@ -237,7 +237,9 @@ private struct SecurityScopedBookmark { } var isStale = false guard let url = resolvBookmark(data, &isStale) else { // get security-scoped URL - SDefaults?.removeObject(forKey: key) // remove data that cannot be resolved + // Note: temporary mitigation measures for occasional failures on some iOS devices +// SDefaults?.removeObject(forKey: key) // remove data that cannot be resolved + logger?.warning("\(#function, privacy: .public) - faild resolv bookmark, return default") return defaultValue } if isStale, url.startAccessingSecurityScopedResource() { // renew URL bookmark From e287a03a575377941fea5fad5fc892db57300eeb Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sat, 29 Nov 2025 09:33:59 +0000 Subject: [PATCH 08/14] ci: update to macos-26 runner image --- .github/workflows/deployment.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deployment.yaml b/.github/workflows/deployment.yaml index f1a99642..f9b2e529 100644 --- a/.github/workflows/deployment.yaml +++ b/.github/workflows/deployment.yaml @@ -9,7 +9,7 @@ jobs: strategy: matrix: # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs platform: [mac, ios] - runs-on: macos-15 # https://github.com/actions/runner-images + runs-on: macos-26 # https://github.com/actions/runner-images steps: - uses: actions/checkout@v4 # https://github.com/actions/checkout with: @@ -37,10 +37,10 @@ jobs: if: ${{ github.event.release.prerelease }} - run: npm run build:${{ matrix.platform }}-safari-15 if: ${{ ! github.event.release.prerelease }} - - name: Set xcode version - run: sudo xcode-select -s "/Applications/Xcode_26_beta_6.app" # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md#xcode - - run: xcodebuild -downloadPlatform iOS # Temporary fix for Xcode_26_beta - if: matrix.platform == 'ios' + # - name: Set xcode version + # run: sudo xcode-select -s "/Applications/Xcode_26.1.1.app" # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md#xcode + # - run: xcodebuild -downloadPlatform iOS # Temporary fix for Xcode_26_beta + # if: matrix.platform == 'ios' - name: Run fastlane id: fastlane working-directory: ./fastlane From 7d77cab92225be44a9a0fe76f6ac3caa5485b200 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sat, 29 Nov 2025 10:53:12 +0000 Subject: [PATCH 09/14] build(gem): update fastlane --- fastlane/Gemfile.lock | 52 +++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/fastlane/Gemfile.lock b/fastlane/Gemfile.lock index 8cbf6d55..c6013a88 100644 --- a/fastlane/Gemfile.lock +++ b/fastlane/Gemfile.lock @@ -1,17 +1,15 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.7) - base64 - nkf - rexml - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) + CFPropertyList (3.0.8) + abbrev (0.1.2) + addressable (2.8.8) + public_suffix (>= 2.0.2, < 8.0) artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1153.0) - aws-sdk-core (3.232.0) + aws-partitions (1.1188.0) + aws-sdk-core (3.239.2) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -19,23 +17,24 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.112.0) - aws-sdk-core (~> 3, >= 3.231.0) + aws-sdk-kms (1.118.0) + aws-sdk-core (~> 3, >= 3.239.1) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.198.0) - aws-sdk-core (~> 3, >= 3.231.0) + aws-sdk-s3 (1.205.0) + aws-sdk-core (~> 3, >= 3.234.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) - base64 (0.3.0) - bigdecimal (3.2.2) + base64 (0.2.0) + bigdecimal (3.3.1) claide (1.1.0) colored (1.2) colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) + csv (3.3.5) declarative (0.0.20) digest-crc (0.7.0) rake (>= 12.0.0, < 14.0.0) @@ -55,9 +54,9 @@ GEM faraday-rack (~> 1.0) faraday-retry (~> 1.0) ruby2_keywords (>= 0.0.4) - faraday-cookie_jar (0.0.7) + faraday-cookie_jar (0.0.8) faraday (>= 0.8.0) - http-cookie (~> 1.0.0) + http-cookie (>= 1.0.0) faraday-em_http (1.0.0) faraday-em_synchrony (1.0.1) faraday-excon (1.1.0) @@ -72,15 +71,18 @@ GEM faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.4.0) - fastlane (2.228.0) + fastlane (2.229.1) CFPropertyList (>= 2.3, < 4.0.0) + abbrev (~> 0.1.2) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) + base64 (~> 0.2.0) bundler (>= 1.12.0, < 3.0.0) colored (~> 1.2) commander (~> 4.6) + csv (~> 3.3) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) excon (>= 0.71.0, < 1.0.0) @@ -100,7 +102,9 @@ GEM jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) + mutex_m (~> 0.3.0) naturally (~> 2.2) + nkf (~> 0.2.0) optparse (>= 0.1.1, < 1.0.0) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) @@ -159,7 +163,7 @@ GEM httpclient (2.9.0) mutex_m jmespath (1.6.2) - json (2.13.2) + json (2.16.0) jwt (2.10.2) base64 logger (1.7.0) @@ -171,17 +175,17 @@ GEM nanaimo (0.4.0) naturally (2.3.0) nkf (0.2.0) - optparse (0.6.0) + optparse (0.8.0) os (1.1.4) plist (3.7.2) - public_suffix (6.0.2) - rake (13.3.0) + public_suffix (7.0.0) + rake (13.3.1) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.4.2) + rexml (3.4.4) rouge (3.28.0) ruby2_keywords (0.0.5) rubyzip (2.4.1) @@ -219,11 +223,11 @@ GEM xcpretty (~> 0.2, >= 0.0.7) PLATFORMS - arm64-darwin-23 + arm64-darwin-25 ruby DEPENDENCIES fastlane BUNDLED WITH - 2.6.9 + 2.7.2 From f9944f7a9decd3c3430a7084f04e2178d2612f84 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sat, 10 Jan 2026 00:16:19 +0000 Subject: [PATCH 10/14] fix: significantly reduces memory usage Avoid compiling RE repeatedly in loops, especially in large metadata scripts. --- xcode/Ext-Safari/Functions.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/xcode/Ext-Safari/Functions.swift b/xcode/Ext-Safari/Functions.swift index 582aef0b..0ebb950f 100644 --- a/xcode/Ext-Safari/Functions.swift +++ b/xcode/Ext-Safari/Functions.swift @@ -134,18 +134,18 @@ func parse(_ content: String) -> [String: Any]? { var metadata = [:] as [String: [String]] // iterate through the possible metadata keys in file if let metas = Range(match.range(at: g2), in: content) { + let p = #"^(?:[ \t]*(?:\/\/)?[ \t]*@)([\w-]+)[ \t]+([^\s]+[^\r\n\t\v\f]*)"# + // this pattern checks for specific keys that won't have values + let p2 = #"^(?:[ \t]*(?:\/\/)?[ \t]*@)(noframes)[ \t]*$"# + // force try b/c pattern is known to be valid regex + let re = try! NSRegularExpression(pattern: p, options: []) + let re2 = try! NSRegularExpression(pattern: p2, options: []) // split metadatas by new line let metaArray = content[metas].split(whereSeparator: \.isNewline) // loop through metadata lines and populate metadata dictionary for meta in metaArray { - let p = #"^(?:[ \t]*(?:\/\/)?[ \t]*@)([\w-]+)[ \t]+([^\s]+[^\r\n\t\v\f]*)"# - // this pattern checks for specific keys that won't have values - let p2 = #"^(?:[ \t]*(?:\/\/)?[ \t]*@)(noframes)[ \t]*$"# // the individual meta string, ie. // @name File Name let metaString = String(meta).trimmingCharacters(in: .whitespaces) - // force try b/c pattern is known to be valid regex - let re = try! NSRegularExpression(pattern: p, options: []) - let re2 = try! NSRegularExpression(pattern: p2, options: []) let range = NSRange(location: 0, length: metaString.utf16.count) // key lines not properly prefixed & without values will be skipped if let m = re.firstMatch(in: metaString, options: [], range: range) { From b0990a56e567d3cfcb0fe3fcb1683d9eafca7abd Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sat, 10 Jan 2026 01:45:50 +0000 Subject: [PATCH 11/14] fix: further reduces memory usage --- xcode/Ext-Safari/Functions.swift | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/xcode/Ext-Safari/Functions.swift b/xcode/Ext-Safari/Functions.swift index 0ebb950f..220a380a 100644 --- a/xcode/Ext-Safari/Functions.swift +++ b/xcode/Ext-Safari/Functions.swift @@ -94,16 +94,20 @@ func isEncoded(_ str: String) -> Bool { return false } +// parser REs - force try b/c pattern is known to be valid regex +let re0 = try! NSRegularExpression(pattern: #"(?:(\/\/ ==UserScript==[ \t]*?\r?\n([\S\s]*?)\r?\n\/\/ ==\/UserScript==)([\S\s]*)|(\/\* ==UserStyle==[ \t]*?\r?\n([\S\s]*?)\r?\n==\/UserStyle== \*\/)([\S\s]*))"#, options: []) +// RE for meta line +let re1 = try! NSRegularExpression(pattern: #"^(?:[ \t]*(?:\/\/)?[ \t]*@)([\w-]+)[ \t]+([^\s]+[^\r\n\t\v\f]*)"#, options: []) +// this pattern checks for specific keys that won't have values +let re2 = try! NSRegularExpression(pattern: #"^(?:[ \t]*(?:\/\/)?[ \t]*@)(noframes)[ \t]*$"#, options: []) + // parser func parse(_ content: String) -> [String: Any]? { // returns structured data from content of file // will fail to parse if metablock or required @name key missing - let pattern = #"(?:(\/\/ ==UserScript==[ \t]*?\r?\n([\S\s]*?)\r?\n\/\/ ==\/UserScript==)([\S\s]*)|(\/\* ==UserStyle==[ \t]*?\r?\n([\S\s]*?)\r?\n==\/UserStyle== \*\/)([\S\s]*))"# - // force try b/c pattern is known to be valid regex - let regex = try! NSRegularExpression(pattern: pattern, options: []) let range = NSRange(location: 0, length: content.utf16.count) // return nil/fail if metablock missing - guard let match = regex.firstMatch(in: content, options: [], range: range) else { + guard let match = re0.firstMatch(in: content, options: [], range: range) else { logger?.debug("\(#function, privacy: .public) - Non matched content: \(content, privacy: .public)") return nil } @@ -134,12 +138,6 @@ func parse(_ content: String) -> [String: Any]? { var metadata = [:] as [String: [String]] // iterate through the possible metadata keys in file if let metas = Range(match.range(at: g2), in: content) { - let p = #"^(?:[ \t]*(?:\/\/)?[ \t]*@)([\w-]+)[ \t]+([^\s]+[^\r\n\t\v\f]*)"# - // this pattern checks for specific keys that won't have values - let p2 = #"^(?:[ \t]*(?:\/\/)?[ \t]*@)(noframes)[ \t]*$"# - // force try b/c pattern is known to be valid regex - let re = try! NSRegularExpression(pattern: p, options: []) - let re2 = try! NSRegularExpression(pattern: p2, options: []) // split metadatas by new line let metaArray = content[metas].split(whereSeparator: \.isNewline) // loop through metadata lines and populate metadata dictionary @@ -148,7 +146,7 @@ func parse(_ content: String) -> [String: Any]? { let metaString = String(meta).trimmingCharacters(in: .whitespaces) let range = NSRange(location: 0, length: metaString.utf16.count) // key lines not properly prefixed & without values will be skipped - if let m = re.firstMatch(in: metaString, options: [], range: range) { + if let m = re1.firstMatch(in: metaString, options: [], range: range) { // force unwrap key & value since matches regex above let key = metaString[Range(m.range(at: 1), in: metaString)!] let value = metaString[Range(m.range(at: 2), in: metaString)!] @@ -1003,6 +1001,9 @@ func stringToRegex(_ stringPattern: String) -> NSRegularExpression? { return regex } +// RE for parse matchPattern +let re3 = try! NSRegularExpression(pattern: #"^(http:|https:|\*:)\/\/((?:\*\.)?(?:[a-z0-9-]+\.)+(?:[a-z0-9]+)|\*\.[a-z]+|\*|[a-z0-9]+)(\/[^\s]*)$"#, options: .caseInsensitive) + func match(_ url: String, _ matchPattern: String) -> Bool { guard let parts = jsLikeURL(url), @@ -1028,10 +1029,8 @@ func match(_ url: String, _ matchPattern: String) -> Bool { if (ptcl != "http:" && ptcl != "https:") { return false } - let partsPattern = #"^(http:|https:|\*:)\/\/((?:\*\.)?(?:[a-z0-9-]+\.)+(?:[a-z0-9]+)|\*\.[a-z]+|\*|[a-z0-9]+)(\/[^\s]*)$"# - let partsPatternReg = try! NSRegularExpression(pattern: partsPattern, options: .caseInsensitive) let range = NSMakeRange(0, matchPattern.utf16.count) - guard let parts = partsPatternReg.firstMatch(in: matchPattern, options: [], range: range) else { + guard let parts = re3.firstMatch(in: matchPattern, options: [], range: range) else { logger?.error("\(#function, privacy: .public) - malformed regex match pattern - \(matchPattern, privacy: .public)") return false } From 012fc8311588839c123fee584d2b2b01764da928 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sun, 10 May 2026 00:35:52 +0000 Subject: [PATCH 12/14] style: use light-dark instead of prefers-color-scheme --- src/shared/variables.css | 122 +++++++++++++++------------------------ 1 file changed, 47 insertions(+), 75 deletions(-) diff --git a/src/shared/variables.css b/src/shared/variables.css index 225c2ae6..f014eba2 100644 --- a/src/shared/variables.css +++ b/src/shared/variables.css @@ -24,80 +24,52 @@ --editor-matched-highlight: rgb(116 178 235 / 0.2); --editor-search-highlight: rgb(255 166 0 / 0.3); - /* Dark theme (default) */ - --color-bg-primary: #323639; - --color-bg-secondary: #2f3337; - --color-bg-theme: var(--color-black); - --color-script-highlighted: #364049; - --color-black: #1d2023; - --color-white: #fff; - --color-blue: #74b1eb; - --color-green: #60f36c; - --color-grey: rgb(255 255 255 / 0.15); - --color-red: #ff453a; - --color-yellow: #e4f360; - --text-color-primary: rgb(255 255 255); - --text-color-secondary: rgb(255 255 255 / 0.65); - --text-color-disabled: rgb(255 255 255 / 0.4); - --box-shadow: 0 0.5rem 1rem 0 rgb(0 0 0 / 0.25); - --button-color: var(--color-black); - --border-color: var(--color-black); + /* Color Theme */ + color-scheme: light dark; + --color-bg-primary: light-dark(#fafafa, #323639); + --color-bg-secondary: light-dark(#f1f1f1, #2f3337); + --color-bg-theme: light-dark(var(--color-white), var(--color-black)); + --color-script-highlighted: light-dark(#e2ebf3, #364049); + --color-black: light-dark(#333, #1d2023); + --color-white: light-dark(#fff, #fff); + --color-blue: light-dark(#317eff, #74b1eb); + --color-green: light-dark(#2bb239, #60f36c); + --color-grey: light-dark(rgb(51 51 51 / 0.15), rgb(255 255 255 / 0.15)); + --color-red: light-dark(#ff453a, #ff453a); + --color-yellow: light-dark(#b8722c, #e4f360); + --text-color-primary: light-dark(rgb(51 51 51), rgb(255 255 255)); + --text-color-secondary: light-dark( + rgb(51 51 51 / 0.65), + rgb(255 255 255 / 0.65) + ); + --text-color-disabled: light-dark( + rgb(51 51 51 / 0.35), + rgb(255 255 255 / 0.4) + ); + --box-shadow: 0 0.5rem 1rem 0 light-dark(rgb(0 0 0 / 0.15), rgb(0 0 0 / 0.25)); + --button-color: light-dark(var(--color-white), var(--color-black)); + --border-color: light-dark(var(--color-grey), var(--color-black)); - /* Editor dark theme colors */ - --editor-invisible: rgb(255 255 255 / 0.15); - --editor-number: #77e26a; - --editor-comment: rgb(255 255 255 / 0.35); - --editor-def: #efc371; - --editor-default: #cdcfd1; - --editor-keyword: #96c3ed; - --editor-atom: #59ebf5; - --editor-operator: #8c99a7; - --editor-property: #e86c8a; - --editor-string: #f5eea2; - --editor-string-2: #cdabff; - --editor-cursor: #e3e7eb; - --editor-matching-bracket-color: #fff; - --editor-matching-bracket-border: var(--editor-number); - --editor-blurred-background: rgb(50 54 57 / 0.65); -} - -/* Light theme */ -@media (prefers-color-scheme: light) { - :root { - color-scheme: light dark; - --color-bg-primary: #fafafa; - --color-bg-secondary: #f1f1f1; - --color-bg-theme: var(--color-white); - --color-script-highlighted: #e2ebf3; - --color-black: #333; - --color-white: #fff; - --color-blue: #317eff; - --color-green: #2bb239; - --color-grey: rgb(51 51 51 / 0.15); - --color-red: #ff453a; - --color-yellow: #b8722c; - --text-color-primary: rgb(51 51 51); - --text-color-secondary: rgb(51 51 51 / 0.65); - --text-color-disabled: rgb(51 51 51 / 0.35); - --box-shadow: 0 0.25rem 1rem 0 rgb(0 0 0 / 0.15); - --button-color: var(--color-white); - --border-color: var(--color-grey); - - /* Editor light theme colors */ - --editor-invisible: rgb(51 51 51 / 0.15); - --editor-number: var(--color-green); - --editor-comment: var(--text-color-disabled); - --editor-def: #b86d17; - --editor-default: #777; - --editor-keyword: #3a68ff; - --editor-atom: #00adad; - --editor-operator: #8c99a7; - --editor-property: #ff453a; - --editor-string: var(--editor-atom); - --editor-string-2: #a34fe6; - --editor-cursor: #7e7e7f; - --editor-matching-bracket-color: var(--text-color-primary); - --editor-matching-bracket-border: #ff00ea; - --editor-blurred-background: rgb(255 255 255 / 0.65); - } + /* Editor colors */ + --editor-invisible: light-dark(rgb(51 51 51 / 0.15), rgb(255 255 255 / 0.15)); + --editor-number: light-dark(var(--color-green), #77e26a); + --editor-comment: light-dark( + var(--text-color-disabled), + rgb(255 255 255 / 0.35) + ); + --editor-def: light-dark(#b86d17, #efc371); + --editor-default: light-dark(#777, #cdcfd1); + --editor-keyword: light-dark(#3a68ff, #96c3ed); + --editor-atom: light-dark(#00adad, #59ebf5); + --editor-operator: light-dark(#8c99a7, #8c99a7); + --editor-property: light-dark(#ff453a, #e86c8a); + --editor-string: light-dark(var(--editor-atom), #f5eea2); + --editor-string-2: light-dark(#a34fe6, #cdabff); + --editor-cursor: light-dark(#7e7e7f, #e3e7eb); + --editor-matching-bracket-color: light-dark(var(--text-color-primary), #fff); + --editor-matching-bracket-border: light-dark(#ff00ea, var(--editor-number)); + --editor-blurred-background: light-dark( + rgb(255 255 255 / 0.65), + rgb(50 54 57 / 0.65) + ); } From dafc2cfa49dc316c3bee95ab7ce24c06b0a521bb Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sun, 10 May 2026 01:02:33 +0000 Subject: [PATCH 13/14] feat: add theme mode setting item --- public/ext/shared/_locales/en/messages.json | 18 ++++++++++++++++++ src/ext/shared/settings.js | 8 ++++++++ 2 files changed, 26 insertions(+) diff --git a/public/ext/shared/_locales/en/messages.json b/public/ext/shared/_locales/en/messages.json index a39bfc24..c17c6cad 100644 --- a/public/ext/shared/_locales/en/messages.json +++ b/public/ext/shared/_locales/en/messages.json @@ -85,6 +85,24 @@ "settings_global_active_desc": { "message": "Toggle on/off script injection for the pages you visit" }, + "settings_theme_mode": { + "message": "Theme mode" + }, + "settings_theme_mode_desc": { + "message": "Toggle theme mode" + }, + "settings_theme_mode_auto": { + "message": "Auto (System)" + }, + "settings_theme_mode_dark": { + "message": "Dark" + }, + "settings_theme_mode_light": { + "message": "Light" + }, + "settings_theme_mode_custom": { + "message": "Custom" + }, "settings_global_exclude_match": { "message": "Global Exclude Match patterns" }, diff --git a/src/ext/shared/settings.js b/src/ext/shared/settings.js index 37d83e1d..6154d6bd 100644 --- a/src/ext/shared/settings.js +++ b/src/ext/shared/settings.js @@ -124,6 +124,14 @@ const settingsDefinition = /** @type {const} */ [ group: "general", nodeType: "Toggle", }, + { + name: "theme_mode", + type: "string", + values: ["auto", "dark", "light"], + default: "auto", + group: "general", + nodeType: "select", + }, { name: "global_active", type: "boolean", From 5705b12d1535099558c0dd7dbb633b283657b6af Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sun, 10 May 2026 01:02:39 +0000 Subject: [PATCH 14/14] feat: aplly theme mode for editor and popup --- src/ext/action-popup/App.svelte | 6 ++++++ src/ext/extension-page/App.svelte | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/src/ext/action-popup/App.svelte b/src/ext/action-popup/App.svelte index 5740e7b5..6d593530 100644 --- a/src/ext/action-popup/App.svelte +++ b/src/ext/action-popup/App.svelte @@ -242,6 +242,12 @@ } platform = pltfm.platform; + /* Override theme mode */ + const theme = await settingsStorage.get("theme_mode"); + if (theme && theme !== "auto") { + document.body.style.setProperty("color-scheme", theme); + } + // display native error if there is const errorNative = await settingsStorage.get("error_native"); if (errorNative.error) { diff --git a/src/ext/extension-page/App.svelte b/src/ext/extension-page/App.svelte index 22487e8f..1c4ce4d3 100644 --- a/src/ext/extension-page/App.svelte +++ b/src/ext/extension-page/App.svelte @@ -19,6 +19,15 @@ } }); + /* Override theme mode */ + $: if ($settings["theme_mode"]) { + if ($settings["theme_mode"] === "auto") { + document.body.style.removeProperty("color-scheme"); + } else { + document.body.style.setProperty("color-scheme", $settings["theme_mode"]); + } + } + // disables default cmd+s (save) and cmd+f (find) behavior function preventKeyCommands(e) { if (e.metaKey && (e.code === "KeyS" || e.code === "KeyF")) {