From 32fde140caae66200c3af8811c9705d341937ee2 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sat, 15 Oct 2022 14:07:21 +0800 Subject: [PATCH 01/14] refactor: add new settings storage --- src/page/App.svelte | 9 +-------- src/page/store.js | 39 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/page/App.svelte b/src/page/App.svelte index 462b664d..9e58dfe8 100644 --- a/src/page/App.svelte +++ b/src/page/App.svelte @@ -41,14 +41,7 @@ onMount(async () => { log.add("Requesting initialization data", "info", false); - const initData = await browser.runtime.sendNativeMessage({name: "PAGE_INIT_DATA"}); - if (initData.error) return console.error(initData.error); - for (const [key, value] of Object.entries(initData)) { - if (value === "true" || value === "false") { - initData[key] = JSON.parse(value); - } - } - settings.set(initData); + if (!settings.init()) return console.error("Initialization failure"); state.add("items-loading"); state.remove("init"); diff --git a/src/page/store.js b/src/page/store.js index f366e2c2..c23462aa 100644 --- a/src/page/store.js +++ b/src/page/store.js @@ -76,7 +76,7 @@ export const state = stateStore(); function settingsStore() { const {subscribe, update, set} = writable({}); - const updateSingleSetting = (key, value) => { + const updateSingleSetting_old = (key, value) => { update(settings => { settings[key] = value; // blacklist not stored in normal setting object in manifest, so handle differently @@ -109,7 +109,42 @@ function settingsStore() { return settings; }); }; - return {subscribe, set, updateSingleSetting}; + const storageKey = "USettings"; + const updateSingleSetting = (key, value) => { + update(settings => { + settings[key] = value; + browser.storage.local.set({[storageKey]: settings}); // Durable Storage + return settings; + }); + // Temporarily keep the old storage method until it is confirmed that all dependencies are removed + updateSingleSetting_old(key, value); + }; + const init_import_from_old = async () => { + log.add("Import settings data from old storage", "info", false); + const initData = await browser.runtime.sendNativeMessage({name: "PAGE_INIT_DATA"}); + if (initData.error) { + console.error(initData.error); + return false; + } + for (const [key, value] of Object.entries(initData)) { + if (value === "true" || value === "false") { + initData[key] = JSON.parse(value); + } + } + set(initData); + browser.storage.local.set({[storageKey]: initData}); + return true; + }; + const init = async () => { + const result = await browser.storage.local.get(storageKey); + if (!result[storageKey]) { + // This will remain for several versions until the majority of users use the new version + return init_import_from_old(); + } + set(result[storageKey]); + return true; + }; + return {subscribe, set, init, updateSingleSetting}; } export const settings = settingsStore(); From 1f53065e207307fd5e523e3e52c3a71f6beed2a1 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sun, 16 Oct 2022 12:41:07 +0800 Subject: [PATCH 02/14] refactor: add new settings define file --- src/shared/settings.js | 250 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 src/shared/settings.js diff --git a/src/shared/settings.js b/src/shared/settings.js new file mode 100644 index 00000000..a0dd2d12 --- /dev/null +++ b/src/shared/settings.js @@ -0,0 +1,250 @@ +const setting_template = { + name: "setting_name", + type: "boolean", + default: true, + writable: true, + platforms: ["macos", "ipados", "ios"], + langLabel: { + en: "engilsh label", + zh_hans: "简体中文标签", + zh_hant: "繁體中文標籤" + }, + langTitle: { + en: "engilsh title", + zh_hans: "简体中文描述", + zh_hant: "簡體中文描述" + }, + group: "General", + legacy: "setting_legacy_name", + nodeType: "Toggle", + nodeClass: {red: false} +}; + +const settings_define = [ + { + name: "global_active", + type: "boolean", + 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: "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_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: "boolean", + default: true, + 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_auto_update", + type: "boolean", + default: false, + 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_double_confirm", + nodeClass: {warn: true} + }, + { + name: "location_path", + type: "boolean", + default: true, + writable: false, + platforms: ["macos", "ipados", "ios"], + langLabel: { + en: "Save Location", + zh_hans: "脚本存储路径" + }, + langTitle: { + en: "where your file are currently located and being saved to (clic to open)", + zh_hans: "简体中文描述" + }, + group: "General", + legacy: "saveLocation", + nodeType: "IconButton" + }, + { + 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_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_descriptions", + type: "boolean", + default: false, + 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_whitespace_show", + 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", + 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" + } +]; + +export const setting_default = setting_template; + +export const settings = settings_define; \ No newline at end of file From a5f0e2dd89ce8e7f66add6a191913c565ee11496 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sun, 16 Oct 2022 18:26:37 +0800 Subject: [PATCH 03/14] refactor: settings export function --- src/shared/settings.js | 44 ++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/src/shared/settings.js b/src/shared/settings.js index a0dd2d12..ca5f1b17 100644 --- a/src/shared/settings.js +++ b/src/shared/settings.js @@ -1,26 +1,39 @@ -const setting_template = { - name: "setting_name", +export const settingTemplate = { + name: "setting_template", type: "boolean", default: true, writable: true, platforms: ["macos", "ipados", "ios"], langLabel: { - en: "engilsh label", + en: "Engilsh label", zh_hans: "简体中文标签", zh_hant: "繁體中文標籤" }, langTitle: { - en: "engilsh title", - zh_hans: "简体中文描述", - zh_hant: "簡體中文描述" + en: "Engilsh title", + zh_hans: "简体中文标题", + zh_hant: "繁體中文標題" }, - group: "General", + group: "Template", legacy: "setting_legacy_name", nodeType: "Toggle", nodeClass: {red: false} }; -const settings_define = [ +const settingDefault = { + name: "setting_default", + type: undefined, + default: undefined, + writable: true, + platforms: ["macos", "ipados", "ios"], + langLabel: {}, + langTitle: {}, + group: "", + nodeType: "", + nodeClass: {} +}; + +const settingsDefine = [ { name: "global_active", type: "boolean", @@ -159,7 +172,7 @@ const settings_define = [ nodeType: "Toggle" }, { - name: "editor_hint", + name: "editor_auto_hint", type: "boolean", default: true, platforms: ["macos"], @@ -210,7 +223,7 @@ const settings_define = [ nodeType: "Toggle" }, { - name: "editor_whitespace_show", + name: "editor_show_whitespace", type: "boolean", default: true, platforms: ["macos"], @@ -245,6 +258,13 @@ const settings_define = [ } ]; -export const setting_default = setting_template; +const storagePrefix = "US_"; -export const settings = settings_define; \ No newline at end of file +export const settings = settingsDefine.reduce((arr, val) => { + val.key = storagePrefix + val.name.toUpperCase(); + // arr[val.key] = Object.assign({}, settingDefault, val); + arr[val.key] = new Proxy(val, { + get: (obj, p) => p in obj ? obj[p] : settingDefault[p] + }); + return arr; +}, {}); From 40e8c136f3f3df500d5aa06c98fb8b19b0cc7d21 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sun, 16 Oct 2022 22:00:52 +0800 Subject: [PATCH 04/14] refactor: add import legacy settings function --- src/shared/settings.js | 81 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/src/shared/settings.js b/src/shared/settings.js index ca5f1b17..f213b4d0 100644 --- a/src/shared/settings.js +++ b/src/shared/settings.js @@ -34,6 +34,13 @@ const settingDefault = { }; const settingsDefine = [ + { + name: "legacy_imported", + type: "boolean", + default: false, + platforms: ["macos"], + group: "Internal" + }, { name: "global_active", type: "boolean", @@ -120,7 +127,7 @@ const settingsDefine = [ }, { name: "location_path", - type: "boolean", + type: "string", default: true, writable: false, platforms: ["macos", "ipados", "ios"], @@ -260,7 +267,7 @@ const settingsDefine = [ const storagePrefix = "US_"; -export const settings = settingsDefine.reduce((arr, val) => { +export const settingsConfig = settingsDefine.reduce((arr, val) => { val.key = storagePrefix + val.name.toUpperCase(); // arr[val.key] = Object.assign({}, settingDefault, val); arr[val.key] = new Proxy(val, { @@ -268,3 +275,73 @@ export const settings = settingsDefine.reduce((arr, val) => { }); return arr; }, {}); + +const settingsKeys = Object.keys(settingsConfig); + +export async function get(key) { + if (typeof key !== "string") return console.error("key should be a string:", key); + key = storagePrefix + key.toUpperCase(); + if (!settingsKeys.includes(key)) { + return console.error("unexpected settings key:", key); + } + const result = await browser.storage.local.get(key); + return result[key] ?? settingsConfig[key].default; +} + +export async function getAll() { + const result = await browser.storage.local.get(settingsKeys); + const settingsDefault = {}; + for (const key in settingsConfig) { + settingsDefault[key] = settingsConfig[key].default; + } + return Object.assign({}, settingsDefault, result); +} + +export function update(key, value) { + key = storagePrefix + key.toUpperCase(); + if (!settingsKeys.includes(key)) { + return console.error("Unexpected key:", key); + } + const type = settingsConfig[key].type; + if (typeof(value) !== type) { + return console.error(`Unexpected value type ${typeof(value)} should ${type}`); + } + browser.storage.local.set({[key]: value}); + return true; +} + +// Note: It seems unnecessary to use this method, comment it out for now +// export function updateAll(settings) { +// for (const key in settings) { +// if (!settingsKeys.includes(key)) { +// return console.error("unexpected settings key:", key); +// } +// const type = settingsConfig[key].type; +// if (typeof(settings.key) !== type) { +// return console.error(`Unexpected value type ${typeof(value)} should ${type}`); +// } +// } +// browser.storage.local.set(settings); +// } + +export async function import_legacy_data() { + console.info("Import settings data from legacy manifest file"); + if (await get("legacy_imported")) return console.info("Legacy settings has already imported"); + const result = await browser.runtime.sendNativeMessage({name: "PAGE_INIT_DATA"}); + if (result.error) return console.error(result.error); + for (const key in settingsConfig) { + const legacy = settingsConfig[key].legacy; + if (legacy in result) { + let value = result[legacy]; + console.log(`Importing legacy setting: ${legacy} - ${value}`); + switch (settingsConfig[key].type) { + case "boolean": value = JSON.parse(value); break; + case "number": value = Number(value); break; + } + if (!update(settingsConfig[key].name, value)) return console.error("Import abort"); + } + } + update("legacy_imported", true); + console.log("Importing legacy settings complete"); + // Send a message to the Swift layer to safely clean up legacy data +} \ No newline at end of file From 017b9a1456d46d473c7ca038b52eaba200f53c6c Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Mon, 17 Oct 2022 00:55:02 +0800 Subject: [PATCH 05/14] refactor: update functions use settings storage --- .../Resources/content.js | 161 +++++++++--------- src/page/App.svelte | 2 +- src/page/store.js | 34 +--- src/popup/App.svelte | 16 +- src/shared/settings.js | 20 ++- 5 files changed, 108 insertions(+), 125 deletions(-) diff --git a/extension/Userscripts Extension/Resources/content.js b/extension/Userscripts Extension/Resources/content.js index 82156c87..7747b92c 100644 --- a/extension/Userscripts Extension/Resources/content.js +++ b/extension/Userscripts Extension/Resources/content.js @@ -323,88 +323,93 @@ function cspFallback(e) { } } -browser.runtime.sendMessage({name: "REQ_USERSCRIPTS"}, response => { - // cancel injection if errors detected - if (!response || response.error) { - console.error(response?.error || "REQ_USERSCRIPTS returned undefined"); - return; - } - // save response locally in case CSP events occur - data = response; - // loop through each userscript and prepare for processing - for (let i = 0; i < data.files.js.length; i++) { - const userscript = data.files.js[i]; - userscript.preCode = ""; - // pass references to the api methods as needed - const gmMethods = []; - const filename = userscript.scriptObject.filename; - const grants = userscript.scriptObject.grant; - const injectInto = userscript.scriptObject["inject-into"]; - // create GM.info object - const scriptData = { - "script": userscript.scriptObject, - "scriptHandler": data.scriptHandler, - "scriptHandlerVersion": data.scriptHandlerVersion, - "scriptMetaStr": userscript.scriptMetaStr - }; - // all userscripts get access to GM.info - gmMethods.push("info: GM_info"); - // if @grant explicitly set to none, empty grants array - if (grants.includes("none")) grants.length = 0; - // @grant values exist for page scoped userscript - if (grants.length && injectInto === "page") { - // remove grants - grants.length = 0; - // log warning - console.warn(`${filename} @grant values removed due to @inject-into value: ${injectInto} - https://github.com/quoid/userscripts/issues/265#issuecomment-1213462394`); +// should import settings.js and use `settingsStorage.get("global_active")` +browser.storage.local.get("US_GLOBAL_ACTIVE").then(CONFIG => { + if (CONFIG?.US_GLOBAL_ACTIVE === false) return console.info("Userscripts off"); + browser.runtime.sendMessage({name: "REQ_USERSCRIPTS"}, response => { + // cancel injection if errors detected + if (!response || response.error) { + console.error(response?.error || "REQ_USERSCRIPTS returned undefined"); + return; } - // @grant exist for auto scoped userscript - if (grants.length && injectInto === "auto") { - // change scope - userscript.scriptObject["inject-into"] = "content"; - // log warning - console.warn(`${filename} @inject-into value set to 'content' due to @grant values: ${grants} - https://github.com/quoid/userscripts/issues/265#issuecomment-1213462394`); - } - // loop through each userscript @grant value, add methods as needed - for (let j = 0; j < grants.length; j++) { - const grant = grants[j]; - const method = grant.split(".")[1] || grant.split(".")[0]; - // ensure API method exists in apis object - if (!Object.keys(apis).includes(method)) continue; - // create the method string to be pushed to methods array - let methodStr = `${method}: apis.${method}`; - // add require variables to specific methods - switch (method) { - case "getValue": - case "setValue": - case "deleteValue": - case "listValues": - methodStr += `.bind({"US_filename": "${filename}"})`; - break; - case "info": - case "GM_info": - continue; - case "xmlHttpRequest": - gmMethods.push("xmlHttpRequest: apis.xhr"); - continue; - case "GM_xmlhttpRequest": - userscript.preCode += "const GM_xmlhttpRequest = apis.xhr;"; - continue; + // save response locally in case CSP events occur + data = response; + // loop through each userscript and prepare for processing + for (let i = 0; i < data.files.js.length; i++) { + const userscript = data.files.js[i]; + userscript.preCode = ""; + // pass references to the api methods as needed + const gmMethods = []; + const filename = userscript.scriptObject.filename; + const grants = userscript.scriptObject.grant; + const injectInto = userscript.scriptObject["inject-into"]; + // create GM.info object + const scriptData = { + "script": userscript.scriptObject, + "scriptHandler": data.scriptHandler, + "scriptHandlerVersion": data.scriptHandlerVersion, + "scriptMetaStr": userscript.scriptMetaStr + }; + // all userscripts get access to GM.info + gmMethods.push("info: GM_info"); + // if @grant explicitly set to none, empty grants array + if (grants.includes("none")) grants.length = 0; + // @grant values exist for page scoped userscript + if (grants.length && injectInto === "page") { + // remove grants + grants.length = 0; + // log warning + console.warn(`${filename} @grant values removed due to @inject-into value: ${injectInto} - https://github.com/quoid/userscripts/issues/265#issuecomment-1213462394`); + } + // @grant exist for auto scoped userscript + if (grants.length && injectInto === "auto") { + // change scope + userscript.scriptObject["inject-into"] = "content"; + // log warning + console.warn(`${filename} @inject-into value set to 'content' due to @grant values: ${grants} - https://github.com/quoid/userscripts/issues/265#issuecomment-1213462394`); } - gmMethods.push(methodStr); + // loop through each userscript @grant value, add methods as needed + for (let j = 0; j < grants.length; j++) { + const grant = grants[j]; + const method = grant.split(".")[1] || grant.split(".")[0]; + // ensure API method exists in apis object + if (!Object.keys(apis).includes(method)) continue; + // create the method string to be pushed to methods array + let methodStr = `${method}: apis.${method}`; + // add require variables to specific methods + switch (method) { + case "getValue": + case "setValue": + case "deleteValue": + case "listValues": + methodStr += `.bind({"US_filename": "${filename}"})`; + break; + case "info": + case "GM_info": + continue; + case "xmlHttpRequest": + gmMethods.push("xmlHttpRequest: apis.xhr"); + continue; + case "GM_xmlhttpRequest": + userscript.preCode += "const GM_xmlhttpRequest = apis.xhr;"; + continue; + } + gmMethods.push(methodStr); + } + // add GM.info + userscript.preCode += `const GM_info = ${JSON.stringify(scriptData)};`; + // add other included GM API methods + userscript.preCode += `const GM = {${gmMethods.join(",")}};`; + // process file for injection + processJS(userscript); } - // add GM.info - userscript.preCode += `const GM_info = ${JSON.stringify(scriptData)};`; - // add other included GM API methods - userscript.preCode += `const GM = {${gmMethods.join(",")}};`; - // process file for injection - processJS(userscript); - } - for (let i = 0; i < data.files.css.length; i++) { - const userstyle = data.files.css[i]; - injectCSS(userstyle.name, userstyle.code); - } + for (let i = 0; i < data.files.css.length; i++) { + const userstyle = data.files.css[i]; + injectCSS(userstyle.name, userstyle.code); + } + }); }); + // listens for messages from background, popup, etc... browser.runtime.onMessage.addListener((request, sender, sendResponse) => { const name = request.name; diff --git a/src/page/App.svelte b/src/page/App.svelte index 9e58dfe8..9ca68949 100644 --- a/src/page/App.svelte +++ b/src/page/App.svelte @@ -41,7 +41,7 @@ onMount(async () => { log.add("Requesting initialization data", "info", false); - if (!settings.init()) return console.error("Initialization failure"); + await settings.init(); state.add("items-loading"); state.remove("init"); diff --git a/src/page/store.js b/src/page/store.js index c23462aa..123123f3 100644 --- a/src/page/store.js +++ b/src/page/store.js @@ -1,5 +1,6 @@ import {writable} from "svelte/store"; import {uniqueId} from "./utils.js"; +import * as settingsStorage from "../shared/settings.js"; function notificationStore() { const {subscribe, update} = writable([]); @@ -109,40 +110,15 @@ function settingsStore() { return settings; }); }; - const storageKey = "USettings"; const updateSingleSetting = (key, value) => { - update(settings => { - settings[key] = value; - browser.storage.local.set({[storageKey]: settings}); // Durable Storage - return settings; - }); + update(settings => (settings[key] = value, settings)); + settingsStorage.set(key, value); // Durable Storage // Temporarily keep the old storage method until it is confirmed that all dependencies are removed updateSingleSetting_old(key, value); }; - const init_import_from_old = async () => { - log.add("Import settings data from old storage", "info", false); - const initData = await browser.runtime.sendNativeMessage({name: "PAGE_INIT_DATA"}); - if (initData.error) { - console.error(initData.error); - return false; - } - for (const [key, value] of Object.entries(initData)) { - if (value === "true" || value === "false") { - initData[key] = JSON.parse(value); - } - } - set(initData); - browser.storage.local.set({[storageKey]: initData}); - return true; - }; const init = async () => { - const result = await browser.storage.local.get(storageKey); - if (!result[storageKey]) { - // This will remain for several versions until the majority of users use the new version - return init_import_from_old(); - } - set(result[storageKey]); - return true; + settingsStorage.import_legacy_data(); + set(await settingsStorage.getAll()); }; return {subscribe, set, init, updateSingleSetting}; } diff --git a/src/popup/App.svelte b/src/popup/App.svelte index ef8a72e2..7db399f7 100644 --- a/src/popup/App.svelte +++ b/src/popup/App.svelte @@ -12,6 +12,7 @@ import iconUpdate from "../shared/img/icon-update.svg"; import iconClear from "../shared/img/icon-clear.svg"; import iconRefresh from "../shared/img/icon-refresh.svg"; + import * as settingsStorage from "../shared/settings.js"; let error = undefined; let active = true; @@ -50,14 +51,9 @@ $: if (platform) document.body.classList.add(platform); - function toggleExtension(e) { - e.preventDefault(); // prevent check state from changing on click - disabled = true; - browser.runtime.sendNativeMessage({name: "POPUP_TOGGLE_EXTENSION"}, response => { - disabled = false; - if (response.error) return error = response.error; - active = !active; - }); + async function toggleExtension(e) { + await settingsStorage.set("global_active", !active); + active = await settingsStorage.get("global_active"); } function updateAll() { @@ -259,9 +255,9 @@ loading = false; disabled = false; return; - } else { - active = init.initData.active === "true" ? true : false; } + + active = await settingsStorage.get("global_active"); // refresh session rules browser.runtime.sendMessage({name: "REFRESH_SESSION_RULES"}); diff --git a/src/shared/settings.js b/src/shared/settings.js index f213b4d0..a53a92d2 100644 --- a/src/shared/settings.js +++ b/src/shared/settings.js @@ -297,7 +297,7 @@ export async function getAll() { return Object.assign({}, settingsDefault, result); } -export function update(key, value) { +export async function set(key, value) { key = storagePrefix + key.toUpperCase(); if (!settingsKeys.includes(key)) { return console.error("Unexpected key:", key); @@ -306,12 +306,17 @@ export function update(key, value) { if (typeof(value) !== type) { return console.error(`Unexpected value type ${typeof(value)} should ${type}`); } - browser.storage.local.set({[key]: value}); - return true; + try { + await browser.storage.local.set({[key]: value}); + return true; + } catch (error) { + console.error(error); + return false; + } } // Note: It seems unnecessary to use this method, comment it out for now -// export function updateAll(settings) { +// export function setAll(settings) { // for (const key in settings) { // if (!settingsKeys.includes(key)) { // return console.error("unexpected settings key:", key); @@ -325,10 +330,10 @@ export function update(key, value) { // } export async function import_legacy_data() { - console.info("Import settings data from legacy manifest file"); if (await get("legacy_imported")) return console.info("Legacy settings has already imported"); const result = await browser.runtime.sendNativeMessage({name: "PAGE_INIT_DATA"}); if (result.error) return console.error(result.error); + console.info("Import settings data from legacy manifest file"); for (const key in settingsConfig) { const legacy = settingsConfig[key].legacy; if (legacy in result) { @@ -338,10 +343,11 @@ export async function import_legacy_data() { case "boolean": value = JSON.parse(value); break; case "number": value = Number(value); break; } - if (!update(settingsConfig[key].name, value)) return console.error("Import abort"); + if (!set(settingsConfig[key].name, value)) return console.error("Import abort"); } } - update("legacy_imported", true); + set("legacy_imported", true); console.log("Importing legacy settings complete"); // Send a message to the Swift layer to safely clean up legacy data + browser.runtime.sendNativeMessage({name: "PAGE_LEGACY_IMPORTED"}); } \ No newline at end of file From e77c94aa60307098a1658aa4b5f3f418ce17887c Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Tue, 18 Oct 2022 02:26:26 +0800 Subject: [PATCH 06/14] refactor: add sync settings storage feature --- src/page/store.js | 4 +- src/popup/App.svelte | 2 +- src/shared/settings.js | 294 +++++++++++++++++++++++++++++++---------- 3 files changed, 224 insertions(+), 76 deletions(-) diff --git a/src/page/store.js b/src/page/store.js index 123123f3..1c30d2db 100644 --- a/src/page/store.js +++ b/src/page/store.js @@ -112,13 +112,13 @@ function settingsStore() { }; const updateSingleSetting = (key, value) => { update(settings => (settings[key] = value, settings)); - settingsStorage.set(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 updateSingleSetting_old(key, value); }; const init = async () => { settingsStorage.import_legacy_data(); - set(await settingsStorage.getAll()); + set(await settingsStorage.get()); }; return {subscribe, set, init, updateSingleSetting}; } diff --git a/src/popup/App.svelte b/src/popup/App.svelte index 7db399f7..be50d240 100644 --- a/src/popup/App.svelte +++ b/src/popup/App.svelte @@ -52,7 +52,7 @@ $: if (platform) document.body.classList.add(platform); async function toggleExtension(e) { - await settingsStorage.set("global_active", !active); + await settingsStorage.set({"global_active": !active}); active = await settingsStorage.get("global_active"); } diff --git a/src/shared/settings.js b/src/shared/settings.js index a53a92d2..143d9b77 100644 --- a/src/shared/settings.js +++ b/src/shared/settings.js @@ -1,29 +1,18 @@ -export const settingTemplate = { - name: "setting_template", - type: "boolean", - default: true, - writable: true, - platforms: ["macos", "ipados", "ios"], - langLabel: { - en: "Engilsh label", - zh_hans: "简体中文标签", - zh_hant: "繁體中文標籤" - }, - langTitle: { - en: "Engilsh title", - zh_hans: "简体中文标题", - zh_hant: "繁體中文標題" - }, - group: "Template", - legacy: "setting_legacy_name", - nodeType: "Toggle", - nodeClass: {red: false} -}; +function deepFreeze(object) { + for (const p in object) { + if (typeof object[p] == "object") { + deepFreeze(object[p]); + } + } + return Object.freeze(object); +} -const settingDefault = { +const settingDefault = deepFreeze({ name: "setting_default", type: undefined, + local: false, default: undefined, + confirm: false, writable: true, platforms: ["macos", "ipados", "ios"], langLabel: {}, @@ -31,19 +20,38 @@ const settingDefault = { group: "", nodeType: "", nodeClass: {} -}; +}); const settingsDefine = [ { name: "legacy_imported", type: "boolean", + local: true, default: false, platforms: ["macos"], group: "Internal" }, + { + name: "settings_sync", + type: "boolean", + local: true, + default: false, + platforms: ["macos", "ipados", "ios"], + langLabel: { + en: "Sync settings", + zh_hans: "同步设置" + }, + langTitle: { + en: "Sync settings across devices", + zh_hans: "跨设备同步设置" + }, + group: "General", + nodeType: "Toggle" + }, { name: "global_active", type: "boolean", + local: true, default: true, platforms: ["macos"], langLabel: { @@ -112,6 +120,7 @@ const settingsDefine = [ name: "scripts_auto_update", type: "boolean", default: false, + confirm: true, platforms: ["macos", "ipados", "ios"], langLabel: { en: "Scripts silent auto update", @@ -122,12 +131,13 @@ const settingsDefine = [ zh_hans: "脚本在后台静默自动更新,这是危险的,可能引入未经确认的恶意代码" }, group: "General", - nodeType: "Toggle_double_confirm", + nodeType: "Toggle", nodeClass: {warn: true} }, { name: "location_path", type: "string", + local: true, default: true, writable: false, platforms: ["macos", "ipados", "ios"], @@ -266,88 +276,226 @@ const settingsDefine = [ ]; const storagePrefix = "US_"; +const storageKey = key => storagePrefix + key.toUpperCase(); +const storageRef = async area => { // storage reference + browser.storage.sync.area = "sync"; + browser.storage.local.area = "local"; + if (area === "sync") return browser.storage.sync; + if (area === "local") return browser.storage.local; + const key = storageKey("settings_sync"); + const result = await browser.storage.local.get(key); + if (result?.[key] === true) { + return browser.storage.sync; + } else { + return browser.storage.local; + } +}; export const settingsConfig = settingsDefine.reduce((arr, val) => { - val.key = storagePrefix + val.name.toUpperCase(); - // arr[val.key] = Object.assign({}, settingDefault, val); - arr[val.key] = new Proxy(val, { - get: (obj, p) => p in obj ? obj[p] : settingDefault[p] - }); + val.key = storageKey(val.name); + arr[val.key] = Object.assign({}, settingDefault, val); return arr; }, {}); -const settingsKeys = Object.keys(settingsConfig); +deepFreeze(settingsConfig); +deepFreeze(settingsDefine); -export async function get(key) { - if (typeof key !== "string") return console.error("key should be a string:", key); - key = storagePrefix + key.toUpperCase(); - if (!settingsKeys.includes(key)) { - return console.error("unexpected settings key:", key); - } - const result = await browser.storage.local.get(key); - return result[key] ?? settingsConfig[key].default; -} +export const settingsKeys = deepFreeze(Object.keys(settingsConfig)); -export async function getAll() { - const result = await browser.storage.local.get(settingsKeys); - const settingsDefault = {}; - for (const key in settingsConfig) { - settingsDefault[key] = settingsConfig[key].default; +export async function get(keys, area) { + if (![undefined, "local", "sync"].includes(area)) { + return console.error("unexpected storage area:", area); + } + if (typeof keys == "string") { // single setting + const key = storageKey(keys); + if (!settingsKeys.includes(key)) { + return console.error("unexpected settings key:", key); + } + settingsConfig[key].local === true && (area = "local"); + const storage = await storageRef(area); + const result = await storage.get(key); + return result[key] ?? settingsConfig[key].default; + } + const complexGet = async areaKeys => { + const storage = await storageRef(area); + const results = {local: {}, sync: {}}; + if (storage.area === "sync") { + if (areaKeys.sync.length) { + results.sync = await storage.get(areaKeys.sync); + } + if (areaKeys.local.length) { + const storage = await storageRef("local"); + results.local = await storage.get(areaKeys.local); + } + } else { + results.local = await storage.get(areaKeys.all); + } + return results; + }; + if (Array.isArray(keys)) { // muilt settings + if (!keys.length) { + return console.error("Settings keys empty:", keys); + } + const areaKeys = {local: [], sync: [], all: []}; + const settingsDefault = {}; + for (const k of keys) { + const key = storageKey(k); + if (!settingsKeys.includes(key)) { + return console.error("unexpected settings key:", key); + } + settingsDefault[key] = settingsConfig[key].default; + settingsConfig[key].local === true + ? areaKeys.local.push(key) + : areaKeys.sync.push(key); + areaKeys.all.push(key); + } + const {local, sync} = complexGet(areaKeys); + return Object.assign(settingsDefault, local, sync); } - return Object.assign({}, settingsDefault, result); + if (typeof keys == "undefined" || keys === null) { // all settings + const areaKeys = {local: [], sync: [], all: []}; + const settingsDefault = {}; + for (const key in settingsConfig) { + settingsDefault[key] = settingsConfig[key].default; + settingsConfig[key].local === true + ? areaKeys.local.push(key) + : areaKeys.sync.push(key); + areaKeys.all.push(key); + } + const {local, sync} = complexGet(areaKeys); + return Object.assign(settingsDefault, local, sync); + } + return console.error("Unexpected arg type:", keys); } -export async function set(key, value) { - key = storagePrefix + key.toUpperCase(); - if (!settingsKeys.includes(key)) { - return console.error("Unexpected key:", key); +export async function set(keys, area) { + if (![undefined, "local", "sync"].includes(area)) { + return console.error("unexpected storage area:", area); } - const type = settingsConfig[key].type; - if (typeof(value) !== type) { - return console.error(`Unexpected value type ${typeof(value)} should ${type}`); + if (typeof keys != "object") { + return console.error("Unexpected arg type:", keys); } + if (!Object.keys(keys).length) { + return console.error("Settings object empty:", keys); + } + const areaKeys = {local: {}, sync: {}, all: {}}; + for (const k in keys) { + const key = storageKey(k); + if (!settingsKeys.includes(key)) { + return console.error("Unexpected settings keys:", key); + } + const type = settingsConfig[key].type; + if (typeof keys[k] != type) { + return console.error(`Unexpected value type ${typeof(value)} should ${type}`); + } + settingsConfig[key].local === true + ? areaKeys.local[key] = keys[k] + : areaKeys.sync[key] = keys[k]; + areaKeys.all[key] = keys[k]; + } + const storage = await storageRef(area); try { - await browser.storage.local.set({[key]: value}); + if (storage.area === "sync") { + if (Object.keys(areaKeys.sync).length) { + await storage.set(areaKeys.sync); + } + if (Object.keys(areaKeys.local).length) { + const storage = await storageRef("local"); + await storage.set(areaKeys.local); + } + } else { + await storage.set(areaKeys.all); + } return true; } catch (error) { - console.error(error); - return false; + return console.error(error); } } -// Note: It seems unnecessary to use this method, comment it out for now -// export function setAll(settings) { -// for (const key in settings) { -// if (!settingsKeys.includes(key)) { -// return console.error("unexpected settings key:", key); -// } -// const type = settingsConfig[key].type; -// if (typeof(settings.key) !== type) { -// return console.error(`Unexpected value type ${typeof(value)} should ${type}`); -// } -// } -// browser.storage.local.set(settings); -// } +export async function reset(keys, area) { // reset to default + if (![undefined, "local", "sync"].includes(area)) { + return console.error("unexpected storage area:", area); + } + if (typeof keys == "string") { // single setting + const key = storageKey(keys); + if (!settingsKeys.includes(key)) { + return console.error("unexpected settings key:", key); + } + settingsConfig[key].local === true && (area = "local"); + const storage = await storageRef(area); + return storage.remove(key); + } + const complexRemove = async areaKeys => { + const storage = await storageRef(area); + try { + if (storage.area === "sync") { + if (areaKeys.sync.length) { + await storage.remove(areaKeys.sync); + } + if (areaKeys.local.length) { + const storage = await storageRef("local"); + await storage.remove(areaKeys.local); + } + } else { + await storage.remove(areaKeys.all); + } + return true; + } catch (error) { + return console.error(error); + } + }; + if (Array.isArray(keys)) { // muilt settings + if (!keys.length) { + return console.error("Settings keys empty:", keys); + } + const areaKeys = {local: [], sync: [], all: []}; + for (const k of keys) { + const key = storageKey(k); + if (!settingsKeys.includes(key)) { + return console.error("unexpected settings key:", key); + } + settingsConfig[key].local === true + ? areaKeys.local.push(key) + : areaKeys.sync.push(key); + areaKeys.all.push(key); + } + return complexRemove(areaKeys); + } + if (typeof keys == "undefined" || keys === null) { // all settings + const areaKeys = {local: [], sync: [], all: []}; + for (const key in settingsConfig) { + settingsConfig[key].local === true + ? areaKeys.local.push(key) + : areaKeys.sync.push(key); + areaKeys.all.push(key); + } + return complexRemove(areaKeys); + } + return console.error("Unexpected arg type:", keys); +} export async function import_legacy_data() { - if (await get("legacy_imported")) return console.info("Legacy settings has already imported"); + const imported = await get("legacy_imported"); + if (imported) return console.info("Legacy settings has already imported"); const result = await browser.runtime.sendNativeMessage({name: "PAGE_INIT_DATA"}); if (result.error) return console.error(result.error); console.info("Import settings data from legacy manifest file"); + const settings = {}; for (const key in settingsConfig) { const legacy = settingsConfig[key].legacy; if (legacy in result) { let value = result[legacy]; - console.log(`Importing legacy setting: ${legacy} - ${value}`); switch (settingsConfig[key].type) { case "boolean": value = JSON.parse(value); break; case "number": value = Number(value); break; } - if (!set(settingsConfig[key].name, value)) return console.error("Import abort"); + console.log(`Importing legacy setting: ${legacy}`, value); + settings[settingsConfig[key].name] = value; } } - set("legacy_imported", true); - console.log("Importing legacy settings complete"); + if (!set(settings)) return console.error("Import legacy settings abort"); + set({"legacy_imported": true}); + console.info("Import legacy settings complete"); // Send a message to the Swift layer to safely clean up legacy data browser.runtime.sendNativeMessage({name: "PAGE_LEGACY_IMPORTED"}); } \ No newline at end of file From eb2522fd199cdc39e7afe243adeb590cfee25a64 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Thu, 20 Oct 2022 02:34:04 +0800 Subject: [PATCH 07/14] refactor: add settings storage legacy functions --- src/page/store.js | 30 ++++------ src/shared/settings.js | 122 +++++++++++++++++++++++++++++++++-------- 2 files changed, 109 insertions(+), 43 deletions(-) diff --git a/src/page/store.js b/src/page/store.js index 1c30d2db..dfc3fd45 100644 --- a/src/page/store.js +++ b/src/page/store.js @@ -89,36 +89,28 @@ function settingsStore() { log.add("Failed to save blacklist to disk", "error", true); } }); - return settings; } - // settings are saved as strings on the swift side - // convert all booleans to strings before dispatching - const settingsClone = {...settings}; - for (const [key, value] of Object.entries(settingsClone)) { - if (typeof value === "boolean") settingsClone[key] = value.toString(); - } - // remove settings in clone that aren't save in user defaults - delete settingsClone.blacklist; - delete settingsClone.version; - // update settings on swift side - const message = {name: "PAGE_UPDATE_SETTINGS", settings: settingsClone}; - browser.runtime.sendNativeMessage(message, response => { - if (response.error) { - log.add(response.error, "error", true); - } - }); return settings; }); }; const updateSingleSetting = (key, value) => { update(settings => (settings[key] = value, settings)); - settingsStorage.set({[key]: value}); // Durable Storage + // For compatibility with legacy setting names only + // Once the new name is used, use settingsStorage.set() + settingsStorage.legacy_set({[key]: value}); // Durable Storage // Temporarily keep the old storage method until it is confirmed that all dependencies are removed updateSingleSetting_old(key, value); }; const init = async () => { settingsStorage.import_legacy_data(); - set(await settingsStorage.get()); + const settings = await settingsStorage.legacy_get(); + // console.log("store.js settingsStore init", settings); + set(settings); + // sync popup, backgound, etc... settings changes + settingsStorage.onChanged((settings, area) => { + console.log(`storage.${area}.onChanged`, settings); + update(obj => Object.assign(obj, settings)); + }); }; return {subscribe, set, init, updateSingleSetting}; } diff --git a/src/shared/settings.js b/src/shared/settings.js index 143d9b77..8d89b003 100644 --- a/src/shared/settings.js +++ b/src/shared/settings.js @@ -11,6 +11,7 @@ const settingDefault = deepFreeze({ name: "setting_default", type: undefined, local: false, + values: [], default: undefined, confirm: false, writable: true, @@ -18,6 +19,7 @@ const settingDefault = deepFreeze({ langLabel: {}, langTitle: {}, group: "", + legacy: "", nodeType: "", nodeClass: {} }); @@ -25,12 +27,20 @@ const settingDefault = deepFreeze({ const settingsDefine = [ { name: "legacy_imported", - type: "boolean", + type: "number", local: true, - default: false, + default: 0, platforms: ["macos"], group: "Internal" }, + { + name: "language_code", + type: "string", + default: "en", + platforms: ["macos", "ipados", "ios"], + group: "Internal", + legacy: "languageCode" + }, { name: "settings_sync", type: "boolean", @@ -102,8 +112,8 @@ const settingsDefine = [ }, { name: "scripts_update_check_interval", - type: "boolean", - default: true, + type: "number", + default: 86400000, platforms: ["macos", "ipados", "ios"], langLabel: { en: "Scripts update check interval", @@ -205,10 +215,28 @@ const settingsDefine = [ 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: false, + default: true, platforms: ["macos"], langLabel: { en: "Show List Descriptions", @@ -291,10 +319,10 @@ const storageRef = async area => { // storage reference } }; -export const settingsConfig = settingsDefine.reduce((arr, val) => { - val.key = storageKey(val.name); - arr[val.key] = Object.assign({}, settingDefault, val); - return arr; +export const settingsConfig = settingsDefine.reduce((p, c) => { + c.key = storageKey(c.name); + p[c.key] = Object.assign({}, settingDefault, c); + return p; }, {}); deepFreeze(settingsConfig); @@ -316,28 +344,32 @@ export async function get(keys, area) { const result = await storage.get(key); return result[key] ?? settingsConfig[key].default; } - const complexGet = async areaKeys => { + const complexGet = async (settingsDefault, areaKeys) => { const storage = await storageRef(area); - const results = {local: {}, sync: {}}; + let local = {}, sync = {}; if (storage.area === "sync") { if (areaKeys.sync.length) { - results.sync = await storage.get(areaKeys.sync); + sync = await storage.get(areaKeys.sync); } if (areaKeys.local.length) { const storage = await storageRef("local"); - results.local = await storage.get(areaKeys.local); + local = await storage.get(areaKeys.local); } } else { - results.local = await storage.get(areaKeys.all); + local = await storage.get(areaKeys.all); } - return results; + const result = Object.assign(settingsDefault, local, sync); + // reduce settings object property name + return Object.entries(result).reduce((p, c) => ( + p[settingsConfig[c[0]].name] = c[1], p + ), {}); }; if (Array.isArray(keys)) { // muilt settings if (!keys.length) { return console.error("Settings keys empty:", keys); } - const areaKeys = {local: [], sync: [], all: []}; const settingsDefault = {}; + const areaKeys = {local: [], sync: [], all: []}; for (const k of keys) { const key = storageKey(k); if (!settingsKeys.includes(key)) { @@ -349,12 +381,11 @@ export async function get(keys, area) { : areaKeys.sync.push(key); areaKeys.all.push(key); } - const {local, sync} = complexGet(areaKeys); - return Object.assign(settingsDefault, local, sync); + return await complexGet(settingsDefault, areaKeys); } if (typeof keys == "undefined" || keys === null) { // all settings - const areaKeys = {local: [], sync: [], all: []}; const settingsDefault = {}; + const areaKeys = {local: [], sync: [], all: []}; for (const key in settingsConfig) { settingsDefault[key] = settingsConfig[key].default; settingsConfig[key].local === true @@ -362,8 +393,7 @@ export async function get(keys, area) { : areaKeys.sync.push(key); areaKeys.all.push(key); } - const {local, sync} = complexGet(areaKeys); - return Object.assign(settingsDefault, local, sync); + return await complexGet(settingsDefault, areaKeys); } return console.error("Unexpected arg type:", keys); } @@ -459,7 +489,7 @@ export async function reset(keys, area) { // reset to default : areaKeys.sync.push(key); areaKeys.all.push(key); } - return complexRemove(areaKeys); + return await complexRemove(areaKeys); } if (typeof keys == "undefined" || keys === null) { // all settings const areaKeys = {local: [], sync: [], all: []}; @@ -469,11 +499,55 @@ export async function reset(keys, area) { // reset to default : areaKeys.sync.push(key); areaKeys.all.push(key); } - return complexRemove(areaKeys); + return await complexRemove(areaKeys); } return console.error("Unexpected arg type:", keys); } +export function onChanged(callback) { + const listener = (changes, area) => { + // console.log(`storage.${area}.onChanged`, changes); + const settings = {}; + for (const key in changes) { + if (!(key in settingsConfig)) continue; + settings[settingsConfig[key].name] = changes[key].newValue; + } + callback(settings, area); + }; + browser.storage.sync.onChanged.addListener(changes => { + listener(changes, "sync"); + }); + browser.storage.local.onChanged.addListener(changes => { + listener(changes, "local"); + }); +} + +export async function legacy_get(keys) { + const result = await get(keys); + // console.log("legacy_get", keys, result); + return Object.entries(result).reduce((p, c) => ( + p[settingsConfig[storageKey(c[0])].legacy || c[0]] = c[1], p + ), {}); +} + +export async function legacy_set(keys) { + if (typeof keys != "object") { + return console.error("Unexpected arg type:", keys); + } + if (!Object.keys(keys).length) { + return console.error("Settings object empty:", keys); + } + const settings = {}; + for (const obj of settingsDefine) { + if (!obj.legacy) continue; + if (obj.legacy in keys) { + settings[obj.name] = keys[obj.legacy]; + } + } + // console.log("legacy_set", keys, settings); + return await set(settings); +} + export async function import_legacy_data() { const imported = await get("legacy_imported"); if (imported) return console.info("Legacy settings has already imported"); @@ -494,7 +568,7 @@ export async function import_legacy_data() { } } if (!set(settings)) return console.error("Import legacy settings abort"); - set({"legacy_imported": true}); + set({"legacy_imported": Date.now()}); console.info("Import legacy settings complete"); // Send a message to the Swift layer to safely clean up legacy data browser.runtime.sendNativeMessage({name: "PAGE_LEGACY_IMPORTED"}); From 230163d46d8ac4f42c6ee370b13d2d7178215d6f Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sat, 22 Oct 2022 17:04:27 +0800 Subject: [PATCH 08/14] fix: restore bind initial value display --- src/page/Components/Settings.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/page/Components/Settings.svelte b/src/page/Components/Settings.svelte index b6b4d499..13f9ccfd 100644 --- a/src/page/Components/Settings.svelte +++ b/src/page/Components/Settings.svelte @@ -120,11 +120,11 @@ From 0107736486eaabe34cc4f4b04198df716498f9fb Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Fri, 28 Oct 2022 09:08:46 +0800 Subject: [PATCH 09/14] refactor: adjust structure and add comments --- src/page/store.js | 40 +++--- src/shared/settings.js | 280 +++++++++++++++++++++++++---------------- 2 files changed, 197 insertions(+), 123 deletions(-) diff --git a/src/page/store.js b/src/page/store.js index dfc3fd45..11a3d15c 100644 --- a/src/page/store.js +++ b/src/page/store.js @@ -77,6 +77,27 @@ export const state = stateStore(); function settingsStore() { const {subscribe, update, set} = writable({}); + const init = async () => { + // import legacy settings data just one-time + await settingsStorage.legacy_import(); + // for compatibility with legacy getting names only + // once all new name is used, use settingsStorage.get() + const settings = await settingsStorage.legacy_get(); + console.info("store.js settingsStore init", settings); + set(settings); + // sync popup, backgound, etc... settings changes + settingsStorage.onChanged((settings, area) => { + console.log(`store.js storage.${area}.onChanged`, settings); + update(obj => Object.assign(obj, settings)); + }); + }; + const reset = async keys => { + await settingsStorage.reset(keys); + // once all new name is used, use settingsStorage.get() + const settings = await settingsStorage.legacy_get(); + console.info("store.js settingsStore reset", settings); + set(settings); + }; const updateSingleSetting_old = (key, value) => { update(settings => { settings[key] = value; @@ -95,24 +116,13 @@ function settingsStore() { }; const updateSingleSetting = (key, value) => { update(settings => (settings[key] = value, settings)); - // For compatibility with legacy setting names only - // Once the new name is used, use settingsStorage.set() + // for compatibility with legacy setting names only + // once all new name is used, use settingsStorage.set() settingsStorage.legacy_set({[key]: value}); // Durable Storage - // Temporarily keep the old storage method until it is confirmed that all dependencies are removed + // temporarily keep the old storage method until it is confirmed that all dependencies are removed updateSingleSetting_old(key, value); }; - const init = async () => { - settingsStorage.import_legacy_data(); - const settings = await settingsStorage.legacy_get(); - // console.log("store.js settingsStore init", settings); - set(settings); - // sync popup, backgound, etc... settings changes - settingsStorage.onChanged((settings, area) => { - console.log(`storage.${area}.onChanged`, settings); - update(obj => Object.assign(obj, settings)); - }); - }; - return {subscribe, set, init, updateSingleSetting}; + return {subscribe, set, init, reset, updateSingleSetting}; } export const settings = settingsStore(); diff --git a/src/shared/settings.js b/src/shared/settings.js index 8d89b003..14936074 100644 --- a/src/shared/settings.js +++ b/src/shared/settings.js @@ -1,11 +1,20 @@ -function deepFreeze(object) { - for (const p in object) { - if (typeof object[p] == "object") { - deepFreeze(object[p]); - } +// wrap a relatively independent settings storage with its own functions + +const storagePrefix = "US_"; +const storageKey = key => storagePrefix + key.toUpperCase(); +const storageRef = async area => { // dynamic storage reference + browser.storage.sync.area = "sync"; + browser.storage.local.area = "local"; + if (area === "sync") return browser.storage.sync; + if (area === "local") return browser.storage.local; + const key = storageKey("settings_sync"); + const result = await browser.storage.local.get(key); + if (result?.[key] === true) { + return browser.storage.sync; + } else { + return browser.storage.local; } - return Object.freeze(object); -} +}; const settingDefault = deepFreeze({ name: "setting_default", @@ -13,8 +22,8 @@ const settingDefault = deepFreeze({ local: false, values: [], default: undefined, + protect: false, confirm: false, - writable: true, platforms: ["macos", "ipados", "ios"], langLabel: {}, langTitle: {}, @@ -24,12 +33,13 @@ const settingDefault = deepFreeze({ nodeClass: {} }); -const settingsDefine = [ +export const settingsDefine = deepFreeze([ { name: "legacy_imported", type: "number", local: true, default: 0, + protect: true, platforms: ["macos"], group: "Internal" }, @@ -46,6 +56,7 @@ const settingsDefine = [ type: "boolean", local: true, default: false, + protect: true, platforms: ["macos", "ipados", "ios"], langLabel: { en: "Sync settings", @@ -144,25 +155,6 @@ const settingsDefine = [ nodeType: "Toggle", nodeClass: {warn: true} }, - { - name: "location_path", - type: "string", - local: true, - default: true, - writable: false, - platforms: ["macos", "ipados", "ios"], - langLabel: { - en: "Save Location", - zh_hans: "脚本存储路径" - }, - langTitle: { - en: "where your file are currently located and being saved to (clic to open)", - zh_hans: "简体中文描述" - }, - group: "General", - legacy: "saveLocation", - nodeType: "IconButton" - }, { name: "global_exclude_match", type: "object", @@ -287,6 +279,7 @@ const settingsDefine = [ { name: "editor_tab_size", type: "number", + values: [2, 4], default: 4, platforms: ["macos"], langLabel: { @@ -301,48 +294,67 @@ const settingsDefine = [ legacy: "tabSize", nodeType: "select" } -]; - -const storagePrefix = "US_"; -const storageKey = key => storagePrefix + key.toUpperCase(); -const storageRef = async area => { // storage reference - browser.storage.sync.area = "sync"; - browser.storage.local.area = "local"; - if (area === "sync") return browser.storage.sync; - if (area === "local") return browser.storage.local; - const key = storageKey("settings_sync"); - const result = await browser.storage.local.get(key); - if (result?.[key] === true) { - return browser.storage.sync; - } else { - return browser.storage.local; - } -}; +].reduce(settingsDefineReduceCallback, {})); -export const settingsConfig = settingsDefine.reduce((p, c) => { - c.key = storageKey(c.name); - p[c.key] = Object.assign({}, settingDefault, c); - return p; -}, {}); +// populate the settingsDefine with settingDefault +// and convert settingsDefine to storageKey object +function settingsDefineReduceCallback(settings, setting) { + setting.key = storageKey(setting.name); + settings[setting.key] = Object.assign({}, settingDefault, setting); + return settings; +} -deepFreeze(settingsConfig); -deepFreeze(settingsDefine); +// prevent settings define from being modified in any case +// otherwise user settings may be lost in the worst case +function deepFreeze(object) { + for (const p in object) { + if (typeof object[p] == "object") { + deepFreeze(object[p]); + } + } + return Object.freeze(object); +} -export const settingsKeys = deepFreeze(Object.keys(settingsConfig)); +// export and define the operation method of settings storage +// they are similar to browser.storage but slightly different export async function get(keys, area) { if (![undefined, "local", "sync"].includes(area)) { - return console.error("unexpected storage area:", area); + return console.error("Unexpected storage area:", area); } + // validate setting value and fix surprises to default + 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 (typeof val != type) { + console.warn(`Unexpected ${key} value type '${typeof(val)}' should '${type}', fix to default`); + return def; + } + // check if value conforms to settingsDefine + const values = settingsDefine[key].values; + if (values.length && !values.includes(val)) { + console.warn(`Unexpected ${key} value '${val}' should one of '${values}', fix to default`); + return def; + } + // verified, pass original value + return val; + }; if (typeof keys == "string") { // single setting const key = storageKey(keys); - if (!settingsKeys.includes(key)) { + // check if key exist in settingsDefine + if (!Object.hasOwn(settingsDefine, key)) { return console.error("unexpected settings key:", key); } - settingsConfig[key].local === true && (area = "local"); + settingsDefine[key].local === true && (area = "local"); const storage = await storageRef(area); const result = await storage.get(key); - return result[key] ?? settingsConfig[key].default; + if (Object.hasOwn(result, key)) { + return valueFix(key, result[key]); + } else { + return settingsDefine[key].default; + } } const complexGet = async (settingsDefault, areaKeys) => { const storage = await storageRef(area); @@ -359,9 +371,9 @@ export async function get(keys, area) { local = await storage.get(areaKeys.all); } const result = Object.assign(settingsDefault, local, sync); - // reduce settings object property name + // revert settings object property name return Object.entries(result).reduce((p, c) => ( - p[settingsConfig[c[0]].name] = c[1], p + p[settingsDefine[c[0]].name] = valueFix(...c), p ), {}); }; if (Array.isArray(keys)) { // muilt settings @@ -372,13 +384,16 @@ export async function get(keys, area) { const areaKeys = {local: [], sync: [], all: []}; for (const k of keys) { const key = storageKey(k); - if (!settingsKeys.includes(key)) { + // check if key exist in settingsDefine + if (!Object.hasOwn(settingsDefine, key)) { return console.error("unexpected settings key:", key); } - settingsDefault[key] = settingsConfig[key].default; - settingsConfig[key].local === true + settingsDefault[key] = settingsDefine[key].default; + // detach only locally stored settings + settingsDefine[key].local === true ? areaKeys.local.push(key) : areaKeys.sync.push(key); + // record all keys in case sync storage is not enabled areaKeys.all.push(key); } return await complexGet(settingsDefault, areaKeys); @@ -386,16 +401,18 @@ export async function get(keys, area) { if (typeof keys == "undefined" || keys === null) { // all settings const settingsDefault = {}; const areaKeys = {local: [], sync: [], all: []}; - for (const key in settingsConfig) { - settingsDefault[key] = settingsConfig[key].default; - settingsConfig[key].local === true + for (const key in settingsDefine) { + settingsDefault[key] = settingsDefine[key].default; + // detach only locally stored settings + settingsDefine[key].local === true ? areaKeys.local.push(key) : areaKeys.sync.push(key); + // record all keys in case sync storage is not enabled areaKeys.all.push(key); } return await complexGet(settingsDefault, areaKeys); } - return console.error("Unexpected arg type:", keys); + return console.error("Unexpected keys type:", keys); } export async function set(keys, area) { @@ -403,7 +420,7 @@ export async function set(keys, area) { return console.error("unexpected storage area:", area); } if (typeof keys != "object") { - return console.error("Unexpected arg type:", keys); + return console.error("Unexpected keys type:", keys); } if (!Object.keys(keys).length) { return console.error("Settings object empty:", keys); @@ -411,19 +428,33 @@ export async function set(keys, area) { const areaKeys = {local: {}, sync: {}, all: {}}; for (const k in keys) { const key = storageKey(k); - if (!settingsKeys.includes(key)) { + // check if key exist in settingsDefine + if (!Object.hasOwn(settingsDefine, key)) { return console.error("Unexpected settings keys:", key); } - const type = settingsConfig[key].type; + // check if value type conforms to settingsDefine + const type = settingsDefine[key].type; if (typeof keys[k] != type) { - return console.error(`Unexpected value type ${typeof(value)} should ${type}`); + if (type === "number" && !isNaN(keys[k])) { // compatible with string numbers + keys[k] = Number(keys[k]); // still store it as a number type + } else { + return console.error(`Unexpected ${k} value type '${typeof(keys[k])}' should '${type}'`); + } + } + // check if value conforms to settingsDefine + const values = settingsDefine[key].values; + if (values.length && !values.includes(keys[k])) { + return console.error(`Unexpected ${k} value '${keys[k]}' should one of '${values}'`); } - settingsConfig[key].local === true + // detach only locally stored settings + settingsDefine[key].local === true ? areaKeys.local[key] = keys[k] : areaKeys.sync[key] = keys[k]; + // record all keys in case sync storage is not enabled areaKeys.all[key] = keys[k]; } const storage = await storageRef(area); + // complexSet try { if (storage.area === "sync") { if (Object.keys(areaKeys.sync).length) { @@ -448,10 +479,15 @@ export async function reset(keys, area) { // reset to default } if (typeof keys == "string") { // single setting const key = storageKey(keys); - if (!settingsKeys.includes(key)) { + // check if key exist in settingsDefine + if (!Object.hasOwn(settingsDefine, key)) { return console.error("unexpected settings key:", key); } - settingsConfig[key].local === true && (area = "local"); + // check if key is protected + if (settingsDefine[key].protect === true) { + return console.error("protected settings key:", key); + } + settingsDefine[key].local === true && (area = "local"); const storage = await storageRef(area); return storage.remove(key); } @@ -481,53 +517,75 @@ export async function reset(keys, area) { // reset to default const areaKeys = {local: [], sync: [], all: []}; for (const k of keys) { const key = storageKey(k); - if (!settingsKeys.includes(key)) { + // check if key exist in settingsDefine + if (!Object.hasOwn(settingsDefine, key)) { return console.error("unexpected settings key:", key); } - settingsConfig[key].local === true + // check if key is protected + if (settingsDefine[key].protect === true) { + return console.error("protected settings key:", key); + } + // detach only locally stored settings + settingsDefine[key].local === true ? areaKeys.local.push(key) : areaKeys.sync.push(key); + // record all keys in case sync storage is not enabled areaKeys.all.push(key); } return await complexRemove(areaKeys); } if (typeof keys == "undefined" || keys === null) { // all settings const areaKeys = {local: [], sync: [], all: []}; - for (const key in settingsConfig) { - settingsConfig[key].local === true + for (const key in settingsDefine) { + // skip protected keys + if (settingsDefine[key].protect === true) continue; + // detach only locally stored settings + settingsDefine[key].local === true ? areaKeys.local.push(key) : areaKeys.sync.push(key); + // record all keys in case sync storage is not enabled areaKeys.all.push(key); } return await complexRemove(areaKeys); } - return console.error("Unexpected arg type:", keys); + return console.error("Unexpected keys type:", keys); } -export function onChanged(callback) { - const listener = (changes, area) => { +// this function is convenient for the svelte store to update the state +export function onChanged(callback) { // complex onChanged + if (typeof callback != "function") { + return console.error("Unexpected callback:", callback); + } + console.info("storage onChanged addListener"); + const handle = (changes, area) => { // console.log(`storage.${area}.onChanged`, changes); - const settings = {}; - for (const key in changes) { - if (!(key in settingsConfig)) continue; - settings[settingsConfig[key].name] = changes[key].newValue; + try { + const settings = {}; + for (const key in changes) { + if (!Object.hasOwn(settingsDefine, key)) continue; + settings[settingsDefine[key].name] = changes[key].newValue; + } + callback(settings, area); + } catch (error) { + console.error("onChanged callback:", error); } - callback(settings, area); }; - browser.storage.sync.onChanged.addListener(changes => { - listener(changes, "sync"); - }); - browser.storage.local.onChanged.addListener(changes => { - listener(changes, "local"); - }); + browser.storage.sync.onChanged.addListener(c => handle(c, "sync")); + browser.storage.local.onChanged.addListener(c => handle(c, "local")); } +// the following functions are used only for compatibility transition periods +// they are deliberately named in snake_case format +// they will be deleted at some point in the future + export async function legacy_get(keys) { const result = await get(keys); // console.log("legacy_get", keys, result); - return Object.entries(result).reduce((p, c) => ( - p[settingsConfig[storageKey(c[0])].legacy || c[0]] = c[1], p - ), {}); + for (const key in result) { + const legacy = settingsDefine[storageKey(key)]?.legacy; + if (legacy) result[legacy] = result[key]; + } + return result; } export async function legacy_set(keys) { @@ -538,38 +596,44 @@ export async function legacy_set(keys) { return console.error("Settings object empty:", keys); } const settings = {}; - for (const obj of settingsDefine) { - if (!obj.legacy) continue; - if (obj.legacy in keys) { - settings[obj.name] = keys[obj.legacy]; + for (const key in settingsDefine) { + const setting = settingsDefine[key]; + if (!setting.legacy) continue; + if (setting.legacy in keys) { + settings[setting.name] = keys[setting.legacy]; } } // console.log("legacy_set", keys, settings); return await set(settings); } -export async function import_legacy_data() { +export async function legacy_import() { const imported = await get("legacy_imported"); if (imported) return console.info("Legacy settings has already imported"); const result = await browser.runtime.sendNativeMessage({name: "PAGE_INIT_DATA"}); if (result.error) return console.error(result.error); console.info("Import settings data from legacy manifest file"); const settings = {}; - for (const key in settingsConfig) { - const legacy = settingsConfig[key].legacy; + for (const key in settingsDefine) { + const legacy = settingsDefine[key].legacy; if (legacy in result) { let value = result[legacy]; - switch (settingsConfig[key].type) { + switch (settingsDefine[key].type) { case "boolean": value = JSON.parse(value); break; case "number": value = Number(value); break; } console.log(`Importing legacy setting: ${legacy}`, value); - settings[settingsConfig[key].name] = value; + settings[settingsDefine[key].name] = value; } } - if (!set(settings)) return console.error("Import legacy settings abort"); - set({"legacy_imported": Date.now()}); - console.info("Import legacy settings complete"); - // Send a message to the Swift layer to safely clean up legacy data - browser.runtime.sendNativeMessage({name: "PAGE_LEGACY_IMPORTED"}); + // import complete tag, to ensure will only be import once + Object.assign(settings, {"legacy_imported": Date.now()}); + if (await set(settings, "local")) { + console.info("Import legacy settings complete"); + // Send a message to the Swift layer to safely clean up legacy data + browser.runtime.sendNativeMessage({name: "PAGE_LEGACY_IMPORTED"}); + return true; + } else { + return console.error("Import legacy settings abort"); + } } \ No newline at end of file From 7fe319eee125f986c2562c009c92ef5bcbec9d65 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Fri, 28 Oct 2022 10:45:31 +0800 Subject: [PATCH 10/14] refactor: swift add legacy import api --- .../Userscripts Extension/Functions.swift | 13 +++++--- .../SafariWebExtensionHandler.swift | 13 ++++++-- src/page/App.svelte | 4 ++- src/page/store.js | 30 ++++++++----------- src/shared/settings.js | 2 +- 5 files changed, 37 insertions(+), 25 deletions(-) diff --git a/extension/Userscripts Extension/Functions.swift b/extension/Userscripts Extension/Functions.swift index adf7a688..c7f95fdf 100644 --- a/extension/Userscripts Extension/Functions.swift +++ b/extension/Userscripts Extension/Functions.swift @@ -1675,16 +1675,21 @@ func popupUpdateSingle(_ filename: String, _ url: String, _ subframeUrls: [Strin // page func getInitData() -> [String: Any]? { - let manifest = getManifest() guard let saveLocation = getSaveLocation() else { err("getInitData failed at (1)") return nil } + return [ + "saveLocation": saveLocation.path, + "version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "??", + "build": Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "??" + ] +} + +func getLegacyData() -> [String: Any]? { + let manifest = getManifest() var data:[String: Any] = manifest.settings data["blacklist"] = manifest.blacklist - data["saveLocation"] = saveLocation.path - data["version"] = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "??" - data["build"] = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "??" return data } diff --git a/extension/Userscripts Extension/SafariWebExtensionHandler.swift b/extension/Userscripts Extension/SafariWebExtensionHandler.swift index 0c3bf641..1e79cce8 100644 --- a/extension/Userscripts Extension/SafariWebExtensionHandler.swift +++ b/extension/Userscripts Extension/SafariWebExtensionHandler.swift @@ -188,13 +188,22 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { } else if name == "PAGE_INIT_DATA" { #if os(macOS) - if let settings = getInitData(), checkDefaultDirectories() { - response.userInfo = [SFExtensionMessageKey: settings] + if let initData = getInitData(), checkDefaultDirectories() { + response.userInfo = [SFExtensionMessageKey: initData] } else { response.userInfo = [SFExtensionMessageKey: ["error": "failed to get init data"]] } #endif } + else if name == "PAGE_LEGACY_IMPORT" { + #if os(macOS) + if let settings = getLegacyData() { + response.userInfo = [SFExtensionMessageKey: settings] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to get legacy data"]] + } + #endif + } else if name == "PAGE_ALL_FILES" { #if os(macOS) if let files = getAllFiles() { diff --git a/src/page/App.svelte b/src/page/App.svelte index 9ca68949..ea6607ff 100644 --- a/src/page/App.svelte +++ b/src/page/App.svelte @@ -41,7 +41,9 @@ onMount(async () => { log.add("Requesting initialization data", "info", false); - await settings.init(); + const initData = await browser.runtime.sendNativeMessage({name: "PAGE_INIT_DATA"}); + if (initData.error) return console.error(initData.error); + await settings.init(initData); state.add("items-loading"); state.remove("init"); diff --git a/src/page/store.js b/src/page/store.js index 11a3d15c..442d99c2 100644 --- a/src/page/store.js +++ b/src/page/store.js @@ -77,14 +77,14 @@ export const state = stateStore(); function settingsStore() { const {subscribe, update, set} = writable({}); - const init = async () => { + const init = async initData => { // import legacy settings data just one-time await settingsStorage.legacy_import(); // for compatibility with legacy getting names only // once all new name is used, use settingsStorage.get() const settings = await settingsStorage.legacy_get(); - console.info("store.js settingsStore init", settings); - set(settings); + console.info("store.js settingsStore init", initData, settings); + set(Object.assign({}, initData, settings)); // sync popup, backgound, etc... settings changes settingsStorage.onChanged((settings, area) => { console.log(`store.js storage.${area}.onChanged`, settings); @@ -99,20 +99,16 @@ function settingsStore() { set(settings); }; const updateSingleSetting_old = (key, value) => { - update(settings => { - settings[key] = value; - // blacklist not stored in normal setting object in manifest, so handle differently - if (key === "blacklist") { - // update blacklist on swift side - const message = {name: "PAGE_UPDATE_BLACKLIST", blacklist: value}; - browser.runtime.sendNativeMessage(message, response => { - if (response.error) { - log.add("Failed to save blacklist to disk", "error", true); - } - }); - } - return settings; - }); + // blacklist not stored in normal setting object in manifest, so handle differently + if (key === "blacklist") { + // update blacklist on swift side + const message = {name: "PAGE_UPDATE_BLACKLIST", blacklist: value}; + browser.runtime.sendNativeMessage(message, response => { + if (response.error) { + log.add("Failed to save blacklist to disk", "error", true); + } + }); + } }; const updateSingleSetting = (key, value) => { update(settings => (settings[key] = value, settings)); diff --git a/src/shared/settings.js b/src/shared/settings.js index 14936074..17ce7fd7 100644 --- a/src/shared/settings.js +++ b/src/shared/settings.js @@ -610,7 +610,7 @@ export async function legacy_set(keys) { export async function legacy_import() { const imported = await get("legacy_imported"); if (imported) return console.info("Legacy settings has already imported"); - const result = await browser.runtime.sendNativeMessage({name: "PAGE_INIT_DATA"}); + const result = await browser.runtime.sendNativeMessage({name: "PAGE_LEGACY_IMPORT"}); if (result.error) return console.error(result.error); console.info("Import settings data from legacy manifest file"); const settings = {}; From 90cec5bb8bef8bce2c2125c3db8addd3a6cf80c6 Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Fri, 4 Nov 2022 06:01:17 +0800 Subject: [PATCH 11/14] refactor: bypass sync storage related methods --- .../Userscripts Extension/Functions.swift | 6 - src/page/store.js | 2 +- src/shared/dev.js | 15 ++- src/shared/settings.js | 118 ++++++++++++------ 4 files changed, 91 insertions(+), 50 deletions(-) diff --git a/extension/Userscripts Extension/Functions.swift b/extension/Userscripts Extension/Functions.swift index c7f95fdf..b2c04f1c 100644 --- a/extension/Userscripts Extension/Functions.swift +++ b/extension/Userscripts Extension/Functions.swift @@ -1950,11 +1950,6 @@ func popupInit() -> [String: String]? { err("Failed to updateDeclarativeNetRequest in popupInit") return nil } - let manifest = getManifest() - guard let active = manifest.settings["active"] else { - err("Failed at getManifest active in popupInit") - return nil - } // pass some info in response guard let saveLocation = getSaveLocation() else { err("Failed at getSaveLocation in popupInit") @@ -1964,7 +1959,6 @@ func popupInit() -> [String: String]? { let requireLocation = getRequireLocation() return [ - "active": active, "saveLocation": saveLocation.absoluteString, "documentsDirectory": documentsDirectory.absoluteString, "requireLocation": requireLocation.absoluteString diff --git a/src/page/store.js b/src/page/store.js index 442d99c2..790704e3 100644 --- a/src/page/store.js +++ b/src/page/store.js @@ -96,7 +96,7 @@ function settingsStore() { // once all new name is used, use settingsStorage.get() const settings = await settingsStorage.legacy_get(); console.info("store.js settingsStore reset", settings); - set(settings); + update(obj => Object.assign(obj, settings)); }; const updateSingleSetting_old = (key, value) => { // blacklist not stored in normal setting object in manifest, so handle differently diff --git a/src/shared/dev.js b/src/shared/dev.js index 1097378f..86ade13b 100644 --- a/src/shared/dev.js +++ b/src/shared/dev.js @@ -85,6 +85,12 @@ const _browser = { console.info(`Got message: ${name}`); let response = {}; if (name === "PAGE_INIT_DATA") { + response = { + saveLocation: "/Users/someone/Library/Directory", + version: "4.0.0", + build: "44" + }; + } else if (name === "PAGE_LEGACY_IMPORT") { response = { active: "true", autoCloseBrackets: "true", @@ -94,13 +100,10 @@ const _browser = { languageCode: "en", lint: "true", log: "false", - saveLocation: "/Users/someone/Library/Directory", showCount: "true", showInvisibles: "true", sortOrder: "lastModifiedDesc", - tabSize: "4", - version: "4.0.0", - build: "44" + tabSize: "4" }; } else if (name === "PAGE_ALL_FILES") { response = []; @@ -416,6 +419,10 @@ const _browser = { }, set() { return new Promise(resolve => setTimeout(() => resolve(), _browser.delay)); + }, + remove: () => new Promise(resolve => setTimeout(() => resolve(), _browser.delay)), + onChanged: { + addListener: () => undefined } } } diff --git a/src/shared/settings.js b/src/shared/settings.js index 17ce7fd7..fb16c559 100644 --- a/src/shared/settings.js +++ b/src/shared/settings.js @@ -2,18 +2,25 @@ const storagePrefix = "US_"; const storageKey = key => storagePrefix + key.toUpperCase(); -const storageRef = async area => { // dynamic storage reference - browser.storage.sync.area = "sync"; +// const storageRef = async area => { // dynamic storage reference +// browser.storage.sync.area = "sync"; +// browser.storage.local.area = "local"; +// if (area === "sync") return browser.storage.sync; +// if (area === "local") return browser.storage.local; +// const key = storageKey("settings_sync"); +// const result = await browser.storage.local.get(key); +// if (result?.[key] === true) { +// return browser.storage.sync; +// } else { +// return browser.storage.local; +// } +// }; + +// 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 +const storageRef = async () => { browser.storage.local.area = "local"; - if (area === "sync") return browser.storage.sync; - if (area === "local") return browser.storage.local; - const key = storageKey("settings_sync"); - const result = await browser.storage.local.get(key); - if (result?.[key] === true) { - return browser.storage.sync; - } else { - return browser.storage.local; - } + return browser.storage.local; }; const settingDefault = deepFreeze({ @@ -52,21 +59,54 @@ export const settingsDefine = deepFreeze([ legacy: "languageCode" }, { - name: "settings_sync", - type: "boolean", - local: true, - default: false, - protect: true, + name: "scripts_settings", + type: "object", + default: {}, platforms: ["macos", "ipados", "ios"], langLabel: { - en: "Sync settings", - zh_hans: "同步设置" + 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: "Sync settings across devices", - zh_hans: "跨设备同步设置" + 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" }, { @@ -88,23 +128,6 @@ export const settingsDefine = deepFreeze([ nodeType: "Toggle", nodeClass: {red: false} }, - { - 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_scripts_update_check", type: "boolean", @@ -132,11 +155,27 @@ export const settingsDefine = deepFreeze([ }, langTitle: { en: "The interval for script update check in background", - zh_hans: "后台脚本更新检查的间隔时间" + 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", + nodeType: "Toggle" + }, { name: "scripts_auto_update", type: "boolean", @@ -570,7 +609,8 @@ export function onChanged(callback) { // complex onChanged console.error("onChanged callback:", error); } }; - browser.storage.sync.onChanged.addListener(c => handle(c, "sync")); + // comment for now same reason as storageRef + // browser.storage.sync.onChanged.addListener(c => handle(c, "sync")); browser.storage.local.onChanged.addListener(c => handle(c, "local")); } From e946398381169cd919aba5f6b1d5ce4af0e9c6bc Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Fri, 4 Nov 2022 06:55:46 +0800 Subject: [PATCH 12/14] chore: content.js conflict resolution --- .../Resources/content.js | 178 +++++++++--------- 1 file changed, 93 insertions(+), 85 deletions(-) diff --git a/extension/Userscripts Extension/Resources/content.js b/extension/Userscripts Extension/Resources/content.js index 7747b92c..8441aabb 100644 --- a/extension/Userscripts Extension/Resources/content.js +++ b/extension/Userscripts Extension/Resources/content.js @@ -241,7 +241,6 @@ function processJS(userscript) { function wrapCode(preCode, code, filename) { return ` (function() { - "use strict"; ${preCode} (function() { const US_filename = "${filename}"; @@ -323,93 +322,90 @@ function cspFallback(e) { } } -// should import settings.js and use `settingsStorage.get("global_active")` -browser.storage.local.get("US_GLOBAL_ACTIVE").then(CONFIG => { - if (CONFIG?.US_GLOBAL_ACTIVE === false) return console.info("Userscripts off"); - browser.runtime.sendMessage({name: "REQ_USERSCRIPTS"}, response => { - // cancel injection if errors detected - if (!response || response.error) { - console.error(response?.error || "REQ_USERSCRIPTS returned undefined"); - return; +const injection = () => browser.runtime.sendMessage({name: "REQ_USERSCRIPTS"}, response => { + // cancel injection if errors detected + if (!response || response.error) { + console.error(response?.error || "REQ_USERSCRIPTS returned undefined"); + return; + } + // save response locally in case CSP events occur + data = response; + // combine regular and context-menu scripts + const scripts = [...data.files.js, ...data.files.menu]; + // loop through each userscript and prepare for processing + for (let i = 0; i < scripts.length; i++) { + const userscript = scripts[i]; + userscript.preCode = ""; + // pass references to the api methods as needed + const gmMethods = []; + const filename = userscript.scriptObject.filename; + const grants = userscript.scriptObject.grant; + const injectInto = userscript.scriptObject["inject-into"]; + // create GM.info object + const scriptData = { + "script": userscript.scriptObject, + "scriptHandler": data.scriptHandler, + "scriptHandlerVersion": data.scriptHandlerVersion, + "scriptMetaStr": userscript.scriptMetaStr + }; + // all userscripts get access to GM.info + gmMethods.push("info: GM_info"); + // if @grant explicitly set to none, empty grants array + if (grants.includes("none")) grants.length = 0; + // @grant values exist for page scoped userscript + if (grants.length && injectInto === "page") { + // remove grants + grants.length = 0; + // log warning + console.warn(`${filename} @grant values removed due to @inject-into value: ${injectInto} - https://github.com/quoid/userscripts/issues/265#issuecomment-1213462394`); } - // save response locally in case CSP events occur - data = response; - // loop through each userscript and prepare for processing - for (let i = 0; i < data.files.js.length; i++) { - const userscript = data.files.js[i]; - userscript.preCode = ""; - // pass references to the api methods as needed - const gmMethods = []; - const filename = userscript.scriptObject.filename; - const grants = userscript.scriptObject.grant; - const injectInto = userscript.scriptObject["inject-into"]; - // create GM.info object - const scriptData = { - "script": userscript.scriptObject, - "scriptHandler": data.scriptHandler, - "scriptHandlerVersion": data.scriptHandlerVersion, - "scriptMetaStr": userscript.scriptMetaStr - }; - // all userscripts get access to GM.info - gmMethods.push("info: GM_info"); - // if @grant explicitly set to none, empty grants array - if (grants.includes("none")) grants.length = 0; - // @grant values exist for page scoped userscript - if (grants.length && injectInto === "page") { - // remove grants - grants.length = 0; - // log warning - console.warn(`${filename} @grant values removed due to @inject-into value: ${injectInto} - https://github.com/quoid/userscripts/issues/265#issuecomment-1213462394`); - } - // @grant exist for auto scoped userscript - if (grants.length && injectInto === "auto") { - // change scope - userscript.scriptObject["inject-into"] = "content"; - // log warning - console.warn(`${filename} @inject-into value set to 'content' due to @grant values: ${grants} - https://github.com/quoid/userscripts/issues/265#issuecomment-1213462394`); - } - // loop through each userscript @grant value, add methods as needed - for (let j = 0; j < grants.length; j++) { - const grant = grants[j]; - const method = grant.split(".")[1] || grant.split(".")[0]; - // ensure API method exists in apis object - if (!Object.keys(apis).includes(method)) continue; - // create the method string to be pushed to methods array - let methodStr = `${method}: apis.${method}`; - // add require variables to specific methods - switch (method) { - case "getValue": - case "setValue": - case "deleteValue": - case "listValues": - methodStr += `.bind({"US_filename": "${filename}"})`; - break; - case "info": - case "GM_info": - continue; - case "xmlHttpRequest": - gmMethods.push("xmlHttpRequest: apis.xhr"); - continue; - case "GM_xmlhttpRequest": - userscript.preCode += "const GM_xmlhttpRequest = apis.xhr;"; - continue; - } - gmMethods.push(methodStr); - } - // add GM.info - userscript.preCode += `const GM_info = ${JSON.stringify(scriptData)};`; - // add other included GM API methods - userscript.preCode += `const GM = {${gmMethods.join(",")}};`; - // process file for injection - processJS(userscript); + // @grant exist for auto scoped userscript + if (grants.length && injectInto === "auto") { + // change scope + userscript.scriptObject["inject-into"] = "content"; + // log warning + console.warn(`${filename} @inject-into value set to 'content' due to @grant values: ${grants} - https://github.com/quoid/userscripts/issues/265#issuecomment-1213462394`); } - for (let i = 0; i < data.files.css.length; i++) { - const userstyle = data.files.css[i]; - injectCSS(userstyle.name, userstyle.code); + // loop through each userscript @grant value, add methods as needed + for (let j = 0; j < grants.length; j++) { + const grant = grants[j]; + const method = grant.split(".")[1] || grant.split(".")[0]; + // ensure API method exists in apis object + if (!Object.keys(apis).includes(method)) continue; + // create the method string to be pushed to methods array + let methodStr = `${method}: apis.${method}`; + // add require variables to specific methods + switch (method) { + case "getValue": + case "setValue": + case "deleteValue": + case "listValues": + methodStr += `.bind({"US_filename": "${filename}"})`; + break; + case "info": + case "GM_info": + continue; + case "xmlHttpRequest": + gmMethods.push("xmlHttpRequest: apis.xhr"); + continue; + case "GM_xmlhttpRequest": + userscript.preCode += "const GM_xmlhttpRequest = apis.xhr;"; + continue; + } + gmMethods.push(methodStr); } - }); + // add GM.info + userscript.preCode += `const GM_info = ${JSON.stringify(scriptData)};`; + // add other included GM API methods + userscript.preCode += `const GM = {${gmMethods.join(",")}};`; + // process file for injection + processJS(userscript); + } + for (let i = 0; i < data.files.css.length; i++) { + const userstyle = data.files.css[i]; + injectCSS(userstyle.name, userstyle.code); + } }); - // listens for messages from background, popup, etc... browser.runtime.onMessage.addListener((request, sender, sendResponse) => { const name = request.name; @@ -456,7 +452,13 @@ browser.runtime.onMessage.addListener((request, sender, sendResponse) => { const item = data.files.menu[i]; if (item.scriptObject.filename === filename) { console.info(`Injecting ${filename} %c(js)`, "color: #fff600"); - sendResponse({code: item.code}); + sendResponse({ + code: wrapCode( + item.preCode, + item.code, + filename + ) + }); return; } } @@ -465,3 +467,9 @@ browser.runtime.onMessage.addListener((request, sender, sendResponse) => { }); // listen for CSP violations document.addEventListener("securitypolicyviolation", cspFallback); + +// should import settings.js and use `settingsStorage.get("global_active")` +browser.storage.local.get("US_GLOBAL_ACTIVE").then(results => { + if (results?.US_GLOBAL_ACTIVE === false) return console.info("Userscripts off"); + injection(); +}); \ No newline at end of file From 17c0557105c6d962311df4ec7d201f08a4ab47bc Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sat, 5 Nov 2022 12:23:27 +0800 Subject: [PATCH 13/14] chore: make some comments clearer --- src/shared/settings.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/shared/settings.js b/src/shared/settings.js index fb16c559..7024da75 100644 --- a/src/shared/settings.js +++ b/src/shared/settings.js @@ -380,12 +380,13 @@ export async function get(keys, area) { // verified, pass original value return val; }; - if (typeof keys == "string") { // single setting + if (typeof keys == "string") { // [single setting] const key = storageKey(keys); // check if key exist in settingsDefine if (!Object.hasOwn(settingsDefine, key)) { return console.error("unexpected settings key:", key); } + // check if only locally stored setting settingsDefine[key].local === true && (area = "local"); const storage = await storageRef(area); const result = await storage.get(key); @@ -415,7 +416,7 @@ export async function get(keys, area) { p[settingsDefine[c[0]].name] = valueFix(...c), p ), {}); }; - if (Array.isArray(keys)) { // muilt settings + if (Array.isArray(keys)) { // [muilt settings] if (!keys.length) { return console.error("Settings keys empty:", keys); } @@ -437,7 +438,7 @@ export async function get(keys, area) { } return await complexGet(settingsDefault, areaKeys); } - if (typeof keys == "undefined" || keys === null) { // all settings + if (typeof keys == "undefined" || keys === null) { // [all settings] const settingsDefault = {}; const areaKeys = {local: [], sync: [], all: []}; for (const key in settingsDefine) { @@ -516,7 +517,7 @@ export async function reset(keys, area) { // reset to default if (![undefined, "local", "sync"].includes(area)) { return console.error("unexpected storage area:", area); } - if (typeof keys == "string") { // single setting + if (typeof keys == "string") { // [single setting] const key = storageKey(keys); // check if key exist in settingsDefine if (!Object.hasOwn(settingsDefine, key)) { @@ -549,7 +550,7 @@ export async function reset(keys, area) { // reset to default return console.error(error); } }; - if (Array.isArray(keys)) { // muilt settings + if (Array.isArray(keys)) { // [muilt settings] if (!keys.length) { return console.error("Settings keys empty:", keys); } @@ -573,7 +574,7 @@ export async function reset(keys, area) { // reset to default } return await complexRemove(areaKeys); } - if (typeof keys == "undefined" || keys === null) { // all settings + if (typeof keys == "undefined" || keys === null) { // [all settings] const areaKeys = {local: [], sync: [], all: []}; for (const key in settingsDefine) { // skip protected keys @@ -609,7 +610,7 @@ export function onChanged(callback) { // complex onChanged console.error("onChanged callback:", error); } }; - // comment for now same reason as storageRef + // comment for now same reason as `storageRef` function // browser.storage.sync.onChanged.addListener(c => handle(c, "sync")); browser.storage.local.onChanged.addListener(c => handle(c, "local")); } @@ -648,8 +649,10 @@ export async function legacy_set(keys) { } export async function legacy_import() { + // if legacy data has already been imported, skip this process const imported = await get("legacy_imported"); if (imported) return console.info("Legacy settings has already imported"); + // start the one-time import process const result = await browser.runtime.sendNativeMessage({name: "PAGE_LEGACY_IMPORT"}); if (result.error) return console.error(result.error); console.info("Import settings data from legacy manifest file"); @@ -670,8 +673,8 @@ export async function legacy_import() { Object.assign(settings, {"legacy_imported": Date.now()}); if (await set(settings, "local")) { console.info("Import legacy settings complete"); - // Send a message to the Swift layer to safely clean up legacy data - browser.runtime.sendNativeMessage({name: "PAGE_LEGACY_IMPORTED"}); + // send a message to the Swift layer to safely clean up legacy data + // browser.runtime.sendNativeMessage({name: "PAGE_LEGACY_IMPORTED"}); return true; } else { return console.error("Import legacy settings abort"); From 87c64d41768e212d6317a43655e97d849fe2657c Mon Sep 17 00:00:00 2001 From: ACTCD <101378590+ACTCD@users.noreply.github.com> Date: Sun, 6 Nov 2022 08:30:17 +0800 Subject: [PATCH 14/14] fix: revert one change of #341 Co-authored-by: quoid --- src/page/Components/Settings.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/page/Components/Settings.svelte b/src/page/Components/Settings.svelte index 13f9ccfd..a8933271 100644 --- a/src/page/Components/Settings.svelte +++ b/src/page/Components/Settings.svelte @@ -68,7 +68,7 @@
state.remove("settings")} + on:click|self={() => state.remove("settings")} in:fade={{duration: 150}} out:fade={{duration: 150, delay: 75}} >