Enhance: make prefs use storage.sync

This commit is contained in:
eight 2018-10-04 17:04:23 +08:00
parent 282bdf7706
commit 874a2da33e
2 changed files with 86 additions and 182 deletions

View File

@ -312,6 +312,21 @@ window.addEventListener('storageReady', function _() {
updateAPI(null, prefs.get('exposeIframes')); 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}) { function webNavigationListener(method, {url, tabId, frameId}) {

View File

@ -2,7 +2,7 @@
'use strict'; 'use strict';
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
var prefs = new function Prefs() { var prefs = (() => {
const defaults = { const defaults = {
'openEditInWindow': false, // new editor opens in a own browser window 'openEditInWindow': false, // new editor opens in a own browser window
'windowPosition': {}, // detached window position 'windowPosition': {}, // detached window position
@ -98,28 +98,27 @@ var prefs = new function Prefs() {
}; };
const values = deepCopy(defaults); const values = deepCopy(defaults);
const affectsIcon = [
'show-badge',
'disableAll',
'badgeDisabled',
'badgeNormal',
'iconset',
];
const onChange = { const onChange = {
any: new Set(), any: new Set(),
specific: new Map(), specific: new Map(),
}; };
// coalesce multiple pref changes in broadcast const initializing = promisify(chrome.storage.sync.get.bind(chrome.storage.sync))('settings')
let broadcastPrefs = {}; .then(result => setAll(result.settings, true));
Object.defineProperties(this, { chrome.storage.onChanged.addListener((changes, area) => {
defaults: {value: deepCopy(defaults)} 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) { get(key, defaultValue) {
if (key in values) { if (key in values) {
return values[key]; return values[key];
@ -132,61 +131,11 @@ var prefs = new function Prefs() {
} }
console.warn("No default preference for '%s'", key); console.warn("No default preference for '%s'", key);
}, },
getAll() { getAll() {
return deepCopy(values); return deepCopy(values);
}, },
set,
set(key, value, {broadcast = true, sync = true, fromBroadcast} = {}) { reset: key => set(key, deepCopy(defaults[key])),
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);
}
},
subscribe(keys, listener) { subscribe(keys, listener) {
// keys: string[] ids // keys: string[] ids
// or a falsy value to subscribe to everything // or a falsy value to subscribe to everything
@ -206,7 +155,6 @@ var prefs = new function Prefs() {
onChange.any.add(listener); onChange.any.add(listener);
} }
}, },
unsubscribe(keys, listener) { unsubscribe(keys, listener) {
if (keys) { if (keys) {
for (const key of keys) { for (const key of keys) {
@ -224,134 +172,75 @@ var prefs = new function Prefs() {
onChange.all.remove(listener); onChange.all.remove(listener);
} }
}, },
}); };
{ function promisify(fn) {
const importFromBG = () => return (...args) =>
API.getPrefs().then(prefs => { new Promise((resolve, reject) => {
for (const id in prefs) { fn(...args, (...result) => {
this.set(id, prefs[id], {fromBroadcast: true}); 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);
} }
}); });
// 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); function setAll(settings, synced) {
for (const key in defaults) { for (const [key, value] of Object.entries(settings)) {
const defaultValue = defaults[key]; set(key, value, synced);
let value = localStorage[key]; }
if (typeof value === 'string') { }
switch (typeof defaultValue) {
case 'boolean': function set(key, value, synced = false) {
value = value.toLowerCase() === 'true'; const oldValue = values[key];
switch (typeof defaults[key]) {
case typeof value:
break;
case 'string':
value = String(value);
break; break;
case 'number': case 'number':
value |= 0; value |= 0;
break; break;
case 'object': case 'boolean':
value = tryJSONparse(value) || defaultValue; value = value === true || value === 'true';
break; break;
} }
} else if (FIREFOX_NO_DOM_STORAGE && BG) { if (equal(value, oldValue)) {
value = BG.localStorage[key]; return;
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; values[key] = value;
} emitChange(key, value);
} if (synced) {
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) {}
});
}
return;
function doBroadcast() {
if (BG && BG === window && !BG.dbExec.initialized) {
window.addEventListener('storageReady', function _() {
window.removeEventListener('storageReady', _);
doBroadcast();
});
return; return;
} }
const affects = { // changes[key] = value;
all: 'disableAll' in broadcastPrefs debounce(syncPrefs);
|| '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');
}
}
notifyAllTabs({method: 'prefChanged', prefs: broadcastPrefs, affects});
broadcastPrefs = {};
} }
function doSyncSet() { function emitChange(key, value) {
chromeSync.setValue('settings', values); 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);
} }
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) { function syncPrefs() {
// our linter runs as a worker so we can reduce the delay and forget the old default values // FIXME: we always set the entire object? Ideally, this should only use `changes`.
if (Number(storage['editor.lintDelay']) === 500) delete storage['editor.lintDelay']; chrome.sync.set('settings', values);
if (Number(storage['editor.lintReportDelay']) === 4500) delete storage['editor.lintReportDelay'];
} }
function equal(a, b) { function equal(a, b) {
@ -383,7 +272,7 @@ var prefs = new function Prefs() {
!Array.from(navigator.plugins).some(p => p.name === 'Shockwave Flash') !Array.from(navigator.plugins).some(p => p.name === 'Shockwave Flash')
); );
} }
}(); })();
// Accepts an array of pref names (values are fetched via prefs.get) // Accepts an array of pref names (values are fetched via prefs.get)