From d581e67a5df2b12965e4ef1571ddd8e5617402b8 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Mon, 11 Dec 2023 03:27:03 +0800 Subject: [PATCH 01/24] fix(style): cancel the use of global styles --- src/ext/extension-page/Components/Editor/Editor.svelte | 1 + .../extension-page/Components/Editor/EditorSearch.svelte | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/ext/extension-page/Components/Editor/Editor.svelte b/src/ext/extension-page/Components/Editor/Editor.svelte index 5d831d39..362d7ed9 100644 --- a/src/ext/extension-page/Components/Editor/Editor.svelte +++ b/src/ext/extension-page/Components/Editor/Editor.svelte @@ -307,6 +307,7 @@ .editor__header__buttons { display: flex; + margin-right: 1rem; } :global(.editor__header__buttons > button:nth-of-type(2)) { diff --git a/src/ext/extension-page/Components/Editor/EditorSearch.svelte b/src/ext/extension-page/Components/Editor/EditorSearch.svelte index cc1f70db..ad132f0e 100644 --- a/src/ext/extension-page/Components/Editor/EditorSearch.svelte +++ b/src/ext/extension-page/Components/Editor/EditorSearch.svelte @@ -168,9 +168,9 @@ border-radius: var(--border-radius); box-shadow: var(--box-shadow); display: flex; - padding: 0.25rem 0; + padding: 0.25rem; position: absolute; - right: 0.5rem; + right: 1.5rem; top: 0; z-index: 4; } @@ -197,10 +197,6 @@ flex-shrink: 0; } - :global(button:nth-of-type(3)) { - margin-right: 0.25rem; - } - :global(div.editor__search button svg) { width: 45%; } From d9f52a714f1e319adf98b8c65f77d7feb79f1708 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Fri, 15 Dec 2023 12:36:05 +0800 Subject: [PATCH 02/24] fix: avoid code scroll bouncing --- .../Components/Editor/CodeMirror.svelte | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/ext/extension-page/Components/Editor/CodeMirror.svelte b/src/ext/extension-page/Components/Editor/CodeMirror.svelte index e0c39081..f2d2d13b 100644 --- a/src/ext/extension-page/Components/Editor/CodeMirror.svelte +++ b/src/ext/extension-page/Components/Editor/CodeMirror.svelte @@ -406,10 +406,10 @@ {#if instance} {/if} + + From 63ccaa72cb97a47e1a4e19fa5383f642ee85a754 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Fri, 15 Dec 2023 13:10:32 +0800 Subject: [PATCH 03/24] refactor: using css instead of window listener --- src/ext/extension-page/App.svelte | 19 ++++++------------- src/ext/extension-page/app.css | 1 + 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/ext/extension-page/App.svelte b/src/ext/extension-page/App.svelte index a457e035..d802643a 100644 --- a/src/ext/extension-page/App.svelte +++ b/src/ext/extension-page/App.svelte @@ -1,5 +1,5 @@ - + {#if $state.includes("init")}
diff --git a/src/ext/extension-page/app.css b/src/ext/extension-page/app.css index 425bcaa4..f34a9b9a 100644 --- a/src/ext/extension-page/app.css +++ b/src/ext/extension-page/app.css @@ -1,6 +1,7 @@ html { font-size: 100%; height: 100vh; + height: 100svh; /* safari 15.4 */ overflow: hidden; } From d8f01d3c7390bfcf58a1f74406aae422774bdbdb Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sat, 30 Dec 2023 18:51:55 +0800 Subject: [PATCH 04/24] fix: replace broken app launch process It seems that due to macOS upgrades, the order in which the launch delegate is called has changed, now `application(_:open:)` is called after `applicationDidFinishLaunching(_:)` (their order itself is also unclear). Replaced with using custom event handler to ensure proper launch flow. --- xcode/App-Mac/AppDelegate.swift | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/xcode/App-Mac/AppDelegate.swift b/xcode/App-Mac/AppDelegate.swift index ffdf0758..cb1a3837 100644 --- a/xcode/App-Mac/AppDelegate.swift +++ b/xcode/App-Mac/AppDelegate.swift @@ -6,16 +6,20 @@ class AppDelegate: NSObject, NSApplicationDelegate { private var window: NSWindow! private var windowForego = false private var windowLoaded = false + private let logger = USLogger(#fileID) @IBOutlet weak var enbaleNativeLogger: NSMenuItem! - func application(_ application: NSApplication, open urls: [URL]) { + @objc func handleGetURL(event: NSAppleEventDescriptor, replyEvent: NSAppleEventDescriptor) { // if open panel is already open, stop processing the URL scheme if NSApplication.shared.keyWindow?.accessibilityIdentifier() == "open-panel" { return } - for url in urls { + // handle URL scheme + if let urlString = event.paramDescriptor(forKeyword: AEKeyword(keyDirectObject))?.stringValue, + let url = URL(string: urlString) { + logger?.info("\(#function, privacy: .public) - \(urlString, privacy: .public)") if url.host == "changesavelocation" { // avoid opening the panel repeatedly and playing unnecessary warning sounds - if NSApplication.shared.keyWindow?.identifier?.rawValue == "changeSaveLocation" { continue } + if NSApplication.shared.keyWindow?.identifier?.rawValue == "changeSaveLocation" { return } if windowLoaded { let viewController = window.contentViewController as? ViewController viewController?.changeSaveLocation(nil) @@ -27,9 +31,23 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } - func applicationDidFinishLaunching(_ aNotification: Notification) { + // https://developer.apple.com/documentation/appkit/nsapplicationdelegate/1428623-applicationwillfinishlaunching/ + func applicationWillFinishLaunching(_ notification: Notification) { + // https://developer.apple.com/documentation/foundation/nsappleeventmanager/1416131-seteventhandler + NSAppleEventManager.shared().setEventHandler( + self, + andSelector: #selector(handleGetURL(event:replyEvent:)), + forEventClass: AEEventClass(kInternetEventClass), + andEventID: AEEventID(kAEGetURL) + ) + } + + // https://developer.apple.com/documentation/appkit/nsapplicationdelegate/1428385-applicationdidfinishlaunching + func applicationDidFinishLaunching(_ notification: Notification) { // Initialize menu items enbaleNativeLogger.state = Preferences.enableLogger ? .on : .off + // Whether to initialize the main view + logger?.debug("\(#function, privacy: .public) - windowForego: \(self.windowForego, privacy: .public)") if windowForego { return } let storyboard = NSStoryboard(name: "View", bundle: Bundle.main) let windowController = storyboard.instantiateInitialController() as! NSWindowController @@ -43,7 +61,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { return true } - func applicationWillTerminate(_ aNotification: Notification) { + func applicationWillTerminate(_ notification: Notification) { // Insert code here to tear down your application } From 10abba429955d8594591d26c53841c025cec9be1 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sun, 7 Jan 2024 17:47:47 +0800 Subject: [PATCH 05/24] chore: increase the priority of prettier --- .vscode/settings.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index f86b8368..24436b8d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,15 @@ }, "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", + "[yaml]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[jsonc]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[css]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[html]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[svelte]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[markdown]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, // https://github.com/microsoft/vscode-eslint#settings-options "eslint.validate": ["javascript", "svelte"], "eslint.experimental.useFlatConfig": true, From 9dbf6ab51ee199319a47fa0d15013bec8b069942 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Mon, 8 Jan 2024 18:19:29 +0800 Subject: [PATCH 06/24] refactor: adjust settings and better jsdoc Revision settings define and remove language content. Improve JSDoc for better IntelliSense in vscode. --- scripts/build-ext-safari-15.js | 5 +- scripts/build-ext-safari-16.4.js | 5 +- scripts/dev-ext-safari.js | 5 +- src/ext/shared/native.js | 2 +- src/ext/shared/settings.js | 647 +++++++++++++------------------ 5 files changed, 284 insertions(+), 380 deletions(-) diff --git a/scripts/build-ext-safari-15.js b/scripts/build-ext-safari-15.js index 3242f700..50a5862e 100644 --- a/scripts/build-ext-safari-15.js +++ b/scripts/build-ext-safari-15.js @@ -31,8 +31,11 @@ const defineConfig = { root: await rootDir(), base: "./", define: { - "import.meta.env.BROWSER": JSON.stringify("safari"), + "import.meta.env.BROWSER": JSON.stringify("Safari"), "import.meta.env.NATIVE_APP": JSON.stringify("app"), + "import.meta.env.SAFARI_PLATFORM": JSON.stringify( + process.env.SAFARI_PLATFORM, + ), }, }; diff --git a/scripts/build-ext-safari-16.4.js b/scripts/build-ext-safari-16.4.js index 12a67481..1c635833 100644 --- a/scripts/build-ext-safari-16.4.js +++ b/scripts/build-ext-safari-16.4.js @@ -30,8 +30,11 @@ const defineConfig = { root: await rootDir(), base: "./", define: { - "import.meta.env.BROWSER": JSON.stringify("safari"), + "import.meta.env.BROWSER": JSON.stringify("Safari"), "import.meta.env.NATIVE_APP": JSON.stringify("app"), + "import.meta.env.SAFARI_PLATFORM": JSON.stringify( + process.env.SAFARI_PLATFORM, + ), }, }; diff --git a/scripts/dev-ext-safari.js b/scripts/dev-ext-safari.js index 2b4e7c25..ecd00126 100644 --- a/scripts/dev-ext-safari.js +++ b/scripts/dev-ext-safari.js @@ -31,8 +31,11 @@ const defineConfig = { root: await rootDir(), base: "./", define: { - "import.meta.env.BROWSER": JSON.stringify("safari"), + "import.meta.env.BROWSER": JSON.stringify("Safari"), "import.meta.env.NATIVE_APP": JSON.stringify("app"), + "import.meta.env.SAFARI_PLATFORM": JSON.stringify( + process.env.SAFARI_PLATFORM, + ), }, }; diff --git a/src/ext/shared/native.js b/src/ext/shared/native.js index 524f3613..82566825 100644 --- a/src/ext/shared/native.js +++ b/src/ext/shared/native.js @@ -12,7 +12,7 @@ const application = () => import.meta.env.NATIVE_APP ?? "application"; /** * @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/connectNative} - * @returns {object} A `runtime.Port` object + * @returns {import("webextension-polyfill").Runtime.Port} A `runtime.Port` object */ export function connectNative() { return browser.runtime.connectNative(application()); diff --git a/src/ext/shared/settings.js b/src/ext/shared/settings.js index 92ed61bf..045cbc30 100644 --- a/src/ext/shared/settings.js +++ b/src/ext/shared/settings.js @@ -8,14 +8,15 @@ const storagePrefix = "US_"; /** * Convert name to storage key * @param {string} name - * @returns {string} prefixed storage key + * @returns prefixed storage key */ const storageKey = (name) => storagePrefix + name.toUpperCase(); /** - * Dynamic storage reference - * @param {"sync"|"local"|undefined} area - * @returns {Promise} + * @typedef {"sync"|"local"|"managed"|"session"} Areas + * + * @param {"sync"|"local"=} area - storage area + * @returns Dynamic storage reference */ const storageRef = async (area) => { const storages = { @@ -30,7 +31,7 @@ const storageRef = async (area) => { }; // https://developer.apple.com/documentation/safariservices/safari_web_extensions/assessing_your_safari_web_extension_s_browser_compatibility#3584139 // since storage sync is not implemented in Safari, currently only returns using local storage - if (import.meta.env.BROWSER === "safari") { + if (import.meta.env.BROWSER === "Safari") { return storages.local; } if (area in storages) { @@ -44,344 +45,236 @@ const storageRef = async (area) => { } }; +/** + * @typedef {Object} Platforms platform availability + * @property {any=} macos - overriding defaults + * @property {any=} ipados - overriding defaults + * @property {any=} ios - overriding defaults + * + * @typedef {Object} Setting The setting item + * @property {string} name - setting's name + * @property {"string"|"number"|"boolean"|"object"|"array"} type - setting's value type + * @property {boolean=} local - local settings will not be synced + * @property {Array=} values - setting's values list + * @property {any} default - setting's default value + * @property {boolean=} disable - disabled settings not be displayed + * @property {boolean=} protect - protected settings cannot be reset + * @property {boolean=} confirm - double confirmation is required when change + * @property {Platforms=} platforms - platform availability and overriding defaults + * @property {"INTERNAL"|"general"|"editor"} group - setting's group name + * @property {string=} legacy - setting's legacy name + * @property {"Toggle"|"select"|"textarea"=} nodeType - setting's node type + * @property {Object=} nodeClass - node class name with setting's value + * + * @typedef {Setting & {key: string}} SettingWithKey The setting item with storage key + */ + +/** @type {Readonly} - Read-only setting template and fallback defaults */ const settingDefault = deepFreeze({ name: "setting_default", type: undefined, local: false, values: [], default: undefined, + disable: false, protect: false, confirm: false, - platforms: ["macos", "ipados", "ios"], - langLabel: {}, - langTitle: {}, - group: "", + platforms: { macos: undefined, ipados: undefined, ios: undefined }, + group: "INTERNAL", legacy: "", - nodeType: "", + nodeType: undefined, nodeClass: {}, }); -export const settingsDefine = deepFreeze( - [ - { - name: "error_native", - type: "object", - local: true, - default: { error: undefined }, - platforms: ["macos", "ipados", "ios"], - group: "Internal", - }, - { - name: "legacy_imported", - type: "number", - local: true, - default: 0, - protect: true, - platforms: ["macos"], - group: "Internal", - }, - { - name: "language_code", - type: "string", - default: "en", - platforms: ["macos", "ipados", "ios"], - group: "Internal", - legacy: "languageCode", - }, - { - name: "scripts_settings", - type: "object", - default: {}, - platforms: ["macos", "ipados", "ios"], - langLabel: { - en: "Scripts update check active", - zh_hans: "脚本更新检查激活", - }, - langTitle: { - en: "Whether to enable each single script update check", - zh_hans: "是否开启单个脚本更新检查", - }, - group: "Internal", - nodeType: "Subpage", - }, - // { - // name: "settings_sync", - // type: "boolean", - // local: true, - // default: false, - // protect: true, - // platforms: ["macos", "ipados", "ios"], - // langLabel: { - // en: "Sync settings", - // zh_hans: "同步设置" - // }, - // langTitle: { - // en: "Sync settings across devices", - // zh_hans: "跨设备同步设置" - // }, - // group: "General", - // nodeType: "Toggle" - // }, - { - name: "toolbar_badge_count", - type: "boolean", - default: true, - platforms: ["macos", "ipados"], - langLabel: { - en: "Show Toolbar Count", - zh_hans: "工具栏图标显示计数徽章", - }, - langTitle: { - en: "displays a badge on the toolbar icon with a number that represents how many enabled scripts match the url for the page you are on", - zh_hans: "简体中文描述", - }, - group: "General", - legacy: "showCount", - nodeType: "Toggle", - }, - { - name: "global_active", - type: "boolean", - local: true, - default: true, - platforms: ["macos"], - langLabel: { - en: "Enable Injection", - zh_hans: "启用注入", - }, - langTitle: { - en: "toggle on/off script injection for the pages you visit", - zh_hans: "简体中文描述", - }, - group: "General", - legacy: "active", - nodeType: "Toggle", - nodeClass: { red: false }, - }, - { - name: "global_scripts_update_check", - type: "boolean", - default: true, - platforms: ["macos", "ipados", "ios"], - langLabel: { - en: "Global scripts update check", - zh_hans: "全局脚本更新检查", - }, - langTitle: { - en: "Whether to enable global periodic script update check", - zh_hans: "是否开启全局定期脚本更新检查", - }, - group: "General", - nodeType: "Toggle", - }, - { - name: "scripts_update_check_interval", - type: "number", - default: 86400000, - platforms: ["macos", "ipados", "ios"], - langLabel: { - en: "Scripts update check interval", - zh_hans: "脚本更新检查间隔", - }, - langTitle: { - en: "The interval for script update check in background", - zh_hans: "脚本更新检查的间隔时间", - }, - group: "General", - nodeType: "Toggle", - }, - { - name: "scripts_update_check_lasttime", - type: "number", - default: 0, - platforms: ["macos", "ipados", "ios"], - langLabel: { - en: "Scripts update check lasttime", - zh_hans: "脚本更新上次检查时间", - }, - langTitle: { - en: "The lasttime for script update check in background", - zh_hans: "后台脚本更新上次检查时间", - }, - group: "Internal", - }, - { - name: "scripts_auto_update", - type: "boolean", - default: false, - confirm: true, - platforms: ["macos", "ipados", "ios"], - langLabel: { - en: "Scripts silent auto update", - zh_hans: "脚本后台静默自动更新", - }, - langTitle: { - en: "Script silently auto-updates in the background, which is dangerous and may introduce unconfirmed malicious code", - zh_hans: - "脚本在后台静默自动更新,这是危险的,可能引入未经确认的恶意代码", - }, - group: "General", - nodeType: "Toggle", - nodeClass: { warn: true }, - }, - { - name: "global_exclude_match", - type: "object", - default: [], - platforms: ["macos", "ipados", "ios"], - langLabel: { - en: "Global exclude match patterns", - zh_hans: "全局排除匹配模式列表", - }, - langTitle: { - en: "this input accepts a comma separated list of @match patterns, a page url that matches against a pattern in this list will be ignored for script injection", - zh_hans: "简体中文描述", - }, - group: "General", - legacy: "blacklist", - nodeType: "textarea", - nodeClass: { red: "blacklistError" }, - }, - { - name: "editor_close_brackets", - type: "boolean", - default: true, - platforms: ["macos"], - langLabel: { - en: "Auto Close Brackets", - zh_hans: "自动关闭括号", - }, - langTitle: { - en: "toggles on/off auto closing of brackets in the editor, this affects the following characters: () [] {} \"\" ''", - zh_hans: "简体中文描述", - }, - group: "Editor", - legacy: "autoCloseBrackets", - nodeType: "Toggle", - }, - { - name: "editor_auto_hint", - type: "boolean", - default: true, - platforms: ["macos"], - langLabel: { - en: "Auto Hint", - zh_hans: "自动提示(Hint)", - }, - langTitle: { - en: "automatically shows completion hints while editing", - zh_hans: "简体中文描述", - }, - group: "Editor", - legacy: "autoHint", - nodeType: "Toggle", - }, - { - name: "editor_list_sort", - type: "string", - values: ["nameAsc", "nameDesc", "lastModifiedAsc", "lastModifiedDesc"], - default: "lastModifiedDesc", - platforms: ["macos"], - langLabel: { - en: "Sort order", - zh_hans: "排序顺序", - }, - langTitle: { - en: "Display order of items in sidebar", - zh_hans: "侧栏中项目的显示顺序", - }, - group: "Editor", - legacy: "sortOrder", - nodeType: "Dropdown", - }, - { - name: "editor_list_descriptions", - type: "boolean", - default: true, - platforms: ["macos"], - langLabel: { - en: "Show List Descriptions", - zh_hans: "显示列表项目描述", - }, - langTitle: { - en: "show or hides the item descriptions in the sidebar", - zh_hans: "简体中文描述", - }, - group: "Editor", - legacy: "descriptions", - nodeType: "Toggle", - }, - { - name: "editor_javascript_lint", - type: "boolean", - default: false, - platforms: ["macos"], - langLabel: { - en: "Javascript Linter", - zh_hans: "Javascript Linter", - }, - langTitle: { - en: "toggles basic Javascript linting within the editor", - zh_hans: "简体中文描述", - }, - group: "Editor", - legacy: "lint", - nodeType: "Toggle", - }, - { - name: "editor_show_whitespace", - type: "boolean", - default: true, - platforms: ["macos"], - langLabel: { - en: "Show whitespace characters", - zh_hans: "显示空白字符", - }, - langTitle: { - en: "toggles the display of invisible characters in the editor", - zh_hans: "简体中文描述", - }, - group: "Editor", - legacy: "showInvisibles", - nodeType: "Toggle", - }, - { - name: "editor_tab_size", - type: "number", - values: [2, 4], - default: 4, - platforms: ["macos"], - langLabel: { - en: "Tab Size", - zh_hans: "制表符大小", - }, - langTitle: { - en: "the number of spaces a tab is equal to while editing", - zh_hans: "简体中文描述", - }, - group: "Editor", - legacy: "tabSize", - nodeType: "select", - }, - ].reduce(settingsDefineReduceCallback, {}), +/** @type {Readonly} - Read-only settings definition */ +const settingsDefinition = [ + { + name: "error_native", + type: "object", + local: true, + default: { error: undefined }, + group: "INTERNAL", + }, + { + name: "legacy_imported", + type: "number", + local: true, + default: 0, + protect: true, + platforms: { macos: undefined }, + group: "INTERNAL", + }, + { + name: "language_code", + type: "string", + default: "en", + group: "INTERNAL", + legacy: "languageCode", + }, + { + name: "settings_sync", + type: "boolean", + local: true, + default: false, + disable: true, + protect: true, + group: "general", + nodeType: "Toggle", + }, + { + name: "toolbar_badge_count", + type: "boolean", + default: true, + platforms: { macos: true, ipados: true, ios: false }, + group: "general", + legacy: "showCount", + nodeType: "Toggle", + }, + { + name: "global_active", + type: "boolean", + local: true, + default: true, + group: "general", + legacy: "active", + nodeType: "Toggle", + nodeClass: { warn: false }, + }, + { + name: "global_scripts_update_check", + type: "boolean", + default: true, + group: "general", + nodeType: "Toggle", + }, + { + name: "scripts_settings", + type: "object", + default: {}, + disable: true, + group: "INTERNAL", + }, + { + name: "scripts_update_check_interval", + type: "number", + values: [0, 1, 3, 7, 15, 30], + default: 0, + group: "general", + nodeType: "select", + }, + { + name: "scripts_update_check_lasttime", + type: "number", + default: 0, + group: "INTERNAL", + }, + { + name: "scripts_update_automation", + type: "boolean", + default: false, + disable: true, + confirm: true, + group: "general", + nodeType: "Toggle", + nodeClass: { warn: true }, + }, + { + name: "global_exclude_match", + type: "object", + default: [], + group: "general", + legacy: "blacklist", + nodeType: "textarea", + }, + { + name: "editor_close_brackets", + type: "boolean", + default: true, + platforms: { macos: undefined }, + group: "editor", + legacy: "autoCloseBrackets", + nodeType: "Toggle", + }, + { + name: "editor_auto_hint", + type: "boolean", + default: true, + platforms: { macos: undefined }, + group: "editor", + legacy: "autoHint", + nodeType: "Toggle", + }, + { + name: "editor_list_sort", + type: "string", + values: ["nameAsc", "nameDesc", "lastModifiedAsc", "lastModifiedDesc"], + default: "lastModifiedDesc", + platforms: { macos: undefined }, + group: "editor", + legacy: "sortOrder", + nodeType: "select", + }, + { + name: "editor_list_descriptions", + type: "boolean", + default: true, + platforms: { macos: undefined }, + group: "editor", + legacy: "descriptions", + nodeType: "Toggle", + }, + { + name: "editor_javascript_lint", + type: "boolean", + default: false, + platforms: { macos: undefined }, + group: "editor", + legacy: "lint", + nodeType: "Toggle", + }, + { + name: "editor_show_whitespace", + type: "boolean", + default: true, + platforms: { macos: undefined }, + group: "editor", + legacy: "showInvisibles", + nodeType: "Toggle", + }, + { + name: "editor_tab_size", + type: "number", + values: [1, 2, 3, 4, 5, 6, 8, 10, 12], + default: 4, + platforms: { macos: undefined }, + group: "editor", + legacy: "tabSize", + nodeType: "select", + }, +]; + +/** @type {Readonly<{[key: string]: SettingWithKey}>} - Read-only settings dictionary */ +export const settingsDictionary = deepFreeze( + settingsDefinition.reduce(settingsDefinitionReduceCallbackFn, {}), ); /** - * populate the settingsDefine with settingDefault - * and convert settingsDefine to storageKey object - * @param {object} settings new settings object - * @param {object} setting each setting define - * @returns {object} {US_GLOBAL_ACTIVE: {key: US_GLOBAL_ACTIVE, name: global_active, ... }, ...} + * populate the settings-define with setting-default + * and convert settings-define to storage-key object + * @param {{[key: string]: SettingWithKey}} settings settings dictionary + * @param {Setting} setting each setting define + * @returns // {US_GLOBAL_ACTIVE: {key: US_GLOBAL_ACTIVE, name: global_active, ... }, ...} */ -function settingsDefineReduceCallback(settings, setting) { - setting.key = storageKey(setting.name); - settings[setting.key] = { ...settingDefault, ...setting }; +function settingsDefinitionReduceCallbackFn(settings, setting) { + const key = storageKey(setting.name); + settings[key] = { ...settingDefault, ...setting, key }; return settings; } /** * prevent settings define from being modified in any case * otherwise user settings may be lost in the worst case + * @type {(o: T) => Readonly} * @param {object} object any object * @returns {object} deep frozen object */ @@ -410,20 +303,21 @@ if (Object.hasOwn === undefined) { /** * settings.get - * @param {string|Array} keys key | array of keys | undefined for all + * @param {string|string[]} keys key | array of keys | undefined for all * @param {"local"|"sync"} area - * @returns {Promise} settings object + * @returns settings object */ export async function get(keys = undefined, area = undefined) { if (![undefined, "local", "sync"].includes(area)) { return console.error("Unexpected storage area:", area); } // validate setting value and fix surprises to default + /** @param {string} key @param {any} val */ const valueFix = (key, val) => { - if (!key || !Object.hasOwn(settingsDefine, key)) return; - const def = settingsDefine[key].default; - // check if value type conforms to settingsDefine - const type = settingsDefine[key].type; + if (!key || !Object.hasOwn(settingsDictionary, key)) return; + const def = settingsDictionary[key].default; + // check if value type conforms to settings-dictionary + const type = settingsDictionary[key].type; // eslint-disable-next-line valid-typeof -- type known to be valid string literal if (typeof val != type) { console.warn( @@ -431,8 +325,8 @@ export async function get(keys = undefined, area = undefined) { ); return def; } - // check if value conforms to settingsDefine - const values = settingsDefine[key].values; + // check if value conforms to settings-dictionary + const values = settingsDictionary[key].values; if (values.length && !values.includes(val)) { console.warn( `Unexpected ${key} value '${val}' should one of '${values}', fix to default`, @@ -445,17 +339,17 @@ export async function get(keys = undefined, area = undefined) { // [single setting] if (typeof keys == "string") { const key = storageKey(keys); - // check if key exist in settingsDefine - if (!Object.hasOwn(settingsDefine, key)) { + // check if key exist in settings-dictionary + if (!Object.hasOwn(settingsDictionary, key)) { return console.error("unexpected settings key:", key); } // check if only locally stored setting // eslint-disable-next-line no-param-reassign -- change the area is expected - settingsDefine[key].local === true && (area = "local"); + settingsDictionary[key].local === true && (area = "local"); const storage = await storageRef(area); const result = await storage.ref.get(key); if (Object.hasOwn(result, key)) return valueFix(key, result[key]); - return settingsDefine[key].default; + return settingsDictionary[key].default; } const complexGet = async (settingsDefault, areaKeys) => { const storage = await storageRef(area); @@ -474,7 +368,7 @@ export async function get(keys = undefined, area = undefined) { const result = Object.assign(settingsDefault, local, sync); // revert settings object property name return Object.entries(result).reduce((p, c) => { - p[settingsDefine[c[0]].name] = valueFix(...c); + p[settingsDictionary[c[0]].name] = valueFix(...c); return p; }, {}); }; @@ -487,13 +381,13 @@ export async function get(keys = undefined, area = undefined) { const areaKeys = { local: [], sync: [], all: [] }; for (const k of keys) { const key = storageKey(k); - // check if key exist in settingsDefine - if (!Object.hasOwn(settingsDefine, key)) { + // check if key exist in settings-dictionary + if (!Object.hasOwn(settingsDictionary, key)) { return console.error("unexpected settings key:", key); } - settingsDefault[key] = settingsDefine[key].default; + settingsDefault[key] = settingsDictionary[key].default; // detach only locally stored settings - settingsDefine[key].local === true + settingsDictionary[key].local === true ? areaKeys.local.push(key) : areaKeys.sync.push(key); // record all keys in case sync storage is not enabled @@ -505,10 +399,10 @@ export async function get(keys = undefined, area = undefined) { if (typeof keys == "undefined" || keys === null) { const settingsDefault = {}; const areaKeys = { local: [], sync: [], all: [] }; - for (const key of Object.keys(settingsDefine)) { - settingsDefault[key] = settingsDefine[key].default; + for (const key of Object.keys(settingsDictionary)) { + settingsDefault[key] = settingsDictionary[key].default; // detach only locally stored settings - settingsDefine[key].local === true + settingsDictionary[key].local === true ? areaKeys.local.push(key) : areaKeys.sync.push(key); // record all keys in case sync storage is not enabled @@ -523,7 +417,6 @@ export async function get(keys = undefined, area = undefined) { * settings.set * @param {object} keys settings object * @param {"local"|"sync"} area - * @returns {Promise} */ export async function set(keys, area = undefined) { if (![undefined, "local", "sync"].includes(area)) { @@ -538,12 +431,12 @@ export async function set(keys, area = undefined) { const areaKeys = { local: {}, sync: {}, all: {} }; for (const k of Object.keys(keys)) { const key = storageKey(k); - // check if key exist in settingsDefine - if (!Object.hasOwn(settingsDefine, key)) { + // check if key exist in settings-dictionary + if (!Object.hasOwn(settingsDictionary, key)) { return console.error("Unexpected settings keys:", key); } - // check if value type conforms to settingsDefine - const type = settingsDefine[key].type; + // check if value type conforms to settings-dictionary + const type = settingsDictionary[key].type; // eslint-disable-next-line valid-typeof -- type known to be valid string literal if (typeof keys[k] != type) { if (type === "number" && !Number.isNaN(Number(keys[k]))) { @@ -555,15 +448,15 @@ export async function set(keys, area = undefined) { ); } } - // check if value conforms to settingsDefine - const values = settingsDefine[key].values; + // check if value conforms to settings-dictionary + const values = settingsDictionary[key].values; if (values.length && !values.includes(keys[k])) { return console.error( `Unexpected ${k} value '${keys[k]}' should one of '${values}'`, ); } // detach only locally stored settings - settingsDefine[key].local === true + settingsDictionary[key].local === true ? (areaKeys.local[key] = keys[k]) : (areaKeys.sync[key] = keys[k]); // record all keys in case sync storage is not enabled @@ -591,9 +484,8 @@ export async function set(keys, area = undefined) { /** * settings.reset * reset to default - * @param {string|Array} keys key | array of keys | undefined for all + * @param {string|string[]} keys key | array of keys | undefined for all * @param {"local"|"sync"} area - * @returns {Promise} */ export async function reset(keys = undefined, area = undefined) { if (![undefined, "local", "sync"].includes(area)) { @@ -602,16 +494,16 @@ export async function reset(keys = undefined, area = undefined) { // [single setting] if (typeof keys == "string") { const key = storageKey(keys); - // check if key exist in settingsDefine - if (!Object.hasOwn(settingsDefine, key)) { + // check if key exist in settings-dictionary + if (!Object.hasOwn(settingsDictionary, key)) { return console.error("unexpected settings key:", key); } // check if key is protected - if (settingsDefine[key].protect === true) { + if (settingsDictionary[key].protect === true) { return console.error("protected settings key:", key); } // eslint-disable-next-line no-param-reassign -- change the area is expected - settingsDefine[key].local === true && (area = "local"); + settingsDictionary[key].local === true && (area = "local"); const storage = await storageRef(area); return storage.ref.remove(key); } @@ -641,16 +533,16 @@ export async function reset(keys = undefined, area = undefined) { const areaKeys = { local: [], sync: [], all: [] }; for (const k of keys) { const key = storageKey(k); - // check if key exist in settingsDefine - if (!Object.hasOwn(settingsDefine, key)) { + // check if key exist in settings-dictionary + if (!Object.hasOwn(settingsDictionary, key)) { return console.error("unexpected settings key:", key); } // check if key is protected - if (settingsDefine[key].protect === true) { + if (settingsDictionary[key].protect === true) { return console.error("protected settings key:", key); } // detach only locally stored settings - settingsDefine[key].local === true + settingsDictionary[key].local === true ? areaKeys.local.push(key) : areaKeys.sync.push(key); // record all keys in case sync storage is not enabled @@ -661,11 +553,11 @@ export async function reset(keys = undefined, area = undefined) { // [all settings] if (typeof keys == "undefined" || keys === null) { const areaKeys = { local: [], sync: [], all: [] }; - for (const key in settingsDefine) { + for (const key in settingsDictionary) { // skip protected keys - if (settingsDefine[key].protect === true) continue; + if (settingsDictionary[key].protect === true) continue; // detach only locally stored settings - settingsDefine[key].local === true + settingsDictionary[key].local === true ? areaKeys.local.push(key) : areaKeys.sync.push(key); // record all keys in case sync storage is not enabled @@ -679,8 +571,11 @@ export async function reset(keys = undefined, area = undefined) { /** * complex onChanged * this function is convenient for the svelte store to update the state - * @param {Function} callback + * @callback onChangedSettingsCallback + * @param {{[key: string]: any}} settings - changed settings + * @param {Areas} area - storage area * @returns {void} + * @param {onChangedSettingsCallback} callback */ export function onChangedSettings(callback) { if (typeof callback != "function") { @@ -690,14 +585,14 @@ export function onChangedSettings(callback) { /** * @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage/onChanged#listener} * @param {object} changes - * @param {"sync"|"local"} area + * @param {Areas} area */ const listener = (changes, area) => { // console.log(`storage.${area}.onChanged`, changes); // DEBUG const settings = {}; for (const key in changes) { - if (!Object.hasOwn(settingsDefine, key)) continue; - settings[settingsDefine[key].name] = changes[key].newValue; + if (!Object.hasOwn(settingsDictionary, key)) continue; + settings[settingsDictionary[key].name] = changes[key].newValue; } try { callback(settings, area); @@ -713,14 +608,14 @@ export function onChangedSettings(callback) { /** * settings.legacyGet - * @param {string|Array.} keys - * @returns {Promise} settings object with legacy keys + * @param {string|string[]} keys + * @returns settings object with legacy keys */ export async function legacyGet(keys = undefined) { const result = await get(keys); // console.log("legacy_get", keys, result); for (const key of Object.keys(result)) { - const legacy = settingsDefine[storageKey(key)]?.legacy; + const legacy = settingsDictionary[storageKey(key)]?.legacy; if (legacy) result[legacy] = result[key]; } return result; @@ -729,7 +624,6 @@ export async function legacyGet(keys = undefined) { /** * settings.legacySet * @param {object} keys legacy keys - * @returns {Promise} */ export async function legacySet(keys) { if (typeof keys != "object") { @@ -739,8 +633,8 @@ export async function legacySet(keys) { return console.error("Settings object empty:", keys); } const settings = {}; - for (const key of Object.keys(settingsDefine)) { - const setting = settingsDefine[key]; + for (const key of Object.keys(settingsDictionary)) { + const setting = settingsDictionary[key]; if (!setting.legacy) continue; if (setting.legacy in keys) { settings[setting.name] = keys[setting.legacy]; @@ -758,14 +652,15 @@ export async function legacyImport() { const result = await browser.runtime.sendNativeMessage("app", { name: "PAGE_LEGACY_IMPORT", }); + if (!result) return console.error("PAGE_LEGACY_IMPORT not response"); if (result.error) return console.error(result.error); console.info("Import settings data from legacy manifest file"); const settings = {}; - for (const key of Object.keys(settingsDefine)) { - const legacy = settingsDefine[key].legacy; + for (const key of Object.keys(settingsDictionary)) { + const legacy = settingsDictionary[key].legacy; if (legacy in result) { let value = result[legacy]; - switch (settingsDefine[key].type) { + switch (settingsDictionary[key].type) { case "boolean": value = JSON.parse(value); break; @@ -774,7 +669,7 @@ export async function legacyImport() { break; } console.info(`Importing legacy setting: ${legacy}`, value); - settings[settingsDefine[key].name] = value; + settings[settingsDictionary[key].name] = value; } } // import complete tag, to ensure will only be import once From ba7afea9e4f96338bc64cb982abdd777fc3b6822 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Mon, 8 Jan 2024 18:26:16 +0800 Subject: [PATCH 07/24] feat: create new extension page app for ios --- src/ext/extension-page/Appios.svelte | 92 +++++++++++++++++++ src/ext/extension-page/app.css | 9 ++ src/ext/extension-page/main.js | 25 ++++- src/ext/extension-page/store.js | 38 ++++++-- .../SafariWebExtensionHandler.swift | 12 +-- 5 files changed, 160 insertions(+), 16 deletions(-) create mode 100644 src/ext/extension-page/Appios.svelte diff --git a/src/ext/extension-page/Appios.svelte b/src/ext/extension-page/Appios.svelte new file mode 100644 index 00000000..ed66fb5c --- /dev/null +++ b/src/ext/extension-page/Appios.svelte @@ -0,0 +1,92 @@ + + +{#if $state.includes("init")} +
+ + {@html logo} + {#if $state.includes("init-error")} + Failed to initialize app, check the browser console + {:else} + Initializing app... + {/if} +
+{/if} +
    + {#each $notifications as item (item.id)} + notifications.remove(item.id)} {item} /> + {/each} +
+{#if $state.includes("settings")} + +{/if} + + diff --git a/src/ext/extension-page/app.css b/src/ext/extension-page/app.css index f34a9b9a..61b9195a 100644 --- a/src/ext/extension-page/app.css +++ b/src/ext/extension-page/app.css @@ -3,6 +3,15 @@ html { height: 100vh; height: 100svh; /* safari 15.4 */ overflow: hidden; + overscroll-behavior: none; +} + +/* ios */ +@supports (-webkit-touch-callout: none) { + html { + height: auto; + overflow: visible; + } } body { diff --git a/src/ext/extension-page/main.js b/src/ext/extension-page/main.js index ec2580e9..7c45ed52 100644 --- a/src/ext/extension-page/main.js +++ b/src/ext/extension-page/main.js @@ -2,6 +2,7 @@ import "../shared/reset.css"; import "../shared/variables.css"; import "./app.css"; import App from "./App.svelte"; +import Appios from "./Appios.svelte"; // vite feat that only import in dev mode if (import.meta.env.MODE === "development") { @@ -14,8 +15,26 @@ if (import.meta.env.MODE === "development") { } } -const app = new App({ - target: document.getElementById("app"), -}); +let app; +const target = document.getElementById("app"); +if (import.meta.env.MODE === "development") { + const platform = await browser.runtime.getPlatformInfo(); + // @ts-ignore -- incomplete polyfill types + if (platform.os === "ios") { + app = new Appios({ target }); + } else { + app = new App({ target }); + } +} else { + if (import.meta.env.SAFARI_PLATFORM === "ios") { + app = new Appios({ target }); + } else { + app = new App({ target }); + } +} + +// const app = new App({ +// target: document.getElementById("app"), +// }); export default app; diff --git a/src/ext/extension-page/store.js b/src/ext/extension-page/store.js index dc9b0234..22550c7c 100644 --- a/src/ext/extension-page/store.js +++ b/src/ext/extension-page/store.js @@ -37,7 +37,7 @@ function stateStore() { // store oldState to see how state transitioned // ex. if (newState === foo && oldState === bar) baz(); let oldState = []; - const add = (stateModifier) => + const add = (stateModifier) => { update((state) => { // list of acceptable states, mostly for state definition tracking const states = [ @@ -68,7 +68,14 @@ function stateStore() { ); return state; }); - const remove = (stateModifier) => + // URL hash handle + const params = new URLSearchParams(location.hash.slice(1)); + if (["settings"].includes(stateModifier)) { + params.set("state", stateModifier); + location.hash = params.toString(); + } + }; + const remove = (stateModifier) => { update((state) => { // save pre-changed state to oldState var oldState = [...state]; @@ -84,20 +91,38 @@ function stateStore() { ); return state; }); + // URL hash handle + const params = new URLSearchParams(location.hash.slice(1)); + const state = params.get("state"); + if (state === stateModifier) { + params.delete("state"); + location.hash = params.toString(); + } + }; const getOldState = () => oldState; - return { subscribe, add, getOldState, remove }; + // URL hash handle + const loadUrlState = () => { + const params = new URLSearchParams(location.hash.slice(1)); + const state = params.get("state"); + state && add(state); + }; + return { subscribe, add, getOldState, remove, loadUrlState }; } export const state = stateStore(); function settingsStore() { const { subscribe, update, set } = writable({}); const init = async (initData) => { - // import legacy settings data just one-time - await settingsStorage.legacyImport(); + if (import.meta.env.SAFARI_PLATFORM === "mac") { + // import legacy settings data just one-time + await settingsStorage.legacyImport(); + } // for compatibility with legacy getting names only // once all new name is used, use settingsStorage.get() const settings = await settingsStorage.legacyGet(); - console.info("store.js settingsStore init", initData, settings); + if (import.meta.env.MODE === "development") { + console.info("store.js settingsStore init", initData, settings); + } set({ ...initData, ...settings }); // sync popup, backgound, etc... settings changes settingsStorage.onChangedSettings((sets, area) => { @@ -135,6 +160,7 @@ function settingsStore() { // for compatibility with legacy setting names only // once all new name is used, use settingsStorage.set() settingsStorage.legacySet({ [key]: value }); // Durable Storage + settingsStorage.set({ [key]: value }); // Durable Storage // temporarily keep the old storage method until it is confirmed that all dependencies are removed updateSingleSettingOld(key, value); }; diff --git a/xcode/Ext-Safari/SafariWebExtensionHandler.swift b/xcode/Ext-Safari/SafariWebExtensionHandler.swift index ac5cec13..b296be2a 100644 --- a/xcode/Ext-Safari/SafariWebExtensionHandler.swift +++ b/xcode/Ext-Safari/SafariWebExtensionHandler.swift @@ -186,13 +186,11 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { } } else if name == "PAGE_INIT_DATA" { - #if os(macOS) - if let initData = getInitData(), checkDefaultDirectories() { - response.userInfo = [SFExtensionMessageKey: initData] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "failed to get init data"]] - } - #endif + if let initData = getInitData(), checkDefaultDirectories() { + response.userInfo = [SFExtensionMessageKey: initData] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to get init data"]] + } } else if name == "PAGE_LEGACY_IMPORT" { #if os(macOS) From 1f79d588f840e69b0806568c27d946916977445c Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Mon, 8 Jan 2024 18:28:18 +0800 Subject: [PATCH 08/24] refactor: extract generic modal wrapper for macos app --- src/ext/extension-page/App.svelte | 16 +++- .../Components/ModalWrapper.svelte | 87 +++++++++++++++++++ 2 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 src/ext/extension-page/Components/ModalWrapper.svelte diff --git a/src/ext/extension-page/App.svelte b/src/ext/extension-page/App.svelte index d802643a..2d0dd096 100644 --- a/src/ext/extension-page/App.svelte +++ b/src/ext/extension-page/App.svelte @@ -5,6 +5,7 @@ import Sidebar from "./Components/Sidebar/Sidebar.svelte"; import Editor from "./Components/Editor/Editor.svelte"; import Settings from "./Components/Settings.svelte"; + import ModalWrapper from "./Components/ModalWrapper.svelte"; import Notification from "./Components/Notification.svelte"; import logo from "../shared/img/logo.svg?raw"; import { connectNative, sendNativeMessage } from "../shared/native.js"; @@ -47,16 +48,19 @@ if (files.error) return console.error(files.error); items.set(files); state.remove("items-loading"); + state.loadUrlState(); }); // handle native app messages - const port = connectNative(); - port.onMessage.addListener((message) => { + const nativePort = connectNative(); + nativePort.onMessage.addListener((message) => { // console.info(message); // DEBUG if (message.name === "SAVE_LOCATION_CHANGED") { window.location.reload(); } }); + + const settingsProps = { nativePort, platform: "macos" }; @@ -81,7 +85,13 @@ notifications.remove(item.id)} {item} /> {/each} -{#if $state.includes("settings")}{/if} +{#if $state.includes("settings")} + state.remove("settings")} + /> +{/if} From e0a01052e805e0566e578e8e21343b1f4bbd3816 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Mon, 8 Jan 2024 18:39:50 +0800 Subject: [PATCH 09/24] feat: setup extension options page entrance --- public/ext/safari-15/manifest.json | 3 +++ public/ext/safari-16.4/manifest.json | 3 +++ public/ext/safari-dev/manifest-ios.json | 2 +- public/ext/safari-dev/manifest-mac.json | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/public/ext/safari-15/manifest.json b/public/ext/safari-15/manifest.json index 99c503ee..1b4e52db 100644 --- a/public/ext/safari-15/manifest.json +++ b/public/ext/safari-15/manifest.json @@ -22,6 +22,9 @@ "32": "images/toolbar-icon-32.png" } }, + "options_ui": { + "page": "dist/entry-ext-extension-page.html#state=settings" + }, "content_scripts": [ { "js": ["dist/content-scripts/userscripts.js"], diff --git a/public/ext/safari-16.4/manifest.json b/public/ext/safari-16.4/manifest.json index 7d2c00da..ab445895 100644 --- a/public/ext/safari-16.4/manifest.json +++ b/public/ext/safari-16.4/manifest.json @@ -19,6 +19,9 @@ "default_popup": "dist/entry-ext-action-popup.html", "default_icon": "images/action.svg" }, + "options_ui": { + "page": "dist/entry-ext-extension-page.html#state=settings" + }, "content_scripts": [ { "js": ["dist/content-scripts/userscripts.js"], diff --git a/public/ext/safari-dev/manifest-ios.json b/public/ext/safari-dev/manifest-ios.json index 2cb0308a..e1cac050 100644 --- a/public/ext/safari-dev/manifest-ios.json +++ b/public/ext/safari-dev/manifest-ios.json @@ -20,7 +20,7 @@ "default_icon": "images/action.svg" }, "options_ui": { - "page": "dist/entry-ext-extension-page.html#settings" + "page": "dist/entry-ext-extension-page.html#state=settings" }, "content_scripts": [ { diff --git a/public/ext/safari-dev/manifest-mac.json b/public/ext/safari-dev/manifest-mac.json index 046f8239..e469aa66 100644 --- a/public/ext/safari-dev/manifest-mac.json +++ b/public/ext/safari-dev/manifest-mac.json @@ -20,7 +20,7 @@ "default_icon": "images/action.svg" }, "options_ui": { - "page": "dist/entry-ext-extension-page.html#settings" + "page": "dist/entry-ext-extension-page.html#state=settings" }, "content_scripts": [ { From a6f9d9b961c365e6c3470e558fcbbea306d3c24a Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Mon, 8 Jan 2024 18:40:30 +0800 Subject: [PATCH 10/24] refactor: introducing new match patterns parser and i18n --- public/ext/shared/_locales/en/messages.json | 201 +++++++++++++++++++ public/ext/shared/_locales/zh/messages.json | 207 ++++++++++++++++++++ src/ext/shared/utils.js | 178 +++++++++++++++++ 3 files changed, 586 insertions(+) create mode 100644 public/ext/shared/_locales/zh/messages.json diff --git a/public/ext/shared/_locales/en/messages.json b/public/ext/shared/_locales/en/messages.json index e840a3a2..9b035e5c 100644 --- a/public/ext/shared/_locales/en/messages.json +++ b/public/ext/shared/_locales/en/messages.json @@ -6,5 +6,206 @@ "extension_description": { "message": "Save and run javascript for the web pages you visit", "description": "Description of what the extension does." + }, + "settings": { + "message": "Settings" + }, + "settings_section_editor": { + "message": "Editor Settings" + }, + "settings_section_general": { + "message": "General Settings" + }, + "settings_section_native": { + "message": "Native Settings" + }, + "settings_section_about": { + "message": "About" + }, + "settings_editor_auto_hint": { + "message": "Auto Hint" + }, + "settings_editor_auto_hint_desc": { + "message": "Automatically shows completion hints while editing" + }, + "settings_editor_close_brackets": { + "message": "Auto Close Brackets" + }, + "settings_editor_close_brackets_desc": { + "message": "Toggles on/off auto closing of brackets in the editor, this affects the following characters: () [] {} \"\" ''" + }, + "settings_editor_javascript_lint": { + "message": "Javascript Linter" + }, + "settings_editor_javascript_lint_desc": { + "message": "Toggles basic Javascript linting within the editor" + }, + "settings_editor_list_descriptions": { + "message": "Show List Descriptions" + }, + "settings_editor_list_descriptions_desc": { + "message": "Show or hides the item descriptions in the sidebar" + }, + "settings_editor_list_sort": { + "message": "Sort order" + }, + "settings_editor_list_sort_nameAsc": { + "message": "Scripts Name: Asc" + }, + "settings_editor_list_sort_nameDesc": { + "message": "Scripts Name: Desc" + }, + "settings_editor_list_sort_lastModifiedAsc": { + "message": "Last Modified: Asc" + }, + "settings_editor_list_sort_lastModifiedDesc": { + "message": "Last Modified: Desc" + }, + "settings_editor_list_sort_desc": { + "message": "Display order of items in sidebar" + }, + "settings_editor_show_whitespace": { + "message": "Show whitespace characters" + }, + "settings_editor_show_whitespace_desc": { + "message": "Toggles the display of invisible characters in the editor" + }, + "settings_editor_tab_size": { + "message": "Tab Size" + }, + "settings_editor_tab_size_desc": { + "message": "Choose the number of spaces a tab is equal to when rendering code" + }, + "settings_global_active": { + "message": "Enable Injection" + }, + "settings_global_active_desc": { + "message": "Toggle on/off script injection for the pages you visit" + }, + "settings_global_exclude_match": { + "message": "Global exclude match patterns" + }, + "settings_global_exclude_match_desc": { + "message": "This input accepts a whitespace (spaces, newlines etc.) separated list of @match patterns, a page url that matches against a pattern in this list will be ignored for script injection" + }, + "settings_global_exclude_match_saving": { + "message": "Saving..." + }, + "settings_global_exclude_match_placeholder": { + "message": "list of @match patterns, for example: \n*://*/*foo.bar\n*://*/*foo.bar?*\n*://*.example.net/*\nhttps://example.net/*/foo/*/\nhttps://*.example.net/a/b/c/?foo=/" + }, + "settings_global_exclude_match_refer": { + "message": "Please refer to:" + }, + "settings_global_scripts_update_check": { + "message": "Global scripts update check" + }, + "settings_global_scripts_update_check_desc": { + "message": "Whether to enable global periodic script update check" + }, + "settings_scripts_settings": { + "message": "Scripts update check active" + }, + "settings_scripts_settings_desc": { + "message": "Whether to enable each single script update check" + }, + "settings_scripts_update_automation": { + "message": "Scripts updates automatically" + }, + "settings_scripts_update_automation_desc": { + "message": "Script silently auto-updates in the background, which is dangerous and may introduce unconfirmed malicious code" + }, + "settings_scripts_update_check_interval": { + "message": "Scripts update check interval" + }, + "settings_scripts_update_check_interval_desc": { + "message": "The interval for script update check in background (days)" + }, + "settings_scripts_update_check_interval_0": { + "message": "Never" + }, + "settings_scripts_update_check_lasttime": { + "message": "Scripts update check lasttime" + }, + "settings_scripts_update_check_lasttime_desc": { + "message": "The lasttime for script update check in background" + }, + "settings_settings_sync": { + "message": "Sync settings" + }, + "settings_settings_sync_desc": { + "message": "Sync settings across devices" + }, + "settings_toolbar_badge_count": { + "message": "Show Toolbar Count" + }, + "settings_toolbar_badge_count_desc": { + "message": "Displays a badge on the toolbar icon with a number that represents how many enabled scripts match the url for the page you are on" + }, + "settings_scripts_directory": { + "message": "Save Location" + }, + "settings_scripts_directory_desc": { + "message": "Path to the folder where user scripts are stored" + }, + "settings_set_scripts_directory": { + "message": "Change save location" + }, + "settings_about_text1": { + "message": "Get more information about this extension by visiting the open source project:" + }, + "settings_about_text2": { + "message": "If you enjoy using this extension, please consider leaving a review on the App Store or sign up to beta test new versions:" + }, + "settings_about_button_repo": { + "message": "Code repository" + }, + "settings_about_button_docs": { + "message": "Documentation" + }, + "settings_about_button_issues": { + "message": "Report bugs" + }, + "settings_about_button_store": { + "message": "Open in the App Store" + }, + "settings_about_button_beta": { + "message": "Sign up for beta testing" + }, + "utils_check_match_patterns_1": { + "message": "The scheme component should one of *, https, http" + }, + "utils_check_match_patterns_2": { + "message": "The scheme and host should separated by `://`" + }, + "utils_check_match_patterns_3": { + "message": "The match pattern has no path component" + }, + "utils_check_match_patterns_4": { + "message": "The `*.` should followed by part of the hostname" + }, + "utils_check_match_patterns_5": { + "message": "The host component length should be 1-255" + }, + "utils_check_match_patterns_6": { + "message": "The `*` should be independent or `*.` at the start" + }, + "utils_check_match_patterns_7": { + "message": "The host component contains empty label(s)" + }, + "utils_check_match_patterns_8": { + "message": "The hostname label cannot start or end with `-` character" + }, + "utils_check_match_patterns_9": { + "message": "The maximum length of the hostname label cannot exceed 63" + }, + "utils_check_match_patterns_10": { + "message": "The host component contains invalid character(s): $1" + }, + "utils_check_match_patterns_11": { + "message": "The path component contains invalid character(s): $1" + }, + "msg_invalid_match_pattern": { + "message": "Invalid match pattern" } } diff --git a/public/ext/shared/_locales/zh/messages.json b/public/ext/shared/_locales/zh/messages.json new file mode 100644 index 00000000..610008bd --- /dev/null +++ b/public/ext/shared/_locales/zh/messages.json @@ -0,0 +1,207 @@ +{ + "extension_description": { + "message": "用户脚本和样式管理器", + "description": "Description of what the extension does." + }, + "settings": { + "message": "设置" + }, + "settings_section_editor": { + "message": "编辑器设置" + }, + "settings_section_general": { + "message": "通用设置" + }, + "settings_section_native": { + "message": "本地设置" + }, + "settings_section_about": { + "message": "关于" + }, + "settings_editor_auto_hint": { + "message": "自动提示(Hint)" + }, + "settings_editor_auto_hint_desc": { + "message": "编辑时自动显示完成提示" + }, + "settings_editor_close_brackets": { + "message": "自动关闭括号" + }, + "settings_editor_close_brackets_desc": { + "message": "在编辑器中启用自动关闭括号,这会影响以下字符:() [] {} \"\" ''" + }, + "settings_editor_javascript_lint": { + "message": "Javascript Linter" + }, + "settings_editor_javascript_lint_desc": { + "message": "在编辑器中启用基本的 Javascript linting 检查" + }, + "settings_editor_list_descriptions": { + "message": "显示列表项目描述" + }, + "settings_editor_list_descriptions_desc": { + "message": "显示或隐藏侧边栏中的用户脚本项目描述" + }, + "settings_editor_list_sort": { + "message": "项目排序顺序" + }, + "settings_editor_list_sort_desc": { + "message": "侧栏中项目的显示顺序" + }, + "settings_editor_list_sort_nameAsc": { + "message": "项目名称: 升序" + }, + "settings_editor_list_sort_nameDesc": { + "message": "项目名称: 降序" + }, + "settings_editor_list_sort_lastModifiedAsc": { + "message": "最后修改: 升序" + }, + "settings_editor_list_sort_lastModifiedDesc": { + "message": "最后修改: 降序" + }, + "settings_editor_show_whitespace": { + "message": "显示空白字符" + }, + "settings_editor_show_whitespace_desc": { + "message": "切换编辑器中不可见字符的显示" + }, + "settings_editor_tab_size": { + "message": "制表符大小" + }, + "settings_editor_tab_size_desc": { + "message": "选择渲染代码时制表符等于的空格数" + }, + "settings_global_active": { + "message": "启用注入" + }, + "settings_global_active_desc": { + "message": "全局脚本注入的开启或关闭" + }, + "settings_global_exclude_match": { + "message": "全局排除匹配模式列表" + }, + "settings_global_exclude_match_desc": { + "message": "此输入接受以空白符(空格、换行等)分隔的 @match 模式列表,与此列表中的模式匹配的页面 URL 将在脚本注入时被忽略" + }, + "settings_global_exclude_match_saving": { + "message": "保存中..." + }, + "settings_global_exclude_match_placeholder": { + "message": "@match 模式列表,例如:\n*://*/*foo.bar\n*://*/*foo.bar?*\n*://*.example.net/*\nhttps://example.net/*/foo/*/\nhttps://*.example.net/a/b/c/?foo=/" + }, + "settings_global_exclude_match_refer": { + "message": "匹配模式结构请参考:" + }, + "settings_global_scripts_update_check": { + "message": "全局脚本更新检查" + }, + "settings_global_scripts_update_check_desc": { + "message": "是否开启全局定期脚本更新检查" + }, + "settings_scripts_settings": { + "message": "脚本更新检查激活" + }, + "settings_scripts_settings_desc": { + "message": "是否开启单个脚本更新检查" + }, + "settings_scripts_update_automation": { + "message": "自动更新脚本" + }, + "settings_scripts_update_automation_desc": { + "message": "脚本在后台静默自动更新,这是危险的,可能引入未经确认的恶意代码" + }, + "settings_scripts_update_check_interval": { + "message": "脚本更新检查间隔" + }, + "settings_scripts_update_check_interval_desc": { + "message": "脚本在后台检查更新的间隔时间(天)" + }, + "settings_scripts_update_check_interval_0": { + "message": "从不" + }, + "settings_scripts_update_check_lasttime": { + "message": "脚本更新上次检查时间" + }, + "settings_scripts_update_check_lasttime_desc": { + "message": "后台脚本更新上次检查时间" + }, + "settings_settings_sync": { + "message": "同步设置" + }, + "settings_settings_sync_desc": { + "message": "跨设备同步设置" + }, + "settings_toolbar_badge_count": { + "message": "工具栏图标显示计数徽章" + }, + "settings_toolbar_badge_count_desc": { + "message": "在工具栏图标上显示一个徽章,其中的数字代表有多少个已启用的脚本与您所在页面的 URL 匹配" + }, + "settings_scripts_directory": { + "message": "保存位置" + }, + "settings_scripts_directory_desc": { + "message": "存储用户脚本的文件夹路径" + }, + "settings_set_scripts_directory": { + "message": "更改保存位置" + }, + "settings_about_text1": { + "message": "获取有关此扩展的更多信息,请访问本开源项目:" + }, + "settings_about_text2": { + "message": "如果您喜欢使用此扩展,请考虑在 App Store 上留下您的评论或注册 Beta 测试新版本:" + }, + "settings_about_button_repo": { + "message": "代码库" + }, + "settings_about_button_docs": { + "message": "文档" + }, + "settings_about_button_issues": { + "message": "报告错误" + }, + "settings_about_button_store": { + "message": "在 App Store 中打开" + }, + "settings_about_button_beta": { + "message": "注册 Beta 测试版" + }, + "utils_check_match_patterns_1": { + "message": "这 scheme 部分应当为 *、https、http 之一" + }, + "utils_check_match_patterns_2": { + "message": "这 scheme 和 host 部分应当用 `://` 分隔" + }, + "utils_check_match_patterns_3": { + "message": "匹配模式缺少 path 部分(至少应当有`/`)" + }, + "utils_check_match_patterns_4": { + "message": "这 `*.` 后面应当跟随主机名的一部分" + }, + "utils_check_match_patterns_5": { + "message": "这 host 部分长度应当为 1-255" + }, + "utils_check_match_patterns_6": { + "message": "这 `*` 应该是独立的或者为 `*.` 在开头" + }, + "utils_check_match_patterns_7": { + "message": "这 host 部分包含一个或多个空标签" + }, + "utils_check_match_patterns_8": { + "message": "主机名标签不能以 `-` 字符开头或结尾" + }, + "utils_check_match_patterns_9": { + "message": "主机名标签最大长度不能超过 63" + }, + "utils_check_match_patterns_10": { + "message": "这 host 部分包含无效字符:$1" + }, + "utils_check_match_patterns_11": { + "message": "这 path 部分包含无效字符:$1" + }, + "msg_invalid_match_pattern": { + "message": "无效的匹配模式" + } +} diff --git a/src/ext/shared/utils.js b/src/ext/shared/utils.js index fae3e29e..1f401f7a 100644 --- a/src/ext/shared/utils.js +++ b/src/ext/shared/utils.js @@ -147,6 +147,184 @@ export function parseMetadata(text) { return metadata; } +/** + * @param {string} input a match pattern + * @typedef {string} value - the match pattern + * @typedef {Object} parsedItem - parsed result + * @property {string} start - leading whitespace + * @property {string} separ - separator whitespace + * @property {value} value - the match pattern + * @property {boolean} error - the match pattern valid or not + * @property {string} point - invalid point or error message + * @returns {{error: boolean, items: parsedItem[], values: value[]}} + */ +export function parseMatchPatterns(input) { + if (typeof input !== "string") return; + const result = { + error: false, + items: [], + values: [], + }; + // match the separated values from input string + const matches = input.matchAll( + /(?^\s*|)(?\S+?)(?\s+|$)/g, + ); + for (const match of matches) { + const item = checkMatchPatterns(match.groups.value); + // setting the global error indicator + if (item.error === true) { + result.error = true; + } + result.items.push({ ...match.groups, ...item }); + result.values.push(item.value.toLowerCase()); + } + return result; +} + +/** + * @param {string} input whitespace separated list of match patterns + * @typedef {Object} checkedItem - checked result + * @property {string} value - the match pattern with fixes + * @property {boolean} error - the match pattern valid or not + * @property {string} point - invalid point or error message + * @returns {checkedItem} + */ +export function checkMatchPatterns(input) { + if (typeof input !== "string") return; + const result = { + value: input, + error: true, + point: "", + }; + if (input === "") { + result.error = false; + result.value = "*://*/*"; + return result; + } + let scheme, host, path; + /** check scheme component */ + if (input.slice(0, 5).toLowerCase() === "https") { + scheme = input.slice(0, 5); + } else if (input.slice(0, 4).toLowerCase() === "http") { + scheme = input.slice(0, 4); + } else if (input.startsWith("*")) { + scheme = "*"; + } else { + // The scheme component should one of *, https, http + result.point = gl("utils_check_match_patterns_1"); + return result; + } + /** check :// separator */ + if (input.slice(scheme.length, scheme.length + 3) !== "://") { + // The scheme and host should separated by `://` + result.point = gl("utils_check_match_patterns_2"); + return result; + } + // separate host and path + const array = input.slice(scheme.length + 3).split("/"); + if (array.length < 2) { + // The match pattern has no path component + result.point = gl("utils_check_match_patterns_3"); + return result; + } + host = array[0]; + path = "/" + array.slice(1).join("/"); + /** check host component */ + if (host === "*.") { + // The `*.` should followed by part of the hostname + result.point = gl("utils_check_match_patterns_4"); + return result; + } + // allow fully qualified domain name (FQDN) + if (host.at(-1) === ".") host = host.slice(0, -1); + if (host.length < 1 || 255 - 1 < host.length) { + // The host component length should be 1-255 + result.point = gl("utils_check_match_patterns_5"); + return result; + } + let labels = []; // domain labels + let hostPart = ""; // rest part that exclude wildcard + if (host.startsWith("*.")) { + hostPart = host.slice(2); + labels = hostPart.split("."); + } else if (host !== "*") { + hostPart = host; + labels = host.split("."); + } + if (hostPart.includes("*")) { + // The `*` should be independent or `*.` at the start + result.point = gl("utils_check_match_patterns_6"); + return result; + } + for (const label of labels) { + if (label.length === 0) { + // The host component contains empty label(s) + result.point = gl("utils_check_match_patterns_7"); + return result; + } + if (label.startsWith("-") || label.endsWith("-")) { + // The label cannot start or end with `-` character + result.point = gl("utils_check_match_patterns_8"); + return result; + } + if (label.length > 63) { + // The maximum length of the label cannot exceed 63 + result.point = gl("utils_check_match_patterns_9"); + return result; + } + } + // allowed character set of host component + const hostInvalidMatches = hostPart.match(/[^A-Za-z0-9.-]/g); + if (hostInvalidMatches) { + const characters = hostInvalidMatches.join(""); + // The host component contains invalid character(s): ${c} + result.point = gl("utils_check_match_patterns_10", characters); + return result; + } + /** check path component */ + // allowed character set of path component + const pathInvalidMatches = path.match( + /[^\w\]\\!$%&'()*+,./:;=?@[^`{|}~-]/g, // toolsGetValidPathCharacters + ); + if (pathInvalidMatches) { + const characters = pathInvalidMatches.join(""); + // The path component contains invalid character(s): ${c} + result.point = gl("utils_check_match_patterns_11", characters); + return result; + } + result.error = false; + return result; +} + +/** Generate valid path characters in browser js runtime */ +export function toolsGetValidPathCharacters() { + const set = new Set(); + for (let i = 32; i < 128; i++) set.add(String.fromCharCode(i)); + set.delete("?"); + set.delete("#"); + const p = [...set].join(""); + set.delete("="); + set.delete("&"); + const s = [...set].join(""); + const url = new URL(`https://host/${p}?1=${s}&${s}=2`); + const possibles = [...url.pathname, ...url.search]; + const characters = [...new Set(possibles)].sort().join(""); + const symbols = characters.match(/[^\w]/g).join(""); + return { + characters, + symbols, + restr: `/\\w${symbols.replace(/[\^[\]-]/g, "\\$&")}]/`, + }; +} + +/** + * get lang + * @param {string} n messageName + * @param {string | string[]} s substitutions + * const gl = browser.i18n.getMessage; // issue: safari return `undefined` + */ +export const gl = (n, s = undefined) => browser.i18n.getMessage(n, s); + export const validGrants = new Set([ "GM.info", "GM_info", From a388190638e2c0bd37212d631ff78c0b7617b7db Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Mon, 8 Jan 2024 18:55:00 +0800 Subject: [PATCH 11/24] refactor: introducing new settings component --- README.md | 8 +- .../extension-page/Components/Settings.svelte | 657 +++++++++++------- src/ext/shared/Components/Toggle.svelte | 11 +- xcode/App-Mac/AppDelegate.swift | 2 + 4 files changed, 413 insertions(+), 265 deletions(-) diff --git a/README.md b/README.md index 5864d9b8..a9051730 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ After installing Userscripts on macOS, you **do not** need to select a userscrip - **Show Toolbar Count** - displays a badge on the toolbar icon with a number that represents how many enabled scripts match the url for the page you are on - **Save Location** - where your file are currently located and being saved to (click the blue text to open location) - **Change Save Location (cogs icon)** - this button, located directly to the right of the save location, is a shortcut for opening the host app, which will allow you to change the save location -- **Global Blacklist** - this input accepts a comma separated list of [`@match` patterns](https://developer.chrome.com/docs/extensions/mv3/match_patterns/), a page url that matches against a pattern in this list will be ignored for script injection +- **Global Blacklist** - this input accepts a comma separated list of `@match` patterns ([Match pattern structure](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns#match_pattern_structure)), a page url that matches against a pattern in this list will be ignored for script injection ### Popup: @@ -125,7 +125,7 @@ Userscripts Safari currently supports the following userscript metadata: - `@name` - This will be the name that displays in the sidebar and be used as the filename - you can _not_ use the same name for multiple files of the same type - `@description`- Use this to describe what your userscript does - this will be displayed in the sidebar - there is a setting to hide descriptions - `@icon` - This doesn't have a function with this userscript manager, but the **first value** provided in the metadata will be accessible in the `GM_/GM.info` object -- `@match` - Domain match patterns - you can use several instances of this field if you'd like multiple domain matches - view [this article for more information on constructing patterns](https://developer.chrome.com/extensions/match_patterns) +- `@match` - Domain match patterns - you can use several instances of this field if you'd like multiple domain matches - please refer to: [Match pattern structure](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns#match_pattern_structure) - **Note:** this extension only supports `http/s` - `@exclude-match` - Domain patterns where you do _not_ want the script to run - `@include` - Used to match against urls for injection, globs and regular expressions are allowed, [read more here](https://wiki.greasespot.net/Include_and_exclude_rules) @@ -325,9 +325,7 @@ The quickest and easiest way to support the project is by [leaving a positive re The second best way to help out is to sign up to beta test new versions of the app. Since this extension values your privacy, and **does not collect any data from users**, it is difficult to gauge how the extension is being used. By signing up to be a beta tester it not only allows you to test upcoming features, but also gives me the opportunity to elicit direct feedback from real users. -**[iOS Beta Sign Up Form](https://forms.gle/QB46uYQHVyCxULue9)** - -**[macOS Beta Sign Up Form](https://forms.gle/cUDtKg1ip4Vc9Xhc7)** +**Please join and test the corresponding beta version in [releases](https://github.com/quoid/userscripts/releases) via the TestFlight public link.** ## Privacy Policy diff --git a/src/ext/extension-page/Components/Settings.svelte b/src/ext/extension-page/Components/Settings.svelte index 0d759d11..51cb25ea 100644 --- a/src/ext/extension-page/Components/Settings.svelte +++ b/src/ext/extension-page/Components/Settings.svelte @@ -1,341 +1,477 @@ -
- - -
state.remove("settings")}>
-