switch to USO-archive for inline search in popup, #1056
This commit is contained in:
commit
ad24ee0c15
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// *************************************************************************
|
// *************************************************************************
|
||||||
|
|
179
background/db.js
179
background/db.js
|
@ -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);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
return dbApi;
|
||||||
|
|
||||||
function prepare() {
|
async function tryUsingIndexedDB() {
|
||||||
return withPromise(shouldUseIndexedDB).then(
|
|
||||||
ok => {
|
|
||||||
if (ok) {
|
|
||||||
useIndexedDB();
|
|
||||||
} else {
|
|
||||||
useChromeStorage();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
err => {
|
|
||||||
useChromeStorage(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldUseIndexedDB() {
|
|
||||||
// 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,115 +31,81 @@ 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 => {
|
const mode = method.startsWith('get') ? 'readonly' : 'readwrite';
|
||||||
if (!method) {
|
const store = (await open()).transaction([STORE], mode).objectStore(STORE);
|
||||||
return database;
|
const fn = method === 'putMany' ? putMany : storeRequest;
|
||||||
}
|
return fn(store, method, ...args);
|
||||||
if (method === 'putMany') {
|
}
|
||||||
return putMany(database, ...args);
|
|
||||||
}
|
function storeRequest(store, method, ...args) {
|
||||||
const mode = method.startsWith('get') ? 'readonly' : 'readwrite';
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = database.transaction(['styles'], mode);
|
const request = store[method](...args);
|
||||||
const store = transaction.objectStore('styles');
|
request.onsuccess = resolve;
|
||||||
return storeRequest(store, method, ...args);
|
request.onerror = reject;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function storeRequest(store, method, ...args) {
|
function putMany(store, _method, items) {
|
||||||
return new Promise((resolve, reject) => {
|
return Promise.all(items.map(item => storeRequest(store, 'put', item)));
|
||||||
const request = store[method](...args);
|
}
|
||||||
request.onsuccess = resolve;
|
|
||||||
request.onerror = reject;
|
function open() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open(DATABASE, 2);
|
||||||
|
request.onsuccess = () => resolve(request.result);
|
||||||
|
request.onerror = reject;
|
||||||
|
request.onupgradeneeded = create;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function create(event) {
|
||||||
|
if (event.oldVersion === 0) {
|
||||||
|
event.target.result.createObjectStore(STORE, {
|
||||||
|
keyPath: 'id',
|
||||||
|
autoIncrement: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function open() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = indexedDB.open('stylish', 2);
|
|
||||||
request.onsuccess = () => resolve(request.result);
|
|
||||||
request.onerror = reject;
|
|
||||||
request.onupgradeneeded = event => {
|
|
||||||
if (event.oldVersion === 0) {
|
|
||||||
event.target.result.createObjectStore('styles', {
|
|
||||||
keyPath: 'id',
|
|
||||||
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)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
82
background/usercss-install-helper.js
Normal file
82
background/usercss-install-helper.js
Normal 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});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
|
@ -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'
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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});
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
122
js/polyfill.js
122
js/polyfill.js
|
@ -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];
|
||||||
|
}
|
||||||
|
return base[prop];
|
||||||
|
},
|
||||||
|
apply: (target, thisArg, args) => base.apply(parent, args)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
window.browser = createTrap(chrome, null);
|
||||||
}
|
}
|
||||||
if (!Object.values) {
|
|
||||||
Object.values = obj => Object.keys(obj).map(k => obj[k]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
try {
|
||||||
if (cls.prototype[key]) {
|
window[storage]._access_check = 1;
|
||||||
continue;
|
delete window[storage]._access_check;
|
||||||
}
|
} catch (err) {
|
||||||
cls.prototype[key] = function (...nodes) {
|
Object.defineProperty(window, storage, {value: {}});
|
||||||
const frag = document.createDocumentFragment();
|
|
||||||
for (const node of nodes) {
|
|
||||||
frag.appendChild(typeof node === 'string' ? document.createTextNode(node) : node);
|
|
||||||
}
|
|
||||||
fn(this, frag);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
if (!localStorage) {
|
|
||||||
throw new Error('localStorage is null');
|
|
||||||
}
|
|
||||||
localStorage._access_check = 1;
|
|
||||||
delete localStorage._access_check;
|
|
||||||
} catch (err) {
|
|
||||||
Object.defineProperty(self, 'localStorage', {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)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -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",
|
||||||
|
|
25
popup.html
25
popup.html
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)) {
|
|
||||||
createStyleElement(style);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
const el = createStyleElement(style);
|
||||||
|
if (!el.parentNode) {
|
||||||
|
installed.appendChild(el);
|
||||||
|
blockPopup(false);
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue
Block a user