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'));
}
// 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}) {

View File

@ -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});
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);
}
});
// 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';
});
}
function setAll(settings, synced) {
for (const [key, value] of Object.entries(settings)) {
set(key, value, synced);
}
}
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 'object':
value = tryJSONparse(value) || defaultValue;
case 'boolean':
value = value === true || value === 'true';
break;
}
} else if (FIREFOX_NO_DOM_STORAGE && BG) {
value = BG.localStorage[key];
value = value === undefined ? defaultValue : value;
localStorage[key] = value;
} else {
value = defaultValue;
if (equal(value, oldValue)) {
return;
}
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) {}
});
}
return;
function doBroadcast() {
if (BG && BG === window && !BG.dbExec.initialized) {
window.addEventListener('storageReady', function _() {
window.removeEventListener('storageReady', _);
doBroadcast();
});
emitChange(key, value);
if (synced) {
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');
}
}
notifyAllTabs({method: 'prefChanged', prefs: broadcastPrefs, affects});
broadcastPrefs = {};
// changes[key] = value;
debounce(syncPrefs);
}
function doSyncSet() {
chromeSync.setValue('settings', values);
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);
}
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)