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:
parent
06823bd5b4
commit
86623a9aab
background
background-worker.jsbackground.jscontent-scripts.jscontext-menus.jsdb-chrome-storage.jsdb.jsicon-manager.jsnavigator-util.jsopenusercss-api.jssearch-db.jsstyle-manager.jsstyle-via-api.jsstyle-via-webrequest.jssync.jsupdate.jsusercss-api-helper.jsusercss-helper.jsusercss-install-helper.js
content
apply.jsinstall-hook-greasyfork.jsinstall-hook-openusercss.jsinstall-hook-usercss.jsinstall-hook-userstyles.js
edit
install-usercss
js
manage
manifest.jsonoptions
popup
|
@ -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');
|
||||
|
|
|
@ -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({
|
||||
url: '/background/background-worker.js',
|
||||
});
|
||||
//#region API
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var browserCommands, contextMenus;
|
||||
Object.assign(API, {
|
||||
|
||||
// *************************************************************************
|
||||
// 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,
|
||||
/** @type {ApiWorker} */
|
||||
worker: workerUtil.createWorker({
|
||||
url: '/background/background-worker.js',
|
||||
}),
|
||||
|
||||
/** @returns {string} */
|
||||
getTabUrlPrefix() {
|
||||
const {url} = this.sender.tab;
|
||||
if (url.startsWith(URLS.ownOrigin)) {
|
||||
|
@ -52,21 +33,68 @@ 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),
|
||||
setPref(key, value) {
|
||||
prefs.set(key, value);
|
||||
},
|
||||
|
||||
openEditor,
|
||||
/**
|
||||
* 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({
|
||||
url: `${u}`,
|
||||
currentWindow: null,
|
||||
newWindow: prefs.get('openEditInWindow') && Object.assign({},
|
||||
prefs.get('openEditInWindow.popup') && {type: 'popup'},
|
||||
prefs.get('windowPosition')),
|
||||
});
|
||||
},
|
||||
|
||||
/* 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 openManage({options = false, search, searchMode} = {}) {
|
||||
let url = chrome.runtime.getURL('manage.html');
|
||||
if (search) {
|
||||
url += `?search=${encodeURIComponent(search)}&searchMode=${searchMode}`;
|
||||
}
|
||||
if (options) {
|
||||
url += '#stylus-options';
|
||||
}
|
||||
let tab = await findExistingTab({
|
||||
url,
|
||||
currentWindow: null,
|
||||
ignoreHash: true,
|
||||
ignoreSearch: true,
|
||||
});
|
||||
if (tab) {
|
||||
await activateTab(tab);
|
||||
if (url !== (tab.pendingUrl || tab.url)) {
|
||||
await msg.sendTab(tab.id, {method: 'pushState', url}).catch(console.error);
|
||||
}
|
||||
return tab;
|
||||
}
|
||||
tab = await getActiveTab();
|
||||
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) {
|
||||
|
@ -86,54 +114,49 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
|
|||
}));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
optionsCustomizeHotkeys() {
|
||||
return browserCommands.openOptions()
|
||||
.then(() => new Promise(resolve => setTimeout(resolve, 500)))
|
||||
.then(() => msg.broadcastExtension({method: 'optionsCustomizeHotkeys'}));
|
||||
//#endregion
|
||||
//#region browserCommands
|
||||
|
||||
const browserCommands = {
|
||||
openManage: () => API.openManage(),
|
||||
openOptions: () => API.openManage({options: true}),
|
||||
styleDisableAll(info) {
|
||||
prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll'));
|
||||
},
|
||||
|
||||
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'},
|
||||
],
|
||||
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) {}
|
||||
});
|
||||
}
|
||||
|
||||
if (chrome.contextMenus) {
|
||||
chrome.contextMenus.onClicked.addListener((info, tab) =>
|
||||
contextMenus[info.menuItemId].click(info, tab));
|
||||
}
|
||||
//#endregion
|
||||
//#region Init
|
||||
|
||||
if (chrome.commands) {
|
||||
// Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350
|
||||
chrome.commands.onCommand.addListener(command => browserCommands[command]());
|
||||
}
|
||||
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) {
|
||||
|
@ -150,188 +173,6 @@ chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
|
|||
}
|
||||
});
|
||||
|
||||
// *************************************************************************
|
||||
// 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
|
||||
}
|
||||
*/
|
||||
const u = new URL(chrome.runtime.getURL('edit.html'));
|
||||
u.search = new URLSearchParams(params);
|
||||
return openURL({
|
||||
url: `${u}`,
|
||||
currentWindow: null,
|
||||
newWindow: prefs.get('openEditInWindow') && Object.assign({},
|
||||
prefs.get('openEditInWindow.popup') && {type: 'popup'},
|
||||
prefs.get('windowPosition')),
|
||||
});
|
||||
}
|
||||
|
||||
async function openManage({options = false, search, searchMode} = {}) {
|
||||
let url = chrome.runtime.getURL('manage.html');
|
||||
if (search) {
|
||||
url += `?search=${encodeURIComponent(search)}&searchMode=${searchMode}`;
|
||||
}
|
||||
if (options) {
|
||||
url += '#stylus-options';
|
||||
}
|
||||
let tab = await findExistingTab({
|
||||
url,
|
||||
currentWindow: null,
|
||||
ignoreHash: true,
|
||||
ignoreSearch: true,
|
||||
});
|
||||
if (tab) {
|
||||
await activateTab(tab);
|
||||
if (url !== (tab.pendingUrl || tab.url)) {
|
||||
await msg.sendTab(tab.id, {method: 'pushState', url}).catch(console.error);
|
||||
}
|
||||
return tab;
|
||||
}
|
||||
tab = await getActiveTab();
|
||||
return isTabReplaceable(tab, url)
|
||||
? activateTab(tab, {url})
|
||||
: browser.tabs.create({url});
|
||||
}
|
||||
//#endregion
|
||||
|
|
|
@ -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
107
background/context-menus.js
Normal 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);
|
||||
}
|
||||
}
|
||||
})();
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
/** @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;
|
||||
}
|
||||
}
|
||||
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)
|
||||
);
|
||||
listeners.forEach(fn => fn(data, this));
|
||||
}
|
||||
|
||||
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 => {
|
||||
const url = tab.pendingUrl || tab.url;
|
||||
if (url === 'chrome://newtab/') {
|
||||
data.url = url;
|
||||
}
|
||||
});
|
||||
/** @this {string} type */
|
||||
function onFakeNavigation(data) {
|
||||
onNavigation.call(this, data);
|
||||
msg.sendTab(data.tabId, {method: 'urlChanged'}, {frameId: data.frameId})
|
||||
.catch(msg.ignoreError);
|
||||
}
|
||||
|
||||
function executeCallbacks(callbacks, data, type) {
|
||||
for (const cb of callbacks) {
|
||||
cb(data, type);
|
||||
/** 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
|
||||
*/
|
||||
|
|
|
@ -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 @@
|
|||
}
|
||||
}
|
||||
`),
|
||||
});
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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,
|
||||
}));
|
||||
|
||||
function handleLivePreviewConnections() {
|
||||
chrome.runtime.onConnect.addListener(port => {
|
||||
if (port.name !== 'livePreview') {
|
||||
return;
|
||||
/** @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());
|
||||
}
|
||||
let id;
|
||||
port.onMessage.addListener(data => {
|
||||
if (!id) {
|
||||
id = data.id;
|
||||
}
|
||||
const style = styles.get(id);
|
||||
style.preview = data;
|
||||
broadcastStyleUpdated(style.preview, 'editPreview');
|
||||
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},
|
||||
});
|
||||
port.onDisconnect.addListener(() => {
|
||||
port = null;
|
||||
if (id) {
|
||||
const style = styles.get(id);
|
||||
if (!style) {
|
||||
// maybe deleted
|
||||
return;
|
||||
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;
|
||||
}
|
||||
style.preview = null;
|
||||
broadcastStyleUpdated(style.data, 'editPreviewEnd');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function escapeRegExp(text) {
|
||||
// https://github.com/lodash/lodash/blob/0843bd46ef805dd03c0c8d804630804f3ba0ca3c/lodash.js#L152
|
||||
return text.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
let diff = -1;
|
||||
if (oldDoc) {
|
||||
diff = compareRevision(oldDoc._rev, doc._rev);
|
||||
if (diff > 0) {
|
||||
sync.put(oldDoc._id, oldDoc._rev);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (diff < 0) {
|
||||
return db.exec('put', doc)
|
||||
.then(event => {
|
||||
doc.id = event.target.result;
|
||||
uuidIndex.set(doc._id, doc.id);
|
||||
return handleSave(doc, 'sync');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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];
|
||||
const match = urlMatchSection(query, section);
|
||||
if (match) {
|
||||
if (match === 'sloppy') {
|
||||
sloppy = true;
|
||||
}
|
||||
sectionMatched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
styles.delete(id);
|
||||
uuidIndex.delete(style.data._id);
|
||||
return msg.broadcast({
|
||||
method: 'styleDeleted',
|
||||
style: {id},
|
||||
});
|
||||
})
|
||||
.then(() => id);
|
||||
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');
|
||||
}));
|
||||
},
|
||||
|
||||
/** @returns {Promise<StyleObj>} */
|
||||
async import(data) {
|
||||
await ready;
|
||||
return handleSave(await saveStyle(data), 'import');
|
||||
},
|
||||
|
||||
/** @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);
|
||||
},
|
||||
|
||||
/** @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 && id2style(id);
|
||||
let diff = -1;
|
||||
if (oldDoc) {
|
||||
diff = compareRevision(oldDoc._rev, doc._rev);
|
||||
if (diff > 0) {
|
||||
API.sync.put(oldDoc._id, oldDoc._rev);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (diff < 0) {
|
||||
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 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');
|
||||
}
|
||||
/** @returns {?StyleObj} */
|
||||
function id2style(id) {
|
||||
return (dataMap.get(id) || {}).style;
|
||||
}
|
||||
|
||||
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);
|
||||
return style;
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (updated.length) {
|
||||
return db.exec('putMany', updated)
|
||||
.then(() => styleList);
|
||||
}
|
||||
return styleList;
|
||||
})
|
||||
.then(styleList => {
|
||||
for (const style of styleList) {
|
||||
fixUsoMd5Issue(style);
|
||||
styles.set(style.id, {
|
||||
appliesTo: new Set(),
|
||||
data: style,
|
||||
});
|
||||
uuidIndex.set(style._id, style.id);
|
||||
}
|
||||
});
|
||||
|
||||
function addMissingProperties(style) {
|
||||
let touched = false;
|
||||
for (const key in ADD_MISSING_PROPS) {
|
||||
if (!style[key]) {
|
||||
style[key] = ADD_MISSING_PROPS[key](style);
|
||||
touched = true;
|
||||
}
|
||||
}
|
||||
// upgrade the old way of customizing local names
|
||||
const {originalName} = style;
|
||||
if (originalName) {
|
||||
touched = true;
|
||||
if (originalName !== style.name) {
|
||||
style.customName = style.name;
|
||||
style.name = originalName;
|
||||
}
|
||||
delete style.originalName;
|
||||
}
|
||||
return touched;
|
||||
async function init() {
|
||||
const styles = await db.exec('getAll') || [];
|
||||
const updated = styles.filter(style =>
|
||||
addMissingProps(style) +
|
||||
addCustomName(style));
|
||||
if (updated.length) {
|
||||
await db.exec('putMany', updated);
|
||||
}
|
||||
for (const style of styles) {
|
||||
fixUsoMd5Issue(style);
|
||||
storeInMap(style);
|
||||
uuidIndex.set(style._id, style.id);
|
||||
}
|
||||
}
|
||||
|
||||
function addMissingProps(style) {
|
||||
let res = 0;
|
||||
for (const key in MISSING_PROPS) {
|
||||
if (!style[key]) {
|
||||
style[key] = MISSING_PROPS[key](style);
|
||||
res = 1;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/** Upgrades the old way of customizing local names */
|
||||
function addCustomName(style) {
|
||||
let res = 0;
|
||||
const {originalName} = style;
|
||||
if (originalName) {
|
||||
res = 1;
|
||||
if (originalName !== style.name) {
|
||||
style.customName = style.name;
|
||||
style.name = originalName;
|
||||
}
|
||||
delete style.originalName;
|
||||
}
|
||||
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
|
||||
})();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 => {
|
||||
if (Object.keys(sections).length) {
|
||||
stylesToPass[req.requestId] = !enabled.xhr ? true :
|
||||
URL.createObjectURL(new Blob([JSON.stringify(sections)])).slice(blobUrlPrefix.length);
|
||||
setTimeout(cleanUp, 600e3, req.requestId);
|
||||
}
|
||||
});
|
||||
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);
|
||||
setTimeout(cleanUp, 600e3, req.requestId);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {chrome.webRequest.WebResponseHeadersDetails} req */
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
||||
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;
|
||||
}
|
||||
emitStatusChange();
|
||||
}
|
||||
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();
|
||||
},
|
||||
|
||||
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();
|
||||
}
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -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,191 +51,159 @@
|
|||
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);
|
||||
if (port) port.postMessage({count: styles.length});
|
||||
log('');
|
||||
log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`);
|
||||
return 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();
|
||||
});
|
||||
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`);
|
||||
await Promise.all(
|
||||
styles.map(style =>
|
||||
checkStyle({style, port, save, ignoreDigest})));
|
||||
if (port) port.postMessage({done: true});
|
||||
if (port) port.disconnect();
|
||||
log('');
|
||||
checkingAll = false;
|
||||
}
|
||||
|
||||
function checkStyle({
|
||||
id,
|
||||
style,
|
||||
port,
|
||||
save = true,
|
||||
ignoreDigest,
|
||||
}) {
|
||||
/*
|
||||
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
|
||||
/**
|
||||
* @param {{
|
||||
id?: number
|
||||
style?: StyleObj
|
||||
port?: chrome.runtime.Port
|
||||
save?: boolean = true
|
||||
ignoreDigest?: boolean
|
||||
}} opts
|
||||
* @returns {{
|
||||
style: StyleObj
|
||||
updated?: boolean
|
||||
error?: any
|
||||
STATES: UpdaterStates
|
||||
}}
|
||||
|
||||
Update check proceeds in these cases:
|
||||
* style has the original digest and it's equal to the current digest
|
||||
* [ignoreDigest: true] style doesn't yet have the original digest but we ignore it
|
||||
* [ignoreDigest: none/false] style doesn't yet have the original digest
|
||||
so we compare the code to the server code and if it's the same we save the digest,
|
||||
otherwise we skip the style and report MAYBE_EDITED status
|
||||
Original style digests are calculated in these cases:
|
||||
* style is installed or updated from server
|
||||
* non-usercss style is checked for an update and styleSectionsEqual considers it unchanged
|
||||
|
||||
'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);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
if (style.usercssData) {
|
||||
return maybeUpdateUsercss();
|
||||
}
|
||||
return maybeUpdateUSO();
|
||||
})
|
||||
.then(maybeSave)
|
||||
.then(reportSuccess)
|
||||
.catch(reportFailure);
|
||||
Update check proceeds in these cases:
|
||||
* style has the original digest and it's equal to the current digest
|
||||
* [ignoreDigest: true] style doesn't yet have the original digest but we ignore it
|
||||
* [ignoreDigest: none/false] style doesn't yet have the original digest
|
||||
so we compare the code to the server code and if it's the same we save the digest,
|
||||
otherwise we skip the style and report MAYBE_EDITED status
|
||||
|
||||
function fetchStyle() {
|
||||
if (style) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return styleManager.get(id)
|
||||
.then(style_ => {
|
||||
style = style_;
|
||||
});
|
||||
'ignoreDigest' option is set on the second manual individual update check on the manage page.
|
||||
*/
|
||||
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})`;
|
||||
}
|
||||
log(`${state} #${style.id} ${style.customName || style.name}`);
|
||||
if (port) port.postMessage(res);
|
||||
return res;
|
||||
|
||||
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 => {
|
||||
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;
|
||||
}
|
||||
return style;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function maybeUpdateUsercss() {
|
||||
// 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:
|
||||
// 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:
|
||||
// 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)) {
|
||||
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);
|
||||
}
|
||||
const json = await tryDownload(style.updateUrl, {responseType: 'json'});
|
||||
if (!styleJSONseemsValid(json)) {
|
||||
return Promise.reject(STATES.ERROR_JSON);
|
||||
}
|
||||
// USO may not provide a correctly updated originalMd5 (#555)
|
||||
json.originalMd5 = md5;
|
||||
return json;
|
||||
}
|
||||
|
||||
async function updateUsercss() {
|
||||
// TODO: when sourceCode is > 100kB use http range request(s) for version check
|
||||
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
|
||||
const sameCode = text === style.sourceCode;
|
||||
return Promise.reject(sameCode ? STATES.SAME_CODE : STATES.SAME_VERSION);
|
||||
}
|
||||
if (delta < 0) {
|
||||
// downgrade is always invalid
|
||||
return Promise.reject(STATES.ERROR_VERSION);
|
||||
}
|
||||
return usercss.buildCode(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;
|
||||
return Promise.reject(STATES.SAME_CODE);
|
||||
});
|
||||
// update digest even if save === false as there might be just a space added etc.
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
81
background/usercss-api-helper.js
Normal file
81
background/usercss-api-helper.js
Normal 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);
|
||||
},
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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}, '*');
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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.postMessage(msg);
|
||||
if (msg.code != null) self.oldCode = msg.code;
|
||||
});
|
||||
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);
|
||||
});
|
||||
// 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
|
||||
|
|
|
@ -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;
|
||||
|
|
22
edit/edit.js
22
edit/edit.js
|
@ -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':
|
||||
|
|
|
@ -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) {}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
oldCode = code;
|
||||
return isSame ? null : code;
|
||||
return async () => {
|
||||
const code = await download(initialUrl);
|
||||
if (oldCode !== code) {
|
||||
oldCode = code;
|
||||
return code;
|
||||
}
|
||||
};
|
||||
return () => download(initialUrl).then(passChangedCode);
|
||||
}
|
||||
function PortDownloader() {
|
||||
const resolvers = new Map();
|
||||
|
|
|
@ -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) {
|
||||
method = 'POST';
|
||||
body = url.slice(queryPos);
|
||||
url = url.slice(0, queryPos);
|
||||
/* 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);
|
||||
}
|
||||
if (headers === undefined) {
|
||||
headers = {
|
||||
'Content-type': 'application/x-www-form-urlencoded',
|
||||
};
|
||||
}
|
||||
}
|
||||
// * 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
|
||||
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();
|
||||
clearTimeout(timer);
|
||||
timer = loadTimeout && setTimeout(onTimeout, loadTimeout);
|
||||
}
|
||||
};
|
||||
xhr.onloadend = event => {
|
||||
clearTimeout(timer);
|
||||
if (event.type !== 'error' && (
|
||||
xhr.status === requiredStatusCode || !requiredStatusCode ||
|
||||
u.protocol === 'file:')) {
|
||||
resolve(expandUsoVars(xhr.response));
|
||||
} else {
|
||||
reject(xhr.status);
|
||||
}
|
||||
};
|
||||
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);
|
||||
});
|
||||
|
|
47
js/msg.js
47
js/msg.js
|
@ -130,27 +130,30 @@ window.INJECTED !== 1 && (() => {
|
|||
},
|
||||
};
|
||||
|
||||
window.API = new Proxy({}, {
|
||||
get(target, name) {
|
||||
// using a named function for convenience when debugging
|
||||
return async function invokeAPI(...args) {
|
||||
if (!bg && chrome.tabs) {
|
||||
bg = await browser.runtime.getBackgroundPage().catch(() => {});
|
||||
}
|
||||
const message = {method: 'invokeAPI', name, args};
|
||||
// content scripts and probably private tabs
|
||||
if (!bg) {
|
||||
return msg.send(message);
|
||||
}
|
||||
// in FF, the object would become a dead object when the window
|
||||
// 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(),
|
||||
url: location.href,
|
||||
});
|
||||
return deepCopy(await res);
|
||||
};
|
||||
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', path, args};
|
||||
// content scripts and probably private tabs
|
||||
if (!bg) {
|
||||
return msg.send(message);
|
||||
}
|
||||
// in FF, the object would become a dead object when the window
|
||||
// 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(path.join('.')) && await getOwnTab(),
|
||||
url: location.href,
|
||||
});
|
||||
return deepCopy(await res);
|
||||
},
|
||||
};
|
||||
window.API = isBg ? {} : new Proxy({PATH: []}, apiHandler);
|
||||
})();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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}));
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
},
|
||||
|
|
|
@ -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}`;
|
||||
}
|
||||
return chrome.i18n.getMessage('optionsSyncStatusSyncing') || 'syncing';
|
||||
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;
|
||||
}
|
||||
if ((status.state === 'connected' || status.state === 'disconnected') && status.errorMessage) {
|
||||
return status.errorMessage;
|
||||
}
|
||||
return chrome.i18n.getMessage(`optionsSyncStatus${capitalize(status.state)}`) || status.state;
|
||||
return res;
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user