From 39a6d1909f4fa7af53c7f5c00dbd5b8c2e083df9 Mon Sep 17 00:00:00 2001 From: eight Date: Fri, 28 Sep 2018 00:26:29 +0800 Subject: [PATCH 01/18] Fix: prefs doesn't work in FF's private windows. Add web-ext. Drop prefs.readOnlyValues --- background/background.js | 8 ++++---- background/util.js | 5 +++++ js/messaging.js | 2 +- js/prefs.js | 13 +++---------- manage/filters.js | 2 +- manage/manage.js | 4 ++-- manifest.json | 1 + options/options.js | 2 +- package.json | 6 ++++-- 9 files changed, 22 insertions(+), 21 deletions(-) create mode 100644 background/util.js diff --git a/background/background.js b/background/background.js index 80f3f13a..98043d26 100644 --- a/background/background.js +++ b/background/background.js @@ -202,7 +202,7 @@ if (chrome.contextMenus) { } item = Object.assign({id}, item); delete item.presentIf; - const prefValue = prefs.readOnlyValues[id]; + const prefValue = prefs.get(id); item.title = chrome.i18n.getMessage(item.title); if (!item.type && typeof prefValue === 'boolean') { item.type = 'checkbox'; @@ -230,7 +230,7 @@ if (chrome.contextMenus) { }; const keys = Object.keys(contextMenus); - prefs.subscribe(keys.filter(id => typeof prefs.readOnlyValues[id] === 'boolean'), toggleCheckmark); + prefs.subscribe(keys.filter(id => typeof prefs.get(id) === 'boolean'), toggleCheckmark); prefs.subscribe(keys.filter(id => contextMenus[id].presentIf), togglePresence); createContextMenus(keys); } @@ -309,7 +309,7 @@ window.addEventListener('storageReady', function _() { window.API_METHODS.getStylesForFrame = enabled ? getStylesForFrame : getStyles; }; prefs.subscribe(['exposeIframes'], updateAPI); - updateAPI(null, prefs.readOnlyValues.exposeIframes); + updateAPI(null, prefs.get('exposeIframes')); } // ************************************************************************* @@ -317,7 +317,7 @@ window.addEventListener('storageReady', function _() { function webNavigationListener(method, {url, tabId, frameId}) { Promise.all([ getStyles({matchUrl: url, asHash: true}), - frameId && prefs.readOnlyValues.exposeIframes && getTab(tabId), + frameId && prefs.get('exposeIframes') && getTab(tabId), ]).then(([styles, tab]) => { if (method && URLS.supported(url) && tabId >= 0) { if (method === 'styleApply') { diff --git a/background/util.js b/background/util.js new file mode 100644 index 00000000..7b85af96 --- /dev/null +++ b/background/util.js @@ -0,0 +1,5 @@ +'use strict'; + +if (typeof localStorage === 'object' && localStorage) { + localStorage._BG_ACCESS = 1; +} diff --git a/js/messaging.js b/js/messaging.js index dcbc1dd7..b4b432de 100644 --- a/js/messaging.js +++ b/js/messaging.js @@ -97,7 +97,7 @@ if (!BG || BG !== window) { BG.API_METHODS = {}; } -const FIREFOX_NO_DOM_STORAGE = FIREFOX && !tryCatch(() => localStorage); +const FIREFOX_NO_DOM_STORAGE = FIREFOX && !tryCatch(() => localStorage && localStorage._BG_ACCESS); if (FIREFOX_NO_DOM_STORAGE) { // may be disabled via dom.storage.enabled Object.defineProperty(window, 'localStorage', {value: {}}); diff --git a/js/prefs.js b/js/prefs.js index b89da994..e92f28dd 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -115,8 +115,7 @@ var prefs = new function Prefs() { let broadcastPrefs = {}; Object.defineProperties(this, { - defaults: {value: deepCopy(defaults)}, - readOnlyValues: {value: {}}, + defaults: {value: deepCopy(defaults)} }); Object.assign(Prefs.prototype, { @@ -154,7 +153,6 @@ var prefs = new function Prefs() { break; } values[key] = value; - defineReadonlyProperty(this.readOnlyValues, key, value); const hasChanged = !equal(value, oldValue); if (!fromBroadcast || FIREFOX_NO_DOM_STORAGE) { localStorage[key] = typeof defaults[key] === 'object' @@ -231,13 +229,9 @@ var prefs = new function Prefs() { { const importFromBG = () => API.getPrefs().then(prefs => { - const props = {}; for (const id in prefs) { - const value = prefs[id]; - values[id] = value; - props[id] = {value: deepCopy(value)}; + this.set(id, prefs[id], {fromBroadcast: true}); } - Object.defineProperties(this.readOnlyValues, props); }); // Unlike chrome.storage or messaging, HTML5 localStorage is synchronous and always ready, // so we'll mirror the prefs to avoid using the wrong defaults during the startup phase @@ -270,7 +264,6 @@ var prefs = new function Prefs() { this.set(key, value, {broadcast: false, sync: false}); } else { values[key] = value; - defineReadonlyProperty(this.readOnlyValues, key, value); } } return Promise.resolve(); @@ -404,7 +397,7 @@ var prefs = new function Prefs() { // Accepts an array of pref names (values are fetched via prefs.get) // and establishes a two-way connection between the document elements and the actual prefs function setupLivePrefs( - IDs = Object.getOwnPropertyNames(prefs.readOnlyValues) + IDs = Object.getOwnPropertyNames(prefs.defaults) .filter(id => $('#' + id)) ) { const checkedProps = {}; diff --git a/manage/filters.js b/manage/filters.js index 24bb8e7b..c1619687 100644 --- a/manage/filters.js +++ b/manage/filters.js @@ -114,7 +114,7 @@ onDOMready().then(() => { } if (value !== undefined) { el.lastValue = value; - if (el.id in prefs.readOnlyValues) { + if (el.id in prefs.defaults) { prefs.set(el.id, false); } } diff --git a/manage/manage.js b/manage/manage.js index 01b005f8..9160d18d 100644 --- a/manage/manage.js +++ b/manage/manage.js @@ -688,8 +688,8 @@ function highlightEditedStyle() { function usePrefsDuringPageLoad() { - for (const id of Object.getOwnPropertyNames(prefs.readOnlyValues)) { - const value = prefs.readOnlyValues[id]; + for (const id of Object.getOwnPropertyNames(prefs.defaults)) { + const value = prefs.get(id); if (value !== true) continue; const el = document.getElementById(id) || id.includes('expanded') && $(`details[data-pref="${id}"]`); diff --git a/manifest.json b/manifest.json index 2a6a3ad2..055e4533 100644 --- a/manifest.json +++ b/manifest.json @@ -23,6 +23,7 @@ ], "background": { "scripts": [ + "background/util.js", "js/messaging.js", "js/storage-util.js", "js/sections-equal.js", diff --git a/options/options.js b/options/options.js index fba0903c..6cbe0054 100644 --- a/options/options.js +++ b/options/options.js @@ -57,7 +57,7 @@ document.onclick = e => { case 'reset': $$('input') - .filter(input => input.id in prefs.readOnlyValues) + .filter(input => input.id in prefs.defaults) .forEach(input => prefs.reset(input.id)); break; diff --git a/package.json b/package.json index 8ebd7796..07a08b7b 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "semver-bundle": "^0.1.1", "stylelint-bundle": "^8.0.0", "stylus-lang-bundle": "^0.54.5", - "updates": "^4.2.1" + "updates": "^4.2.1", + "web-ext": "^2.9.1" }, "scripts": { "lint": "eslint **/*.js || true", @@ -27,6 +28,7 @@ "update-node": "updates -u && node tools/remove-modules.js && npm install", "update-codemirror": "node tools/update-libraries.js && node tools/update-codemirror-themes.js", "update-versions": "node tools/update-versions", - "zip": "npm run update-versions && node tools/zip.js" + "zip": "npm run update-versions && node tools/zip.js", + "start-firefox": "web-ext run" } } From e4135ce35de2b264d7560b37ca2344f6f206db59 Mon Sep 17 00:00:00 2001 From: eight Date: Fri, 28 Sep 2018 11:57:34 +0800 Subject: [PATCH 02/18] Fix: remove unused function --- js/prefs.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/js/prefs.js b/js/prefs.js index e92f28dd..638b79eb 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -354,14 +354,6 @@ var prefs = new function Prefs() { if (Number(storage['editor.lintReportDelay']) === 4500) delete storage['editor.lintReportDelay']; } - function defineReadonlyProperty(obj, key, value) { - const copy = deepCopy(value); - if (typeof copy === 'object') { - Object.freeze(copy); - } - Object.defineProperty(obj, key, {value: copy, configurable: true}); - } - function equal(a, b) { if (!a || !b || typeof a !== 'object' || typeof b !== 'object') { return a === b; From 282bdf77067a75f2959082152e913a84414a0625 Mon Sep 17 00:00:00 2001 From: eight Date: Wed, 3 Oct 2018 20:24:06 +0800 Subject: [PATCH 03/18] Fix: numbers are not compared correctly --- js/prefs.js | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/js/prefs.js b/js/prefs.js index 638b79eb..f3862b89 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -392,17 +392,15 @@ function setupLivePrefs( IDs = Object.getOwnPropertyNames(prefs.defaults) .filter(id => $('#' + id)) ) { - const checkedProps = {}; for (const id of IDs) { const element = $('#' + id); - checkedProps[id] = element.type === 'checkbox' ? 'checked' : 'value'; updateElement({id, element, force: true}); element.addEventListener('change', onChange); } prefs.subscribe(IDs, (id, value) => updateElement({id, value})); function onChange() { - const value = this[checkedProps[this.id]]; + const value = getInputValue(this); if (prefs.get(this.id) !== value) { prefs.set(this.id, value); } @@ -417,10 +415,25 @@ function setupLivePrefs( prefs.unsubscribe(IDs, updateElement); return; } - const prop = checkedProps[id]; - if (force || element[prop] !== value) { - element[prop] = value; - element.dispatchEvent(new Event('change', {bubbles: true, cancelable: true})); + setInputValue(element, value, force); + } + function getInputValue(input) { + if (input.type === 'checkbox') { + return input.checked; + } + if (input.type === 'number') { + return Number(input.value); + } + return input.value; + } + function setInputValue(input, value, force = false) { + if (force || getInputValue(input) !== value) { + if (input.type === 'checkbox') { + input.checked = value; + } else { + input.value = value; + } + input.dispatchEvent(new Event('change', {bubbles: true, cancelable: true})); } } } From 874a2da33e42f32d02f9d5e61ddbb3e6b1b0044e Mon Sep 17 00:00:00 2001 From: eight Date: Thu, 4 Oct 2018 17:04:23 +0800 Subject: [PATCH 04/18] Enhance: make prefs use storage.sync --- background/background.js | 15 +++ js/prefs.js | 253 +++++++++++---------------------------- 2 files changed, 86 insertions(+), 182 deletions(-) diff --git a/background/background.js b/background/background.js index 98043d26..840f7b8b 100644 --- a/background/background.js +++ b/background/background.js @@ -312,6 +312,21 @@ window.addEventListener('storageReady', function _() { updateAPI(null, prefs.get('exposeIframes')); } +// register hotkeys +if (FIREFOX && browser.commands && browser.commands.update) { + const hotkeyPrefs = Object.keys(prefs.defaults).filter(k => k.startsWith('hotkey.')); + this.subscribe(hotkeyPrefs, (name, value) => { + try { + name = name.split('.')[1]; + if (value.trim()) { + browser.commands.update({name, shortcut: value}).catch(ignoreChromeError); + } else { + browser.commands.reset(name).catch(ignoreChromeError); + } + } catch (e) {} + }); +} + // ************************************************************************* function webNavigationListener(method, {url, tabId, frameId}) { diff --git a/js/prefs.js b/js/prefs.js index f3862b89..889d5825 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -2,7 +2,7 @@ 'use strict'; // eslint-disable-next-line no-var -var prefs = new function Prefs() { +var prefs = (() => { const defaults = { 'openEditInWindow': false, // new editor opens in a own browser window 'windowPosition': {}, // detached window position @@ -98,28 +98,27 @@ var prefs = new function Prefs() { }; const values = deepCopy(defaults); - const affectsIcon = [ - 'show-badge', - 'disableAll', - 'badgeDisabled', - 'badgeNormal', - 'iconset', - ]; - const onChange = { any: new Set(), specific: new Map(), }; - // coalesce multiple pref changes in broadcast - let broadcastPrefs = {}; + const initializing = promisify(chrome.storage.sync.get.bind(chrome.storage.sync))('settings') + .then(result => setAll(result.settings, true)); - Object.defineProperties(this, { - defaults: {value: deepCopy(defaults)} + chrome.storage.onChanged.addListener((changes, area) => { + if (area !== 'sync' || !changes.settings || !changes.settings.newValue) { + return; + } + initializing.then(() => setAll(changes.settings.newValue, true)); }); - Object.assign(Prefs.prototype, { + // coalesce multiple pref changes in broadcast + // let changes = {}; + return { + initializing, + defaults, get(key, defaultValue) { if (key in values) { return values[key]; @@ -132,61 +131,11 @@ var prefs = new function Prefs() { } console.warn("No default preference for '%s'", key); }, - getAll() { return deepCopy(values); }, - - set(key, value, {broadcast = true, sync = true, fromBroadcast} = {}) { - const oldValue = values[key]; - switch (typeof defaults[key]) { - case typeof value: - break; - case 'string': - value = String(value); - break; - case 'number': - value |= 0; - break; - case 'boolean': - value = value === true || value === 'true'; - break; - } - values[key] = value; - const hasChanged = !equal(value, oldValue); - if (!fromBroadcast || FIREFOX_NO_DOM_STORAGE) { - localStorage[key] = typeof defaults[key] === 'object' - ? JSON.stringify(value) - : value; - } - if (!fromBroadcast && broadcast && hasChanged) { - this.broadcast(key, value, {sync}); - } - if (hasChanged) { - const specific = onChange.specific.get(key); - if (typeof specific === 'function') { - specific(key, value); - } else if (specific instanceof Set) { - for (const listener of specific.values()) { - listener(key, value); - } - } - for (const listener of onChange.any.values()) { - listener(key, value); - } - } - }, - - reset: key => this.set(key, deepCopy(defaults[key])), - - broadcast(key, value, {sync = true} = {}) { - broadcastPrefs[key] = value; - debounce(doBroadcast); - if (sync) { - debounce(doSyncSet); - } - }, - + set, + reset: key => set(key, deepCopy(defaults[key])), subscribe(keys, listener) { // keys: string[] ids // or a falsy value to subscribe to everything @@ -206,7 +155,6 @@ var prefs = new function Prefs() { onChange.any.add(listener); } }, - unsubscribe(keys, listener) { if (keys) { for (const key of keys) { @@ -224,134 +172,75 @@ var prefs = new function Prefs() { onChange.all.remove(listener); } }, - }); + }; - { - const importFromBG = () => - API.getPrefs().then(prefs => { - for (const id in prefs) { - this.set(id, prefs[id], {fromBroadcast: true}); - } - }); - // Unlike chrome.storage or messaging, HTML5 localStorage is synchronous and always ready, - // so we'll mirror the prefs to avoid using the wrong defaults during the startup phase - const importFromLocalStorage = () => { - forgetOutdatedDefaults(localStorage); - for (const key in defaults) { - const defaultValue = defaults[key]; - let value = localStorage[key]; - if (typeof value === 'string') { - switch (typeof defaultValue) { - case 'boolean': - value = value.toLowerCase() === 'true'; - break; - case 'number': - value |= 0; - break; - case 'object': - value = tryJSONparse(value) || defaultValue; - break; + function promisify(fn) { + return (...args) => + new Promise((resolve, reject) => { + fn(...args, (...result) => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError); + } else if (result.length === 0) { + resolve(undefined); + } else if (result.length === 1) { + resolve(result[0]); + } else { + resolve(result); } - } else if (FIREFOX_NO_DOM_STORAGE && BG) { - value = BG.localStorage[key]; - value = value === undefined ? defaultValue : value; - localStorage[key] = value; - } else { - value = defaultValue; - } - if (BG === window) { - // when in bg page, .set() will write to localStorage - this.set(key, value, {broadcast: false, sync: false}); - } else { - values[key] = value; - } - } - return Promise.resolve(); - }; - (FIREFOX_NO_DOM_STORAGE && !BG ? importFromBG() : importFromLocalStorage()).then(() => { - if (BG && BG !== window) return; - if (BG === window) { - affectsIcon.forEach(key => this.broadcast(key, values[key], {sync: false})); - chromeSync.getValue('settings').then(settings => importFromSync.call(this, settings)); - } - chrome.storage.onChanged.addListener((changes, area) => { - if (area === 'sync' && 'settings' in changes) { - importFromSync.call(this, changes.settings.newValue); - } + }); }); - }); } - // any access to chrome API takes time due to initialization of bindings - window.addEventListener('load', function _() { - window.removeEventListener('load', _); - chrome.runtime.onMessage.addListener(msg => { - if (msg.prefs) { - for (const id in msg.prefs) { - prefs.set(id, msg.prefs[id], {fromBroadcast: true}); - } - } - }); - }); - - // register hotkeys - if (FIREFOX && (browser.commands || {}).update) { - const hotkeyPrefs = Object.keys(values).filter(k => k.startsWith('hotkey.')); - this.subscribe(hotkeyPrefs, (name, value) => { - try { - name = name.split('.')[1]; - if (value.trim()) { - browser.commands.update({name, shortcut: value}).catch(ignoreChromeError); - } else { - browser.commands.reset(name).catch(ignoreChromeError); - } - } catch (e) {} - }); + function setAll(settings, synced) { + for (const [key, value] of Object.entries(settings)) { + set(key, value, synced); + } } - return; - - function doBroadcast() { - if (BG && BG === window && !BG.dbExec.initialized) { - window.addEventListener('storageReady', function _() { - window.removeEventListener('storageReady', _); - doBroadcast(); - }); + function set(key, value, synced = false) { + const oldValue = values[key]; + switch (typeof defaults[key]) { + case typeof value: + break; + case 'string': + value = String(value); + break; + case 'number': + value |= 0; + break; + case 'boolean': + value = value === true || value === 'true'; + break; + } + if (equal(value, oldValue)) { return; } - const affects = { - all: 'disableAll' in broadcastPrefs - || 'exposeIframes' in broadcastPrefs, - }; - if (!affects.all) { - for (const key in broadcastPrefs) { - affects.icon = affects.icon || affectsIcon.includes(key); - affects.popup = affects.popup || key.startsWith('popup'); - affects.editor = affects.editor || key.startsWith('editor'); - affects.manager = affects.manager || key.startsWith('manage'); + values[key] = value; + emitChange(key, value); + if (synced) { + return; + } + // changes[key] = value; + debounce(syncPrefs); + } + + function emitChange(key, value) { + const specific = onChange.specific.get(key); + if (typeof specific === 'function') { + specific(key, value); + } else if (specific instanceof Set) { + for (const listener of specific.values()) { + listener(key, value); } } - notifyAllTabs({method: 'prefChanged', prefs: broadcastPrefs, affects}); - broadcastPrefs = {}; - } - - function doSyncSet() { - chromeSync.setValue('settings', values); - } - - function importFromSync(synced = {}) { - forgetOutdatedDefaults(synced); - for (const key in defaults) { - if (key in synced) { - this.set(key, synced[key], {sync: false}); - } + for (const listener of onChange.any.values()) { + listener(key, value); } } - function forgetOutdatedDefaults(storage) { - // our linter runs as a worker so we can reduce the delay and forget the old default values - if (Number(storage['editor.lintDelay']) === 500) delete storage['editor.lintDelay']; - if (Number(storage['editor.lintReportDelay']) === 4500) delete storage['editor.lintReportDelay']; + function syncPrefs() { + // FIXME: we always set the entire object? Ideally, this should only use `changes`. + chrome.sync.set('settings', values); } function equal(a, b) { @@ -383,7 +272,7 @@ var prefs = new function Prefs() { !Array.from(navigator.plugins).some(p => p.name === 'Shockwave Flash') ); } -}(); +})(); // Accepts an array of pref names (values are fetched via prefs.get) From 3af310c3412d7f2f1e565d354191ea814731effa Mon Sep 17 00:00:00 2001 From: eight Date: Thu, 4 Oct 2018 17:09:26 +0800 Subject: [PATCH 05/18] Fix: open-manager has no default value --- background/background.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/background/background.js b/background/background.js index 840f7b8b..e2bf4fef 100644 --- a/background/background.js +++ b/background/background.js @@ -202,11 +202,10 @@ if (chrome.contextMenus) { } item = Object.assign({id}, item); delete item.presentIf; - const prefValue = prefs.get(id); item.title = chrome.i18n.getMessage(item.title); - if (!item.type && typeof prefValue === 'boolean') { + if (!item.type && typeof prefs.defaults[id] === 'boolean') { item.type = 'checkbox'; - item.checked = prefValue; + item.checked = prefs.get(id); } if (!item.contexts) { item.contexts = ['browser_action']; @@ -230,7 +229,7 @@ if (chrome.contextMenus) { }; const keys = Object.keys(contextMenus); - prefs.subscribe(keys.filter(id => typeof prefs.get(id) === 'boolean'), toggleCheckmark); + prefs.subscribe(keys.filter(id => typeof prefs.defaults[id] === 'boolean'), toggleCheckmark); prefs.subscribe(keys.filter(id => contextMenus[id].presentIf), togglePresence); createContextMenus(keys); } From dd2b8ed0918fcdece1a37b28d4f6351723bf83f9 Mon Sep 17 00:00:00 2001 From: eight Date: Thu, 4 Oct 2018 17:18:38 +0800 Subject: [PATCH 06/18] Fix: type error --- js/prefs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/prefs.js b/js/prefs.js index 889d5825..5703e370 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -240,7 +240,7 @@ var prefs = (() => { function syncPrefs() { // FIXME: we always set the entire object? Ideally, this should only use `changes`. - chrome.sync.set('settings', values); + chrome.storage.sync.set({settings: values}); } function equal(a, b) { From 10f9449144b87ac5733886bc03f53c7710629175 Mon Sep 17 00:00:00 2001 From: eight Date: Thu, 4 Oct 2018 17:46:45 +0800 Subject: [PATCH 07/18] Change: move setupLivePrefs to dom.js. Remove prefs.js dependencies --- js/dom.js | 52 ++++++++++++++++++++++++++++++++++++ js/prefs.js | 76 ++++++++++++++--------------------------------------- 2 files changed, 71 insertions(+), 57 deletions(-) diff --git a/js/dom.js b/js/dom.js index c956d729..7ffb9ce6 100644 --- a/js/dom.js +++ b/js/dom.js @@ -392,3 +392,55 @@ function moveFocus(rootElement, step) { } } } + +// Accepts an array of pref names (values are fetched via prefs.get) +// and establishes a two-way connection between the document elements and the actual prefs +function setupLivePrefs( + IDs = Object.getOwnPropertyNames(prefs.defaults) + .filter(id => $('#' + id)) +) { + for (const id of IDs) { + const element = $('#' + id); + updateElement({id, element, force: true}); + element.addEventListener('change', onChange); + } + prefs.subscribe(IDs, (id, value) => updateElement({id, value})); + + function onChange() { + const value = getInputValue(this); + if (prefs.get(this.id) !== value) { + prefs.set(this.id, value); + } + } + function updateElement({ + id, + value = prefs.get(id), + element = $('#' + id), + force, + }) { + if (!element) { + prefs.unsubscribe(IDs, updateElement); + return; + } + setInputValue(element, value, force); + } + function getInputValue(input) { + if (input.type === 'checkbox') { + return input.checked; + } + if (input.type === 'number') { + return Number(input.value); + } + return input.value; + } + function setInputValue(input, value, force = false) { + if (force || getInputValue(input) !== value) { + if (input.type === 'checkbox') { + input.checked = value; + } else { + input.value = value; + } + input.dispatchEvent(new Event('change', {bubbles: true, cancelable: true})); + } + } +} diff --git a/js/prefs.js b/js/prefs.js index 5703e370..262eabf8 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -113,6 +113,8 @@ var prefs = (() => { initializing.then(() => setAll(changes.settings.newValue, true)); }); + let timer; + // coalesce multiple pref changes in broadcast // let changes = {}; @@ -217,11 +219,10 @@ var prefs = (() => { } values[key] = value; emitChange(key, value); - if (synced) { + if (synced || timer) { return; } - // changes[key] = value; - debounce(syncPrefs); + timer = setTimeout(syncPrefs); } function emitChange(key, value) { @@ -241,6 +242,7 @@ var prefs = (() => { function syncPrefs() { // FIXME: we always set the entire object? Ideally, this should only use `changes`. chrome.storage.sync.set({settings: values}); + timer = null; } function equal(a, b) { @@ -263,7 +265,7 @@ var prefs = (() => { } function contextDeleteMissing() { - return CHROME && ( + return /Chrome\/\d+/.test(navigator.userAgent) && ( // detect browsers without Delete by looking at the end of UA string /Vivaldi\/[\d.]+$/.test(navigator.userAgent) || // Chrome and co. @@ -272,57 +274,17 @@ var prefs = (() => { !Array.from(navigator.plugins).some(p => p.name === 'Shockwave Flash') ); } + + function deepCopy(obj) { + if (!obj || typeof obj !== 'object') { + return obj; + } + if (Array.isArray(obj)) { + return obj.map(deepCopy); + } + return Object.keys(obj).reduce((output, key) => { + output[key] = deepCopy(obj[key]); + return output; + }, {}); + } })(); - - -// Accepts an array of pref names (values are fetched via prefs.get) -// and establishes a two-way connection between the document elements and the actual prefs -function setupLivePrefs( - IDs = Object.getOwnPropertyNames(prefs.defaults) - .filter(id => $('#' + id)) -) { - for (const id of IDs) { - const element = $('#' + id); - updateElement({id, element, force: true}); - element.addEventListener('change', onChange); - } - prefs.subscribe(IDs, (id, value) => updateElement({id, value})); - - function onChange() { - const value = getInputValue(this); - if (prefs.get(this.id) !== value) { - prefs.set(this.id, value); - } - } - function updateElement({ - id, - value = prefs.get(id), - element = $('#' + id), - force, - }) { - if (!element) { - prefs.unsubscribe(IDs, updateElement); - return; - } - setInputValue(element, value, force); - } - function getInputValue(input) { - if (input.type === 'checkbox') { - return input.checked; - } - if (input.type === 'number') { - return Number(input.value); - } - return input.value; - } - function setInputValue(input, value, force = false) { - if (force || getInputValue(input) !== value) { - if (input.type === 'checkbox') { - input.checked = value; - } else { - input.value = value; - } - input.dispatchEvent(new Event('change', {bubbles: true, cancelable: true})); - } - } -} From 8a6e8ac03a53746c838803addfcb619f67f4b832 Mon Sep 17 00:00:00 2001 From: eight Date: Thu, 4 Oct 2018 18:05:41 +0800 Subject: [PATCH 08/18] Change: drop prefChanged, use prefs service --- content/apply.js | 12 +++--------- edit/edit.js | 8 +++----- manifest.json | 2 +- popup/popup.js | 20 ++++++++------------ 4 files changed, 15 insertions(+), 27 deletions(-) diff --git a/content/apply.js b/content/apply.js index d18b0a22..9f5c66dc 100644 --- a/content/apply.js +++ b/content/apply.js @@ -34,6 +34,9 @@ window.addEventListener(chrome.runtime.id, orphanCheck, true); } + prefs.subscribe(['disableAll'], (key, value) => doDisableAll(value)); + prefs.subscribe(['exposeIframes'], (key, value) => doExposeIframes(value)); + function requestStyles(options, callback = applyStyles) { if (!chrome.app && document instanceof XMLDocument) { chrome.runtime.sendMessage({method: 'styleViaAPI', action: 'styleApply'}); @@ -140,15 +143,6 @@ replaceAll(request.styles); break; - case 'prefChanged': - if ('disableAll' in request.prefs) { - doDisableAll(request.prefs.disableAll); - } - if ('exposeIframes' in request.prefs) { - doExposeIframes(request.prefs.exposeIframes); - } - break; - case 'ping': sendResponse(true); break; diff --git a/edit/edit.js b/edit/edit.js index 8e62f9c6..90668561 100644 --- a/edit/edit.js +++ b/edit/edit.js @@ -55,6 +55,9 @@ Promise.all([ } }); +prefs.subscribe(['editor.smartIndent'], (key, value) => + CodeMirror.setOption('smartIndent', value)); + function preinit() { // make querySelectorAll enumeration code readable ['forEach', 'some', 'indexOf', 'map'].forEach(method => { @@ -183,11 +186,6 @@ function onRuntimeMessage(request) { break; } break; - case 'prefChanged': - if ('editor.smartIndent' in request.prefs) { - CodeMirror.setOption('smartIndent', request.prefs['editor.smartIndent']); - } - break; case 'editDeleteText': document.execCommand('delete'); break; diff --git a/manifest.json b/manifest.json index 055e4533..46e29f54 100644 --- a/manifest.json +++ b/manifest.json @@ -58,7 +58,7 @@ "run_at": "document_start", "all_frames": true, "match_about_blank": true, - "js": ["content/apply.js"] + "js": ["js/prefs.js", "content/apply.js"] }, { "matches": ["http://userstyles.org/*", "https://userstyles.org/*"], diff --git a/popup/popup.js b/popup/popup.js index bea9498d..f7909e46 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -28,6 +28,14 @@ getActiveTab().then(tab => chrome.runtime.onMessage.addListener(onRuntimeMessage); +prefs.subscribe(['popup.stylesFirst'], (key, stylesFirst) => { + const actions = $('body > .actions'); + const before = stylesFirst ? actions : actions.nextSibling; + document.body.insertBefore(installed, before); +}); +prefs.subscribe(['popupWidth'], (key, value) => setPopupWidth(value)); +prefs.subscribe(['popup.borders'], (key, value) => toggleSideBorders(value)); + function onRuntimeMessage(msg) { switch (msg.method) { case 'styleAdded': @@ -38,18 +46,6 @@ function onRuntimeMessage(msg) { case 'styleDeleted': handleDelete(msg.id); break; - case 'prefChanged': - if ('popup.stylesFirst' in msg.prefs) { - const stylesFirst = msg.prefs['popup.stylesFirst']; - const actions = $('body > .actions'); - const before = stylesFirst ? actions : actions.nextSibling; - document.body.insertBefore(installed, before); - } else if ('popupWidth' in msg.prefs) { - setPopupWidth(msg.prefs.popupWidth); - } else if ('popup.borders' in msg.prefs) { - toggleSideBorders(msg.prefs['popup.borders']); - } - break; } dispatchEvent(new CustomEvent(msg.method, {detail: msg})); } From d35f92250e52c5347e49c2be10ac52d6379e789f Mon Sep 17 00:00:00 2001 From: eight Date: Thu, 4 Oct 2018 18:08:19 +0800 Subject: [PATCH 09/18] Fixme: styleViaAPI --- content/apply.js | 1 + 1 file changed, 1 insertion(+) diff --git a/content/apply.js b/content/apply.js index 9f5c66dc..e65975bf 100644 --- a/content/apply.js +++ b/content/apply.js @@ -34,6 +34,7 @@ window.addEventListener(chrome.runtime.id, orphanCheck, true); } + // FIXME: does it work with styleViaAPI? prefs.subscribe(['disableAll'], (key, value) => doDisableAll(value)); prefs.subscribe(['exposeIframes'], (key, value) => doExposeIframes(value)); From 829a134ed101ae0e69c755862941987a2c894e6f Mon Sep 17 00:00:00 2001 From: eight Date: Thu, 4 Oct 2018 18:10:53 +0800 Subject: [PATCH 10/18] Fix: this -> prefs --- background/background.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/background.js b/background/background.js index e2bf4fef..5b8fe7dc 100644 --- a/background/background.js +++ b/background/background.js @@ -314,7 +314,7 @@ window.addEventListener('storageReady', function _() { // register hotkeys if (FIREFOX && browser.commands && browser.commands.update) { const hotkeyPrefs = Object.keys(prefs.defaults).filter(k => k.startsWith('hotkey.')); - this.subscribe(hotkeyPrefs, (name, value) => { + prefs.subscribe(hotkeyPrefs, (name, value) => { try { name = name.split('.')[1]; if (value.trim()) { From 56b737b65a61c38c4904a931de311d0fc8c7fd73 Mon Sep 17 00:00:00 2001 From: eight Date: Thu, 4 Oct 2018 18:14:57 +0800 Subject: [PATCH 11/18] Remove unused FIREFOX_NO_DOM_STORAGE --- background/util.js | 5 ----- js/messaging.js | 7 ------- js/prefs.js | 1 - 3 files changed, 13 deletions(-) delete mode 100644 background/util.js diff --git a/background/util.js b/background/util.js deleted file mode 100644 index 7b85af96..00000000 --- a/background/util.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -if (typeof localStorage === 'object' && localStorage) { - localStorage._BG_ACCESS = 1; -} diff --git a/js/messaging.js b/js/messaging.js index b4b432de..eab3bc7f 100644 --- a/js/messaging.js +++ b/js/messaging.js @@ -97,13 +97,6 @@ if (!BG || BG !== window) { BG.API_METHODS = {}; } -const FIREFOX_NO_DOM_STORAGE = FIREFOX && !tryCatch(() => localStorage && localStorage._BG_ACCESS); -if (FIREFOX_NO_DOM_STORAGE) { - // may be disabled via dom.storage.enabled - Object.defineProperty(window, 'localStorage', {value: {}}); - Object.defineProperty(window, 'sessionStorage', {value: {}}); -} - // eslint-disable-next-line no-var var API = (() => { return new Proxy(() => {}, { diff --git a/js/prefs.js b/js/prefs.js index 262eabf8..e3894b23 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -1,4 +1,3 @@ -/* global prefs: true, contextMenus, FIREFOX_NO_DOM_STORAGE */ 'use strict'; // eslint-disable-next-line no-var From 5c0288e9baf6cb0ba2f0dbc535cfe41244aeeb6c Mon Sep 17 00:00:00 2001 From: eight Date: Thu, 4 Oct 2018 19:20:11 +0800 Subject: [PATCH 12/18] fixup! Remove unused FIREFOX_NO_DOM_STORAGE --- manifest.json | 1 - 1 file changed, 1 deletion(-) diff --git a/manifest.json b/manifest.json index 46e29f54..0571b624 100644 --- a/manifest.json +++ b/manifest.json @@ -23,7 +23,6 @@ ], "background": { "scripts": [ - "background/util.js", "js/messaging.js", "js/storage-util.js", "js/sections-equal.js", From 0d0e1b4dc07f2768ac98805a1f34b70bb18caa83 Mon Sep 17 00:00:00 2001 From: eight Date: Thu, 4 Oct 2018 19:20:36 +0800 Subject: [PATCH 13/18] Fix: update all icons when some prefs changed --- background/background.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/background/background.js b/background/background.js index 5b8fe7dc..c5bbb902 100644 --- a/background/background.js +++ b/background/background.js @@ -144,6 +144,18 @@ prefs.subscribe(['iconset'], () => styles: {}, })); +prefs.subscribe([ + 'show-badge', + 'disableAll', + 'badgeDisabled', + 'badgeNormal', + 'iconset', +], () => + queryTabs().then(tabs => + tabs.map(t => updateIcon({tab: t})) + ) +) + // ************************************************************************* chrome.runtime.onInstalled.addListener(({reason}) => { if (reason !== 'update') return; From 630725196f7fe042b954070d45733a0e0f24b3b1 Mon Sep 17 00:00:00 2001 From: eight Date: Thu, 4 Oct 2018 19:22:44 +0800 Subject: [PATCH 14/18] fixup! Fix: update all icons when some prefs changed --- background/background.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/background/background.js b/background/background.js index c5bbb902..74dbd8cd 100644 --- a/background/background.js +++ b/background/background.js @@ -154,7 +154,7 @@ prefs.subscribe([ queryTabs().then(tabs => tabs.map(t => updateIcon({tab: t})) ) -) +); // ************************************************************************* chrome.runtime.onInstalled.addListener(({reason}) => { From 7be6a1cba904252eedbfe178627ac41cca7ea785 Mon Sep 17 00:00:00 2001 From: eight Date: Thu, 4 Oct 2018 19:24:35 +0800 Subject: [PATCH 15/18] Add: applications --- manifest.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/manifest.json b/manifest.json index 0571b624..989cca36 100644 --- a/manifest.json +++ b/manifest.json @@ -110,5 +110,11 @@ "options_ui": { "page": "options.html", "chrome_style": false + }, + "applications": { + "gecko": { + "id": "{7a7a4a92-a2a0-41d1-9fd7-1e92480d612d}", + "strict_min_version": "53" + } } } From 2328cf623a06581edee5272ed0113c0a37d4f9c7 Mon Sep 17 00:00:00 2001 From: eight Date: Thu, 4 Oct 2018 19:34:22 +0800 Subject: [PATCH 16/18] Change: start-firefox -> start --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 07a08b7b..eea82dfc 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,6 @@ "update-codemirror": "node tools/update-libraries.js && node tools/update-codemirror-themes.js", "update-versions": "node tools/update-versions", "zip": "npm run update-versions && node tools/zip.js", - "start-firefox": "web-ext run" + "start": "web-ext run" } } From dc5f3e209fdc37136b947145ef0f808946de0780 Mon Sep 17 00:00:00 2001 From: eight Date: Thu, 4 Oct 2018 19:34:36 +0800 Subject: [PATCH 17/18] Fix: settings could be empty on the first install --- js/prefs.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/js/prefs.js b/js/prefs.js index e3894b23..fbf59e30 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -103,7 +103,11 @@ var prefs = (() => { }; const initializing = promisify(chrome.storage.sync.get.bind(chrome.storage.sync))('settings') - .then(result => setAll(result.settings, true)); + .then(result => { + if (result.settings) { + setAll(result.settings, true); + } + }); chrome.storage.onChanged.addListener((changes, area) => { if (area !== 'sync' || !changes.settings || !changes.settings.newValue) { From 81e4823f4602226eaeb5126deca93b0618b7c153 Mon Sep 17 00:00:00 2001 From: eight Date: Thu, 4 Oct 2018 19:39:59 +0800 Subject: [PATCH 18/18] Debounce updateAllTabsIcon --- background/background.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/background/background.js b/background/background.js index 74dbd8cd..d436bc13 100644 --- a/background/background.js +++ b/background/background.js @@ -150,11 +150,7 @@ prefs.subscribe([ 'badgeDisabled', 'badgeNormal', 'iconset', -], () => - queryTabs().then(tabs => - tabs.map(t => updateIcon({tab: t})) - ) -); +], () => debounce(updateAllTabsIcon)); // ************************************************************************* chrome.runtime.onInstalled.addListener(({reason}) => { @@ -534,3 +530,9 @@ function onRuntimeMessage(msg, sender, sendResponse) { respond(result); } } + +function updateAllTabsIcon() { + return queryTabs().then(tabs => + tabs.map(t => updateIcon({tab: t})) + ); +}