API.* groups + async'ify

* API.styles.*
* API.usercss.*
* API.sync.*
* API.worker.*
* API.updater.*
* simplify db: resolve with result
* remove API.download
* simplify download()
* remove noCode param as it wastes more time/memory on copying
* styleManager: switch style<->data names to reflect their actual contents
* inline method bodies to avoid indirection and enable better autocomplete/hint/jump support in IDE
This commit is contained in:
tophf 2020-11-18 21:19:32 +03:00
parent 06823bd5b4
commit 86623a9aab
41 changed files with 1367 additions and 1574 deletions

View File

@ -4,6 +4,7 @@
importScripts('/js/worker-util.js');
const {loadScript} = workerUtil;
/** @namespace ApiWorker */
workerUtil.createAPI({
parseMozFormat(arg) {
loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js');

View File

@ -1,49 +1,30 @@
/* global download prefs openURL FIREFOX CHROME
URLS ignoreChromeError chromeLocal semverCompare
styleManager msg navigatorUtil workerUtil contentScripts sync
findExistingTab activateTab isTabReplaceable getActiveTab
/* global
activateTab
API
chromeLocal
findExistingTab
FIREFOX
getActiveTab
isTabReplaceable
msg
openURL
prefs
semverCompare
URLS
workerUtil
*/
'use strict';
// eslint-disable-next-line no-var
var backgroundWorker = workerUtil.createWorker({
//#region API
Object.assign(API, {
/** @type {ApiWorker} */
worker: workerUtil.createWorker({
url: '/background/background-worker.js',
});
// eslint-disable-next-line no-var
var browserCommands, contextMenus;
// *************************************************************************
// browser commands
browserCommands = {
openManage,
openOptions: () => openManage({options: true}),
styleDisableAll(info) {
prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll'));
},
reload: () => chrome.runtime.reload(),
};
window.API_METHODS = Object.assign(window.API_METHODS || {}, {
deleteStyle: styleManager.deleteStyle,
editSave: styleManager.editSave,
findStyle: styleManager.findStyle,
getAllStyles: styleManager.getAllStyles, // used by importer
getSectionsByUrl: styleManager.getSectionsByUrl,
getStyle: styleManager.get,
getStylesByUrl: styleManager.getStylesByUrl,
importStyle: styleManager.importStyle,
importManyStyles: styleManager.importMany,
installStyle: styleManager.installStyle,
styleExists: styleManager.styleExists,
toggleStyle: styleManager.toggleStyle,
addInclusion: styleManager.addInclusion,
removeInclusion: styleManager.removeInclusion,
addExclusion: styleManager.addExclusion,
removeExclusion: styleManager.removeExclusion,
}),
/** @returns {string} */
getTabUrlPrefix() {
const {url} = this.sender.tab;
if (url.startsWith(URLS.ownOrigin)) {
@ -52,252 +33,22 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
return url.match(/^([\w-]+:\/+[^/#]+)/)[1];
},
download(msg) {
delete msg.method;
return download(msg.url, msg);
},
parseCss({code}) {
return backgroundWorker.parseMozFormat({code});
},
/** @returns {Prefs} */
getPrefs: () => prefs.values,
setPref: (key, value) => prefs.set(key, value),
openEditor,
/* Same as openURL, the only extra prop in `opts` is `message` - it'll be sent when the tab is ready,
which is needed in the popup, otherwise another extension could force the tab to open in foreground
thus auto-closing the popup (in Chrome at least) and preventing the sendMessage code from running */
async openURL(opts) {
const tab = await openURL(opts);
if (opts.message) {
await onTabReady(tab);
await msg.sendTab(tab.id, opts.message);
}
return tab;
function onTabReady(tab) {
return new Promise((resolve, reject) =>
setTimeout(function ping(numTries = 10, delay = 100) {
msg.sendTab(tab.id, {method: 'ping'})
.catch(() => false)
.then(pong => pong
? resolve(tab)
: numTries && setTimeout(ping, delay, numTries - 1, delay * 1.5) ||
reject('timeout'));
}));
}
setPref(key, value) {
prefs.set(key, value);
},
optionsCustomizeHotkeys() {
return browserCommands.openOptions()
.then(() => new Promise(resolve => setTimeout(resolve, 500)))
.then(() => msg.broadcastExtension({method: 'optionsCustomizeHotkeys'}));
},
syncStart: sync.start,
syncStop: sync.stop,
syncNow: sync.syncNow,
getSyncStatus: sync.getStatus,
syncLogin: sync.login,
openManage,
});
// *************************************************************************
// register all listeners
msg.on(onRuntimeMessage);
// tell apply.js to refresh styles for non-committed navigation
navigatorUtil.onUrlChange(({tabId, frameId}, type) => {
if (type !== 'committed') {
msg.sendTab(tabId, {method: 'urlChanged'}, {frameId})
.catch(msg.ignoreError);
}
});
if (FIREFOX) {
// FF misses some about:blank iframes so we inject our content script explicitly
navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, {
url: [
{urlEquals: 'about:blank'},
],
});
}
if (chrome.contextMenus) {
chrome.contextMenus.onClicked.addListener((info, tab) =>
contextMenus[info.menuItemId].click(info, tab));
}
if (chrome.commands) {
// Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350
chrome.commands.onCommand.addListener(command => browserCommands[command]());
}
// *************************************************************************
chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
if (reason !== 'update') return;
if (semverCompare(previousVersion, '1.5.13') <= 0) {
// Removing unused stuff
// TODO: delete this entire block by the middle of 2021
try {
localStorage.clear();
} catch (e) {}
setTimeout(async () => {
const del = Object.keys(await chromeLocal.get())
.filter(key => key.startsWith('usoSearchCache'));
if (del.length) chromeLocal.remove(del);
}, 15e3);
}
});
// *************************************************************************
// context menus
contextMenus = {
'show-badge': {
title: 'menuShowBadge',
click: info => prefs.set(info.menuItemId, info.checked),
},
'disableAll': {
title: 'disableAllStyles',
click: browserCommands.styleDisableAll,
},
'open-manager': {
title: 'openStylesManager',
click: browserCommands.openManage,
},
'open-options': {
title: 'openOptions',
click: browserCommands.openOptions,
},
'reload': {
presentIf: async () => (await browser.management.getSelf()).installType === 'development',
title: 'reload',
click: browserCommands.reload,
},
'editor.contextDelete': {
presentIf: () => !FIREFOX && prefs.get('editor.contextDelete'),
title: 'editDeleteText',
type: 'normal',
contexts: ['editable'],
documentUrlPatterns: [URLS.ownOrigin + 'edit*'],
click: (info, tab) => {
msg.sendTab(tab.id, {method: 'editDeleteText'}, undefined, 'extension')
.catch(msg.ignoreError);
},
},
};
async function createContextMenus(ids) {
for (const id of ids) {
let item = contextMenus[id];
if (item.presentIf && !await item.presentIf()) {
continue;
}
item = Object.assign({id}, item);
delete item.presentIf;
item.title = chrome.i18n.getMessage(item.title);
if (!item.type && typeof prefs.defaults[id] === 'boolean') {
item.type = 'checkbox';
item.checked = prefs.get(id);
}
if (!item.contexts) {
item.contexts = ['browser_action'];
}
delete item.click;
chrome.contextMenus.create(item, ignoreChromeError);
}
}
if (chrome.contextMenus) {
// "Delete" item in context menu for browsers that don't have it
if (CHROME &&
// looking at the end of UA string
/(Vivaldi|Safari)\/[\d.]+$/.test(navigator.userAgent) &&
// skip forks with Flash as those are likely to have the menu e.g. CentBrowser
!Array.from(navigator.plugins).some(p => p.name === 'Shockwave Flash')) {
prefs.defaults['editor.contextDelete'] = true;
}
// circumvent the bug with disabling check marks in Chrome 62-64
const toggleCheckmark = CHROME >= 62 && CHROME <= 64 ?
(id => chrome.contextMenus.remove(id, () => createContextMenus([id]) + ignoreChromeError())) :
((id, checked) => chrome.contextMenus.update(id, {checked}, ignoreChromeError));
const togglePresence = (id, checked) => {
if (checked) {
createContextMenus([id]);
} else {
chrome.contextMenus.remove(id, ignoreChromeError);
}
};
const keys = Object.keys(contextMenus);
prefs.subscribe(keys.filter(id => typeof prefs.defaults[id] === 'boolean'), toggleCheckmark);
prefs.subscribe(keys.filter(id => contextMenus[id].presentIf && id in prefs.defaults), togglePresence);
createContextMenus(keys);
}
// reinject content scripts when the extension is reloaded/updated. Firefox
// would handle this automatically.
if (!FIREFOX) {
setTimeout(contentScripts.injectToAllTabs, 0);
}
// register hotkeys
if (FIREFOX && browser.commands && browser.commands.update) {
const hotkeyPrefs = Object.keys(prefs.defaults).filter(k => k.startsWith('hotkey.'));
prefs.subscribe(hotkeyPrefs, (name, value) => {
try {
name = name.split('.')[1];
if (value.trim()) {
browser.commands.update({name, shortcut: value});
} else {
browser.commands.reset(name);
}
} catch (e) {}
});
}
msg.broadcast({method: 'backgroundReady'});
function webNavIframeHelperFF({tabId, frameId}) {
if (!frameId) return;
msg.sendTab(tabId, {method: 'ping'}, {frameId})
.catch(() => false)
.then(pong => {
if (pong) return;
// insert apply.js to iframe
const files = chrome.runtime.getManifest().content_scripts[0].js;
for (const file of files) {
chrome.tabs.executeScript(tabId, {
frameId,
file,
matchAboutBlank: true,
}, ignoreChromeError);
}
});
}
function onRuntimeMessage(msg, sender) {
if (msg.method !== 'invokeAPI') {
return;
}
const fn = window.API_METHODS[msg.name];
if (!fn) {
throw new Error(`unknown API: ${msg.name}`);
}
const res = fn.apply({msg, sender}, msg.args);
return res === undefined ? null : res;
}
function openEditor(params) {
/* Open the editor. Activate if it is already opened
params: {
id?: Number,
domain?: String,
'url-prefix'?: String
}
/**
* Opens the editor or activates an existing tab
* @param {{
id?: number
domain?: string
'url-prefix'?: string
}} params
* @returns {Promise<chrome.tabs.Tab>}
*/
openEditor(params) {
const u = new URL(chrome.runtime.getURL('edit.html'));
u.search = new URLSearchParams(params);
return openURL({
@ -307,9 +58,10 @@ function openEditor(params) {
prefs.get('openEditInWindow.popup') && {type: 'popup'},
prefs.get('windowPosition')),
});
}
},
async function openManage({options = false, search, searchMode} = {}) {
/** @returns {Promise<chrome.tabs.Tab>} */
async openManage({options = false, search, searchMode} = {}) {
let url = chrome.runtime.getURL('manage.html');
if (search) {
url += `?search=${encodeURIComponent(search)}&searchMode=${searchMode}`;
@ -334,4 +86,93 @@ async function openManage({options = false, search, searchMode} = {}) {
return isTabReplaceable(tab, url)
? activateTab(tab, {url})
: browser.tabs.create({url});
},
/**
* Same as openURL, the only extra prop in `opts` is `message` - it'll be sent
* when the tab is ready, which is needed in the popup, otherwise another
* extension could force the tab to open in foreground thus auto-closing the
* popup (in Chrome at least) and preventing the sendMessage code from running
* @returns {Promise<chrome.tabs.Tab>}
*/
async openURL(opts) {
const tab = await openURL(opts);
if (opts.message) {
await onTabReady(tab);
await msg.sendTab(tab.id, opts.message);
}
return tab;
function onTabReady(tab) {
return new Promise((resolve, reject) =>
setTimeout(function ping(numTries = 10, delay = 100) {
msg.sendTab(tab.id, {method: 'ping'})
.catch(() => false)
.then(pong => pong
? resolve(tab)
: numTries && setTimeout(ping, delay, numTries - 1, delay * 1.5) ||
reject('timeout'));
}));
}
},
});
//#endregion
//#region browserCommands
const browserCommands = {
openManage: () => API.openManage(),
openOptions: () => API.openManage({options: true}),
styleDisableAll(info) {
prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll'));
},
reload: () => chrome.runtime.reload(),
};
if (chrome.commands) {
chrome.commands.onCommand.addListener(command => browserCommands[command]());
}
if (FIREFOX && browser.commands && browser.commands.update) {
// register hotkeys in FF
const hotkeyPrefs = Object.keys(prefs.defaults).filter(k => k.startsWith('hotkey.'));
prefs.subscribe(hotkeyPrefs, (name, value) => {
try {
name = name.split('.')[1];
if (value.trim()) {
browser.commands.update({name, shortcut: value});
} else {
browser.commands.reset(name);
}
} catch (e) {}
});
}
//#endregion
//#region Init
msg.on((msg, sender) => {
if (msg.method === 'invokeAPI') {
const fn = msg.path.reduce((res, name) => res && res[name], API);
if (!fn) throw new Error(`Unknown API.${msg.path.join('.')}`);
const res = fn.apply({msg, sender}, msg.args);
return res === undefined ? null : res;
}
});
chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
if (reason !== 'update') return;
if (semverCompare(previousVersion, '1.5.13') <= 0) {
// Removing unused stuff
// TODO: delete this entire block by the middle of 2021
try {
localStorage.clear();
} catch (e) {}
setTimeout(async () => {
const del = Object.keys(await chromeLocal.get())
.filter(key => key.startsWith('usoSearchCache'));
if (del.length) chromeLocal.remove(del);
}, 15e3);
}
});
msg.broadcast({method: 'backgroundReady'});
//#endregion

View File

@ -1,8 +1,18 @@
/* global msg ignoreChromeError URLS */
/* exported contentScripts */
/* global
FIREFOX
ignoreChromeError
msg
URLS
*/
'use strict';
const contentScripts = (() => {
/*
Reinject content scripts when the extension is reloaded/updated.
Firefox handles this automatically.
*/
// eslint-disable-next-line no-unused-expressions
!FIREFOX && (() => {
const NTP = 'chrome://newtab/';
const ALL_URLS = '<all_urls>';
const SCRIPTS = chrome.runtime.getManifest().content_scripts;
@ -18,21 +28,7 @@ const contentScripts = (() => {
const busyTabs = new Set();
let busyTabsTimer;
// expose version on greasyfork/sleazyfork 1) info page and 2) code page
const urlMatches = '/scripts/\\d+[^/]*(/code)?([?#].*)?$';
chrome.webNavigation.onCommitted.addListener(({tabId}) => {
chrome.tabs.executeScript(tabId, {
file: '/content/install-hook-greasyfork.js',
runAt: 'document_start',
});
}, {
url: [
{hostEquals: 'greasyfork.org', urlMatches},
{hostEquals: 'sleazyfork.org', urlMatches},
],
});
return {injectToTab, injectToAllTabs};
setTimeout(injectToAllTabs);
function injectToTab({url, tabId, frameId = null}) {
for (const script of SCRIPTS) {

107
background/context-menus.js Normal file
View File

@ -0,0 +1,107 @@
/* global
browserCommands
CHROME
FIREFOX
ignoreChromeError
msg
prefs
URLS
*/
'use strict';
// eslint-disable-next-line no-unused-expressions
chrome.contextMenus && (() => {
const contextMenus = {
'show-badge': {
title: 'menuShowBadge',
click: info => prefs.set(info.menuItemId, info.checked),
},
'disableAll': {
title: 'disableAllStyles',
click: browserCommands.styleDisableAll,
},
'open-manager': {
title: 'openStylesManager',
click: browserCommands.openManage,
},
'open-options': {
title: 'openOptions',
click: browserCommands.openOptions,
},
'reload': {
presentIf: async () => (await browser.management.getSelf()).installType === 'development',
title: 'reload',
click: browserCommands.reload,
},
'editor.contextDelete': {
presentIf: () => !FIREFOX && prefs.get('editor.contextDelete'),
title: 'editDeleteText',
type: 'normal',
contexts: ['editable'],
documentUrlPatterns: [URLS.ownOrigin + 'edit*'],
click: (info, tab) => {
msg.sendTab(tab.id, {method: 'editDeleteText'}, undefined, 'extension')
.catch(msg.ignoreError);
},
},
};
// "Delete" item in context menu for browsers that don't have it
if (CHROME &&
// looking at the end of UA string
/(Vivaldi|Safari)\/[\d.]+$/.test(navigator.userAgent) &&
// skip forks with Flash as those are likely to have the menu e.g. CentBrowser
!Array.from(navigator.plugins).some(p => p.name === 'Shockwave Flash')) {
prefs.defaults['editor.contextDelete'] = true;
}
const keys = Object.keys(contextMenus);
prefs.subscribe(keys.filter(id => typeof prefs.defaults[id] === 'boolean'),
CHROME >= 62 && CHROME <= 64 ? toggleCheckmarkBugged : toggleCheckmark);
prefs.subscribe(keys.filter(id => contextMenus[id].presentIf && id in prefs.defaults),
togglePresence);
createContextMenus(keys);
chrome.contextMenus.onClicked.addListener((info, tab) =>
contextMenus[info.menuItemId].click(info, tab));
async function createContextMenus(ids) {
for (const id of ids) {
let item = contextMenus[id];
if (item.presentIf && !await item.presentIf()) {
continue;
}
item = Object.assign({id}, item);
delete item.presentIf;
item.title = chrome.i18n.getMessage(item.title);
if (!item.type && typeof prefs.defaults[id] === 'boolean') {
item.type = 'checkbox';
item.checked = prefs.get(id);
}
if (!item.contexts) {
item.contexts = ['browser_action'];
}
delete item.click;
chrome.contextMenus.create(item, ignoreChromeError);
}
}
function toggleCheckmark(id, checked) {
chrome.contextMenus.update(id, {checked}, ignoreChromeError);
}
/** Circumvents the bug with disabling check marks in Chrome 62-64 */
async function toggleCheckmarkBugged(id) {
await browser.contextMenus.remove(id).catch(ignoreChromeError);
createContextMenus([id]);
}
function togglePresence(id, checked) {
if (checked) {
createContextMenus([id]);
} else {
chrome.contextMenus.remove(id, ignoreChromeError);
}
}
})();

View File

@ -35,20 +35,9 @@ function createChromeStorageDB() {
}),
};
return {exec};
function exec(method, ...args) {
if (METHODS[method]) {
return METHODS[method](...args)
.then(result => {
if (method === 'putMany' && result.map) {
return result.map(r => ({target: {result: r}}));
}
return {target: {result}};
});
}
return Promise.reject(new Error(`unknown DB method ${method}`));
}
return {
exec: (method, ...args) => METHODS[method](...args),
};
function prepareInc() {
if (INC) return Promise.resolve();

View File

@ -33,18 +33,17 @@ const db = (() => {
case false: break;
default: await testDB();
}
return useIndexedDB();
chromeLocal.setValue(FALLBACK, false);
return dbExecIndexedDB;
}
async function testDB() {
let e = await dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1);
// throws if result is null
e = e.target.result[0];
e = e[0]; // throws if result is null
const id = `${performance.now()}.${Math.random()}.${Date.now()}`;
await dbExecIndexedDB('put', {id});
e = await dbExecIndexedDB('get', id);
// throws if result or id is null
await dbExecIndexedDB('delete', e.target.result.id);
await dbExecIndexedDB('delete', e.id); // throws if `e` or id is null
}
function useChromeStorage(err) {
@ -56,11 +55,6 @@ const db = (() => {
return createChromeStorageDB().exec;
}
function useIndexedDB() {
chromeLocal.setValue(FALLBACK, false);
return dbExecIndexedDB;
}
async function dbExecIndexedDB(method, ...args) {
const mode = method.startsWith('get') ? 'readonly' : 'readwrite';
const store = (await open()).transaction([STORE], mode).objectStore(STORE);
@ -70,8 +64,9 @@ const db = (() => {
function storeRequest(store, method, ...args) {
return new Promise((resolve, reject) => {
/** @type {IDBRequest} */
const request = store[method](...args);
request.onsuccess = resolve;
request.onsuccess = () => resolve(request.result);
request.onerror = reject;
});
}

View File

@ -1,4 +1,4 @@
/* global prefs debounce iconUtil FIREFOX CHROME VIVALDI tabManager navigatorUtil API_METHODS */
/* global prefs debounce iconUtil FIREFOX CHROME VIVALDI tabManager navigatorUtil API */
/* exported iconManager */
'use strict';
@ -27,7 +27,7 @@ const iconManager = (() => {
refreshAllIcons();
});
Object.assign(API_METHODS, {
Object.assign(API, {
/** @param {(number|string)[]} styleIds
* @param {boolean} [lazyBadge=false] preventing flicker during page load */
updateIconBadge(styleIds, {lazyBadge} = {}) {
@ -53,7 +53,7 @@ const iconManager = (() => {
function onPortDisconnected({sender}) {
if (tabManager.get(sender.tab.id, 'styleIds')) {
API_METHODS.updateIconBadge.call({sender}, [], {lazyBadge: true});
API.updateIconBadge.call({sender}, [], {lazyBadge: true});
}
}

View File

@ -1,75 +1,103 @@
/* global CHROME URLS */
/* exported navigatorUtil */
/* global
CHROME
FIREFOX
ignoreChromeError
msg
URLS
*/
'use strict';
const navigatorUtil = (() => {
const handler = {
urlChange: null,
};
return extendNative({onUrlChange});
(() => {
/** @type {Set<function(data: Object, type: string)>} */
const listeners = new Set();
/** @type {NavigatorUtil} */
const navigatorUtil = window.navigatorUtil = new Proxy({
onUrlChange(fn) {
listeners.add(fn);
},
}, {
get(target, prop) {
return target[prop] ||
(target = chrome.webNavigation[prop]).addListener.bind(target);
},
});
function onUrlChange(fn) {
initUrlChange();
handler.urlChange.push(fn);
navigatorUtil.onCommitted(onNavigation.bind('committed'));
navigatorUtil.onHistoryStateUpdated(onFakeNavigation.bind('history'));
navigatorUtil.onReferenceFragmentUpdated(onFakeNavigation.bind('hash'));
navigatorUtil.onCommitted(runGreasyforkContentScript, {
// expose style version on greasyfork/sleazyfork 1) info page and 2) code page
url: ['greasyfork', 'sleazyfork'].map(host => ({
hostEquals: host + '.org',
urlMatches: '/scripts/\\d+[^/]*(/code)?([?#].*)?$',
})),
});
if (FIREFOX) {
navigatorUtil.onDOMContentLoaded(runMainContentScripts, {
url: [{
urlEquals: 'about:blank',
}],
});
}
function initUrlChange() {
if (handler.urlChange) {
return;
}
handler.urlChange = [];
chrome.webNavigation.onCommitted.addListener(data =>
fixNTPUrl(data)
.then(() => executeCallbacks(handler.urlChange, data, 'committed'))
.catch(console.error)
);
chrome.webNavigation.onHistoryStateUpdated.addListener(data =>
fixNTPUrl(data)
.then(() => executeCallbacks(handler.urlChange, data, 'historyStateUpdated'))
.catch(console.error)
);
chrome.webNavigation.onReferenceFragmentUpdated.addListener(data =>
fixNTPUrl(data)
.then(() => executeCallbacks(handler.urlChange, data, 'referenceFragmentUpdated'))
.catch(console.error)
);
}
function fixNTPUrl(data) {
if (
!CHROME ||
!URLS.chromeProtectsNTP ||
!data.url.startsWith('https://www.google.') ||
!data.url.includes('/_/chrome/newtab?')
) {
return Promise.resolve();
}
return browser.tabs.get(data.tabId)
.then(tab => {
/** @this {string} type */
async function onNavigation(data) {
if (CHROME &&
URLS.chromeProtectsNTP &&
data.url.startsWith('https://www.google.') &&
data.url.includes('/_/chrome/newtab?')) {
// Modern Chrome switched to WebUI NTP so this is obsolete, but there may be exceptions
// TODO: investigate, and maybe use a separate listener for CHROME <= ver
const tab = await browser.tabs.get(data.tabId);
const url = tab.pendingUrl || tab.url;
if (url === 'chrome://newtab/') {
data.url = url;
}
});
}
listeners.forEach(fn => fn(data, this));
}
function executeCallbacks(callbacks, data, type) {
for (const cb of callbacks) {
cb(data, type);
/** @this {string} type */
function onFakeNavigation(data) {
onNavigation.call(this, data);
msg.sendTab(data.tabId, {method: 'urlChanged'}, {frameId: data.frameId})
.catch(msg.ignoreError);
}
/** FF misses some about:blank iframes so we inject our content script explicitly */
async function runMainContentScripts({tabId, frameId}) {
if (frameId &&
!await msg.sendTab(tabId, {method: 'ping'}, {frameId}).catch(ignoreChromeError)) {
for (const file of chrome.runtime.getManifest().content_scripts[0].js) {
chrome.tabs.executeScript(tabId, {
frameId,
file,
matchAboutBlank: true,
}, ignoreChromeError);
}
}
}
function extendNative(target) {
return new Proxy(target, {
get: (target, prop) => {
if (target[prop]) {
return target[prop];
}
return chrome.webNavigation[prop].addListener.bind(chrome.webNavigation[prop]);
},
function runGreasyforkContentScript({tabId}) {
chrome.tabs.executeScript(tabId, {
file: '/content/install-hook-greasyfork.js',
runAt: 'document_start',
});
}
})();
/**
* @typedef NavigatorUtil
* @property {NavigatorUtilEvent} onBeforeNavigate
* @property {NavigatorUtilEvent} onCommitted
* @property {NavigatorUtilEvent} onCompleted
* @property {NavigatorUtilEvent} onCreatedNavigationTarget
* @property {NavigatorUtilEvent} onDOMContentLoaded
* @property {NavigatorUtilEvent} onErrorOccurred
* @property {NavigatorUtilEvent} onHistoryStateUpdated
* @property {NavigatorUtilEvent} onReferenceFragmentUpdated
* @property {NavigatorUtilEvent} onTabReplaced
*/
/**
* @typedef {function(cb: function, filters: WebNavigationEventFilter?)} NavigatorUtilEvent
*/

View File

@ -1,3 +1,4 @@
/* global API */
'use strict';
(() => {
@ -40,7 +41,7 @@
.then(res => res.json());
};
window.API_METHODS = Object.assign(window.API_METHODS || {}, {
API.openusercss = {
/**
* This function can be used to retrieve a theme object from the
* GraphQL API, set above
@ -98,5 +99,5 @@
}
}
`),
});
};
})();

View File

@ -1,8 +1,7 @@
/* global
API_METHODS
API
debounce
stringAsRegExp
styleManager
tryRegExp
usercss
*/
@ -50,16 +49,16 @@
* @param {number[]} [params.ids] - if not specified, all styles are searched
* @returns {number[]} - array of matched styles ids
*/
API_METHODS.searchDB = async ({query, mode = 'all', ids}) => {
API.searchDB = async ({query, mode = 'all', ids}) => {
let res = [];
if (mode === 'url' && query) {
res = (await styleManager.getStylesByUrl(query)).map(r => r.data.id);
res = (await API.styles.getByUrl(query)).map(r => r.style.id);
} else if (mode in MODES) {
const modeHandler = MODES[mode];
const m = /^\/(.+?)\/([gimsuy]*)$/.exec(query);
const rx = m && tryRegExp(m[1], m[2]);
const test = rx ? rx.test.bind(rx) : makeTester(query);
res = (await styleManager.getAllStyles())
res = (await API.styles.getAll())
.filter(style =>
(!ids || ids.includes(style.id)) &&
(!query || modeHandler(style, test)))

View File

@ -1,7 +1,16 @@
/* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */
/* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty styleSectionGlobal
getStyleWithNoCode msg prefs sync URLS */
/* exported styleManager */
/* global
API
calcStyleDigest
createCache
db
msg
prefs
stringAsRegExp
styleCodeEmpty
styleSectionGlobal
tryRegExp
URLS
*/
'use strict';
/*
@ -13,41 +22,34 @@ The live preview feature relies on `runtime.connect` and `port.onDisconnect`
to cleanup the temporary code. See /edit/live-preview.js.
*/
/** @type {styleManager} */
const styleManager = (() => {
const preparing = prepare();
/* exported styleManager */
const styleManager = API.styles = (() => {
/* styleId => {
data: styleData,
preview: styleData,
appliesTo: Set<url>
} */
const styles = new Map();
//#region Declarations
const ready = init();
/**
* @typedef StyleMapData
* @property {StyleObj} style
* @property {?StyleObj} [preview]
* @property {Set<string>} appliesTo - urls
*/
/** @type {Map<number,StyleMapData>} */
const dataMap = new Map();
const uuidIndex = new Map();
/* url => {
maybeMatch: Set<styleId>,
sections: Object<styleId => {
id: styleId,
code: Array<String>
}>
} */
/** @typedef {Object<styleId,{id: number, code: string[]}>} StyleSectionsToApply */
/** @type {Map<string,{maybeMatch: Set<styleId>, sections: StyleSectionsToApply}>} */
const cachedStyleForUrl = createCache({
onDeleted: (url, cache) => {
for (const section of Object.values(cache.sections)) {
const style = styles.get(section.id);
if (style) {
style.appliesTo.delete(url);
}
const data = id2data(section.id);
if (data) data.appliesTo.delete(url);
}
},
});
const BAD_MATCHER = {test: () => false};
const compileRe = createCompiler(text => `^(${text})$`);
const compileSloppyRe = createCompiler(text => `^${text}$`);
const compileExclusion = createCompiler(buildExclusion);
const DUMMY_URL = {
hash: '',
host: '',
@ -62,287 +64,256 @@ const styleManager = (() => {
searchParams: new URLSearchParams(),
username: '',
};
const MISSING_PROPS = {
name: style => `ID: ${style.id}`,
_id: () => uuidv4(),
_rev: () => Date.now(),
};
const DELETE_IF_NULL = ['id', 'customName'];
//#endregion
handleLivePreviewConnections();
chrome.runtime.onConnect.addListener(handleLivePreview);
//#region Public surface
// Sorted alphabetically
return {
return Object.assign(/** @namespace styleManager */{
compareRevision,
}, ensurePrepared(/** @namespace styleManager */{
get,
getByUUID,
getSectionsByUrl,
putByUUID,
installStyle,
deleteStyle,
deleteByUUID,
editSave,
findStyle,
importStyle,
importMany,
toggleStyle,
getAllStyles, // used by import-export
getStylesByUrl, // used by popup
styleExists,
addExclusion,
removeExclusion,
addInclusion,
removeInclusion,
/** @returns {Promise<number>} style id */
async delete(id, reason) {
await ready;
const data = id2data(id);
await db.exec('delete', id);
if (reason !== 'sync') {
API.sync.delete(data.style._id, Date.now());
}
for (const url of data.appliesTo) {
const cache = cachedStyleForUrl.get(url);
if (cache) delete cache.sections[id];
}
dataMap.delete(id);
uuidIndex.delete(data.style._id);
await msg.broadcast({
method: 'styleDeleted',
style: {id},
});
return id;
},
/** @returns {Promise<number>} style id */
async deleteByUUID(_id, rev) {
await ready;
const id = uuidIndex.get(_id);
const oldDoc = id && id2style(id);
if (oldDoc && compareRevision(oldDoc._rev, rev) <= 0) {
// FIXME: does it make sense to set reason to 'sync' in deleteByUUID?
return API.styles.delete(id, 'sync');
}
},
/** @returns {Promise<StyleObj>} */
async editSave(style) {
await ready;
style = mergeWithMapped(style);
style.updateDate = Date.now();
return handleSave(await saveStyle(style), 'editSave');
},
/** @returns {Promise<?StyleObj>} */
async find(filter) {
await ready;
const filterEntries = Object.entries(filter);
for (const {style} of dataMap.values()) {
if (filterEntries.every(([key, val]) => style[key] === val)) {
return style;
}
}
return null;
},
/** @returns {Promise<StyleObj[]>} */
async getAll() {
await ready;
return Array.from(dataMap.values(), data2style);
},
/** @returns {Promise<StyleObj>} */
async getByUUID(uuid) {
await ready;
return id2style(uuidIndex.get(uuid));
},
/** @returns {Promise<StyleSectionsToApply>} */
async getSectionsByUrl(url, id, isInitialApply) {
await ready;
let cache = cachedStyleForUrl.get(url);
if (!cache) {
cache = {
sections: {},
maybeMatch: new Set(),
};
buildCache(cache, url, dataMap.values());
cachedStyleForUrl.set(url, cache);
} else if (cache.maybeMatch.size) {
buildCache(cache, url, Array.from(cache.maybeMatch, id2data).filter(Boolean));
}
const res = id
? cache.sections[id] ? {[id]: cache.sections[id]} : {}
: cache.sections;
// Avoiding flicker of needlessly applied styles by providing both styles & pref in one API call
return isInitialApply && prefs.get('disableAll')
? Object.assign({disableAll: true}, res)
: res;
},
/** @returns {Promise<StyleObj>} */
async get(id) {
await ready;
return id2style(id);
},
/** @returns {Promise<StylesByUrlResult[]>} */
async getByUrl(url, id = null) {
await ready;
// FIXME: do we want to cache this? Who would like to open popup rapidly
// or search the DB with the same URL?
const result = [];
const styles = id
? [id2style(id)].filter(Boolean)
: Array.from(dataMap.values(), data2style);
const query = createMatchQuery(url);
for (const style of styles) {
let excluded = false;
let sloppy = false;
let sectionMatched = false;
const match = urlMatchStyle(query, style);
// TODO: enable this when the function starts returning false
// if (match === false) {
// continue;
// }
if (match === 'excluded') {
excluded = true;
}
for (const section of style.sections) {
if (styleSectionGlobal(section) && styleCodeEmpty(section.code)) {
continue;
}
const match = urlMatchSection(query, section);
if (match) {
if (match === 'sloppy') {
sloppy = true;
}
sectionMatched = true;
break;
}
}
if (sectionMatched) {
result.push(/** @namespace StylesByUrlResult */{style, excluded, sloppy});
}
}
return result;
},
/** @returns {Promise<StyleObj[]>} */
async importMany(items) {
await ready;
items.forEach(beforeSave);
const events = await db.exec('putMany', items);
return Promise.all(items.map((item, i) => {
afterSave(item, events[i]);
return handleSave(item, 'import');
}));
},
function handleLivePreviewConnections() {
chrome.runtime.onConnect.addListener(port => {
if (port.name !== 'livePreview') {
return;
}
let id;
port.onMessage.addListener(data => {
if (!id) {
id = data.id;
}
const style = styles.get(id);
style.preview = data;
broadcastStyleUpdated(style.preview, 'editPreview');
});
port.onDisconnect.addListener(() => {
port = null;
if (id) {
const style = styles.get(id);
if (!style) {
// maybe deleted
return;
}
style.preview = null;
broadcastStyleUpdated(style.data, 'editPreviewEnd');
}
});
});
}
/** @returns {Promise<StyleObj>} */
async import(data) {
await ready;
return handleSave(await saveStyle(data), 'import');
},
function escapeRegExp(text) {
// https://github.com/lodash/lodash/blob/0843bd46ef805dd03c0c8d804630804f3ba0ca3c/lodash.js#L152
return text.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
}
/** @returns {Promise<StyleObj>} */
async install(style, reason = null) {
await ready;
reason = reason || dataMap.has(style.id) ? 'update' : 'install';
style = mergeWithMapped(style);
const url = !style.url && style.updateUrl && (
URLS.extractUsoArchiveInstallUrl(style.updateUrl) ||
URLS.extractGreasyForkInstallUrl(style.updateUrl)
);
if (url) style.url = style.installationUrl = url;
style.originalDigest = await calcStyleDigest(style);
// FIXME: update updateDate? what about usercss config?
return handleSave(await saveStyle(style), reason);
},
function get(id, noCode = false) {
const data = styles.get(id).data;
return noCode ? getStyleWithNoCode(data) : data;
}
function getByUUID(uuid) {
const id = uuidIndex.get(uuid);
if (id) {
return get(id);
}
}
function getAllStyles() {
return [...styles.values()].map(s => s.data);
}
function compareRevision(rev1, rev2) {
return rev1 - rev2;
}
function putByUUID(doc) {
/** @returns {Promise<?StyleObj>} */
async putByUUID(doc) {
await ready;
const id = uuidIndex.get(doc._id);
if (id) {
doc.id = id;
} else {
delete doc.id;
}
const oldDoc = id && styles.has(id) && styles.get(id).data;
const oldDoc = id && id2style(id);
let diff = -1;
if (oldDoc) {
diff = compareRevision(oldDoc._rev, doc._rev);
if (diff > 0) {
sync.put(oldDoc._id, oldDoc._rev);
API.sync.put(oldDoc._id, oldDoc._rev);
return;
}
}
if (diff < 0) {
return db.exec('put', doc)
.then(event => {
doc.id = event.target.result;
doc.id = await db.exec('put', doc);
uuidIndex.set(doc._id, doc.id);
return handleSave(doc, 'sync');
});
}
},
/** @returns {Promise<number>} style id */
async toggle(id, enabled) {
await ready;
const style = Object.assign({}, id2style(id), {enabled});
handleSave(await saveStyle(style), 'toggle', false);
return id;
},
// using bind() to skip step-into when debugging
/** @returns {Promise<StyleObj>} */
addExclusion: addIncludeExclude.bind(null, 'exclusions'),
/** @returns {Promise<StyleObj>} */
addInclusion: addIncludeExclude.bind(null, 'inclusions'),
/** @returns {Promise<?StyleObj>} */
removeExclusion: removeIncludeExclude.bind(null, 'exclusions'),
/** @returns {Promise<?StyleObj>} */
removeInclusion: removeIncludeExclude.bind(null, 'inclusions'),
};
//#endregion
//#region Implementation
/** @returns {StyleMapData} */
function id2data(id) {
return dataMap.get(id);
}
function toggleStyle(id, enabled) {
const style = styles.get(id);
const data = Object.assign({}, style.data, {enabled});
return saveStyle(data)
.then(newData => handleSave(newData, 'toggle', false))
.then(() => id);
/** @returns {?StyleObj} */
function id2style(id) {
return (dataMap.get(id) || {}).style;
}
// used by install-hook-userstyles.js
function findStyle(filter, noCode = false) {
for (const style of styles.values()) {
if (filterMatch(filter, style.data)) {
return noCode ? getStyleWithNoCode(style.data) : style.data;
}
}
return null;
}
function styleExists(filter) {
return [...styles.values()].some(s => filterMatch(filter, s.data));
}
function filterMatch(filter, target) {
for (const key of Object.keys(filter)) {
if (filter[key] !== target[key]) {
return false;
}
}
return true;
}
function importStyle(data) {
// FIXME: is it a good idea to save the data directly?
return saveStyle(data)
.then(newData => handleSave(newData, 'import'));
}
function importMany(items) {
items.forEach(beforeSave);
return db.exec('putMany', items)
.then(events => {
for (let i = 0; i < items.length; i++) {
afterSave(items[i], events[i].target.result);
}
return Promise.all(items.map(i => handleSave(i, 'import')));
});
}
function installStyle(data, reason = null) {
const style = styles.get(data.id);
if (!style) {
data = Object.assign(createNewStyle(), data);
} else {
data = Object.assign({}, style.data, data);
}
if (!reason) {
reason = style ? 'update' : 'install';
}
let url = !data.url && data.updateUrl;
if (url) {
const usoId = URLS.extractUsoArchiveId(url);
url = usoId && `${URLS.usoArchive}?style=${usoId}` ||
URLS.extractGreasyForkId(url) && url.match(/^.*?\/\d+/)[0];
if (url) data.url = data.installationUrl = url;
}
// FIXME: update updateDate? what about usercss config?
return calcStyleDigest(data)
.then(digest => {
data.originalDigest = digest;
return saveStyle(data);
})
.then(newData => handleSave(newData, reason));
}
function editSave(data) {
const style = styles.get(data.id);
if (style) {
data = Object.assign({}, style.data, data);
} else {
data = Object.assign(createNewStyle(), data);
}
data.updateDate = Date.now();
return saveStyle(data)
.then(newData => handleSave(newData, 'editSave'));
}
function addIncludeExclude(id, rule, type) {
const data = Object.assign({}, styles.get(id).data);
if (!data[type]) {
data[type] = [];
}
if (data[type].includes(rule)) {
throw new Error('The rule already exists');
}
data[type] = data[type].concat([rule]);
return saveStyle(data)
.then(newData => handleSave(newData, 'styleSettings'));
}
function removeIncludeExclude(id, rule, type) {
const data = Object.assign({}, styles.get(id).data);
if (!data[type]) {
return;
}
if (!data[type].includes(rule)) {
return;
}
data[type] = data[type].filter(r => r !== rule);
return saveStyle(data)
.then(newData => handleSave(newData, 'styleSettings'));
}
function addExclusion(id, rule) {
return addIncludeExclude(id, rule, 'exclusions');
}
function removeExclusion(id, rule) {
return removeIncludeExclude(id, rule, 'exclusions');
}
function addInclusion(id, rule) {
return addIncludeExclude(id, rule, 'inclusions');
}
function removeInclusion(id, rule) {
return removeIncludeExclude(id, rule, 'inclusions');
}
function deleteStyle(id, reason) {
const style = styles.get(id);
const rev = Date.now();
return db.exec('delete', id)
.then(() => {
if (reason !== 'sync') {
sync.delete(style.data._id, rev);
}
for (const url of style.appliesTo) {
const cache = cachedStyleForUrl.get(url);
if (cache) {
delete cache.sections[id];
}
}
styles.delete(id);
uuidIndex.delete(style.data._id);
return msg.broadcast({
method: 'styleDeleted',
style: {id},
});
})
.then(() => id);
}
function deleteByUUID(_id, rev) {
const id = uuidIndex.get(_id);
const oldDoc = id && styles.has(id) && styles.get(id).data;
if (oldDoc && compareRevision(oldDoc._rev, rev) <= 0) {
// FIXME: does it make sense to set reason to 'sync' in deleteByUUID?
return deleteStyle(id, 'sync');
}
}
function ensurePrepared(methods) {
const prepared = {};
for (const [name, fn] of Object.entries(methods)) {
prepared[name] = (...args) =>
preparing.then(() => fn(...args));
}
return prepared;
/** @returns {?StyleObj} */
function data2style(data) {
return data && data.style;
}
/** @returns {StyleObj} */
function createNewStyle() {
return {
return /** @namespace StyleObj */{
enabled: true,
updateUrl: null,
md5Url: null,
@ -352,43 +323,105 @@ const styleManager = (() => {
};
}
function broadcastStyleUpdated(data, reason, method = 'styleUpdated', codeIsUpdated = true) {
const style = styles.get(data.id);
/** @returns {void} */
function storeInMap(style) {
dataMap.set(style.id, {
style,
appliesTo: new Set(),
});
}
/** @returns {StyleObj} */
function mergeWithMapped(style) {
return Object.assign({},
id2style(style.id) || createNewStyle(),
style);
}
function handleLivePreview(port) {
if (port.name !== 'livePreview') {
return;
}
let id;
port.onMessage.addListener(style => {
if (!id) id = style.id;
const data = id2data(id);
data.preview = style;
broadcastStyleUpdated(style, 'editPreview');
});
port.onDisconnect.addListener(() => {
port = null;
if (id) {
const data = id2data(id);
if (data) {
data.preview = null;
broadcastStyleUpdated(data.style, 'editPreviewEnd');
}
}
});
}
function compareRevision(rev1, rev2) {
return rev1 - rev2;
}
async function addIncludeExclude(type, id, rule) {
await ready;
const style = Object.assign({}, id2style(id));
const list = style[type] || (style[type] = []);
if (list.includes(rule)) {
throw new Error('The rule already exists');
}
style[type] = list.concat([rule]);
return handleSave(await saveStyle(style), 'styleSettings');
}
async function removeIncludeExclude(type, id, rule) {
await ready;
const style = Object.assign({}, id2style(id));
const list = style[type];
if (!list || !list.includes(rule)) {
return;
}
style[type] = list.filter(r => r !== rule);
return handleSave(await saveStyle(style), 'styleSettings');
}
function broadcastStyleUpdated(style, reason, method = 'styleUpdated', codeIsUpdated = true) {
const {id} = style;
const data = id2data(id);
const excluded = new Set();
const updated = new Set();
for (const [url, cache] of cachedStyleForUrl.entries()) {
if (!style.appliesTo.has(url)) {
cache.maybeMatch.add(data.id);
if (!data.appliesTo.has(url)) {
cache.maybeMatch.add(id);
continue;
}
const code = getAppliedCode(createMatchQuery(url), data);
if (!code) {
excluded.add(url);
delete cache.sections[data.id];
} else {
const code = getAppliedCode(createMatchQuery(url), style);
if (code) {
updated.add(url);
cache.sections[data.id] = {
id: data.id,
code,
};
cache.sections[id] = {id, code};
} else {
excluded.add(url);
delete cache.sections[id];
}
}
style.appliesTo = updated;
data.appliesTo = updated;
return msg.broadcast({
method,
style: {
id: data.id,
md5Url: data.md5Url,
enabled: data.enabled,
},
reason,
codeIsUpdated,
style: {
id,
md5Url: style.md5Url,
enabled: style.enabled,
},
});
}
function beforeSave(style) {
if (!style.name) {
throw new Error('style name is empty');
throw new Error('Style name is empty');
}
for (const key of DELETE_IF_NULL) {
if (style[key] == null) {
@ -407,114 +440,29 @@ const styleManager = (() => {
style.id = newId;
}
uuidIndex.set(style._id, style.id);
sync.put(style._id, style._rev);
API.sync.put(style._id, style._rev);
}
function saveStyle(style) {
async function saveStyle(style) {
beforeSave(style);
return db.exec('put', style)
.then(event => {
afterSave(style, event.target.result);
const newId = await db.exec('put', style);
afterSave(style, newId);
return style;
});
}
function handleSave(data, reason, codeIsUpdated) {
const style = styles.get(data.id);
let method;
if (!style) {
styles.set(data.id, {
appliesTo: new Set(),
data,
});
method = 'styleAdded';
function handleSave(style, reason, codeIsUpdated) {
const data = id2data(style.id);
const method = data ? 'styleUpdated' : 'styleAdded';
if (!data) {
storeInMap(style);
} else {
style.data = data;
method = 'styleUpdated';
data.style = style;
}
broadcastStyleUpdated(data, reason, method, codeIsUpdated);
return data;
broadcastStyleUpdated(style, reason, method, codeIsUpdated);
return style;
}
// get styles matching a URL, including sloppy regexps and excluded items.
function getStylesByUrl(url, id = null) {
// FIXME: do we want to cache this? Who would like to open popup rapidly
// or search the DB with the same URL?
const result = [];
const datas = !id ? [...styles.values()].map(s => s.data) :
styles.has(id) ? [styles.get(id).data] : [];
const query = createMatchQuery(url);
for (const data of datas) {
let excluded = false;
let sloppy = false;
let sectionMatched = false;
const match = urlMatchStyle(query, data);
// TODO: enable this when the function starts returning false
// if (match === false) {
// continue;
// }
if (match === 'excluded') {
excluded = true;
}
for (const section of data.sections) {
if (styleSectionGlobal(section) && styleCodeEmpty(section.code)) {
continue;
}
const match = urlMatchSection(query, section);
if (match) {
if (match === 'sloppy') {
sloppy = true;
}
sectionMatched = true;
break;
}
}
if (sectionMatched) {
result.push({data, excluded, sloppy});
}
}
return result;
}
function getSectionsByUrl(url, id, isInitialApply) {
let cache = cachedStyleForUrl.get(url);
if (!cache) {
cache = {
sections: {},
maybeMatch: new Set(),
};
buildCache(styles.values());
cachedStyleForUrl.set(url, cache);
} else if (cache.maybeMatch.size) {
buildCache(
[...cache.maybeMatch]
.filter(i => styles.has(i))
.map(i => styles.get(i))
);
}
const res = id
? cache.sections[id] ? {[id]: cache.sections[id]} : {}
: cache.sections;
// Avoiding flicker of needlessly applied styles by providing both styles & pref in one API call
return isInitialApply && prefs.get('disableAll')
? Object.assign({disableAll: true}, res)
: res;
function buildCache(styleList) {
const query = createMatchQuery(url);
for (const {appliesTo, data, preview} of styleList) {
const code = getAppliedCode(query, preview || data);
if (code) {
cache.sections[data.id] = {
id: data.id,
code,
};
appliesTo.add(url);
}
}
}
}
function getAppliedCode(query, data) {
if (urlMatchStyle(query, data) !== true) {
return;
@ -528,60 +476,45 @@ const styleManager = (() => {
return code.length && code;
}
function prepare() {
const ADD_MISSING_PROPS = {
name: style => `ID: ${style.id}`,
_id: () => uuidv4(),
_rev: () => Date.now(),
};
return db.exec('getAll')
.then(event => event.target.result || [])
.then(styleList => {
// setup missing _id, _rev
const updated = [];
for (const style of styleList) {
if (addMissingProperties(style)) {
updated.push(style);
}
}
async function init() {
const styles = await db.exec('getAll') || [];
const updated = styles.filter(style =>
addMissingProps(style) +
addCustomName(style));
if (updated.length) {
return db.exec('putMany', updated)
.then(() => styleList);
await db.exec('putMany', updated);
}
return styleList;
})
.then(styleList => {
for (const style of styleList) {
for (const style of styles) {
fixUsoMd5Issue(style);
styles.set(style.id, {
appliesTo: new Set(),
data: style,
});
storeInMap(style);
uuidIndex.set(style._id, style.id);
}
});
}
function addMissingProperties(style) {
let touched = false;
for (const key in ADD_MISSING_PROPS) {
function addMissingProps(style) {
let res = 0;
for (const key in MISSING_PROPS) {
if (!style[key]) {
style[key] = ADD_MISSING_PROPS[key](style);
touched = true;
style[key] = MISSING_PROPS[key](style);
res = 1;
}
}
// upgrade the old way of customizing local names
return res;
}
/** Upgrades the old way of customizing local names */
function addCustomName(style) {
let res = 0;
const {originalName} = style;
if (originalName) {
touched = true;
res = 1;
if (originalName !== style.name) {
style.customName = style.name;
style.name = originalName;
}
delete style.originalName;
}
return touched;
}
return res;
}
function urlMatchStyle(query, style) {
@ -652,7 +585,8 @@ const styleManager = (() => {
}
function compileGlob(text) {
return escapeRegExp(text).replace(/\\\\\\\*|\\\*/g, m => m.length > 2 ? m : '.*');
return stringAsRegExp(text, '', true)
.replace(/\\\\\\\*|\\\*/g, m => m.length > 2 ? m : '.*');
}
function buildExclusion(text) {
@ -706,6 +640,18 @@ const styleManager = (() => {
};
}
function buildCache(cache, url, styleList) {
const query = createMatchQuery(url);
for (const {style, appliesTo, preview} of styleList) {
const code = getAppliedCode(query, preview || style);
if (code) {
const id = style.id;
cache.sections[id] = {id, code};
appliesTo.add(url);
}
}
}
function createURL(url) {
try {
return new URL(url);
@ -726,4 +672,5 @@ const styleManager = (() => {
function hex4dashed(num, i) {
return (num + 0x10000).toString(16).slice(-4) + (i >= 1 && i <= 4 ? '-' : '');
}
//#endregion
})();

View File

@ -1,7 +1,7 @@
/* global API_METHODS styleManager CHROME prefs */
/* global API CHROME prefs */
'use strict';
API_METHODS.styleViaAPI = !CHROME && (() => {
API.styleViaAPI = !CHROME && (() => {
const ACTIONS = {
styleApply,
styleDeleted,
@ -37,7 +37,7 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
throw new Error('we do not count styles for frames');
}
const {frameStyles} = getCachedData(tab.id, frameId);
API_METHODS.updateIconBadge.call({sender}, Object.keys(frameStyles));
API.updateIconBadge.call({sender}, Object.keys(frameStyles));
}
function styleApply({id = null, ignoreUrlCheck = false}, {tab, frameId, url}) {
@ -48,7 +48,7 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
if (id === null && !ignoreUrlCheck && frameStyles.url === url) {
return NOP;
}
return styleManager.getSectionsByUrl(url, id).then(sections => {
return API.styles.getSectionsByUrl(url, id).then(sections => {
const tasks = [];
for (const section of Object.values(sections)) {
const styleId = section.id;

View File

@ -1,4 +1,8 @@
/* global API CHROME prefs */
/* global
API
CHROME
prefs
*/
'use strict';
// eslint-disable-next-line no-unused-expressions
@ -67,14 +71,14 @@ CHROME && (async () => {
}
/** @param {chrome.webRequest.WebRequestBodyDetails} req */
function prepareStyles(req) {
API.getSectionsByUrl(req.url).then(sections => {
async function prepareStyles(req) {
const sections = await API.styles.getSectionsByUrl(req.url);
if (Object.keys(sections).length) {
stylesToPass[req.requestId] = !enabled.xhr ? true :
URL.createObjectURL(new Blob([JSON.stringify(sections)])).slice(blobUrlPrefix.length);
URL.createObjectURL(new Blob([JSON.stringify(sections)]))
.slice(blobUrlPrefix.length);
setTimeout(cleanUp, 600e3, req.requestId);
}
});
}
/** @param {chrome.webRequest.WebResponseHeadersDetails} req */

View File

@ -1,13 +1,23 @@
/* global dbToCloud styleManager chromeLocal prefs tokenManager msg */
/* global
API
chromeLocal
dbToCloud
msg
prefs
styleManager
tokenManager
*/
/* exported sync */
'use strict';
const sync = (() => {
const sync = API.sync = (() => {
const SYNC_DELAY = 1; // minutes
const SYNC_INTERVAL = 30; // minutes
/** @typedef API.sync.Status */
const status = {
/** @type {'connected'|'connecting'|'disconnected'|'disconnecting'} */
state: 'disconnected',
syncing: false,
progress: null,
@ -18,21 +28,30 @@ const sync = (() => {
let currentDrive;
const ctrl = dbToCloud.dbToCloud({
onGet(id) {
return styleManager.getByUUID(id);
return API.styles.getByUUID(id);
},
onPut(doc) {
return styleManager.putByUUID(doc);
return API.styles.putByUUID(doc);
},
onDelete(id, rev) {
return styleManager.deleteByUUID(id, rev);
return API.styles.deleteByUUID(id, rev);
},
onFirstSync() {
return styleManager.getAllStyles()
.then(styles => {
styles.forEach(i => ctrl.put(i._id, i._rev));
});
async onFirstSync() {
for (const i of await API.styles.getAll()) {
ctrl.put(i._id, i._rev);
}
},
onProgress(e) {
if (e.phase === 'start') {
status.syncing = true;
} else if (e.phase === 'end') {
status.syncing = false;
status.progress = null;
} else {
status.progress = e;
}
emitStatusChange();
},
onProgress,
compareRevision(a, b) {
return styleManager.compareRevision(a, b);
},
@ -46,55 +65,126 @@ const sync = (() => {
},
});
const initializing = prefs.initializing.then(() => {
prefs.subscribe(['sync.enabled'], onPrefChange);
onPrefChange(null, prefs.get('sync.enabled'));
const ready = prefs.initializing.then(() => {
prefs.subscribe('sync.enabled',
(_, val) => val === 'none'
? sync.stop()
: sync.start(val, true),
{now: true});
});
chrome.alarms.onAlarm.addListener(info => {
if (info.name === 'syncNow') {
syncNow().catch(console.error);
sync.syncNow();
}
});
return Object.assign({
getStatus: () => status,
}, ensurePrepared({
start,
stop,
put: (...args) => {
if (!currentDrive) return;
schedule();
return ctrl.put(...args);
},
delete: (...args) => {
// Sorted alphabetically
return {
async delete(...args) {
await ready;
if (!currentDrive) return;
schedule();
return ctrl.delete(...args);
},
syncNow,
login,
}));
function ensurePrepared(obj) {
return Object.entries(obj).reduce((o, [key, fn]) => {
o[key] = (...args) =>
initializing.then(() => fn(...args));
return o;
}, {});
/**
* @returns {Promise<API.sync.Status>}
*/
async getStatus() {
return status;
},
async login(name = prefs.get('sync.enabled')) {
await ready;
try {
await tokenManager.getToken(name, true);
} catch (err) {
if (/Authorization page could not be loaded/i.test(err.message)) {
// FIXME: Chrome always fails at the first login so we try again
await tokenManager.getToken(name);
}
throw err;
}
status.login = true;
emitStatusChange();
},
function onProgress(e) {
if (e.phase === 'start') {
status.syncing = true;
} else if (e.phase === 'end') {
status.syncing = false;
status.progress = null;
} else {
status.progress = e;
async put(...args) {
await ready;
if (!currentDrive) return;
schedule();
return ctrl.put(...args);
},
async start(name, fromPref = false) {
await ready;
if (currentDrive) {
return;
}
currentDrive = getDrive(name);
ctrl.use(currentDrive);
status.state = 'connecting';
status.currentDriveName = currentDrive.name;
status.login = true;
emitStatusChange();
try {
if (!fromPref) {
await sync.login(name).catch(handle401Error);
}
await sync.syncNow();
status.errorMessage = null;
} catch (err) {
status.errorMessage = err.message;
// FIXME: should we move this logic to options.js?
if (!fromPref) {
console.error(err);
return sync.stop();
}
}
prefs.set('sync.enabled', name);
status.state = 'connected';
schedule(SYNC_INTERVAL);
emitStatusChange();
},
async stop() {
await ready;
if (!currentDrive) {
return;
}
chrome.alarms.clear('syncNow');
status.state = 'disconnecting';
emitStatusChange();
try {
await ctrl.stop();
await tokenManager.revokeToken(currentDrive.name);
await chromeLocal.remove(`sync/state/${currentDrive.name}`);
} catch (e) {
}
currentDrive = null;
prefs.set('sync.enabled', 'none');
status.state = 'disconnected';
status.currentDriveName = null;
status.login = false;
emitStatusChange();
},
async syncNow() {
await ready;
if (!currentDrive) {
return Promise.reject(new Error('cannot sync when disconnected'));
}
try {
await (ctrl.isInit() ? ctrl.syncNow() : ctrl.start()).catch(handle401Error);
status.errorMessage = null;
} catch (err) {
status.errorMessage = err.message;
}
emitStatusChange();
}
},
};
function schedule(delay = SYNC_DELAY) {
chrome.alarms.create('syncNow', {
@ -103,106 +193,25 @@ const sync = (() => {
});
}
function onPrefChange(key, value) {
if (value === 'none') {
stop().catch(console.error);
} else {
start(value, true).catch(console.error);
}
}
function withFinally(p, cleanup) {
return p.then(
result => {
cleanup(undefined, result);
return result;
},
err => {
cleanup(err);
throw err;
}
);
}
function syncNow() {
if (!currentDrive) {
return Promise.reject(new Error('cannot sync when disconnected'));
}
return withFinally(
(ctrl.isInit() ? ctrl.syncNow() : ctrl.start())
.catch(handle401Error),
err => {
status.errorMessage = err ? err.message : null;
emitStatusChange();
}
);
}
function handle401Error(err) {
async function handle401Error(err) {
let emit;
if (err.code === 401) {
return tokenManager.revokeToken(currentDrive.name)
.catch(console.error)
.then(() => {
status.login = false;
emitStatusChange();
throw err;
});
await tokenManager.revokeToken(currentDrive.name).catch(console.error);
emit = true;
} else if (/User interaction required|Requires user interaction/i.test(err.message)) {
emit = true;
}
if (/User interaction required|Requires user interaction/i.test(err.message)) {
if (emit) {
status.login = false;
emitStatusChange();
}
throw err;
return Promise.reject(err);
}
function emitStatusChange() {
msg.broadcastExtension({method: 'syncStatusUpdate', status});
}
function login(name = prefs.get('sync.enabled')) {
return tokenManager.getToken(name, true)
.catch(err => {
if (/Authorization page could not be loaded/i.test(err.message)) {
// FIXME: Chrome always fails at the first login so we try again
return tokenManager.getToken(name);
}
throw err;
})
.then(() => {
status.login = true;
emitStatusChange();
});
}
function start(name, fromPref = false) {
if (currentDrive) {
return Promise.resolve();
}
currentDrive = getDrive(name);
ctrl.use(currentDrive);
status.state = 'connecting';
status.currentDriveName = currentDrive.name;
status.login = true;
emitStatusChange();
return withFinally(
(fromPref ? Promise.resolve() : login(name))
.catch(handle401Error)
.then(() => syncNow()),
err => {
status.errorMessage = err ? err.message : null;
// FIXME: should we move this logic to options.js?
if (err && !fromPref) {
console.error(err);
return stop();
}
prefs.set('sync.enabled', name);
schedule(SYNC_INTERVAL);
status.state = 'connected';
emitStatusChange();
}
);
}
function getDrive(name) {
if (name === 'dropbox' || name === 'google' || name === 'onedrive') {
return dbToCloud.drive[name]({
@ -211,26 +220,4 @@ const sync = (() => {
}
throw new Error(`unknown cloud name: ${name}`);
}
function stop() {
if (!currentDrive) {
return Promise.resolve();
}
chrome.alarms.clear('syncNow');
status.state = 'disconnecting';
emitStatusChange();
return withFinally(
ctrl.stop()
.then(() => tokenManager.revokeToken(currentDrive.name))
.then(() => chromeLocal.remove(`sync/state/${currentDrive.name}`)),
() => {
currentDrive = null;
prefs.set('sync.enabled', 'none');
status.state = 'disconnected';
status.currentDriveName = null;
status.login = false;
emitStatusChange();
}
);
}
})();

View File

@ -1,27 +1,23 @@
/* global
API_METHODS
API
calcStyleDigest
chromeLocal
debounce
download
getStyleWithNoCode
ignoreChromeError
prefs
semverCompare
styleJSONseemsValid
styleManager
styleSectionsEqual
tryJSONparse
usercss
*/
'use strict';
(() => {
const STATES = {
const STATES = /** @namespace UpdaterStates */{
UPDATED: 'updated',
SKIPPED: 'skipped',
UNREACHABLE: 'server unreachable',
// details for SKIPPED status
EDITED: 'locally edited',
MAYBE_EDITED: 'may be locally edited',
@ -32,20 +28,22 @@
ERROR_JSON: 'error: JSON is invalid',
ERROR_VERSION: 'error: version is older than installed style',
};
const ALARM_NAME = 'scheduledUpdate';
const MIN_INTERVAL_MS = 60e3;
const RETRY_ERRORS = [
503, // service unavailable
429, // too many requests
];
let lastUpdateTime;
let checkingAll = false;
let logQueue = [];
let logLastWriteTime = 0;
const retrying = new Set();
API_METHODS.updateCheckAll = checkAllStyles;
API_METHODS.updateCheck = checkStyle;
API_METHODS.getUpdaterStates = () => STATES;
API.updater = {
checkAllStyles,
checkStyle,
getStates: () => STATES,
};
chromeLocal.getValue('lastUpdateTime').then(val => {
lastUpdateTime = val || Date.now();
@ -53,45 +51,46 @@
chrome.alarms.onAlarm.addListener(onAlarm);
});
return {checkAllStyles, checkStyle, STATES};
function checkAllStyles({
async function checkAllStyles({
save = true,
ignoreDigest,
observe,
} = {}) {
resetInterval();
checkingAll = true;
retrying.clear();
const port = observe && chrome.runtime.connect({name: 'updater'});
return styleManager.getAllStyles().then(styles => {
styles = styles.filter(style => style.updateUrl);
const styles = (await API.styles.getAll())
.filter(style => style.updateUrl);
if (port) port.postMessage({count: styles.length});
log('');
log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`);
return Promise.all(
await Promise.all(
styles.map(style =>
checkStyle({style, port, save, ignoreDigest})));
}).then(() => {
if (port) port.postMessage({done: true});
if (port) port.disconnect();
log('');
checkingAll = false;
retrying.clear();
});
}
function checkStyle({
id,
style,
port,
save = true,
ignoreDigest,
}) {
/*
/**
* @param {{
id?: number
style?: StyleObj
port?: chrome.runtime.Port
save?: boolean = true
ignoreDigest?: boolean
}} opts
* @returns {{
style: StyleObj
updated?: boolean
error?: any
STATES: UpdaterStates
}}
Original style digests are calculated in these cases:
* style is installed or updated from server
* style is checked for an update and its code is equal to the server code
* non-usercss style is checked for an update and styleSectionsEqual considers it unchanged
Update check proceeds in these cases:
* style has the original digest and it's equal to the current digest
@ -102,142 +101,109 @@
'ignoreDigest' option is set on the second manual individual update check on the manage page.
*/
return fetchStyle()
.then(() => {
if (!ignoreDigest) {
return calcStyleDigest(style)
.then(checkIfEdited);
async function checkStyle(opts) {
const {
id,
style = await API.styles.get(id),
ignoreDigest,
port,
save,
} = opts;
const ucd = style.usercssData;
let res, state;
try {
await checkIfEdited();
res = {
style: await (ucd ? updateUsercss : updateUSO)().then(maybeSave),
updated: true,
};
state = STATES.UPDATED;
} catch (err) {
const error = err === 0 && STATES.UNREACHABLE ||
err && err.message ||
err;
res = {error, style, STATES};
state = `${STATES.SKIPPED} (${error})`;
}
})
.then(() => {
if (style.usercssData) {
return maybeUpdateUsercss();
}
return maybeUpdateUSO();
})
.then(maybeSave)
.then(reportSuccess)
.catch(reportFailure);
log(`${state} #${style.id} ${style.customName || style.name}`);
if (port) port.postMessage(res);
return res;
function fetchStyle() {
if (style) {
return Promise.resolve();
}
return styleManager.get(id)
.then(style_ => {
style = style_;
});
}
function reportSuccess(saved) {
log(STATES.UPDATED + ` #${style.id} ${style.customName || style.name}`);
const info = {updated: true, style: saved};
if (port) port.postMessage(info);
return info;
}
function reportFailure(error) {
if ((
error === 503 || // Service Unavailable
error === 429 // Too Many Requests
) && !retrying.has(id)) {
retrying.add(id);
return new Promise(resolve => {
setTimeout(() => {
resolve(checkStyle({id, style, port, save, ignoreDigest}));
}, 1000);
});
}
error = error === 0 ? 'server unreachable' : error;
// UserCSS metadata error returns an object; e.g. "Invalid @var color..."
if (typeof error === 'object' && error.message) {
error = error.message;
}
log(STATES.SKIPPED + ` (${error}) #${style.id} ${style.customName || style.name}`);
const info = {error, STATES, style: getStyleWithNoCode(style)};
if (port) port.postMessage(info);
return info;
}
function checkIfEdited(digest) {
if (style.originalDigest && style.originalDigest !== digest) {
async function checkIfEdited() {
if (!ignoreDigest &&
style.originalDigest &&
style.originalDigest !== await calcStyleDigest(style)) {
return Promise.reject(STATES.EDITED);
}
}
function maybeUpdateUSO() {
return download(style.md5Url).then(md5 => {
async function updateUSO() {
const md5 = await tryDownload(style.md5Url);
if (!md5 || md5.length !== 32) {
return Promise.reject(STATES.ERROR_MD5);
}
if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
return Promise.reject(STATES.SAME_MD5);
}
// USO can't handle POST requests for style json
return download(style.updateUrl, {body: null})
.then(text => {
const style = tryJSONparse(text);
if (style) {
// USO may not provide a correctly updated originalMd5 (#555)
style.originalMd5 = md5;
const json = await tryDownload(style.updateUrl, {responseType: 'json'});
if (!styleJSONseemsValid(json)) {
return Promise.reject(STATES.ERROR_JSON);
}
return style;
});
});
// USO may not provide a correctly updated originalMd5 (#555)
json.originalMd5 = md5;
return json;
}
function maybeUpdateUsercss() {
async function updateUsercss() {
// TODO: when sourceCode is > 100kB use http range request(s) for version check
return download(style.updateUrl).then(text =>
usercss.buildMeta(text).then(json => {
const {usercssData: {version}} = style;
const {usercssData: {version: newVersion}} = json;
switch (Math.sign(semverCompare(version, newVersion))) {
case 0:
const text = await tryDownload(style.updateUrl);
const json = await usercss.buildMeta(text);
const delta = semverCompare(json.usercssData.version, ucd.version);
if (!delta && !ignoreDigest) {
// re-install is invalid in a soft upgrade
if (!ignoreDigest) {
const sameCode = text === style.sourceCode;
return Promise.reject(sameCode ? STATES.SAME_CODE : STATES.SAME_VERSION);
}
break;
case 1:
if (delta < 0) {
// downgrade is always invalid
return Promise.reject(STATES.ERROR_VERSION);
}
return usercss.buildCode(json);
})
);
}
function maybeSave(json = {}) {
// usercss is already validated while building
if (!json.usercssData && !styleJSONseemsValid(json)) {
return Promise.reject(STATES.ERROR_JSON);
}
async function maybeSave(json) {
json.id = style.id;
json.updateDate = Date.now();
// keep current state
delete json.customName;
delete json.enabled;
const newStyle = Object.assign({}, style, json);
if (!style.usercssData && styleSectionsEqual(json, style)) {
// update digest even if save === false as there might be just a space added etc.
return styleManager.installStyle(newStyle)
.then(saved => {
style.originalDigest = saved.originalDigest;
if (!ucd && styleSectionsEqual(json, style)) {
style.originalDigest = (await API.styles.install(newStyle)).originalDigest;
return Promise.reject(STATES.SAME_CODE);
});
}
if (!style.originalDigest && !ignoreDigest) {
return Promise.reject(STATES.MAYBE_EDITED);
}
return !save ? newStyle :
(ucd ? API.usercss : API.styles).install(newStyle);
}
return save ?
API_METHODS[json.usercssData ? 'installUsercss' : 'installStyle'](newStyle) :
newStyle;
async function tryDownload(url, params) {
let {retryDelay = 1000} = opts;
while (true) {
try {
return await download(url, params);
} catch (code) {
if (!RETRY_ERRORS.includes(code) ||
retryDelay > MIN_INTERVAL_MS) {
return Promise.reject(code);
}
}
retryDelay *= 1.25;
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
}
}

View File

@ -0,0 +1,81 @@
/* global
API
deepCopy
usercss
*/
'use strict';
API.usercss = {
async build({
styleId,
sourceCode,
vars,
checkDup,
metaOnly,
assignVars,
}) {
let style = await usercss.buildMeta(sourceCode);
const dup = (checkDup || assignVars) &&
await API.usercss.find(styleId ? {id: styleId} : style);
if (!metaOnly) {
if (vars || assignVars) {
await usercss.assignVars(style, vars ? {usercssData: {vars}} : dup);
}
style = await usercss.buildCode(style);
}
return {style, dup};
},
async buildMeta(style) {
if (style.usercssData) {
return style;
}
// allow sourceCode to be normalized
const {sourceCode} = style;
delete style.sourceCode;
return Object.assign(await usercss.buildMeta(sourceCode), style);
},
async configVars(id, vars) {
let style = deepCopy(await API.styles.get(id));
style.usercssData.vars = vars;
style = await usercss.buildCode(style);
style = await API.styles.install(style, 'config');
return style.usercssData.vars;
},
async editSave(style) {
return API.styles.editSave(await API.usercss.parse(style));
},
async find(styleOrData) {
if (styleOrData.id) {
return API.styles.get(styleOrData.id);
}
const {name, namespace} = styleOrData.usercssData || styleOrData;
for (const dup of await API.styles.getAll()) {
const data = dup.usercssData;
if (data &&
data.name === name &&
data.namespace === namespace) {
return dup;
}
}
},
async install(style) {
return API.styles.install(await API.usercss.parse(style));
},
async parse(style) {
style = await API.usercss.buildMeta(style);
// preserve style.vars during update
const dup = await API.usercss.find(style);
if (dup) {
style.id = dup.id;
await usercss.assignVars(style, dup);
}
return usercss.buildCode(style);
},
};

View File

@ -1,132 +0,0 @@
/* global API_METHODS usercss styleManager deepCopy */
/* exported usercssHelper */
'use strict';
const usercssHelper = (() => {
API_METHODS.installUsercss = installUsercss;
API_METHODS.editSaveUsercss = editSaveUsercss;
API_METHODS.configUsercssVars = configUsercssVars;
API_METHODS.buildUsercss = build;
API_METHODS.buildUsercssMeta = buildMeta;
API_METHODS.findUsercss = find;
function buildMeta(style) {
if (style.usercssData) {
return Promise.resolve(style);
}
// allow sourceCode to be normalized
const {sourceCode} = style;
delete style.sourceCode;
return usercss.buildMeta(sourceCode)
.then(newStyle => Object.assign(newStyle, style));
}
function assignVars(style) {
return find(style)
.then(dup => {
if (dup) {
style.id = dup.id;
// preserve style.vars during update
return usercss.assignVars(style, dup)
.then(() => style);
}
return style;
});
}
/**
* Parse the source, find the duplication, and build sections with variables
* @param _
* @param {String} _.sourceCode
* @param {Boolean=} _.checkDup
* @param {Boolean=} _.metaOnly
* @param {Object} _.vars
* @param {Boolean=} _.assignVars
* @returns {Promise<{style, dup:Boolean?}>}
*/
function build({
styleId,
sourceCode,
checkDup,
metaOnly,
vars,
assignVars = false,
}) {
return usercss.buildMeta(sourceCode)
.then(style => {
const findDup = checkDup || assignVars ?
find(styleId ? {id: styleId} : style) : Promise.resolve();
return Promise.all([
metaOnly ? style : doBuild(style, findDup),
findDup,
]);
})
.then(([style, dup]) => ({style, dup}));
function doBuild(style, findDup) {
if (vars || assignVars) {
const getOld = vars ? Promise.resolve({usercssData: {vars}}) : findDup;
return getOld
.then(oldStyle => usercss.assignVars(style, oldStyle))
.then(() => usercss.buildCode(style));
}
return usercss.buildCode(style);
}
}
// Build the style within aditional properties then inherit variable values
// from the old style.
function parse(style) {
return buildMeta(style)
.then(buildMeta)
.then(assignVars)
.then(usercss.buildCode);
}
// FIXME: simplify this to `installUsercss(sourceCode)`?
function installUsercss(style) {
return parse(style)
.then(styleManager.installStyle);
}
// FIXME: simplify this to `editSaveUsercss({sourceCode, exclusions})`?
function editSaveUsercss(style) {
return parse(style)
.then(styleManager.editSave);
}
function configUsercssVars(id, vars) {
return styleManager.get(id)
.then(style => {
const newStyle = deepCopy(style);
newStyle.usercssData.vars = vars;
return usercss.buildCode(newStyle);
})
.then(style => styleManager.installStyle(style, 'config'))
.then(style => style.usercssData.vars);
}
/**
* @param {Style|{name:string, namespace:string}} styleOrData
* @returns {Style}
*/
function find(styleOrData) {
if (styleOrData.id) {
return styleManager.get(styleOrData.id);
}
const {name, namespace} = styleOrData.usercssData || styleOrData;
return styleManager.getAllStyles().then(styleList => {
for (const dup of styleList) {
const data = dup.usercssData;
if (!data) continue;
if (data.name === name &&
data.namespace === namespace) {
return dup;
}
}
});
}
})();

View File

@ -1,5 +1,5 @@
/* global
API_METHODS
API
download
openURL
tabManager
@ -25,7 +25,7 @@
isContentTypeText((await fetch(url, {method: 'HEAD'})).headers.get('content-type'))
) && download(url);
API_METHODS.getUsercssInstallCode = url => {
API.usercss.getInstallCode = url => {
// when the installer tab is reloaded after the cache is expired, this will throw intentionally
const {code, timer} = installCodeCache[url];
clearInstallCode(url);

View File

@ -60,7 +60,7 @@ self.INJECTED !== 1 && (() => {
await API.styleViaAPI({method: 'styleApply'});
} else {
const styles = chrome.app && !chrome.tabs && getStylesViaXhr() ||
await API.getSectionsByUrl(getMatchUrl(), null, true);
await API.styles.getSectionsByUrl(getMatchUrl(), null, true);
if (styles.disableAll) {
delete styles.disableAll;
styleInjector.toggle(false);
@ -117,7 +117,7 @@ self.INJECTED !== 1 && (() => {
case 'styleUpdated':
if (request.style.enabled) {
API.getSectionsByUrl(getMatchUrl(), request.style.id)
API.styles.getSectionsByUrl(getMatchUrl(), request.style.id)
.then(sections => {
if (!sections[request.style.id]) {
styleInjector.remove(request.style.id);
@ -132,13 +132,13 @@ self.INJECTED !== 1 && (() => {
case 'styleAdded':
if (request.style.enabled) {
API.getSectionsByUrl(getMatchUrl(), request.style.id)
API.styles.getSectionsByUrl(getMatchUrl(), request.style.id)
.then(styleInjector.apply);
}
break;
case 'urlChanged':
API.getSectionsByUrl(getMatchUrl())
API.styles.getSectionsByUrl(getMatchUrl())
.then(styleInjector.replace);
break;

View File

@ -13,7 +13,7 @@ if (window.INJECTED_GREASYFORK !== 1) {
e.data.name &&
e.data.type === 'style-version-query') {
removeEventListener('message', onMessage);
const style = await API.findUsercss(e.data) || {};
const style = await API.usercss.find(e.data) || {};
const {version} = style.usercssData || {};
postMessage({type: 'style-version', version}, '*');
}

View File

@ -34,7 +34,7 @@
&& event.data.type === 'ouc-is-installed'
&& allowedOrigins.includes(event.origin)
) {
API.findUsercss({
API.usercss.find({
name: event.data.name,
namespace: event.data.namespace,
}).then(style => {
@ -129,7 +129,7 @@
&& event.data.type === 'ouc-install-usercss'
&& allowedOrigins.includes(event.origin)
) {
API.installUsercss({
API.usercss.install({
name: event.data.title,
sourceCode: event.data.code,
}).then(style => {

View File

@ -1,19 +1,21 @@
'use strict';
// preventing reregistration if reinjected by tabs.executeScript for whatever reason, just in case
if (typeof self.oldCode !== 'string') {
self.oldCode = (document.querySelector('body > pre') || document.body).textContent;
if (typeof window.oldCode !== 'string') {
window.oldCode = (document.querySelector('body > pre') || document.body).textContent;
chrome.runtime.onConnect.addListener(port => {
if (port.name !== 'downloadSelf') return;
port.onMessage.addListener(({id, force}) => {
fetch(location.href, {mode: 'same-origin'})
.then(r => r.text())
.then(code => ({id, code: force || code !== self.oldCode ? code : null}))
.catch(error => ({id, error: error.message || `${error}`}))
.then(msg => {
port.onMessage.addListener(async ({id, force}) => {
const msg = {id};
try {
const code = await (await fetch(location.href, {mode: 'same-origin'})).text();
if (code !== window.oldCode || force) {
msg.code = window.oldCode = code;
}
} catch (error) {
msg.error = error.message || `${error}`;
}
port.postMessage(msg);
if (msg.code != null) self.oldCode = msg.code;
});
});
// FF keeps content scripts connected on navigation https://github.com/openstyles/stylus/issues/864
addEventListener('pagehide', () => port.disconnect(), {once: true});
@ -21,4 +23,4 @@ if (typeof self.oldCode !== 'string') {
}
// passing the result to tabs.executeScript
self.oldCode; // eslint-disable-line no-unused-expressions
window.oldCode; // eslint-disable-line no-unused-expressions

View File

@ -24,7 +24,7 @@
let currentMd5;
const md5Url = getMeta('stylish-md5-url') || `https://update.userstyles.org/${styleId}.md5`;
Promise.all([
API.findStyle({md5Url}),
API.styles.find({md5Url}),
getResource(md5Url),
onDOMready(),
]).then(checkUpdatability);
@ -154,9 +154,9 @@
function doInstall() {
let oldStyle;
return API.findStyle({
return API.styles.find({
md5Url: getMeta('stylish-md5-url') || location.href,
}, true)
})
.then(_oldStyle => {
oldStyle = _oldStyle;
return oldStyle ?
@ -187,7 +187,7 @@
return;
}
// Update originalMd5 since USO changed it (2018-11-11) to NOT match the current md5
return API.installStyle(Object.assign(json, addProps, {originalMd5: currentMd5}))
return API.styles.install(Object.assign(json, addProps, {originalMd5: currentMd5}))
.then(style => {
if (!isNew && style.updateUrl.includes('?')) {
enableUpdateButton(true);
@ -218,20 +218,15 @@
return e ? e.getAttribute('href') : null;
}
function getResource(url, options) {
if (url.startsWith('#')) {
return Promise.resolve(document.getElementById(url.slice(1)).textContent);
async function getResource(url, type = 'text') {
try {
return url.startsWith('#')
? document.getElementById(url.slice(1)).textContent
: await (await fetch(url))[type];
} catch (error) {
alert('Error\n' + error.message);
return Promise.reject(error);
}
return API.download(Object.assign({
url,
timeout: 60e3,
// USO can't handle POST requests for style json
body: null,
}, options))
.catch(error => {
alert('Error' + (error ? '\n' + error : ''));
throw error;
});
}
// USO providing md5Url as "https://update.update.userstyles.org/#####.md5"
@ -244,7 +239,7 @@
}
function getStyleJson() {
return getResource(getStyleURL(), {responseType: 'json'})
return getResource(getStyleURL(), 'json')
.then(style => {
if (!style || !Array.isArray(style.sections) || style.sections.length) {
return style;
@ -254,7 +249,7 @@
return style;
}
return getResource(getMeta('stylish-update-url'))
.then(code => API.parseCss({code}))
.then(code => API.worker.parseMozFormat({code}))
.then(result => {
style.sections = result.sections;
return style;

View File

@ -110,7 +110,7 @@ lazyInit();
async function initStyle() {
const params = new URLSearchParams(location.search);
const id = Number(params.get('id'));
style = id ? await API.getStyle(id) : initEmptyStyle(params);
style = id ? await API.styles.get(id) : initEmptyStyle(params);
// switching the mode here to show the correct page ASAP, usually before DOMContentLoaded
editor.isUsercss = Boolean(style.usercssData || !style.id && prefs.get('newStyleAsUsercss'));
document.documentElement.classList.toggle('usercss', editor.isUsercss);
@ -426,26 +426,18 @@ function lazyInit() {
}
function onRuntimeMessage(request) {
const {style} = request;
switch (request.method) {
case 'styleUpdated':
if (
editor.style.id === request.style.id &&
!['editPreview', 'editPreviewEnd', 'editSave', 'config']
.includes(request.reason)
) {
Promise.resolve(
request.codeIsUpdated === false ?
request.style : API.getStyle(request.style.id)
)
.then(newStyle => {
editor.replaceStyle(newStyle, request.codeIsUpdated);
});
if (editor.style.id === style.id &&
!['editPreview', 'editPreviewEnd', 'editSave', 'config'].includes(request.reason)) {
Promise.resolve(request.codeIsUpdated === false ? style : API.styles.get(style.id))
.then(newStyle => editor.replaceStyle(newStyle, request.codeIsUpdated));
}
break;
case 'styleDeleted':
if (editor.style.id === request.style.id) {
if (editor.style.id === style.id) {
closeCurrentTab();
break;
}
break;
case 'editDeleteText':

View File

@ -117,7 +117,7 @@ function SectionsEditor() {
if (!validate(newStyle)) {
return;
}
newStyle = await API.editSave(newStyle);
newStyle = await API.styles.editSave(newStyle);
destroyRemovedSections();
sessionStore.justEditedStyleId = newStyle.id;
editor.replaceStyle(newStyle, false);
@ -384,7 +384,7 @@ function SectionsEditor() {
t('importPreprocessor'), 'pre-line',
t('importPreprocessorTitle'))
) {
const {sections, errors} = await API.parseCss({code});
const {sections, errors} = await API.worker.parseMozFormat({code});
// shouldn't happen but just in case
if (!sections.length || errors.length) {
throw errors;
@ -403,7 +403,7 @@ function SectionsEditor() {
async function getPreprocessor(code) {
try {
return (await API.buildUsercssMeta({sourceCode: code})).usercssData.preprocessor;
return (await API.usercss.buildMeta({sourceCode: code})).usercssData.preprocessor;
} catch (e) {}
}

View File

@ -97,7 +97,7 @@ function SourceEditor() {
}
function preprocess(style) {
return API.buildUsercss({
return API.usercss.build({
styleId: style.id,
sourceCode: style.sourceCode,
assignVars: true,
@ -231,7 +231,7 @@ function SourceEditor() {
if (!dirty.isDirty()) return;
const code = cm.getValue();
return ensureUniqueStyle(code)
.then(() => API.editSaveUsercss({
.then(() => API.usercss.editSave({
id: style.id,
enabled: style.enabled,
sourceCode: code,
@ -265,7 +265,7 @@ function SourceEditor() {
function ensureUniqueStyle(code) {
return style.id ? Promise.resolve() :
API.buildUsercss({
API.usercss.build({
sourceCode: code,
checkDup: true,
metaOnly: true,

View File

@ -176,7 +176,7 @@
function initSourceCode(sourceCode) {
cm.setValue(sourceCode);
cm.refresh();
API.buildUsercss({sourceCode, checkDup: true})
API.usercss.build({sourceCode, checkDup: true})
.then(init)
.catch(err => {
$('#header').classList.add('meta-init-error');
@ -248,7 +248,7 @@
data.version,
]))
).then(ok => ok &&
API.installUsercss(style)
API.usercss.install(style)
.then(install)
.catch(err => messageBox.alert(t('styleInstallFailed', err), 'pre'))
);
@ -317,7 +317,7 @@
let sequence = null;
if (tabId < 0) {
getData = DirectDownloader();
sequence = API.getUsercssInstallCode(initialUrl)
sequence = API.usercss.getInstallCode(initialUrl)
.then(code => code || getData())
.catch(getData);
} else {
@ -372,19 +372,20 @@
cm.setValue(code);
cm.setCursor(cursor);
cm.scrollTo(scrollInfo.left, scrollInfo.top);
return API.installUsercss({id, sourceCode: code})
return API.usercss.install({id, sourceCode: code})
.then(updateMeta)
.catch(showError);
});
}
function DirectDownloader() {
let oldCode = null;
const passChangedCode = code => {
const isSame = code === oldCode;
return async () => {
const code = await download(initialUrl);
if (oldCode !== code) {
oldCode = code;
return isSame ? null : code;
return code;
}
};
return () => download(initialUrl).then(passChangedCode);
}
function PortDownloader() {
const resolvers = new Map();

View File

@ -84,10 +84,13 @@ const URLS = {
url &&
url.startsWith(URLS.usoArchiveRaw) &&
parseInt(url.match(/\/(\d+)\.user\.css|$/)[1]),
extractUsoArchiveInstallUrl: url => {
const id = URLS.extractUsoArchiveId(url);
return id ? `${URLS.usoArchive}?style=${id}` : '';
},
extractGreasyForkId: url =>
/^https:\/\/(?:greasy|sleazy)fork\.org\/scripts\/(\d+)[^/]*\/code\/[^/]*\.user\.css$/.test(url) &&
RegExp.$1,
extractGreasyForkInstallUrl: url =>
/^(https:\/\/(?:greasy|sleazy)fork\.org\/scripts\/\d+)[^/]*\/code\/[^/]*\.user\.css$|$/.exec(url)[1],
supported: url => (
url.startsWith('http') ||
@ -98,9 +101,7 @@ const URLS = {
),
};
if (chrome.extension.getBackgroundPage && chrome.extension.getBackgroundPage() === window) {
window.API_METHODS = {};
} else {
if (!chrome.extension.getBackgroundPage || chrome.extension.getBackgroundPage() !== window) {
const cls = FIREFOX ? 'firefox' : OPERA ? 'opera' : VIVALDI ? 'vivaldi' : '';
if (cls) document.documentElement.classList.add(cls);
}
@ -226,8 +227,9 @@ function activateTab(tab, {url, index, openerTabId} = {}) {
}
function stringAsRegExp(s, flags) {
return new RegExp(s.replace(/[{}()[\]\\.+*?^$|]/g, '\\$&'), flags);
function stringAsRegExp(s, flags, asString) {
s = s.replace(/[{}()[\]\\.+*?^$|]/g, '\\$&');
return asString ? s : new RegExp(s, flags);
}
@ -371,70 +373,49 @@ function download(url, {
requiredStatusCode = 200,
timeout = 60e3, // connection timeout, USO is that bad
loadTimeout = 2 * 60e3, // data transfer timeout (counted from the first remote response)
headers = {
'Content-type': 'application/x-www-form-urlencoded',
},
headers,
} = {}) {
const queryPos = url.indexOf('?');
if (queryPos > 0 && body === undefined) {
/* USO can't handle POST requests for style json and XHR/fetch can't handle super long URL
* so we need to collapse all long variables and expand them in the response */
const queryPos = url.startsWith(URLS.uso) ? url.indexOf('?') : -1;
if (queryPos >= 0) {
if (body === undefined) {
method = 'POST';
body = url.slice(queryPos);
url = url.slice(0, queryPos);
}
// * USO can't handle POST requests for style json
// * XHR/fetch can't handle long URL
// So we need to collapse all long variables and expand them in the response
if (headers === undefined) {
headers = {
'Content-type': 'application/x-www-form-urlencoded',
};
}
}
const usoVars = [];
return new Promise((resolve, reject) => {
let xhr;
const xhr = new XMLHttpRequest();
const u = new URL(collapseUsoVars(url));
const onTimeout = () => {
if (xhr) xhr.abort();
xhr.abort();
reject(new Error('Timeout fetching ' + u.href));
};
let timer = setTimeout(onTimeout, timeout);
const switchTimer = () => {
clearTimeout(timer);
timer = loadTimeout && setTimeout(onTimeout, loadTimeout);
};
if (u.protocol === 'file:' && FIREFOX) { // TODO: maybe remove this since FF68+ can't do it anymore
// https://stackoverflow.com/questions/42108782/firefox-webextensions-get-local-files-content-by-path
// FIXME: add FetchController when it is available.
fetch(u.href, {mode: 'same-origin'})
.then(r => {
switchTimer();
return r.status === 200 ? r.text() : Promise.reject(r.status);
})
.catch(reject)
.then(text => {
clearTimeout(timer);
resolve(text);
});
return;
}
xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState >= XMLHttpRequest.HEADERS_RECEIVED) {
xhr.onreadystatechange = null;
switchTimer();
}
};
xhr.onloadend = event => {
clearTimeout(timer);
if (event.type !== 'error' && (
xhr.status === requiredStatusCode || !requiredStatusCode ||
u.protocol === 'file:')) {
resolve(expandUsoVars(xhr.response));
} else {
reject(xhr.status);
timer = loadTimeout && setTimeout(onTimeout, loadTimeout);
}
};
xhr.onerror = xhr.onloadend;
xhr.onload = () =>
xhr.status === requiredStatusCode || !requiredStatusCode || u.protocol === 'file:'
? resolve(expandUsoVars(xhr.response))
: reject(xhr.status);
xhr.onerror = () => reject(xhr.status);
xhr.onloadend = () => clearTimeout(timer);
xhr.responseType = responseType;
xhr.open(method, u.href, true);
for (const key in headers) {
xhr.setRequestHeader(key, headers[key]);
xhr.open(method, u.href);
for (const [name, value] of Object.entries(headers || {})) {
xhr.setRequestHeader(name, value);
}
xhr.send(body);
});

View File

@ -130,14 +130,17 @@ window.INJECTED !== 1 && (() => {
},
};
window.API = new Proxy({}, {
get(target, name) {
// using a named function for convenience when debugging
return async function invokeAPI(...args) {
const apiHandler = !isBg && {
get({PATH}, name) {
const fn = () => {};
fn.PATH = [...PATH, name];
return new Proxy(fn, apiHandler);
},
async apply({PATH: path}, thisObj, args) {
if (!bg && chrome.tabs) {
bg = await browser.runtime.getBackgroundPage().catch(() => {});
}
const message = {method: 'invokeAPI', name, args};
const message = {method: 'invokeAPI', path, args};
// content scripts and probably private tabs
if (!bg) {
return msg.send(message);
@ -146,11 +149,11 @@ window.INJECTED !== 1 && (() => {
// is closed, so we have to clone the object into background.
const res = bg.msg._execute(TARGETS.extension, bg.deepCopy(message), {
frameId: 0, // false in case of our Options frame but we really want to fetch styles early
tab: NEEDS_TAB_IN_SENDER.includes(name) && await getOwnTab(),
tab: NEEDS_TAB_IN_SENDER.includes(path.join('.')) && await getOwnTab(),
url: location.href,
});
return deepCopy(await res);
};
},
});
};
window.API = isBg ? {} : new Proxy({PATH: []}, apiHandler);
})();

View File

@ -6,7 +6,7 @@
window.INJECTED !== 1 && (() => {
const STORAGE_KEY = 'settings';
const clone = msg.isBg ? deepCopy : (val => JSON.parse(JSON.stringify(val)));
const defaults = {
const defaults = /** @namespace Prefs */{
'openEditInWindow': false, // new editor opens in a own browser window
'openEditInWindow.popup': false, // new editor opens in a simplified browser window without omnibox
'windowPosition': {}, // detached window position

View File

@ -1,4 +1,4 @@
/* global backgroundWorker */
/* global API */
/* exported usercss */
'use strict';
@ -33,7 +33,7 @@ const usercss = (() => {
throw new Error('can not find metadata');
}
return backgroundWorker.parseUsercssMeta(match[0], match.index)
return API.worker.parseUsercssMeta(match[0], match.index)
.catch(err => {
if (err.code) {
const args = ERR_ARGS_IS_LIST.has(err.code) ? drawList(err.args) : err.args;
@ -68,7 +68,7 @@ const usercss = (() => {
*/
function buildCode(style, allowErrors) {
const match = style.sourceCode.match(RX_META);
return backgroundWorker.compileUsercss(
return API.worker.compileUsercss(
style.usercssData.preprocessor,
style.sourceCode.slice(0, match.index) + style.sourceCode.slice(match.index + match[0].length),
style.usercssData.vars
@ -95,7 +95,7 @@ const usercss = (() => {
vars[key].value = oldVars[key].value;
}
}
return backgroundWorker.nullifyInvalidVars(vars)
return API.worker.nullifyInvalidVars(vars)
.then(vars => {
style.usercssData.vars = vars;
});

View File

@ -128,7 +128,7 @@ function configDialog(style) {
return;
}
if (!bgStyle) {
API.getStyle(style.id, true)
API.styles.get(style.id)
.catch(() => ({}))
.then(bgStyle => save({anyChangeIsDirty}, bgStyle));
return;
@ -182,7 +182,7 @@ function configDialog(style) {
return;
}
saving = true;
return API.configUsercssVars(style.id, style.usercssData.vars)
return API.usercss.configVars(style.id, style.usercssData.vars)
.then(newVars => {
varsInitial = getInitialValues(newVars);
vars.forEach(va => onchange({target: va.input, justSaved: true}));

View File

@ -109,7 +109,7 @@ function importFromFile({fileTypeFilter, file} = {}) {
async function importFromString(jsonString) {
const json = tryJSONparse(jsonString);
const oldStyles = Array.isArray(json) && json.length ? await API.getAllStyles() : [];
const oldStyles = Array.isArray(json) && json.length ? await API.styles.getAll() : [];
const oldStylesById = new Map(oldStyles.map(style => [style.id, style]));
const oldStylesByName = new Map(oldStyles.map(style => [style.name.trim(), style]));
const items = [];
@ -126,7 +126,7 @@ async function importFromString(jsonString) {
await Promise.all(json.map(analyze));
bulkChangeQueue.length = 0;
bulkChangeQueue.time = performance.now();
(await API.importManyStyles(items))
(await API.styles.importMany(items))
.forEach((style, i) => updateStats(style, infos[i]));
return done();
@ -290,10 +290,10 @@ async function importFromString(jsonString) {
];
let tasks = Promise.resolve();
for (const id of newIds) {
tasks = tasks.then(() => API.deleteStyle(id));
tasks = tasks.then(() => API.styles.delete(id));
const oldStyle = oldStylesById.get(id);
if (oldStyle) {
tasks = tasks.then(() => API.importStyle(oldStyle));
tasks = tasks.then(() => API.styles.import(oldStyle));
}
}
// taskUI is superfast and updates style list only in this page,
@ -338,7 +338,7 @@ async function exportToFile() {
Object.assign({
[prefs.STORAGE_KEY]: prefs.values,
}, await chromeSync.getLZValues()),
...await API.getAllStyles(),
...await API.styles.getAll(),
];
const text = JSON.stringify(data, null, ' ');
const type = 'application/json';

View File

@ -94,7 +94,7 @@ const handleEvent = {};
(async () => {
const query = router.getSearch('search');
const [styles, ids, el] = await Promise.all([
API.getAllStyles(),
API.styles.getAll(),
query && API.searchDB({query, mode: router.getSearch('searchMode')}),
waitForSelector('#installed'), // needed to avoid flicker due to an extra frame and layout shift
prefs.initializing,
@ -469,7 +469,7 @@ Object.assign(handleEvent, {
},
toggle(event, entry) {
API.toggleStyle(entry.styleId, this.matches('.enable') || this.checked);
API.styles.toggle(entry.styleId, this.matches('.enable') || this.checked);
},
check(event, entry) {
@ -481,7 +481,7 @@ Object.assign(handleEvent, {
event.preventDefault();
const json = entry.updatedCode;
json.id = entry.styleId;
API[json.usercssData ? 'installUsercss' : 'installStyle'](json);
(json.usercssData ? API.usercss : API.styles).install(json);
},
delete(event, entry) {
@ -496,7 +496,7 @@ Object.assign(handleEvent, {
})
.then(({button}) => {
if (button === 0) {
API.deleteStyle(id);
API.styles.delete(id);
}
});
const deleteButton = $('#message-box-buttons > button');
@ -599,7 +599,7 @@ function handleBulkChange() {
}
function handleUpdateForId(id, opts) {
return API.getStyle(id).then(style => {
return API.styles.get(id).then(style => {
handleUpdate(style, opts);
bulkChangeQueue.time = performance.now();
});
@ -697,7 +697,7 @@ function switchUI({styleOnly} = {}) {
let iconsMissing = iconsEnabled && !$('.applies-to img');
if (changed.enabled || (iconsMissing && !createStyleElement.parts)) {
installed.textContent = '';
API.getAllStyles().then(showStyles);
API.styles.getAll().then(showStyles);
return;
}
if (changed.sliders && newUI.enabled) {

View File

@ -53,7 +53,7 @@ function checkUpdateAll() {
chrome.runtime.onConnect.removeListener(onConnect);
});
API.updateCheckAll({
API.updater.checkAllStyles({
save: false,
observe: true,
ignoreDigest,
@ -98,7 +98,7 @@ function checkUpdate(entry, {single} = {}) {
$('.update-note', entry).textContent = t('checkingForUpdate');
$('.check-update', entry).title = '';
if (single) {
API.updateCheck({
API.updater.checkStyle({
save: false,
id: entry.styleId,
ignoreDigest: entry.classList.contains('update-problem'),
@ -221,7 +221,7 @@ function showUpdateHistory(event) {
let deleted = false;
Promise.all([
chromeLocal.getValue('updateLog'),
API.getUpdaterStates(),
API.updater.getStates(),
]).then(([lines = [], states]) => {
logText = lines.join('\n');
messageBox({

View File

@ -52,12 +52,13 @@
"background/tab-manager.js",
"background/icon-manager.js",
"background/background.js",
"background/usercss-helper.js",
"background/usercss-api-helper.js",
"background/usercss-install-helper.js",
"background/style-via-api.js",
"background/style-via-webrequest.js",
"background/search-db.js",
"background/update.js",
"background/context-menus.js",
"background/openusercss-api.js"
]
},

View File

@ -1,7 +1,25 @@
/* global messageBox msg setupLivePrefs enforceInputRange
$ $$ $create $createLink
FIREFOX OPERA CHROME URLS openURL prefs t API ignoreChromeError
CHROME_HAS_BORDER_BUG capitalize */
/* global
$
$$
$create
$createLink
API
capitalize
CHROME
CHROME_HAS_BORDER_BUG
enforceInputRange
FIREFOX
getEventKeyName
ignoreChromeError
messageBox
msg
openURL
OPERA
prefs
setupLivePrefs
t
URLS
*/
'use strict';
setupLivePrefs();
@ -44,7 +62,7 @@ if (CHROME && !chrome.declarativeContent) {
prefs.initializing.then(() => {
el.checked = false;
});
el.addEventListener('click', () => {
el.on('click', () => {
if (el.checked) {
chrome.permissions.request({permissions: ['declarativeContent']}, ignoreChromeError);
}
@ -101,84 +119,75 @@ document.onclick = e => {
// sync to cloud
(() => {
const cloud = document.querySelector('.sync-options .cloud-name');
const connectButton = document.querySelector('.sync-options .connect');
const disconnectButton = document.querySelector('.sync-options .disconnect');
const syncButton = document.querySelector('.sync-options .sync-now');
const statusText = document.querySelector('.sync-options .sync-status');
const loginButton = document.querySelector('.sync-options .sync-login');
const elCloud = $('.sync-options .cloud-name');
const elStart = $('.sync-options .connect');
const elStop = $('.sync-options .disconnect');
const elSyncNow = $('.sync-options .sync-now');
const elStatus = $('.sync-options .sync-status');
const elLogin = $('.sync-options .sync-login');
/** @type {API.sync.Status} */
let status = {};
msg.onExtension(e => {
if (e.method === 'syncStatusUpdate') {
status = e.status;
updateButtons();
setStatus(e.status);
}
});
API.sync.getStatus()
.then(setStatus);
API.getSyncStatus()
.then(_status => {
status = _status;
updateButtons();
elCloud.on('change', updateButtons);
for (const [btn, fn] of [
[elStart, () => API.sync.start(elCloud.value)],
[elStop, API.sync.stop],
[elSyncNow, API.sync.syncNow],
[elLogin, API.sync.login],
]) {
btn.on('click', e => {
if (getEventKeyName(e) === 'L') {
fn();
}
});
function validClick(e) {
return e.button === 0 && !e.ctrl && !e.alt && !e.shift;
}
cloud.addEventListener('change', updateButtons);
function setStatus(newStatus) {
status = newStatus;
updateButtons();
}
function updateButtons() {
const isConnected = status.state === 'connected';
const isDisconnected = status.state === 'disconnected';
if (status.currentDriveName) {
cloud.value = status.currentDriveName;
elCloud.value = status.currentDriveName;
}
cloud.disabled = status.state !== 'disconnected';
connectButton.disabled = status.state !== 'disconnected' || cloud.value === 'none';
disconnectButton.disabled = status.state !== 'connected' || status.syncing;
syncButton.disabled = status.state !== 'connected' || status.syncing;
statusText.textContent = getStatusText();
loginButton.style.display = status.state === 'connected' && !status.login ? '' : 'none';
for (const [el, enable] of [
[elCloud, isDisconnected],
[elStart, isDisconnected && elCloud.value !== 'none'],
[elStop, isConnected && !status.syncing],
[elSyncNow, isConnected && !status.syncing],
]) {
el.disabled = !enable;
}
elStatus.textContent = getStatusText();
elLogin.hidden = !isConnected || status.login;
}
function getStatusText() {
// chrome.i18n.getMessage is used instead of t() because calculated ids may be absent
let res;
if (status.syncing) {
if (status.progress) {
const {phase, loaded, total} = status.progress;
return chrome.i18n.getMessage(`optionsSyncStatus${capitalize(phase)}`, [loaded + 1, total]) ||
`${phase} ${loaded} / ${total}`;
const {phase, loaded, total} = status.progress || {};
res = phase
? chrome.i18n.getMessage(`optionsSyncStatus${capitalize(phase)}`, [loaded + 1, total]) ||
`${phase} ${loaded} / ${total}`
: t('optionsSyncStatusSyncing');
} else {
const {state, errorMessage} = status;
res = (state === 'connected' || state === 'disconnected') && errorMessage ||
chrome.i18n.getMessage(`optionsSyncStatus${capitalize(state)}`) || state;
}
return chrome.i18n.getMessage('optionsSyncStatusSyncing') || 'syncing';
return res;
}
if ((status.state === 'connected' || status.state === 'disconnected') && status.errorMessage) {
return status.errorMessage;
}
return chrome.i18n.getMessage(`optionsSyncStatus${capitalize(status.state)}`) || status.state;
}
connectButton.addEventListener('click', e => {
if (validClick(e)) {
API.syncStart(cloud.value).catch(console.error);
}
});
disconnectButton.addEventListener('click', e => {
if (validClick(e)) {
API.syncStop().catch(console.error);
}
});
syncButton.addEventListener('click', e => {
if (validClick(e)) {
API.syncNow().catch(console.error);
}
});
loginButton.addEventListener('click', e => {
if (validClick(e)) {
API.syncLogin().catch(console.error);
}
});
})();
function checkUpdates() {
@ -193,7 +202,7 @@ function checkUpdates() {
chrome.runtime.onConnect.removeListener(onConnect);
});
API.updateCheckAll({observe: true});
API.updater.checkAllStyles({observe: true});
function observer(info) {
if ('count' in info) {
@ -223,7 +232,7 @@ function setupRadioButtons() {
// group all radio-inputs by name="prefName" attribute
for (const el of $$('input[type="radio"][name]')) {
(sets[el.name] = sets[el.name] || []).push(el);
el.addEventListener('change', onChange);
el.on('change', onChange);
}
// select the input corresponding to the actual pref value
for (const name in sets) {

View File

@ -89,7 +89,7 @@ const hotkeys = (() => {
if (!match && $('input', entry).checked !== enable || entry.classList.contains(match)) {
results.push(entry.id);
task = task
.then(() => API.toggleStyle(entry.styleId, enable))
.then(() => API.styles.toggle(entry.styleId, enable))
.then(() => {
entry.classList.toggle('enabled', enable);
entry.classList.toggle('disabled', !enable);

View File

@ -81,7 +81,7 @@ const initializing = (async () => {
/* Merges the extra props from API into style data.
* When `id` is specified returns a single object otherwise an array */
async function getStyleDataMerged(url, id) {
const styles = (await API.getStylesByUrl(url, id))
.map(r => Object.assign(r.data, r));
const styles = (await API.styles.getByUrl(url, id))
.map(r => Object.assign(r.style, r));
return id ? styles[0] : styles;
}

View File

@ -143,7 +143,7 @@ async function initPopup(frames) {
switch (e.target.dataset.cmd) {
case 'ok':
hideModal(this, {animate: true});
API.deleteStyle(Number(id));
API.styles.delete(Number(id));
break;
case 'cancel':
showModal($('.menu', $.entry(id)), '.menu-close');
@ -464,20 +464,19 @@ Object.assign(handleEvent, {
event.preventDefault();
},
toggle(event) {
async toggle(event) {
// when fired on checkbox, prevent the parent label from seeing the event, see #501
event.stopPropagation();
API
.toggleStyle(handleEvent.getClickedStyleId(event), this.checked)
.then(() => resortEntries());
await API.styles.toggle(handleEvent.getClickedStyleId(event), this.checked);
resortEntries();
},
toggleExclude(event, type) {
const entry = handleEvent.getClickedStyleElement(event);
if (event.target.checked) {
API.addExclusion(entry.styleMeta.id, getExcludeRule(type));
API.styles.addExclusion(entry.styleMeta.id, getExcludeRule(type));
} else {
API.removeExclusion(entry.styleMeta.id, getExcludeRule(type));
API.styles.removeExclusion(entry.styleMeta.id, getExcludeRule(type));
}
},
@ -503,7 +502,7 @@ Object.assign(handleEvent, {
configure(event) {
const {styleId, styleIsUsercss} = handleEvent.getClickedStyleElement(event);
if (styleIsUsercss) {
API.getStyle(styleId, true).then(style => {
API.styles.get(styleId).then(style => {
hotkeys.setState(false);
configDialog(style).then(() => {
hotkeys.setState(true);

View File

@ -149,7 +149,7 @@ window.addEventListener('showStyles:done', () => {
addEventListener('styleAdded', async ({detail: {style}}) => {
restoreScrollPosition();
const usoId = calcUsoId(style) ||
calcUsoId(await API.getStyle(style.id, true));
calcUsoId(await API.styles.get(style.id));
if (usoId && results.find(r => r.i === usoId)) {
renderActionButtons(usoId, style.id);
}
@ -194,7 +194,7 @@ window.addEventListener('showStyles:done', () => {
results = await search({retry});
}
if (results.length) {
const installedStyles = await API.getAllStyles();
const installedStyles = await API.styles.getAll();
const allUsoIds = new Set(installedStyles.map(calcUsoId));
results = results.filter(r => !allUsoIds.has(r.i));
}
@ -419,7 +419,7 @@ window.addEventListener('showStyles:done', () => {
const updateUrl = `${URLS.usoArchiveRaw}usercss/${id}.user.css`;
try {
const sourceCode = await download(updateUrl);
const style = await API.installUsercss({sourceCode, updateUrl});
const style = await API.usercss.install({sourceCode, updateUrl});
renderFullInfo(entry, style);
} catch (reason) {
error(`Error while downloading usoID:${id}\nReason: ${reason}`);
@ -432,7 +432,7 @@ window.addEventListener('showStyles:done', () => {
function uninstall() {
const entry = this.closest('.search-result');
saveScrollPosition(entry);
API.deleteStyle(entry._result.installedStyleId);
API.styles.delete(entry._result.installedStyleId);
}
function saveScrollPosition(entry) {