switch to USO-archive for inline search in popup, #1056

This commit is contained in:
tophf 2020-10-11 16:53:42 +03:00 committed by GitHub
commit ad24ee0c15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 614 additions and 956 deletions

View File

@ -1,7 +1,7 @@
# https://github.com/eslint/eslint/blob/master/docs/rules/README.md # https://github.com/eslint/eslint/blob/master/docs/rules/README.md
parserOptions: parserOptions:
ecmaVersion: 2015 ecmaVersion: 2017
env: env:
browser: true browser: true

View File

@ -1173,6 +1173,10 @@
"message": "Case-sensitive", "message": "Case-sensitive",
"description": "Tooltip for the 'Aa' icon that enables case-sensitive search in the editor shown on Ctrl-F" "description": "Tooltip for the 'Aa' icon that enables case-sensitive search in the editor shown on Ctrl-F"
}, },
"searchGlobalStyles": {
"message": "Also search global styles",
"description": "Checkbox label in the popup's inline style search, shown when the text to search is entered"
},
"searchNumberOfResults": { "searchNumberOfResults": {
"message": "Number of matches", "message": "Number of matches",
"description": "Tooltip for the number of found search results in the editor shown on Ctrl-F" "description": "Tooltip for the number of found search results in the editor shown on Ctrl-F"
@ -1181,6 +1185,10 @@
"message": "Number of matches in code and applies-to values", "message": "Number of matches in code and applies-to values",
"description": "Tooltip for the number of found search results in the editor shown on Ctrl-F" "description": "Tooltip for the number of found search results in the editor shown on Ctrl-F"
}, },
"searchStyleQueryHint": {
"message": "Search style names case-insensitively:\nsome words - all words in any order\n\"some phrase\" - this exact phrase without quotes\n2020 - a year like this also shows styles updated in 2020",
"description": "Tooltip shown for the text input in the popup's inline style finder"
},
"searchRegexp": { "searchRegexp": {
"message": "Use /re/ syntax for regexp search", "message": "Use /re/ syntax for regexp search",
"description": "Label after the search input field in the editor shown on Ctrl-F" "description": "Label after the search input field in the editor shown on Ctrl-F"

View File

@ -12,7 +12,6 @@ createAPI({
compileUsercss, compileUsercss,
parseUsercssMeta(text, indexOffset = 0) { parseUsercssMeta(text, indexOffset = 0) {
loadScript( loadScript(
'/js/polyfill.js',
'/vendor/usercss-meta/usercss-meta.min.js', '/vendor/usercss-meta/usercss-meta.min.js',
'/vendor-overwrites/colorpicker/colorconverter.js', '/vendor-overwrites/colorpicker/colorconverter.js',
'/js/meta-parser.js' '/js/meta-parser.js'
@ -21,7 +20,6 @@ createAPI({
}, },
nullifyInvalidVars(vars) { nullifyInvalidVars(vars) {
loadScript( loadScript(
'/js/polyfill.js',
'/vendor/usercss-meta/usercss-meta.min.js', '/vendor/usercss-meta/usercss-meta.min.js',
'/vendor-overwrites/colorpicker/colorconverter.js', '/vendor-overwrites/colorpicker/colorconverter.js',
'/js/meta-parser.js' '/js/meta-parser.js'

View File

@ -1,8 +1,8 @@
/* global download prefs openURL FIREFOX CHROME /* global download prefs openURL FIREFOX CHROME
URLS ignoreChromeError usercssHelper URLS ignoreChromeError chromeLocal semverCompare
styleManager msg navigatorUtil workerUtil contentScripts sync styleManager msg navigatorUtil workerUtil contentScripts sync
findExistingTab activateTab isTabReplaceable getActiveTab findExistingTab activateTab isTabReplaceable getActiveTab
tabManager */ */
'use strict'; 'use strict';
@ -111,14 +111,6 @@ navigatorUtil.onUrlChange(({tabId, frameId}, type) => {
} }
}); });
tabManager.onUpdate(({tabId, url, oldUrl = ''}) => {
if (usercssHelper.testUrl(url) && !oldUrl.startsWith(URLS.installUsercss)) {
usercssHelper.testContents(tabId, url).then(data => {
if (data.code) usercssHelper.openInstallerPage(tabId, url, data);
});
}
});
if (FIREFOX) { if (FIREFOX) {
// FF misses some about:blank iframes so we inject our content script explicitly // FF misses some about:blank iframes so we inject our content script explicitly
navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, { navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, {
@ -139,7 +131,7 @@ if (chrome.commands) {
} }
// ************************************************************************* // *************************************************************************
chrome.runtime.onInstalled.addListener(({reason}) => { chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
// save install type: "admin", "development", "normal", "sideload" or "other" // save install type: "admin", "development", "normal", "sideload" or "other"
// "normal" = addon installed from webstore // "normal" = addon installed from webstore
chrome.management.getSelf(info => { chrome.management.getSelf(info => {
@ -156,6 +148,14 @@ chrome.runtime.onInstalled.addListener(({reason}) => {
}); });
// themes may change // themes may change
delete localStorage.codeMirrorThemes; delete localStorage.codeMirrorThemes;
// inline search cache for USO is not needed anymore, TODO: remove this by the middle of 2021
if (semverCompare(previousVersion, '1.5.13') <= 0) {
setTimeout(async () => {
const del = Object.keys(await chromeLocal.get())
.filter(key => key.startsWith('usoSearchCache'));
if (del.length) chromeLocal.remove(del);
}, 15e3);
}
}); });
// ************************************************************************* // *************************************************************************

View File

@ -1,4 +1,4 @@
/* global chromeLocal ignoreChromeError workerUtil createChromeStorageDB */ /* global chromeLocal workerUtil createChromeStorageDB */
/* exported db */ /* exported db */
/* /*
Initialize a database. There are some problems using IndexedDB in Firefox: Initialize a database. There are some problems using IndexedDB in Firefox:
@ -10,29 +10,18 @@ https://www.reddit.com/r/firefox/comments/7ijuaq/firefox_59_webextensions_can_us
'use strict'; 'use strict';
const db = (() => { const db = (() => {
let exec; const DATABASE = 'stylish';
const preparing = prepare(); const STORE = 'styles';
return { const FALLBACK = 'dbInChromeStorage';
exec: (...args) => const dbApi = {
preparing.then(() => exec(...args)) async exec(...args) {
}; dbApi.exec = await tryUsingIndexedDB().catch(useChromeStorage);
return dbApi.exec(...args);
function prepare() {
return withPromise(shouldUseIndexedDB).then(
ok => {
if (ok) {
useIndexedDB();
} else {
useChromeStorage();
}
}, },
err => { };
useChromeStorage(err); return dbApi;
}
);
}
function shouldUseIndexedDB() { async function tryUsingIndexedDB() {
// we use chrome.storage.local fallback if IndexedDB doesn't save data, // we use chrome.storage.local fallback if IndexedDB doesn't save data,
// which, once detected on the first run, is remembered in chrome.storage.local // which, once detected on the first run, is remembered in chrome.storage.local
// for reliablility and in localStorage for fast synchronous access // for reliablility and in localStorage for fast synchronous access
@ -42,86 +31,53 @@ const db = (() => {
if (typeof indexedDB === 'undefined') { if (typeof indexedDB === 'undefined') {
throw new Error('indexedDB is undefined'); throw new Error('indexedDB is undefined');
} }
// test localStorage switch (await getFallback()) {
const fallbackSet = localStorage.dbInChromeStorage; case true: throw null;
if (fallbackSet === 'true') { case false: break;
return false; default: await testDB();
} }
if (fallbackSet === 'false') { return useIndexedDB();
return true;
}
// test storage.local
return chromeLocal.get('dbInChromeStorage')
.then(data => {
if (data && data.dbInChromeStorage) {
return false;
}
return testDBSize()
.then(ok => ok || testDBMutation());
});
} }
function withPromise(fn) { async function getFallback() {
try { return localStorage[FALLBACK] === 'true' ? true :
return Promise.resolve(fn()); localStorage[FALLBACK] === 'false' ? false :
} catch (err) { chromeLocal.getValue(FALLBACK);
return Promise.reject(err);
}
} }
function testDBSize() { async function testDB() {
return dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1) let e = await dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1);
.then(event => ( // throws if result is null
event.target.result && e = e.target.result[0];
event.target.result.length && const id = `${performance.now()}.${Math.random()}.${Date.now()}`;
event.target.result[0] await dbExecIndexedDB('put', {id});
)); e = await dbExecIndexedDB('get', id);
} // throws if result or id is null
await dbExecIndexedDB('delete', e.target.result.id);
function testDBMutation() {
return dbExecIndexedDB('put', {id: -1})
.then(() => dbExecIndexedDB('get', -1))
.then(event => {
if (!event.target.result) {
throw new Error('failed to get previously put item');
}
if (event.target.result.id !== -1) {
throw new Error('item id is wrong');
}
return dbExecIndexedDB('delete', -1);
})
.then(() => true);
} }
function useChromeStorage(err) { function useChromeStorage(err) {
exec = createChromeStorageDB().exec; chromeLocal.setValue(FALLBACK, true);
chromeLocal.set({dbInChromeStorage: true}, ignoreChromeError);
if (err) { if (err) {
chromeLocal.setValue('dbInChromeStorageReason', workerUtil.cloneError(err)); chromeLocal.setValue(FALLBACK + 'Reason', workerUtil.cloneError(err));
console.warn('Failed to access indexedDB. Switched to storage API.', err); console.warn('Failed to access indexedDB. Switched to storage API.', err);
} }
localStorage.dbInChromeStorage = 'true'; localStorage[FALLBACK] = 'true';
return createChromeStorageDB().exec;
} }
function useIndexedDB() { function useIndexedDB() {
exec = dbExecIndexedDB; chromeLocal.setValue(FALLBACK, false);
chromeLocal.set({dbInChromeStorage: false}, ignoreChromeError); localStorage[FALLBACK] = 'false';
localStorage.dbInChromeStorage = 'false'; return dbExecIndexedDB;
} }
function dbExecIndexedDB(method, ...args) { async function dbExecIndexedDB(method, ...args) {
return open().then(database => {
if (!method) {
return database;
}
if (method === 'putMany') {
return putMany(database, ...args);
}
const mode = method.startsWith('get') ? 'readonly' : 'readwrite'; const mode = method.startsWith('get') ? 'readonly' : 'readwrite';
const transaction = database.transaction(['styles'], mode); const store = (await open()).transaction([STORE], mode).objectStore(STORE);
const store = transaction.objectStore('styles'); const fn = method === 'putMany' ? putMany : storeRequest;
return storeRequest(store, method, ...args); return fn(store, method, ...args);
}); }
function storeRequest(store, method, ...args) { function storeRequest(store, method, ...args) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -131,26 +87,25 @@ const db = (() => {
}); });
} }
function putMany(store, _method, items) {
return Promise.all(items.map(item => storeRequest(store, 'put', item)));
}
function open() { function open() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const request = indexedDB.open('stylish', 2); const request = indexedDB.open(DATABASE, 2);
request.onsuccess = () => resolve(request.result); request.onsuccess = () => resolve(request.result);
request.onerror = reject; request.onerror = reject;
request.onupgradeneeded = event => { request.onupgradeneeded = create;
});
}
function create(event) {
if (event.oldVersion === 0) { if (event.oldVersion === 0) {
event.target.result.createObjectStore('styles', { event.target.result.createObjectStore(STORE, {
keyPath: 'id', keyPath: 'id',
autoIncrement: true, autoIncrement: true,
}); });
} }
};
});
}
function putMany(database, items) {
const transaction = database.transaction(['styles'], 'readwrite');
const store = transaction.objectStore('styles');
return Promise.all(items.map(item => storeRequest(store, 'put', item)));
}
} }
})(); })();

View File

@ -1,6 +1,6 @@
/* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */ /* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */
/* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty styleSectionGlobal /* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty styleSectionGlobal
getStyleWithNoCode msg sync uuidv4 */ getStyleWithNoCode msg sync uuidv4 URLS */
/* exported styleManager */ /* exported styleManager */
'use strict'; 'use strict';
@ -226,6 +226,13 @@ const styleManager = (() => {
if (!reason) { if (!reason) {
reason = style ? 'update' : 'install'; 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? // FIXME: update updateDate? what about usercss config?
return calcStyleDigest(data) return calcStyleDigest(data)
.then(digest => { .then(digest => {

View File

@ -1,15 +1,8 @@
/* global API_METHODS usercss styleManager deepCopy openURL download URLS */ /* global API_METHODS usercss styleManager deepCopy */
/* exported usercssHelper */ /* exported usercssHelper */
'use strict'; 'use strict';
const usercssHelper = (() => { const usercssHelper = (() => {
const installCodeCache = {};
const clearInstallCode = url => delete installCodeCache[url];
const isResponseText = r => /^text\/(css|plain)(;.*?)?$/i.test(r.headers.get('content-type'));
// in Firefox we have to use a content script to read file://
const fileLoader = !chrome.app && // not relying on navigator.ua which can be spoofed
(tabId => browser.tabs.executeScript(tabId, {file: '/content/install-hook-usercss.js'}).then(r => r[0]));
API_METHODS.installUsercss = installUsercss; API_METHODS.installUsercss = installUsercss;
API_METHODS.editSaveUsercss = editSaveUsercss; API_METHODS.editSaveUsercss = editSaveUsercss;
API_METHODS.configUsercssVars = configUsercssVars; API_METHODS.configUsercssVars = configUsercssVars;
@ -17,50 +10,6 @@ const usercssHelper = (() => {
API_METHODS.buildUsercss = build; API_METHODS.buildUsercss = build;
API_METHODS.findUsercss = find; API_METHODS.findUsercss = find;
API_METHODS.getUsercssInstallCode = url => {
// when the installer tab is reloaded after the cache is expired, this will throw intentionally
const {code, timer} = installCodeCache[url];
clearInstallCode(url);
clearTimeout(timer);
return code;
};
return {
testUrl(url) {
return url.includes('.user.') &&
/^(https?|file|ftps?):/.test(url) &&
/\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]);
},
/** @return {Promise<{ code:string, inTab:boolean } | false>} */
testContents(tabId, url) {
const isFile = url.startsWith('file:');
const inTab = isFile && Boolean(fileLoader);
return Promise.resolve(isFile || fetch(url, {method: 'HEAD'}).then(isResponseText))
.then(ok => ok && (inTab ? fileLoader(tabId) : download(url)))
.then(code => /==userstyle==/i.test(code) && {code, inTab});
},
openInstallerPage(tabId, url, {code, inTab} = {}) {
const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`;
if (inTab) {
browser.tabs.get(tabId).then(tab =>
openURL({
url: `${newUrl}&tabId=${tabId}`,
active: tab.active,
index: tab.index + 1,
openerTabId: tabId,
currentWindow: null,
}));
} else {
const timer = setTimeout(clearInstallCode, 10e3, url);
installCodeCache[url] = {code, timer};
chrome.tabs.update(tabId, {url: newUrl});
}
},
};
function buildMeta(style) { function buildMeta(style) {
if (style.usercssData) { if (style.usercssData) {
return Promise.resolve(style); return Promise.resolve(style);

View File

@ -0,0 +1,82 @@
/* global API_METHODS openURL download URLS tabManager */
'use strict';
(() => {
const installCodeCache = {};
const clearInstallCode = url => delete installCodeCache[url];
const isContentTypeText = type => /^text\/(css|plain)(;.*?)?$/i.test(type);
// in Firefox we have to use a content script to read file://
const fileLoader = !chrome.app && (
async tabId =>
(await browser.tabs.executeScript(tabId, {file: '/content/install-hook-usercss.js'}))[0]);
const urlLoader =
async (tabId, url) => (
url.startsWith('file:') ||
tabManager.get(tabId, isContentTypeText.name) ||
isContentTypeText((await fetch(url, {method: 'HEAD'})).headers.get('content-type'))
) && download(url);
API_METHODS.getUsercssInstallCode = url => {
// when the installer tab is reloaded after the cache is expired, this will throw intentionally
const {code, timer} = installCodeCache[url];
clearInstallCode(url);
clearTimeout(timer);
return code;
};
// Faster installation on known distribution sites to avoid flicker of css text
chrome.webRequest.onBeforeSendHeaders.addListener(({tabId, url}) => {
openInstallerPage(tabId, url, {});
// Silently suppressing navigation like it never happened
return {redirectUrl: 'javascript:void 0'}; // eslint-disable-line no-script-url
}, {
urls: [
URLS.usoArchiveRaw + 'usercss/*.user.css',
'*://greasyfork.org/scripts/*/code/*.user.css',
'*://sleazyfork.org/scripts/*/code/*.user.css',
],
types: ['main_frame'],
}, ['blocking']);
// Remember Content-Type to avoid re-fetching of the headers in urlLoader as it can be very slow
chrome.webRequest.onHeadersReceived.addListener(({tabId, responseHeaders}) => {
const h = responseHeaders.find(h => h.name.toLowerCase() === 'content-type');
tabManager.set(tabId, isContentTypeText.name, h && isContentTypeText(h.value) || undefined);
}, {
urls: '%css,%css?*,%styl,%styl?*'.replace(/%/g, '*://*/*.user.').split(','),
types: ['main_frame'],
}, ['responseHeaders']);
tabManager.onUpdate(async ({tabId, url, oldUrl = ''}) => {
if (url.includes('.user.') &&
/^(https?|file|ftps?):/.test(url) &&
/\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]) &&
!oldUrl.startsWith(URLS.installUsercss)) {
const inTab = url.startsWith('file:') && Boolean(fileLoader);
const code = await (inTab ? fileLoader : urlLoader)(tabId, url);
if (/==userstyle==/i.test(code)) {
openInstallerPage(tabId, url, {code, inTab});
}
}
});
function openInstallerPage(tabId, url, {code, inTab} = {}) {
const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`;
if (inTab) {
browser.tabs.get(tabId).then(tab =>
openURL({
url: `${newUrl}&tabId=${tabId}`,
active: tab.active,
index: tab.index + 1,
openerTabId: tabId,
currentWindow: null,
}));
} else {
const timer = setTimeout(clearInstallCode, 10e3, url);
installCodeCache[url] = {code, timer};
chrome.tabs.update(tabId, {url: newUrl});
}
}
})();

View File

@ -16,7 +16,6 @@ createAPI({
}, },
metalint: code => { metalint: code => {
loadScript( loadScript(
'/js/polyfill.js',
'/vendor/usercss-meta/usercss-meta.min.js', '/vendor/usercss-meta/usercss-meta.min.js',
'/vendor-overwrites/colorpicker/colorconverter.js', '/vendor-overwrites/colorpicker/colorconverter.js',
'/js/meta-parser.js' '/js/meta-parser.js'

View File

@ -46,7 +46,7 @@ function createSourceEditor({style, onTitleChanged}) {
metaCompiler.onUpdated(meta => { metaCompiler.onUpdated(meta => {
style.usercssData = meta; style.usercssData = meta;
style.name = meta.name; style.name = meta.name;
style.url = meta.homepageURL; style.url = meta.homepageURL || style.installationUrl;
updateMeta(); updateMeta();
}); });

View File

@ -318,7 +318,9 @@
let sequence = null; let sequence = null;
if (tabId < 0) { if (tabId < 0) {
getData = DirectDownloader(); getData = DirectDownloader();
sequence = API.getUsercssInstallCode(initialUrl).catch(getData); sequence = API.getUsercssInstallCode(initialUrl)
.then(code => code || getData())
.catch(getData);
} else { } else {
getData = PortDownloader(); getData = PortDownloader();
sequence = getData({timer: false}); sequence = getData({timer: false});

View File

@ -98,7 +98,11 @@ document.addEventListener('wheel', event => {
return; return;
} }
if (el.tagName === 'SELECT') { if (el.tagName === 'SELECT') {
el.selectedIndex = Math.max(0, Math.min(el.length - 1, el.selectedIndex + Math.sign(event.deltaY))); const old = el.selectedIndex;
el.selectedIndex = Math.max(0, Math.min(el.length - 1, old + Math.sign(event.deltaY)));
if (el.selectedIndex !== old) {
el.dispatchEvent(new Event('change', {bubbles: true}));
}
event.preventDefault(); event.preventDefault();
} }
event.stopImmediatePropagation(); event.stopImmediatePropagation();

View File

@ -62,7 +62,19 @@ const URLS = {
// TODO: remove when "minimum_chrome_version": "61" or higher // TODO: remove when "minimum_chrome_version": "61" or higher
chromeProtectsNTP: CHROME >= 61, chromeProtectsNTP: CHROME >= 61,
userstylesOrgJson: 'https://userstyles.org/styles/chrome/', uso: 'https://userstyles.org/',
usoJson: 'https://userstyles.org/styles/chrome/',
usoArchive: 'https://33kk.github.io/uso-archive/',
usoArchiveRaw: 'https://raw.githubusercontent.com/33kk/uso-archive/flomaster/data/',
extractUsoArchiveId: url =>
url &&
url.startsWith(URLS.usoArchiveRaw) &&
parseInt(url.match(/\/(\d+)\.user\.css|$/)[1]),
extractGreasyForkId: url =>
/^https:\/\/(?:greasy|sleazy)fork\.org\/scripts\/(\d+)[^/]*\/code\/[^/]*\.user\.css$/.test(url) &&
RegExp.$1,
supported: url => ( supported: url => (
url.startsWith('http') && (FIREFOX || !url.startsWith(URLS.browserWebStore)) || url.startsWith('http') && (FIREFOX || !url.startsWith(URLS.browserWebStore)) ||
@ -438,7 +450,7 @@ function download(url, {
function collapseUsoVars(url) { function collapseUsoVars(url) {
if (queryPos < 0 || if (queryPos < 0 ||
url.length < 2000 || url.length < 2000 ||
!url.startsWith(URLS.userstylesOrgJson) || !url.startsWith(URLS.usoJson) ||
!/^get$/i.test(method)) { !/^get$/i.test(method)) {
return url; return url;
} }

View File

@ -3,27 +3,33 @@
// eslint-disable-next-line no-unused-expressions // eslint-disable-next-line no-unused-expressions
self.INJECTED !== 1 && (() => { self.INJECTED !== 1 && (() => {
// this part runs in workers, content scripts, our extension pages //#region for content scripts and our extension pages
if (!Object.entries) { if (!window.browser || !browser.runtime) {
Object.entries = obj => Object.keys(obj).map(k => [k, obj[k]]); const createTrap = (base, parent) => {
const target = typeof base === 'function' ? () => {} : {};
target.isTrap = true;
return new Proxy(target, {
get: (target, prop) => {
if (target[prop]) return target[prop];
if (base[prop] && (typeof base[prop] === 'object' || typeof base[prop] === 'function')) {
target[prop] = createTrap(base[prop], base);
return target[prop];
} }
if (!Object.values) { return base[prop];
Object.values = obj => Object.keys(obj).map(k => obj[k]); },
apply: (target, thisArg, args) => base.apply(parent, args)
});
};
window.browser = createTrap(chrome, null);
} }
// don't use self.chrome. It is undefined in Firefox
if (typeof chrome !== 'object') return;
// the rest is for content scripts and our extension pages
self.browser = polyfillBrowser();
/* Promisifies the specified `chrome` methods into `browser`. /* Promisifies the specified `chrome` methods into `browser`.
The definitions is an object like this: { The definitions is an object like this: {
'storage.sync': ['get', 'set'], // if deeper than one level, combine the path via `.` 'storage.sync': ['get', 'set'], // if deeper than one level, combine the path via `.`
windows: ['create', 'update'], // items and sub-objects will only be created if present in `chrome` windows: ['create', 'update'], // items and sub-objects will only be created if present in `chrome`
} */ } */
self.promisifyChrome = definitions => { window.promisifyChrome = definitions => {
for (const [scopeName, methods] of Object.entries(definitions)) { for (const [scopeName, methods] of Object.entries(definitions)) {
const path = scopeName.split('.'); const path = scopeName.split('.');
const src = path.reduce((obj, p) => obj && obj[p], chrome); const src = path.reduce((obj, p) => obj && obj[p], chrome);
@ -43,90 +49,18 @@ self.INJECTED !== 1 && (() => {
}; };
if (!chrome.tabs) return; if (!chrome.tabs) return;
// the rest is for our extension pages
if (typeof document === 'object') { //#endregion
const ELEMENT_METH = { //#region for our extension pages
append: {
base: [Element, Document, DocumentFragment],
fn: (node, frag) => {
node.appendChild(frag);
}
},
prepend: {
base: [Element, Document, DocumentFragment],
fn: (node, frag) => {
node.insertBefore(frag, node.firstChild);
}
},
before: {
base: [Element, CharacterData, DocumentType],
fn: (node, frag) => {
node.parentNode.insertBefore(frag, node);
}
},
after: {
base: [Element, CharacterData, DocumentType],
fn: (node, frag) => {
node.parentNode.insertBefore(frag, node.nextSibling);
}
}
};
for (const [key, {base, fn}] of Object.entries(ELEMENT_METH)) { for (const storage of ['localStorage', 'sessionStorage']) {
for (const cls of base) {
if (cls.prototype[key]) {
continue;
}
cls.prototype[key] = function (...nodes) {
const frag = document.createDocumentFragment();
for (const node of nodes) {
frag.appendChild(typeof node === 'string' ? document.createTextNode(node) : node);
}
fn(this, frag);
};
}
}
}
try { try {
if (!localStorage) { window[storage]._access_check = 1;
throw new Error('localStorage is null'); delete window[storage]._access_check;
}
localStorage._access_check = 1;
delete localStorage._access_check;
} catch (err) { } catch (err) {
Object.defineProperty(self, 'localStorage', {value: {}}); Object.defineProperty(window, storage, {value: {}});
} }
try {
if (!sessionStorage) {
throw new Error('sessionStorage is null');
}
sessionStorage._access_check = 1;
delete sessionStorage._access_check;
} catch (err) {
Object.defineProperty(self, 'sessionStorage', {value: {}});
} }
function polyfillBrowser() { //#endregion
if (typeof browser === 'object' && browser.runtime) {
return browser;
}
return createTrap(chrome, null);
function createTrap(base, parent) {
const target = typeof base === 'function' ? () => {} : {};
target.isTrap = true;
return new Proxy(target, {
get: (target, prop) => {
if (target[prop]) return target[prop];
if (base[prop] && (typeof base[prop] === 'object' || typeof base[prop] === 'function')) {
target[prop] = createTrap(base[prop], base);
return target[prop];
}
return base[prop];
},
apply: (target, thisArg, args) => base.apply(parent, args)
});
}
}
})(); })();

View File

@ -1,7 +1,7 @@
{ {
"name": "Stylus", "name": "Stylus",
"version": "1.5.13", "version": "1.5.13",
"minimum_chrome_version": "49", "minimum_chrome_version": "55",
"description": "__MSG_description__", "description": "__MSG_description__",
"homepage_url": "https://add0n.com/stylus.html", "homepage_url": "https://add0n.com/stylus.html",
"manifest_version": 2, "manifest_version": 2,
@ -51,6 +51,7 @@
"background/icon-manager.js", "background/icon-manager.js",
"background/background.js", "background/background.js",
"background/usercss-helper.js", "background/usercss-helper.js",
"background/usercss-install-helper.js",
"background/style-via-api.js", "background/style-via-api.js",
"background/search-db.js", "background/search-db.js",
"background/update.js", "background/update.js",

View File

@ -120,9 +120,7 @@
<div class="search-result-actions"> <div class="search-result-actions">
<button class="search-result-install hidden" i18n-text="installButton"></button> <button class="search-result-install hidden" i18n-text="installButton"></button>
<button class="search-result-uninstall hidden" i18n-text="deleteStyleLabel"></button> <button class="search-result-uninstall hidden" i18n-text="deleteStyleLabel"></button>
<button class="search-result-customize hidden" <button class="search-result-customize hidden" i18n-text="configureStyle"></button>
i18n-text="configureStyle"
i18n-title="configureStyleOnHomepage"></button>
</div> </div>
<dl class="search-result-meta"> <dl class="search-result-meta">
<div data-type="author"> <div data-type="author">
@ -254,6 +252,27 @@
<div id="search-results-error" class="hidden"></div> <div id="search-results-error" class="hidden"></div>
<div id="search-results" class="hidden"> <div id="search-results" class="hidden">
<div class="search-results-nav" data-type="top"></div> <div class="search-results-nav" data-type="top"></div>
<div id="search-params">
<input id="search-query" type="search" i18n-placeholder="search"
i18n-title="searchStyleQueryHint">
<div class="select-resizer">
<select id="search-order" i18n-title="sortStylesHelpTitle">
<option value="n" i18n-text="genericTitle">
<option value="u" i18n-text="searchResultUpdated">
<option value="t" i18n-text="searchResultInstallCount">
<option value="w" i18n-text="searchResultWeeklyCount">
<option value="r" i18n-text="searchResultRating">
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
<label>
<span class="checkbox-container">
<input id="search-globals" type="checkbox" checked>
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</span>
<span i18n-text="searchGlobalStyles"></span>
</label>
</div>
<div id="search-results-list"></div> <div id="search-results-list"></div>
<div class="search-results-nav" data-type="bottom"></div> <div class="search-results-nav" data-type="bottom"></div>
</div> </div>

View File

@ -33,7 +33,8 @@ const hotkeys = (() => {
} }
function onKeyDown(event) { function onKeyDown(event) {
if (event.ctrlKey || event.altKey || event.metaKey || !enabled) { if (event.ctrlKey || event.altKey || event.metaKey || !enabled ||
/^(text|search)$/.test((document.activeElement || {}).type)) {
return; return;
} }
let entry; let entry;

View File

@ -13,7 +13,8 @@ const handleEvent = {};
const ABOUT_BLANK = 'about:blank'; const ABOUT_BLANK = 'about:blank';
const ENTRY_ID_PREFIX_RAW = 'style-'; const ENTRY_ID_PREFIX_RAW = 'style-';
const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW;
$.entry = styleOrId => $(`#${ENTRY_ID_PREFIX_RAW}${styleOrId.id || styleOrId}`);
if (CHROME >= 66 && CHROME <= 69) { // Chrome 66-69 adds a gap, https://crbug.com/821143 if (CHROME >= 66 && CHROME <= 69) { // Chrome 66-69 adds a gap, https://crbug.com/821143
document.head.appendChild($create('style', 'html { overflow: overlay }')); document.head.appendChild($create('style', 'html { overflow: overlay }'));
@ -27,7 +28,7 @@ initTabUrls()
onDOMready().then(() => initPopup(frames)), onDOMready().then(() => initPopup(frames)),
...frames ...frames
.filter(f => f.url && !f.isDupe) .filter(f => f.url && !f.isDupe)
.map(({url}) => API.getStylesByUrl(url).then(styles => ({styles, url}))), .map(({url}) => getStyleDataMerged(url).then(styles => ({styles, url}))),
])) ]))
.then(([, ...results]) => { .then(([, ...results]) => {
if (results[0]) { if (results[0]) {
@ -53,17 +54,19 @@ if (CHROME_HAS_BORDER_BUG) {
} }
function onRuntimeMessage(msg) { function onRuntimeMessage(msg) {
if (!tabURL) return;
let ready = Promise.resolve();
switch (msg.method) { switch (msg.method) {
case 'styleAdded': case 'styleAdded':
case 'styleUpdated': case 'styleUpdated':
if (msg.reason === 'editPreview' || msg.reason === 'editPreviewEnd') return; if (msg.reason === 'editPreview' || msg.reason === 'editPreviewEnd') return;
handleUpdate(msg); ready = handleUpdate(msg);
break; break;
case 'styleDeleted': case 'styleDeleted':
handleDelete(msg.style.id); handleDelete(msg.style.id);
break; break;
} }
dispatchEvent(new CustomEvent(msg.method, {detail: msg})); ready.then(() => dispatchEvent(new CustomEvent(msg.method, {detail: msg})));
} }
@ -141,8 +144,7 @@ function initPopup(frames) {
} }
if (!tabURL) { if (!tabURL) {
document.body.classList.add('blocked'); blockPopup();
document.body.insertBefore(template.unavailableInfo, document.body.firstChild);
return; return;
} }
@ -315,24 +317,30 @@ function showStyles(frameResults) {
const entries = new Map(); const entries = new Map();
frameResults.forEach(({styles = [], url}, index) => { frameResults.forEach(({styles = [], url}, index) => {
styles.forEach(style => { styles.forEach(style => {
const {id} = style.data; const {id} = style;
if (!entries.has(id)) { if (!entries.has(id)) {
style.frameUrl = index === 0 ? '' : url; style.frameUrl = index === 0 ? '' : url;
entries.set(id, createStyleElement(Object.assign(style.data, style))); entries.set(id, createStyleElement(style));
} }
}); });
}); });
if (entries.size) { if (entries.size) {
installed.append(...sortStyles([...entries.values()])); resortEntries([...entries.values()]);
} else { } else {
installed.appendChild(template.noStyles.cloneNode(true)); installed.appendChild(template.noStyles);
} }
window.dispatchEvent(new Event('showStyles:done')); window.dispatchEvent(new Event('showStyles:done'));
} }
function resortEntries(entries) {
// `entries` is specified only at startup, after that we respect the prefs
if (entries || prefs.get('popup.autoResort')) {
installed.append(...sortStyles(entries || $$('.entry', installed)));
}
}
function createStyleElement(style) { function createStyleElement(style) {
let entry = $(ENTRY_ID_PREFIX + style.id); let entry = $.entry(style);
if (!entry) { if (!entry) {
entry = template.style.cloneNode(true); entry = template.style.cloneNode(true);
entry.setAttribute('style-id', style.id); entry.setAttribute('style-id', style.id);
@ -469,11 +477,7 @@ Object.assign(handleEvent, {
event.stopPropagation(); event.stopPropagation();
API API
.toggleStyle(handleEvent.getClickedStyleId(event), this.checked) .toggleStyle(handleEvent.getClickedStyleId(event), this.checked)
.then(() => { .then(() => resortEntries());
if (prefs.get('popup.autoResort')) {
installed.append(...sortStyles($$('.entry', installed)));
}
});
}, },
toggleExclude(event, type) { toggleExclude(event, type) {
@ -672,38 +676,25 @@ Object.assign(handleEvent, {
}); });
function handleUpdate({style, reason}) { async function handleUpdate({style, reason}) {
if (!tabURL) return; if (reason !== 'toggle' || !$.entry(style)) {
style = await getStyleDataMerged(tabURL, style.id);
fetchStyle() if (!style) return;
.then(style => {
if (!style) {
return;
} }
if ($(ENTRY_ID_PREFIX + style.id)) { const el = createStyleElement(style);
createStyleElement(style); if (!el.parentNode) {
return; installed.appendChild(el);
} blockPopup(false);
document.body.classList.remove('blocked');
$$.remove('.blocked-info, #no-styles');
createStyleElement(style);
})
.catch(console.error);
function fetchStyle() {
if (reason === 'toggle' && $(ENTRY_ID_PREFIX + style.id)) {
return Promise.resolve(style);
}
return API.getStylesByUrl(tabURL, style.id)
.then(([result]) => result && Object.assign(result.data, result));
} }
resortEntries();
} }
function handleDelete(id) { function handleDelete(id) {
$.remove(ENTRY_ID_PREFIX + id); const el = $.entry(id);
if (!$('.entry')) { if (el) {
installed.appendChild(template.noStyles.cloneNode(true)); el.remove();
if (!$('.entry')) installed.appendChild(template.noStyles);
} }
} }
@ -721,3 +712,21 @@ function waitForTabUrlFF(tab) {
]); ]);
}); });
} }
/* 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));
return id ? styles[0] : styles;
}
function blockPopup(isBlocked = true) {
document.body.classList.toggle('blocked', isBlocked);
if (isBlocked) {
document.body.prepend(template.unavailableInfo);
} else {
template.unavailableInfo.remove();
template.noStyles.remove();
}
}

View File

@ -54,21 +54,15 @@ body.search-results-shown {
background-color: #fff; background-color: #fff;
} }
.search-result .lds-spinner { #search-results .lds-spinner {
transform: scale(.5); transform: scale(.5);
filter: invert(1) drop-shadow(1px 1px 3px #000); filter: invert(1) drop-shadow(1px 1px 3px #000);
} }
.search-result-empty .lds-spinner { #search-results .search-result-empty .lds-spinner {
transform: scale(.5);
filter: opacity(.2); filter: opacity(.2);
} }
.search-result-fadein {
animation: fadein 1s;
animation-fill-mode: both;
}
.search-result-screenshot { .search-result-screenshot {
height: 140px; height: 140px;
width: 100%; width: 100%;
@ -257,6 +251,24 @@ body.search-results-shown {
padding-left: 16px; padding-left: 16px;
} }
#search-params {
display: flex;
position: relative;
margin-top: -.5rem;
margin-bottom: 1.25rem;
flex-wrap: wrap;
}
#search-params > * {
margin-top: .5rem;
}
#search-query {
min-width: 3em;
margin-right: .5em;
flex: 1 1 0;
}
/* spinner: https://github.com/loadingio/css-spinner */ /* spinner: https://github.com/loadingio/css-spinner */
.lds-spinner { .lds-spinner {
-webkit-user-select: none; -webkit-user-select: none;

File diff suppressed because it is too large Load Diff