FF: support private/container tabs
This commit is contained in:
parent
62e333a0ba
commit
3418ac9cb9
10
.eslintrc
10
.eslintrc
|
@ -13,9 +13,11 @@ globals:
|
|||
KEEP_CHANNEL_OPEN: false
|
||||
CHROME: false
|
||||
FIREFOX: false
|
||||
VIVALDI: false
|
||||
OPERA: false
|
||||
URLS: false
|
||||
BG: false
|
||||
API: false
|
||||
notifyAllTabs: false
|
||||
sendMessage: false
|
||||
queryTabs: false
|
||||
|
@ -33,10 +35,6 @@ globals:
|
|||
tryJSONparse: false
|
||||
debounce: false
|
||||
deepCopy: false
|
||||
onBackgroundReady: false
|
||||
deleteStyleSafe: false
|
||||
getStylesSafe: false
|
||||
saveStyleSafe: false
|
||||
sessionStorageHash: false
|
||||
download: false
|
||||
invokeOrPostpone: false
|
||||
|
@ -63,6 +61,10 @@ globals:
|
|||
# prefs.js
|
||||
prefs: false
|
||||
setupLivePrefs: false
|
||||
# storage-util.js
|
||||
chromeLocal: false
|
||||
chromeSync: false
|
||||
LZString: false
|
||||
|
||||
rules:
|
||||
accessor-pairs: [2]
|
||||
|
|
|
@ -1,9 +1,38 @@
|
|||
/* global dbExec, getStyles, saveStyle */
|
||||
/* global handleCssTransitionBug */
|
||||
/* global usercssHelper openEditor */
|
||||
/* global styleViaAPI */
|
||||
/*
|
||||
global dbExec getStyles saveStyle deleteStyle
|
||||
global handleCssTransitionBug detectSloppyRegexps
|
||||
global openEditor
|
||||
global styleViaAPI
|
||||
global loadScript
|
||||
global updater
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var API_METHODS = {
|
||||
|
||||
getStyles,
|
||||
saveStyle,
|
||||
deleteStyle,
|
||||
|
||||
download: msg => download(msg.url),
|
||||
getPrefs: () => prefs.getAll(),
|
||||
healthCheck: () => dbExec().then(() => true),
|
||||
|
||||
detectSloppyRegexps,
|
||||
openEditor,
|
||||
updateIcon,
|
||||
|
||||
closeTab: (msg, sender, respond) => {
|
||||
chrome.tabs.remove(msg.tabId || sender.tab.id, () => {
|
||||
if (chrome.runtime.lastError && msg.tabId !== sender.tab.id) {
|
||||
respond(new Error(chrome.runtime.lastError.message));
|
||||
}
|
||||
});
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var browserCommands, contextMenus;
|
||||
|
||||
|
@ -55,9 +84,17 @@ if (!chrome.browserAction ||
|
|||
window.updateIcon = () => {};
|
||||
}
|
||||
|
||||
const tabIcons = new Map();
|
||||
chrome.tabs.onRemoved.addListener(tabId => tabIcons.delete(tabId));
|
||||
chrome.tabs.onReplaced.addListener((added, removed) => tabIcons.delete(removed));
|
||||
|
||||
// *************************************************************************
|
||||
// set the default icon displayed after a tab is created until webNavigation kicks in
|
||||
prefs.subscribe(['iconset'], () => updateIcon({id: undefined}, {}));
|
||||
prefs.subscribe(['iconset'], () =>
|
||||
updateIcon({
|
||||
tab: {id: undefined},
|
||||
styles: {},
|
||||
}));
|
||||
|
||||
// *************************************************************************
|
||||
{
|
||||
|
@ -160,7 +197,10 @@ if (chrome.contextMenus) {
|
|||
window.addEventListener('storageReady', function _() {
|
||||
window.removeEventListener('storageReady', _);
|
||||
|
||||
updateIcon({id: undefined}, {});
|
||||
updateIcon({
|
||||
tab: {id: undefined},
|
||||
styles: {},
|
||||
});
|
||||
|
||||
const NTP = 'chrome://newtab/';
|
||||
const ALL_URLS = '<all_urls>';
|
||||
|
@ -223,7 +263,8 @@ function webNavigationListener(method, {url, tabId, frameId}) {
|
|||
}
|
||||
// main page frame id is 0
|
||||
if (frameId === 0) {
|
||||
updateIcon({id: tabId, url}, styles);
|
||||
tabIcons.delete(tabId);
|
||||
updateIcon({tab: {id: tabId, url}, styles});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -256,13 +297,13 @@ function webNavUsercssInstallerFF(data) {
|
|||
getTab(tabId),
|
||||
]).then(([pong, tab]) => {
|
||||
if (pong !== true && tab.url !== 'about:blank') {
|
||||
usercssHelper.openInstallPage(tab, {direct: true});
|
||||
API_METHODS.installUsercss({direct: true}, {tab});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function updateIcon(tab, styles) {
|
||||
function updateIcon({tab, styles}) {
|
||||
if (tab.id < 0) {
|
||||
return;
|
||||
}
|
||||
|
@ -277,38 +318,44 @@ function updateIcon(tab, styles) {
|
|||
.then(url => getStyles({matchUrl: url, enabled: true, asHash: true}))
|
||||
.then(stylesReceived);
|
||||
|
||||
function countStyles(styles) {
|
||||
if (Array.isArray(styles)) return styles.length;
|
||||
return Object.keys(styles).reduce((sum, id) => sum + !isNaN(Number(id)), 0);
|
||||
}
|
||||
|
||||
function stylesReceived(styles) {
|
||||
let numStyles = styles.length;
|
||||
if (numStyles === undefined) {
|
||||
// for 'styles' asHash:true fake the length by counting numeric ids manually
|
||||
numStyles = 0;
|
||||
for (const id of Object.keys(styles)) {
|
||||
numStyles += id.match(/^\d+$/) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
const numStyles = countStyles(styles);
|
||||
const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll');
|
||||
const postfix = disableAll ? 'x' : numStyles === 0 ? 'w' : '';
|
||||
const color = prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal');
|
||||
const text = prefs.get('show-badge') && numStyles ? String(numStyles) : '';
|
||||
const iconset = ['', 'light/'][prefs.get('iconset')] || '';
|
||||
const path = 'images/icon/' + iconset;
|
||||
chrome.browserAction.setIcon({
|
||||
tabId: tab.id,
|
||||
path: {
|
||||
const tabIcon = tabIcons.get(tab.id) || {};
|
||||
if (tabIcon.iconType !== iconset + postfix) {
|
||||
tabIcons.set(tab.id, tabIcon);
|
||||
tabIcon.iconType = iconset + postfix;
|
||||
const paths = {};
|
||||
if (FIREFOX || CHROME >= 2883 && !VIVALDI) {
|
||||
// Material Design 2016 new size is 16px
|
||||
16: `${path}16${postfix}.png`,
|
||||
32: `${path}32${postfix}.png`,
|
||||
paths['16'] = `${path}16${postfix}.png`;
|
||||
paths['32'] = `${path}32${postfix}.png`;
|
||||
} else {
|
||||
// Chromium forks or non-chromium browsers may still use the traditional 19px
|
||||
19: `${path}19${postfix}.png`,
|
||||
38: `${path}38${postfix}.png`,
|
||||
// TODO: add Edge preferred sizes: 20, 25, 30, 40
|
||||
},
|
||||
}, () => {
|
||||
if (chrome.runtime.lastError || tab.id === undefined) {
|
||||
return;
|
||||
paths['19'] = `${path}19${postfix}.png`;
|
||||
paths['38'] = `${path}38${postfix}.png`;
|
||||
}
|
||||
// Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor
|
||||
chrome.browserAction.setIcon({tabId: tab.id, path: paths}, ignoreChromeError);
|
||||
}
|
||||
if (tab.id === undefined) return;
|
||||
let defaultIcon = tabIcons.get(undefined);
|
||||
if (!defaultIcon) tabIcons.set(undefined, (defaultIcon = {}));
|
||||
if (defaultIcon.color !== color) {
|
||||
defaultIcon.color = color;
|
||||
chrome.browserAction.setBadgeBackgroundColor({color});
|
||||
}
|
||||
if (tabIcon.text !== text) {
|
||||
tabIcon.text = text;
|
||||
setTimeout(() => {
|
||||
getTab(tab.id).then(realTab => {
|
||||
// skip pre-rendered tabs
|
||||
|
@ -317,67 +364,31 @@ function updateIcon(tab, styles) {
|
|||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function onRuntimeMessage(request, sender, sendResponseInternal) {
|
||||
const sendResponse = data => {
|
||||
// wrap Error object instance as {__ERROR__: message} - will be unwrapped in sendMessage
|
||||
if (data instanceof Error) {
|
||||
data = {__ERROR__: data.message};
|
||||
}
|
||||
// prevent browser exception bug on sending a response to a closed tab
|
||||
tryCatch(sendResponseInternal, data);
|
||||
};
|
||||
switch (request.method) {
|
||||
case 'getStyles':
|
||||
getStyles(request).then(sendResponse);
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
|
||||
case 'saveStyle':
|
||||
saveStyle(request).then(sendResponse);
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
|
||||
case 'saveUsercss':
|
||||
usercssHelper.save(request, true).then(sendResponse);
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
|
||||
case 'buildUsercss':
|
||||
usercssHelper.build(request, true).then(sendResponse);
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
|
||||
case 'healthCheck':
|
||||
dbExec()
|
||||
.then(() => sendResponse(true))
|
||||
.catch(() => sendResponse(false));
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
|
||||
case 'styleViaAPI':
|
||||
styleViaAPI(request, sender);
|
||||
return;
|
||||
|
||||
case 'download':
|
||||
download(request.url)
|
||||
.then(sendResponse)
|
||||
.catch(() => sendResponse(null));
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
|
||||
case 'openUsercssInstallPage':
|
||||
usercssHelper.openInstallPage(sender.tab, request).then(sendResponse);
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
|
||||
case 'closeTab':
|
||||
chrome.tabs.remove(request.tabId || sender.tab.id, () => {
|
||||
if (chrome.runtime.lastError && request.tabId !== sender.tab.id) {
|
||||
sendResponse(new Error(chrome.runtime.lastError.message));
|
||||
}
|
||||
});
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
|
||||
case 'openEditor':
|
||||
openEditor(request.id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function onRuntimeMessage(msg, sender, sendResponse) {
|
||||
const fn = API_METHODS[msg.method];
|
||||
if (!fn) return;
|
||||
|
||||
// wrap 'Error' object instance as {__ERROR__: message},
|
||||
// which will be unwrapped by sendMessage,
|
||||
// and prevent exceptions on sending to a closed tab
|
||||
const respond = data =>
|
||||
tryCatch(sendResponse,
|
||||
data instanceof Error ? {__ERROR__: data.message} : data);
|
||||
|
||||
const result = fn(msg, sender, respond);
|
||||
if (result instanceof Promise) {
|
||||
result
|
||||
.catch(e => ({__ERROR__: e instanceof Error ? e.message : e}))
|
||||
.then(respond);
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
} else if (result === KEEP_CHANNEL_OPEN) {
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
} else if (result !== undefined) {
|
||||
respond(result);
|
||||
}
|
||||
}
|
||||
|
|
100
background/search-db.js
Normal file
100
background/search-db.js
Normal file
|
@ -0,0 +1,100 @@
|
|||
/* global API_METHODS filterStyles cachedStyles */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
// toLocaleLowerCase cache, autocleared after 1 minute
|
||||
const cache = new Map();
|
||||
// top-level style properties to be searched
|
||||
const PARTS = {
|
||||
name: searchText,
|
||||
url: searchText,
|
||||
sourceCode: searchText,
|
||||
sections: searchSections,
|
||||
};
|
||||
|
||||
/**
|
||||
* @param params
|
||||
* @param {string} params.query - 1. url:someurl 2. text (may contain quoted parts like "qUot Ed")
|
||||
* @param {number[]} [params.ids] - if not specified, all styles are searched
|
||||
* @returns {number[]} - array of matched styles ids
|
||||
*/
|
||||
API_METHODS.searchDB = ({query, ids}) => {
|
||||
let rx, words, icase, matchUrl;
|
||||
query = query.trim();
|
||||
|
||||
if (/^url:/i.test(query)) {
|
||||
matchUrl = query.slice(query.indexOf(':') + 1).trim();
|
||||
if (matchUrl) {
|
||||
return filterStyles({matchUrl}).map(style => style.id);
|
||||
}
|
||||
}
|
||||
if (query.startsWith('/') && /^\/(.+?)\/([gimsuy]*)$/.test(query)) {
|
||||
rx = tryRegExp(RegExp.$1, RegExp.$2);
|
||||
}
|
||||
if (!rx) {
|
||||
words = query
|
||||
.split(/(".*?")|\s+/)
|
||||
.filter(Boolean)
|
||||
.map(w => w.startsWith('"') && w.endsWith('"')
|
||||
? w.slice(1, -1)
|
||||
: w)
|
||||
.filter(w => w.length > 1);
|
||||
words = words.length ? words : [query];
|
||||
icase = words.some(w => w === lower(w));
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const item of ids || cachedStyles.list) {
|
||||
const id = isNaN(item) ? item.id : item;
|
||||
if (!query || words && !words.length) {
|
||||
results.push(id);
|
||||
continue;
|
||||
}
|
||||
const style = isNaN(item) ? item : cachedStyles.byId.get(item);
|
||||
if (!style) continue;
|
||||
for (const part in PARTS) {
|
||||
const text = style[part];
|
||||
if (text && PARTS[part](text, rx, words, icase)) {
|
||||
results.push(id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cache.size) debounce(clearCache, 60e3);
|
||||
return results;
|
||||
};
|
||||
|
||||
function searchText(text, rx, words, icase) {
|
||||
if (rx) return rx.test(text);
|
||||
for (let pass = 1; pass <= (icase ? 2 : 1); pass++) {
|
||||
if (words.every(w => text.includes(w))) return true;
|
||||
text = lower(text);
|
||||
}
|
||||
}
|
||||
|
||||
function searchSections(sections, rx, words, icase) {
|
||||
for (const section of sections) {
|
||||
for (const prop in section) {
|
||||
const value = section[prop];
|
||||
if (typeof value === 'string') {
|
||||
if (searchText(value, rx, words, icase)) return true;
|
||||
} else if (Array.isArray(value)) {
|
||||
if (value.some(str => searchText(str, rx, words, icase))) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lower(text) {
|
||||
let result = cache.get(text);
|
||||
if (result) return result;
|
||||
result = text.toLocaleLowerCase();
|
||||
cache.set(text, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function clearCache() {
|
||||
cache.clear();
|
||||
}
|
||||
})();
|
|
@ -1,4 +1,4 @@
|
|||
/* global LZString */
|
||||
/* global getStyleWithNoCode */
|
||||
'use strict';
|
||||
|
||||
const RX_NAMESPACE = new RegExp([/[\s\r\n]*/,
|
||||
|
@ -29,54 +29,6 @@ var cachedStyles = {
|
|||
},
|
||||
};
|
||||
|
||||
window.LZString = window.LZString || window.LZStringUnsafe;
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var [chromeLocal, chromeSync] = [
|
||||
chrome.storage.local,
|
||||
chrome.storage.sync,
|
||||
].map(storage => {
|
||||
const wrapper = {
|
||||
get(options) {
|
||||
return new Promise(resolve => {
|
||||
storage.get(options, data => resolve(data));
|
||||
});
|
||||
},
|
||||
set(data) {
|
||||
return new Promise(resolve => {
|
||||
storage.set(data, () => resolve(data));
|
||||
});
|
||||
},
|
||||
remove(keyOrKeys) {
|
||||
return new Promise(resolve => {
|
||||
storage.remove(keyOrKeys, resolve);
|
||||
});
|
||||
},
|
||||
getValue(key) {
|
||||
return wrapper.get(key).then(data => data[key]);
|
||||
},
|
||||
setValue(key, value) {
|
||||
return wrapper.set({[key]: value});
|
||||
},
|
||||
getLZValue(key) {
|
||||
return wrapper.getLZValues([key]).then(data => data[key]);
|
||||
},
|
||||
getLZValues(keys) {
|
||||
return wrapper.get(keys).then((data = {}) => {
|
||||
for (const key of keys) {
|
||||
const value = data[key];
|
||||
data[key] = value && tryJSONparse(LZString.decompressFromUTF16(value));
|
||||
}
|
||||
return data;
|
||||
});
|
||||
},
|
||||
setLZValue(key, value) {
|
||||
return wrapper.set({[key]: LZString.compressToUTF16(JSON.stringify(value))});
|
||||
}
|
||||
};
|
||||
return wrapper;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var dbExec = dbExecIndexedDB;
|
||||
dbExec.initialized = false;
|
||||
|
@ -247,6 +199,7 @@ function filterStyles({
|
|||
matchUrl = null,
|
||||
md5Url = null,
|
||||
asHash = null,
|
||||
omitCode,
|
||||
strictRegexp = true, // used by the popup to detect bad regexps
|
||||
} = {}) {
|
||||
enabled = enabled === null || typeof enabled === 'boolean' ? enabled :
|
||||
|
@ -274,24 +227,34 @@ function filterStyles({
|
|||
|
||||
const cacheKey = [enabled, id, matchUrl, md5Url, asHash, strictRegexp].join('\t');
|
||||
const cached = cachedStyles.filters.get(cacheKey);
|
||||
let styles;
|
||||
if (cached) {
|
||||
cached.hits++;
|
||||
cached.lastHit = Date.now();
|
||||
return asHash
|
||||
styles = asHash
|
||||
? Object.assign(blankHash, cached.styles)
|
||||
: cached.styles;
|
||||
: cached.styles.slice();
|
||||
} else {
|
||||
styles = filterStylesInternal({
|
||||
enabled,
|
||||
id,
|
||||
matchUrl,
|
||||
md5Url,
|
||||
asHash,
|
||||
strictRegexp,
|
||||
blankHash,
|
||||
cacheKey,
|
||||
});
|
||||
}
|
||||
|
||||
return filterStylesInternal({
|
||||
enabled,
|
||||
id,
|
||||
matchUrl,
|
||||
md5Url,
|
||||
asHash,
|
||||
strictRegexp,
|
||||
blankHash,
|
||||
cacheKey,
|
||||
});
|
||||
if (!omitCode) return styles;
|
||||
if (!asHash) return styles.map(getStyleWithNoCode);
|
||||
for (const id in styles) {
|
||||
const style = styles[id];
|
||||
if (style && style.sections) {
|
||||
styles[id] = getStyleWithNoCode(style);
|
||||
}
|
||||
}
|
||||
return styles;
|
||||
}
|
||||
|
||||
|
||||
|
@ -427,6 +390,7 @@ function saveStyle(style) {
|
|||
md5Url: null,
|
||||
url: null,
|
||||
originalMd5: null,
|
||||
installDate: Date.now(),
|
||||
}, style);
|
||||
return write(style);
|
||||
}
|
||||
|
@ -797,3 +761,47 @@ function handleCssTransitionBug({tabId, frameId, url, styles}) {
|
|||
return RX_CSS_TRANSITION_DETECTOR.test(code.substr(Math.max(0, pos - 10), 50));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
According to CSS4 @document specification the entire URL must match.
|
||||
Stylish-for-Chrome implemented it incorrectly since the very beginning.
|
||||
We'll detect styles that abuse the bug by finding the sections that
|
||||
would have been applied by Stylish but not by us as we follow the spec.
|
||||
Additionally we'll check for invalid regexps.
|
||||
*/
|
||||
function detectSloppyRegexps({matchUrl, ids}) {
|
||||
const results = [];
|
||||
for (const id of ids) {
|
||||
const style = cachedStyles.byId.get(id);
|
||||
if (!style) continue;
|
||||
// make sure all regexps are compiled
|
||||
const rxCache = cachedStyles.regexps;
|
||||
let hasRegExp = false;
|
||||
for (const section of style.sections) {
|
||||
for (const regexp of section.regexps) {
|
||||
hasRegExp = true;
|
||||
for (let pass = 1; pass <= 2; pass++) {
|
||||
const cacheKey = pass === 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp;
|
||||
if (!rxCache.has(cacheKey)) {
|
||||
// according to CSS4 @document specification the entire URL must match
|
||||
const anchored = pass === 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$';
|
||||
// create in the bg context to avoid leaking of "dead objects"
|
||||
const rx = tryRegExp(anchored);
|
||||
rxCache.set(cacheKey, rx || false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasRegExp) continue;
|
||||
const applied = getApplicableSections({style, matchUrl});
|
||||
const wannabe = getApplicableSections({style, matchUrl, strictRegexp: false});
|
||||
results.push({
|
||||
id,
|
||||
applied,
|
||||
skipped: wannabe.length - applied.length,
|
||||
hasInvalidRegexps: wannabe.some(({regexps}) => regexps.some(rx => !rxCache.has(rx))),
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* global getStyles */
|
||||
/* global getStyles API_METHODS */
|
||||
'use strict';
|
||||
|
||||
const styleViaAPI = !CHROME && (() => {
|
||||
API_METHODS.styleViaAPI = !CHROME && (() => {
|
||||
const ACTIONS = {
|
||||
styleApply,
|
||||
styleDeleted,
|
||||
|
|
|
@ -1,47 +1,74 @@
|
|||
/* global getStyles, saveStyle, styleSectionsEqual, chromeLocal */
|
||||
/* global calcStyleDigest */
|
||||
/* global usercss semverCompare usercssHelper */
|
||||
/*
|
||||
global getStyles saveStyle styleSectionsEqual
|
||||
global calcStyleDigest cachedStyles getStyleWithNoCode
|
||||
global usercss semverCompare
|
||||
global API_METHODS
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var updater = {
|
||||
var updater = (() => {
|
||||
|
||||
COUNT: 'count',
|
||||
UPDATED: 'updated',
|
||||
SKIPPED: 'skipped',
|
||||
DONE: 'done',
|
||||
const STATES = {
|
||||
UPDATED: 'updated',
|
||||
SKIPPED: 'skipped',
|
||||
|
||||
// details for SKIPPED status
|
||||
EDITED: 'locally edited',
|
||||
MAYBE_EDITED: 'may be locally edited',
|
||||
SAME_MD5: 'up-to-date: MD5 is unchanged',
|
||||
SAME_CODE: 'up-to-date: code sections are unchanged',
|
||||
SAME_VERSION: 'up-to-date: version is unchanged',
|
||||
ERROR_MD5: 'error: MD5 is invalid',
|
||||
ERROR_JSON: 'error: JSON is invalid',
|
||||
ERROR_VERSION: 'error: version is older than installed style',
|
||||
// details for SKIPPED status
|
||||
EDITED: 'locally edited',
|
||||
MAYBE_EDITED: 'may be locally edited',
|
||||
SAME_MD5: 'up-to-date: MD5 is unchanged',
|
||||
SAME_CODE: 'up-to-date: code sections are unchanged',
|
||||
SAME_VERSION: 'up-to-date: version is unchanged',
|
||||
ERROR_MD5: 'error: MD5 is invalid',
|
||||
ERROR_JSON: 'error: JSON is invalid',
|
||||
ERROR_VERSION: 'error: version is older than installed style',
|
||||
};
|
||||
|
||||
lastUpdateTime: parseInt(localStorage.lastUpdateTime) || Date.now(),
|
||||
let lastUpdateTime = parseInt(localStorage.lastUpdateTime) || Date.now();
|
||||
let checkingAll = false;
|
||||
let logQueue = [];
|
||||
let logLastWriteTime = 0;
|
||||
|
||||
checkAllStyles({observer = () => {}, save = true, ignoreDigest} = {}) {
|
||||
updater.resetInterval();
|
||||
updater.checkAllStyles.running = true;
|
||||
API_METHODS.updateCheckAll = checkAllStyles;
|
||||
API_METHODS.updateCheck = checkStyle;
|
||||
API_METHODS.getUpdaterStates = () => updater.STATES;
|
||||
|
||||
prefs.subscribe(['updateInterval'], schedule);
|
||||
schedule();
|
||||
|
||||
return {checkAllStyles, checkStyle, STATES};
|
||||
|
||||
function checkAllStyles({
|
||||
save = true,
|
||||
ignoreDigest,
|
||||
observe,
|
||||
} = {}) {
|
||||
resetInterval();
|
||||
checkingAll = true;
|
||||
const port = observe && chrome.runtime.connect({name: 'updater'});
|
||||
return getStyles({}).then(styles => {
|
||||
styles = styles.filter(style => style.updateUrl);
|
||||
observer(updater.COUNT, styles.length);
|
||||
updater.log('');
|
||||
updater.log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`);
|
||||
if (port) port.postMessage({count: styles.length});
|
||||
log('');
|
||||
log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`);
|
||||
return Promise.all(
|
||||
styles.map(style =>
|
||||
updater.checkStyle({style, observer, save, ignoreDigest})));
|
||||
checkStyle({style, port, save, ignoreDigest})));
|
||||
}).then(() => {
|
||||
observer(updater.DONE);
|
||||
updater.log('');
|
||||
updater.checkAllStyles.running = false;
|
||||
if (port) port.postMessage({done: true});
|
||||
if (port) port.disconnect();
|
||||
log('');
|
||||
checkingAll = false;
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
checkStyle({style, observer = () => {}, save = true, ignoreDigest}) {
|
||||
function checkStyle({
|
||||
id,
|
||||
style = cachedStyles.byId.get(id),
|
||||
port,
|
||||
save = true,
|
||||
ignoreDigest,
|
||||
}) {
|
||||
/*
|
||||
Original style digests are calculated in these cases:
|
||||
* style is installed or updated from server
|
||||
|
@ -65,29 +92,33 @@ var updater = {
|
|||
.catch(reportFailure);
|
||||
|
||||
function reportSuccess(saved) {
|
||||
observer(updater.UPDATED, saved);
|
||||
updater.log(updater.UPDATED + ` #${style.id} ${style.name}`);
|
||||
log(STATES.UPDATED + ` #${style.id} ${style.name}`);
|
||||
const info = {updated: true, style: saved};
|
||||
if (port) port.postMessage(info);
|
||||
return info;
|
||||
}
|
||||
|
||||
function reportFailure(err) {
|
||||
observer(updater.SKIPPED, style, err);
|
||||
err = err === 0 ? 'server unreachable' : err;
|
||||
updater.log(updater.SKIPPED + ` (${err}) #${style.id} ${style.name}`);
|
||||
function reportFailure(error) {
|
||||
error = error === 0 ? 'server unreachable' : error;
|
||||
log(STATES.SKIPPED + ` (${error}) #${style.id} ${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) {
|
||||
return Promise.reject(updater.EDITED);
|
||||
return Promise.reject(STATES.EDITED);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeUpdateUSO() {
|
||||
return download(style.md5Url).then(md5 => {
|
||||
if (!md5 || md5.length !== 32) {
|
||||
return Promise.reject(updater.ERROR_MD5);
|
||||
return Promise.reject(STATES.ERROR_MD5);
|
||||
}
|
||||
if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
|
||||
return Promise.reject(updater.SAME_MD5);
|
||||
return Promise.reject(STATES.SAME_MD5);
|
||||
}
|
||||
return download(style.updateUrl)
|
||||
.then(text => tryJSONparse(text));
|
||||
|
@ -104,14 +135,14 @@ var updater = {
|
|||
case 0:
|
||||
// re-install is invalid in a soft upgrade
|
||||
if (!ignoreDigest) {
|
||||
return Promise.reject(updater.SAME_VERSION);
|
||||
return Promise.reject(STATES.SAME_VERSION);
|
||||
} else if (text === style.sourceCode) {
|
||||
return Promise.reject(updater.SAME_CODE);
|
||||
return Promise.reject(STATES.SAME_CODE);
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
// downgrade is always invalid
|
||||
return Promise.reject(updater.ERROR_VERSION);
|
||||
return Promise.reject(STATES.ERROR_VERSION);
|
||||
}
|
||||
return usercss.buildCode(json);
|
||||
});
|
||||
|
@ -120,8 +151,9 @@ var updater = {
|
|||
function maybeSave(json = {}) {
|
||||
// usercss is already validated while building
|
||||
if (!json.usercssData && !styleJSONseemsValid(json)) {
|
||||
return Promise.reject(updater.ERROR_JSON);
|
||||
return Promise.reject(STATES.ERROR_JSON);
|
||||
}
|
||||
|
||||
json.id = style.id;
|
||||
json.updateDate = Date.now();
|
||||
json.reason = 'update';
|
||||
|
@ -139,15 +171,16 @@ var updater = {
|
|||
if (styleSectionsEqual(json, style)) {
|
||||
// update digest even if save === false as there might be just a space added etc.
|
||||
saveStyle(Object.assign(json, {reason: 'update-digest'}));
|
||||
return Promise.reject(updater.SAME_CODE);
|
||||
} else if (!style.originalDigest && !ignoreDigest) {
|
||||
return Promise.reject(updater.MAYBE_EDITED);
|
||||
return Promise.reject(STATES.SAME_CODE);
|
||||
}
|
||||
|
||||
return !save ? json :
|
||||
json.usercssData
|
||||
? usercssHelper.save(json)
|
||||
: saveStyle(json);
|
||||
if (!style.originalDigest && !ignoreDigest) {
|
||||
return Promise.reject(STATES.MAYBE_EDITED);
|
||||
}
|
||||
|
||||
return save ?
|
||||
API_METHODS[json.usercssData ? 'saveUsercss' : 'saveStyle'](json) :
|
||||
json;
|
||||
}
|
||||
|
||||
function styleJSONseemsValid(json) {
|
||||
|
@ -157,49 +190,47 @@ var updater = {
|
|||
&& typeof json.sections.every === 'function'
|
||||
&& typeof json.sections[0].code === 'string';
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
schedule() {
|
||||
function schedule() {
|
||||
const interval = prefs.get('updateInterval') * 60 * 60 * 1000;
|
||||
if (interval) {
|
||||
const elapsed = Math.max(0, Date.now() - updater.lastUpdateTime);
|
||||
debounce(updater.checkAllStyles, Math.max(10e3, interval - elapsed));
|
||||
const elapsed = Math.max(0, Date.now() - lastUpdateTime);
|
||||
debounce(checkAllStyles, Math.max(10e3, interval - elapsed));
|
||||
} else {
|
||||
debounce.unregister(updater.checkAllStyles);
|
||||
debounce.unregister(checkAllStyles);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
resetInterval() {
|
||||
localStorage.lastUpdateTime = updater.lastUpdateTime = Date.now();
|
||||
updater.schedule();
|
||||
},
|
||||
function resetInterval() {
|
||||
localStorage.lastUpdateTime = lastUpdateTime = Date.now();
|
||||
schedule();
|
||||
}
|
||||
|
||||
log: (() => {
|
||||
let queue = [];
|
||||
let lastWriteTime = 0;
|
||||
return text => {
|
||||
queue.push({text, time: new Date().toLocaleString()});
|
||||
debounce(flushQueue, text && updater.checkAllStyles.running ? 1000 : 0);
|
||||
};
|
||||
function flushQueue() {
|
||||
chromeLocal.getValue('updateLog').then((lines = []) => {
|
||||
const time = Date.now() - lastWriteTime > 11e3 ? queue[0].time + ' ' : '';
|
||||
if (!queue[0].text) {
|
||||
queue.shift();
|
||||
if (lines[lines.length - 1]) {
|
||||
lines.push('');
|
||||
}
|
||||
}
|
||||
lines.splice(0, lines.length - 1000);
|
||||
lines.push(time + queue[0].text);
|
||||
lines.push(...queue.slice(1).map(item => item.text));
|
||||
chromeLocal.setValue('updateLog', lines);
|
||||
lastWriteTime = Date.now();
|
||||
queue = [];
|
||||
});
|
||||
function log(text) {
|
||||
logQueue.push({text, time: new Date().toLocaleString()});
|
||||
debounce(flushQueue, text && checkingAll ? 1000 : 0);
|
||||
}
|
||||
|
||||
function flushQueue(stored) {
|
||||
if (!stored) {
|
||||
chrome.storage.local.get('updateLog', flushQueue);
|
||||
return;
|
||||
}
|
||||
})(),
|
||||
};
|
||||
const lines = stored.lines || [];
|
||||
const time = Date.now() - logLastWriteTime > 11e3 ?
|
||||
logQueue[0].time + ' ' :
|
||||
'';
|
||||
if (!logQueue[0].text) {
|
||||
logQueue.shift();
|
||||
if (lines[lines.length - 1]) lines.push('');
|
||||
}
|
||||
lines.splice(0, lines.length - 1000);
|
||||
lines.push(time + (logQueue[0] && logQueue[0].text || ''));
|
||||
lines.push(...logQueue.slice(1).map(item => item.text));
|
||||
|
||||
updater.schedule();
|
||||
prefs.subscribe(['updateInterval'], updater.schedule);
|
||||
chrome.storage.local.set({updateLog: lines});
|
||||
logLastWriteTime = Date.now();
|
||||
logQueue = [];
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
/* global usercss saveStyle getStyles chromeLocal */
|
||||
/* global API_METHODS usercss saveStyle getStyles chromeLocal cachedStyles */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var usercssHelper = (() => {
|
||||
(() => {
|
||||
|
||||
API_METHODS.saveUsercss = save;
|
||||
API_METHODS.buildUsercss = build;
|
||||
API_METHODS.installUsercss = install;
|
||||
|
||||
const TEMP_CODE_PREFIX = 'tempUsercssCode';
|
||||
const TEMP_CODE_CLEANUP_DELAY = 60e3;
|
||||
|
@ -48,31 +51,25 @@ var usercssHelper = (() => {
|
|||
return usercss.buildCode(style);
|
||||
}
|
||||
|
||||
function wrapReject(pending) {
|
||||
return pending
|
||||
.catch(err => new Error(Array.isArray(err) ? err.join('\n') : err.message || String(err)));
|
||||
}
|
||||
|
||||
// Parse the source and find the duplication
|
||||
function build({sourceCode, checkDup = false}, noReject) {
|
||||
const pending = buildMeta({sourceCode})
|
||||
function build({sourceCode, checkDup = false}) {
|
||||
return buildMeta({sourceCode})
|
||||
.then(style => Promise.all([
|
||||
buildCode(style),
|
||||
checkDup && findDup(style)
|
||||
]))
|
||||
.then(([style, dup]) => ({style, dup}));
|
||||
|
||||
return noReject ? wrapReject(pending) : pending;
|
||||
}
|
||||
|
||||
function save(style, noReject) {
|
||||
const pending = buildMeta(style)
|
||||
function save(style) {
|
||||
if (!style.sourceCode) {
|
||||
style.sourceCode = cachedStyles.byId.get(style.id).sourceCode;
|
||||
}
|
||||
return buildMeta(style)
|
||||
.then(assignVars)
|
||||
.then(buildCode)
|
||||
.then(saveStyle);
|
||||
|
||||
return noReject ? wrapReject(pending) : pending;
|
||||
|
||||
function assignVars(style) {
|
||||
if (style.reason === 'config' && style.id) {
|
||||
return style;
|
||||
|
@ -105,11 +102,12 @@ var usercssHelper = (() => {
|
|||
);
|
||||
}
|
||||
|
||||
function openInstallPage(tab, {url = tab.url, direct, downloaded} = {}) {
|
||||
function install({url, direct, downloaded}, {tab}) {
|
||||
url = url || tab.url;
|
||||
if (direct && !downloaded) {
|
||||
prefetchCodeForInstallation(tab.id, url);
|
||||
}
|
||||
return wrapReject(openURL({
|
||||
return openURL({
|
||||
url: '/install-usercss.html' +
|
||||
'?updateUrl=' + encodeURIComponent(url) +
|
||||
'&tabId=' + tab.id +
|
||||
|
@ -117,7 +115,7 @@ var usercssHelper = (() => {
|
|||
index: tab.index + 1,
|
||||
openerTabId: tab.id,
|
||||
currentWindow: null,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function prefetchCodeForInstallation(tabId, url) {
|
||||
|
@ -131,6 +129,4 @@ var usercssHelper = (() => {
|
|||
setTimeout(() => chromeLocal.remove(key), TEMP_CODE_CLEANUP_DELAY);
|
||||
});
|
||||
}
|
||||
|
||||
return {build, save, findDup, openInstallPage};
|
||||
})();
|
||||
|
|
|
@ -48,8 +48,8 @@
|
|||
asHash: true,
|
||||
}, options);
|
||||
// On own pages we request the styles directly to minimize delay and flicker
|
||||
if (typeof getStylesSafe === 'function') {
|
||||
getStylesSafe(request).then(callback);
|
||||
if (typeof API === 'function') {
|
||||
API.getStyles(request).then(callback);
|
||||
} else {
|
||||
chrome.runtime.sendMessage(request, callback);
|
||||
}
|
||||
|
|
|
@ -97,7 +97,7 @@ function initUsercssInstall() {
|
|||
});
|
||||
});
|
||||
chrome.runtime.sendMessage({
|
||||
method: 'openUsercssInstallPage',
|
||||
method: 'installUsercss',
|
||||
url: location.href,
|
||||
}, r => r && r.__ERROR__ && alert(r.__ERROR__));
|
||||
}
|
||||
|
|
|
@ -198,7 +198,14 @@
|
|||
if (url.startsWith('#')) {
|
||||
resolve(document.getElementById(url.slice(1)).textContent);
|
||||
} else {
|
||||
chrome.runtime.sendMessage({method: 'download', url}, resolve);
|
||||
chrome.runtime.sendMessage({method: 'download', url}, result => {
|
||||
const error = result && result.__ERROR__;
|
||||
if (error) {
|
||||
alert('Error' + (error ? '\n' + error : ''));
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
<script src="js/localization.js"></script>
|
||||
<script src="js/script-loader.js"></script>
|
||||
<script src="js/moz-parser.js"></script>
|
||||
<script src="js/storage-util.js"></script>
|
||||
<script src="content/apply.js"></script>
|
||||
<script src="edit/lint.js"></script>
|
||||
<script src="edit/util.js"></script>
|
||||
|
|
25
edit/edit.js
25
edit/edit.js
|
@ -44,7 +44,7 @@ Promise.all([
|
|||
if (usercss) {
|
||||
editor = createSourceEditor(style);
|
||||
} else {
|
||||
initWithSectionStyle({style});
|
||||
initWithSectionStyle(style);
|
||||
document.addEventListener('wheel', scrollEntirePageOnCtrlShift);
|
||||
}
|
||||
});
|
||||
|
@ -155,14 +155,17 @@ function onRuntimeMessage(request) {
|
|||
request.reason !== 'editSave' &&
|
||||
request.reason !== 'config') {
|
||||
// code-less style from notifyAllTabs
|
||||
if ((request.style.sections[0] || {}).code === null) {
|
||||
request.style = BG.cachedStyles.byId.get(request.style.id);
|
||||
}
|
||||
if (isUsercss(request.style)) {
|
||||
editor.replaceStyle(request.style, request.codeIsUpdated);
|
||||
} else {
|
||||
initWithSectionStyle(request);
|
||||
}
|
||||
const {sections, id} = request.style;
|
||||
((sections[0] || {}).code === null
|
||||
? API.getStyles({id})
|
||||
: Promise.resolve([request.style])
|
||||
).then(([style]) => {
|
||||
if (isUsercss(style)) {
|
||||
editor.replaceStyle(style, request.codeIsUpdated);
|
||||
} else {
|
||||
initWithSectionStyle(style, request.codeIsUpdated);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'styleDeleted':
|
||||
|
@ -228,7 +231,7 @@ function initStyleData() {
|
|||
)
|
||||
],
|
||||
});
|
||||
return getStylesSafe({id: id || -1})
|
||||
return API.getStyles({id: id || -1})
|
||||
.then(([style = createEmptyStyle()]) => {
|
||||
styleId = style.id;
|
||||
if (styleId) sessionStorage.justEditedStyleId = styleId;
|
||||
|
@ -344,7 +347,7 @@ function save() {
|
|||
return;
|
||||
}
|
||||
|
||||
saveStyleSafe({
|
||||
API.saveStyle({
|
||||
id: styleId,
|
||||
name: $('#name').value.trim(),
|
||||
enabled: $('#enabled').checked,
|
||||
|
|
10
edit/lint.js
10
edit/lint.js
|
@ -121,12 +121,12 @@ var linterConfig = {
|
|||
config = this.fallbackToDefaults(config);
|
||||
const linter = linterConfig.getName();
|
||||
this[linter] = config;
|
||||
BG.chromeSync.setLZValue(this.storageName[linter], config);
|
||||
chromeSync.setLZValue(this.storageName[linter], config);
|
||||
return config;
|
||||
},
|
||||
|
||||
loadAll() {
|
||||
return BG.chromeSync.getLZValues([
|
||||
return chromeSync.getLZValues([
|
||||
'editorCSSLintConfig',
|
||||
'editorStylelintConfig',
|
||||
]).then(data => {
|
||||
|
@ -167,10 +167,8 @@ var linterConfig = {
|
|||
},
|
||||
|
||||
init() {
|
||||
if (!linterConfig.init.pending) {
|
||||
linterConfig.init.pending = linterConfig.loadAll();
|
||||
}
|
||||
return linterConfig.init.pending;
|
||||
if (!this.init.pending) this.init.pending = this.loadAll();
|
||||
return this.init.pending;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ global showAppliesToHelp beautify regExpTester setGlobalProgress setCleanSection
|
|||
*/
|
||||
'use strict';
|
||||
|
||||
function initWithSectionStyle({style, codeIsUpdated}) {
|
||||
function initWithSectionStyle(style, codeIsUpdated) {
|
||||
$('#name').value = style.name || '';
|
||||
$('#enabled').checked = style.enabled !== false;
|
||||
$('#url').href = style.url || '';
|
||||
|
|
|
@ -106,7 +106,7 @@ function createSourceEditor(style) {
|
|||
`.replace(/^\s+/gm, '');
|
||||
dirty.clear('sourceGeneration');
|
||||
style.sourceCode = '';
|
||||
BG.chromeSync.getLZValue('usercssTemplate').then(code => {
|
||||
chromeSync.getLZValue('usercssTemplate').then(code => {
|
||||
style.sourceCode = code || DEFAULT_CODE;
|
||||
cm.startOperation();
|
||||
cm.setValue(style.sourceCode);
|
||||
|
@ -216,8 +216,8 @@ function createSourceEditor(style) {
|
|||
return;
|
||||
}
|
||||
const code = cm.getValue();
|
||||
return onBackgroundReady()
|
||||
.then(() => BG.usercssHelper.save({
|
||||
return (
|
||||
API.saveUsercss({
|
||||
reason: 'editSave',
|
||||
id: style.id,
|
||||
enabled: style.enabled,
|
||||
|
@ -228,8 +228,8 @@ function createSourceEditor(style) {
|
|||
.catch(err => {
|
||||
if (err.message === t('styleMissingMeta', 'name')) {
|
||||
messageBox.confirm(t('usercssReplaceTemplateConfirmation')).then(ok => ok &&
|
||||
BG.chromeSync.setLZValue('usercssTemplate', code)
|
||||
.then(() => BG.chromeSync.getLZValue('usercssTemplate'))
|
||||
chromeSync.setLZValue('usercssTemplate', code)
|
||||
.then(() => chromeSync.getLZValue('usercssTemplate'))
|
||||
.then(saved => saved !== code && messageBox.alert(t('syncStorageErrorSaving'))));
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -200,7 +200,7 @@
|
|||
if (!liveReload && !prefs.get('openEditInWindow')) {
|
||||
chrome.tabs.update({url: '/edit.html?id=' + style.id});
|
||||
} else {
|
||||
BG.openEditor(style.id);
|
||||
API.openEditor({id: style.id});
|
||||
if (!liveReload) {
|
||||
closeCurrentTab();
|
||||
}
|
||||
|
@ -212,8 +212,8 @@
|
|||
function initSourceCode(sourceCode) {
|
||||
cm.setValue(sourceCode);
|
||||
cm.refresh();
|
||||
BG.usercssHelper.build(BG.deepCopy({sourceCode, checkDup: true}))
|
||||
.then(r => init(deepCopy(r)))
|
||||
API.buildUsercss({sourceCode, checkDup: true})
|
||||
.then(r => init(r instanceof Object ? r : deepCopy(r)))
|
||||
.catch(err => {
|
||||
$('.header').classList.add('meta-init-error');
|
||||
showError(err);
|
||||
|
@ -222,7 +222,7 @@
|
|||
|
||||
function buildWarning(err) {
|
||||
const contents = Array.isArray(err) ?
|
||||
$create('pre', err.join('\n')) :
|
||||
[$create('pre', err.join('\n'))] :
|
||||
[err && err.message || err || 'Unknown error'];
|
||||
if (Number.isInteger(err.index)) {
|
||||
const pos = cm.posFromIndex(err.index);
|
||||
|
@ -283,8 +283,8 @@
|
|||
data.version,
|
||||
]))
|
||||
).then(ok => ok &&
|
||||
BG.usercssHelper.save(BG.deepCopy(Object.assign(style, dup && {reason: 'update'})))
|
||||
.then(r => install(deepCopy(r)))
|
||||
API.saveUsercss(Object.assign(style, dup && {reason: 'update'}))
|
||||
.then(r => install(r instanceof Object ? r : deepCopy(r)))
|
||||
.catch(err => messageBox.alert(t('styleInstallFailed', err)))
|
||||
);
|
||||
};
|
||||
|
|
|
@ -64,7 +64,8 @@ onDOMready().then(() => {
|
|||
if (!chrome.app && chrome.windows) {
|
||||
// die if unable to access BG directly
|
||||
chrome.windows.getCurrent(wnd => {
|
||||
if (!BG && wnd.incognito) {
|
||||
if (!BG && wnd.incognito &&
|
||||
!location.pathname.includes('popup.html')) {
|
||||
// private windows can't get bg page
|
||||
location.href = '/msgbox/dysfunctional.html';
|
||||
throw 0;
|
||||
|
|
181
js/messaging.js
181
js/messaging.js
|
@ -1,14 +1,18 @@
|
|||
/* global BG: true, onRuntimeMessage, applyOnMessage, handleUpdate, handleDelete */
|
||||
/* global FIREFOX: true */
|
||||
/*
|
||||
global BG: true
|
||||
global FIREFOX: true
|
||||
global onRuntimeMessage applyOnMessage
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
// keep message channel open for sendResponse in chrome.runtime.onMessage listener
|
||||
const KEEP_CHANNEL_OPEN = true;
|
||||
|
||||
const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]);
|
||||
const OPERA = CHROME && parseFloat(navigator.userAgent.match(/\bOPR\/(\d+\.\d+)|$/)[1]);
|
||||
const OPERA = Boolean(chrome.app) && parseFloat(navigator.userAgent.match(/\bOPR\/(\d+\.\d+)|$/)[1]);
|
||||
const VIVALDI = Boolean(chrome.app) && navigator.userAgent.includes('Vivaldi');
|
||||
const ANDROID = !chrome.windows;
|
||||
let FIREFOX = !CHROME && parseFloat(navigator.userAgent.match(/\bFirefox\/(\d+\.\d+)|$/)[1]);
|
||||
let FIREFOX = !chrome.app && parseFloat(navigator.userAgent.match(/\bFirefox\/(\d+\.\d+)|$/)[1]);
|
||||
|
||||
if (!CHROME && !chrome.browserAction.openPopup) {
|
||||
// in FF pre-57 legacy addons can override useragent so we assume the worst
|
||||
|
@ -65,13 +69,14 @@ if (!BG || BG !== window) {
|
|||
document.documentElement.classList.add('firefox');
|
||||
} else if (OPERA) {
|
||||
document.documentElement.classList.add('opera');
|
||||
} else if (chrome.app && navigator.userAgent.includes('Vivaldi')) {
|
||||
document.documentElement.classList.add('vivaldi');
|
||||
} else {
|
||||
if (VIVALDI) document.documentElement.classList.add('vivaldi');
|
||||
}
|
||||
// TODO: remove once our manifest's minimum_chrome_version is 50+
|
||||
// Chrome 49 doesn't report own extension pages in webNavigation apparently
|
||||
if (CHROME && CHROME < 2661) {
|
||||
getActiveTab().then(BG.updateIcon);
|
||||
getActiveTab().then(tab =>
|
||||
window.API.updateIcon({tab}));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,6 +87,60 @@ if (FIREFOX_NO_DOM_STORAGE) {
|
|||
Object.defineProperty(window, 'sessionStorage', {value: {}});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var API = (() => {
|
||||
return new Proxy(() => {}, {
|
||||
get: (target, name) =>
|
||||
name === 'remoteCall' ?
|
||||
remoteCall :
|
||||
arg => invokeBG(name, arg),
|
||||
});
|
||||
|
||||
function remoteCall(name, arg, remoteWindow) {
|
||||
let thing = window[name] || window.API_METHODS[name];
|
||||
if (typeof thing === 'function') {
|
||||
thing = thing(arg);
|
||||
}
|
||||
if (!thing || typeof thing !== 'object') {
|
||||
return thing;
|
||||
} else if (thing instanceof Promise) {
|
||||
return thing.then(product => remoteWindow.deepCopy(product));
|
||||
} else {
|
||||
return remoteWindow.deepCopy(thing);
|
||||
}
|
||||
}
|
||||
|
||||
function invokeBG(name, arg = {}) {
|
||||
if (BG && (name in BG || name in BG.API_METHODS)) {
|
||||
const call = BG !== window ?
|
||||
BG.API.remoteCall(name, BG.deepCopy(arg), window) :
|
||||
remoteCall(name, arg, BG);
|
||||
return Promise.resolve(call);
|
||||
}
|
||||
if (BG && BG.getStyles) {
|
||||
throw new Error('Bad API method', name, arg);
|
||||
}
|
||||
if (FIREFOX) {
|
||||
arg.method = name;
|
||||
return sendMessage(arg);
|
||||
}
|
||||
return onBackgroundReady().then(() => invokeBG(name, arg));
|
||||
}
|
||||
|
||||
function onBackgroundReady() {
|
||||
return BG && BG.getStyles ? Promise.resolve() : new Promise(function ping(resolve) {
|
||||
sendMessage({method: 'healthCheck'}, health => {
|
||||
if (health !== undefined) {
|
||||
BG = chrome.extension.getBackgroundPage();
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(ping, 0, resolve);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
function notifyAllTabs(msg) {
|
||||
const originalMessage = msg;
|
||||
|
@ -99,6 +158,12 @@ function notifyAllTabs(msg) {
|
|||
const affectsIcon = affectsAll || msg.affects.icon;
|
||||
const affectsPopup = affectsAll || msg.affects.popup;
|
||||
const affectsSelf = affectsPopup || msg.prefs;
|
||||
// notify background page and all open popups
|
||||
if (affectsSelf) {
|
||||
msg.tabId = undefined;
|
||||
sendMessage(msg, ignoreChromeError);
|
||||
}
|
||||
// notify tabs
|
||||
if (affectsTabs || affectsIcon) {
|
||||
const notifyTab = tab => {
|
||||
// own pages will be notified via runtime.sendMessage later
|
||||
|
@ -109,8 +174,9 @@ function notifyAllTabs(msg) {
|
|||
msg.tabId = tab.id;
|
||||
sendMessage(msg, ignoreChromeError);
|
||||
}
|
||||
if (affectsIcon && BG) {
|
||||
BG.updateIcon(tab);
|
||||
if (affectsIcon) {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
debounce(API.updateIcon, 0, {tab});
|
||||
}
|
||||
};
|
||||
// list all tabs including chrome-extension:// which can be ours
|
||||
|
@ -132,11 +198,6 @@ function notifyAllTabs(msg) {
|
|||
if (typeof applyOnMessage !== 'undefined') {
|
||||
applyOnMessage(originalMessage);
|
||||
}
|
||||
// notify background page and all open popups
|
||||
if (affectsSelf) {
|
||||
msg.tabId = undefined;
|
||||
sendMessage(msg, ignoreChromeError);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -294,10 +355,9 @@ function ignoreChromeError() {
|
|||
|
||||
|
||||
function getStyleWithNoCode(style) {
|
||||
const stripped = Object.assign({}, style, {sections: []});
|
||||
for (const section of style.sections) {
|
||||
stripped.sections.push(Object.assign({}, section, {code: null}));
|
||||
}
|
||||
const stripped = deepCopy(style);
|
||||
for (const section of stripped.sections) section.code = null;
|
||||
stripped.sourceCode = null;
|
||||
return stripped;
|
||||
}
|
||||
|
||||
|
@ -343,31 +403,23 @@ const debounce = Object.assign((fn, delay, ...args) => {
|
|||
|
||||
|
||||
function deepCopy(obj) {
|
||||
return obj !== null && obj !== undefined && typeof obj === 'object'
|
||||
? deepMerge(typeof obj.slice === 'function' ? [] : {}, obj)
|
||||
: obj;
|
||||
}
|
||||
|
||||
|
||||
function deepMerge(target, ...args) {
|
||||
const isArray = typeof target.slice === 'function';
|
||||
for (const obj of args) {
|
||||
if (isArray && obj !== null && obj !== undefined) {
|
||||
for (const element of obj) {
|
||||
target.push(deepCopy(element));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (const k in obj) {
|
||||
const value = obj[k];
|
||||
if (k in target && typeof value === 'object' && value !== null) {
|
||||
deepMerge(target[k], value);
|
||||
} else {
|
||||
target[k] = deepCopy(value);
|
||||
}
|
||||
if (!obj || typeof obj !== 'object') return obj;
|
||||
// N.B. a copy should be an explicitly literal
|
||||
if (Array.isArray(obj)) {
|
||||
const copy = [];
|
||||
for (const v of obj) {
|
||||
copy.push(!v || typeof v !== 'object' ? v : deepCopy(v));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
return target;
|
||||
const copy = {};
|
||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
for (const k in obj) {
|
||||
if (!hasOwnProperty.call(obj, k)) continue;
|
||||
const v = obj[k];
|
||||
copy[k] = !v || typeof v !== 'object' ? v : deepCopy(v);
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
|
||||
|
@ -390,51 +442,6 @@ function sessionStorageHash(name) {
|
|||
}
|
||||
|
||||
|
||||
function onBackgroundReady() {
|
||||
return BG && BG.getStyles ? Promise.resolve() : new Promise(function ping(resolve) {
|
||||
sendMessage({method: 'healthCheck'}, health => {
|
||||
if (health !== undefined) {
|
||||
BG = chrome.extension.getBackgroundPage();
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(ping, 0, resolve);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage
|
||||
function getStylesSafe(options) {
|
||||
return onBackgroundReady()
|
||||
.then(() => BG.getStyles(options));
|
||||
}
|
||||
|
||||
|
||||
function saveStyleSafe(style) {
|
||||
return onBackgroundReady()
|
||||
.then(() => BG.saveStyle(BG.deepCopy(style)))
|
||||
.then(savedStyle => {
|
||||
if (style.notify === false) {
|
||||
handleUpdate(savedStyle, style);
|
||||
}
|
||||
return savedStyle;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function deleteStyleSafe({id, notify = true} = {}) {
|
||||
return onBackgroundReady()
|
||||
.then(() => BG.deleteStyle({id, notify}))
|
||||
.then(() => {
|
||||
if (!notify) {
|
||||
handleDelete(id);
|
||||
}
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function download(url, {
|
||||
method = url.includes('?') ? 'POST' : 'GET',
|
||||
body = url.includes('?') ? url.slice(url.indexOf('?')) : null,
|
||||
|
@ -489,7 +496,7 @@ function invokeOrPostpone(isInvoke, fn, ...args) {
|
|||
}
|
||||
|
||||
|
||||
function openEditor(id) {
|
||||
function openEditor({id}) {
|
||||
let url = '/edit.html';
|
||||
if (id) {
|
||||
url += `?id=${id}`;
|
||||
|
|
133
js/prefs.js
133
js/prefs.js
|
@ -148,17 +148,13 @@ var prefs = new function Prefs() {
|
|||
values[key] = value;
|
||||
defineReadonlyProperty(this.readOnlyValues, key, value);
|
||||
const hasChanged = !equal(value, oldValue);
|
||||
if (!fromBroadcast) {
|
||||
if (BG && BG !== window) {
|
||||
BG.prefs.set(key, BG.deepCopy(value), {broadcast, sync});
|
||||
} else {
|
||||
localStorage[key] = typeof defaults[key] === 'object'
|
||||
? JSON.stringify(value)
|
||||
: value;
|
||||
if (broadcast && hasChanged) {
|
||||
this.broadcast(key, value, {sync});
|
||||
}
|
||||
}
|
||||
if (!fromBroadcast || FIREFOX_NO_DOM_STORAGE) {
|
||||
localStorage[key] = typeof defaults[key] === 'object'
|
||||
? JSON.stringify(value)
|
||||
: value;
|
||||
}
|
||||
if (!fromBroadcast && broadcast && hasChanged) {
|
||||
this.broadcast(key, value, {sync});
|
||||
}
|
||||
if (hasChanged) {
|
||||
const specific = onChange.specific.get(key);
|
||||
|
@ -175,8 +171,6 @@ var prefs = new function Prefs() {
|
|||
}
|
||||
},
|
||||
|
||||
remove: key => this.set(key, undefined),
|
||||
|
||||
reset: key => this.set(key, deepCopy(defaults[key])),
|
||||
|
||||
broadcast(key, value, {sync = true} = {}) {
|
||||
|
@ -226,62 +220,63 @@ var prefs = new function Prefs() {
|
|||
},
|
||||
});
|
||||
|
||||
// Unlike sync, HTML5 localStorage is ready at browser startup
|
||||
// so we'll mirror the prefs to avoid using the wrong defaults
|
||||
// during the startup phase
|
||||
for (const key in defaults) {
|
||||
const defaultValue = defaults[key];
|
||||
let value = localStorage[key];
|
||||
if (typeof value === 'string') {
|
||||
switch (typeof defaultValue) {
|
||||
case 'boolean':
|
||||
value = value.toLowerCase() === 'true';
|
||||
break;
|
||||
case 'number':
|
||||
value |= 0;
|
||||
break;
|
||||
case 'object':
|
||||
value = tryJSONparse(value) || defaultValue;
|
||||
break;
|
||||
}
|
||||
} else if (FIREFOX_NO_DOM_STORAGE && BG) {
|
||||
value = BG.localStorage[key];
|
||||
value = value === undefined ? defaultValue : value;
|
||||
} else {
|
||||
value = defaultValue;
|
||||
}
|
||||
if (BG === window) {
|
||||
// when in bg page, .set() will write to localStorage
|
||||
this.set(key, value, {broadcast: false, sync: false});
|
||||
} else {
|
||||
values[key] = value;
|
||||
defineReadonlyProperty(this.readOnlyValues, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!BG || BG === window) {
|
||||
affectsIcon.forEach(key => this.broadcast(key, values[key], {sync: false}));
|
||||
|
||||
const importFromSync = (synced = {}) => {
|
||||
{
|
||||
const importFromBG = () =>
|
||||
API.getPrefs().then(prefs => {
|
||||
const props = {};
|
||||
for (const id in prefs) {
|
||||
const value = prefs[id];
|
||||
values[id] = value;
|
||||
props[id] = {value: deepCopy(value)};
|
||||
}
|
||||
Object.defineProperties(this.readOnlyValues, props);
|
||||
});
|
||||
// Unlike chrome.storage or messaging, HTML5 localStorage is synchronous and always ready,
|
||||
// so we'll mirror the prefs to avoid using the wrong defaults during the startup phase
|
||||
const importFromLocalStorage = () => {
|
||||
for (const key in defaults) {
|
||||
if (key in synced) {
|
||||
this.set(key, synced[key], {sync: false});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getSync().get('settings', ({settings} = {}) => importFromSync(settings));
|
||||
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (area === 'sync' && 'settings' in changes) {
|
||||
const synced = changes.settings.newValue;
|
||||
if (synced) {
|
||||
importFromSync(synced);
|
||||
const defaultValue = defaults[key];
|
||||
let value = localStorage[key];
|
||||
if (typeof value === 'string') {
|
||||
switch (typeof defaultValue) {
|
||||
case 'boolean':
|
||||
value = value.toLowerCase() === 'true';
|
||||
break;
|
||||
case 'number':
|
||||
value |= 0;
|
||||
break;
|
||||
case 'object':
|
||||
value = tryJSONparse(value) || defaultValue;
|
||||
break;
|
||||
}
|
||||
} else if (FIREFOX_NO_DOM_STORAGE && BG) {
|
||||
value = BG.localStorage[key];
|
||||
value = value === undefined ? defaultValue : value;
|
||||
localStorage[key] = value;
|
||||
} else {
|
||||
// user manually deleted our settings, we'll recreate them
|
||||
getSync().set({'settings': values});
|
||||
value = defaultValue;
|
||||
}
|
||||
if (BG === window) {
|
||||
// when in bg page, .set() will write to localStorage
|
||||
this.set(key, value, {broadcast: false, sync: false});
|
||||
} else {
|
||||
values[key] = value;
|
||||
defineReadonlyProperty(this.readOnlyValues, key, value);
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
(FIREFOX_NO_DOM_STORAGE && !BG ? importFromBG() : importFromLocalStorage()).then(() => {
|
||||
if (BG && BG !== window) return;
|
||||
if (BG === window) {
|
||||
affectsIcon.forEach(key => this.broadcast(key, values[key], {sync: false}));
|
||||
getSync().get('settings', data => importFromSync.call(this, data.settings));
|
||||
}
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (area === 'sync' && 'settings' in changes) {
|
||||
importFromSync.call(this, changes.settings.newValue);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -350,6 +345,14 @@ var prefs = new function Prefs() {
|
|||
};
|
||||
}
|
||||
|
||||
function importFromSync(synced = {}) {
|
||||
for (const key in defaults) {
|
||||
if (key in synced) {
|
||||
this.set(key, synced[key], {sync: false});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function defineReadonlyProperty(obj, key, value) {
|
||||
const copy = deepCopy(value);
|
||||
if (typeof copy === 'object') {
|
||||
|
|
99
js/storage-util.js
Normal file
99
js/storage-util.js
Normal file
|
@ -0,0 +1,99 @@
|
|||
/* global LZString loadScript */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var [chromeLocal, chromeSync] = [
|
||||
chrome.storage.local,
|
||||
chrome.storage.sync,
|
||||
].map(storage => {
|
||||
const wrapper = {
|
||||
get(options) {
|
||||
return new Promise(resolve => {
|
||||
storage.get(options, data => resolve(data));
|
||||
});
|
||||
},
|
||||
set(data) {
|
||||
return new Promise(resolve => {
|
||||
storage.set(data, () => resolve(data));
|
||||
});
|
||||
},
|
||||
remove(keyOrKeys) {
|
||||
return new Promise(resolve => {
|
||||
storage.remove(keyOrKeys, resolve);
|
||||
});
|
||||
},
|
||||
getValue(key) {
|
||||
return wrapper.get(key).then(data => data[key]);
|
||||
},
|
||||
setValue(key, value) {
|
||||
return wrapper.set({[key]: value});
|
||||
},
|
||||
loadLZStringScript() {
|
||||
return Promise.resolve(
|
||||
window.LZString ||
|
||||
loadScript('/vendor/lz-string/lz-string-unsafe.js').then(() => {
|
||||
window.LZString = window.LZStringUnsafe;
|
||||
}));
|
||||
},
|
||||
getLZValue(key) {
|
||||
return wrapper.getLZValues([key]).then(data => data[key]);
|
||||
},
|
||||
getLZValues(keys) {
|
||||
return Promise.all([
|
||||
wrapper.get(keys),
|
||||
wrapper.loadLZStringScript(),
|
||||
]).then(([data = {}]) => {
|
||||
for (const key of keys) {
|
||||
const value = data[key];
|
||||
data[key] = value && tryJSONparse(LZString.decompressFromUTF16(value));
|
||||
}
|
||||
return data;
|
||||
});
|
||||
},
|
||||
setLZValue(key, value) {
|
||||
return wrapper.loadLZStringScript().then(() =>
|
||||
wrapper.set({
|
||||
[key]: LZString.compressToUTF16(JSON.stringify(value)),
|
||||
}));
|
||||
}
|
||||
};
|
||||
return wrapper;
|
||||
});
|
||||
|
||||
|
||||
function styleSectionsEqual({sections: a}, {sections: b}) {
|
||||
if (!a || !b) {
|
||||
return undefined;
|
||||
}
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
// order of sections should be identical to account for the case of multiple
|
||||
// sections matching the same URL because the order of rules is part of cascading
|
||||
return a.every((sectionA, index) => propertiesEqual(sectionA, b[index]));
|
||||
|
||||
function propertiesEqual(secA, secB) {
|
||||
for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
|
||||
if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b);
|
||||
}
|
||||
|
||||
function equalOrEmpty(a, b, telltale, comparator) {
|
||||
const typeA = a && typeof a[telltale] === 'function';
|
||||
const typeB = b && typeof b[telltale] === 'function';
|
||||
return (
|
||||
(a === null || a === undefined || (typeA && !a.length)) &&
|
||||
(b === null || b === undefined || (typeB && !b.length))
|
||||
) || typeA && typeB && a.length === b.length && comparator(a, b);
|
||||
}
|
||||
|
||||
function arrayMirrors(array1, array2) {
|
||||
return (
|
||||
array1.every(el => array2.includes(el)) &&
|
||||
array2.every(el => array1.includes(el))
|
||||
);
|
||||
}
|
||||
}
|
17
manage.html
17
manage.html
|
@ -150,13 +150,18 @@
|
|||
<script src="js/prefs.js"></script>
|
||||
<script src="content/apply.js"></script>
|
||||
<script src="js/localization.js"></script>
|
||||
<script src="js/storage-util.js"></script>
|
||||
<script src="manage/filters.js"></script>
|
||||
<script src="manage/updater-ui.js"></script>
|
||||
<script src="manage/object-diff.js"></script>
|
||||
<script src="vendor-overwrites/colorpicker/colorpicker.js"></script>
|
||||
<script src="manage/config-dialog.js"></script>
|
||||
<script src="manage/sort.js"></script>
|
||||
<script src="manage/manage.js"></script>
|
||||
|
||||
<script src="vendor-overwrites/colorpicker/colorpicker.js" async></script>
|
||||
<script src="manage/config-dialog.js" async></script>
|
||||
<script src="manage/updater-ui.js" async></script>
|
||||
<script src="manage/object-diff.js" async></script>
|
||||
<script src="manage/import-export.js" async></script>
|
||||
<script src="msgbox/msgbox.js" async></script>
|
||||
<script src="manage/incremental-search.js" async></script>
|
||||
</head>
|
||||
|
||||
<body id="stylus-manage" i18n-dragndrop-hint="dragDropMessage">
|
||||
|
@ -358,10 +363,6 @@
|
|||
|
||||
<div id="installed"></div>
|
||||
|
||||
<script src="manage/import-export.js"></script>
|
||||
<script src="msgbox/msgbox.js"></script>
|
||||
<script src="manage/incremental-search.js" async></script>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none !important;">
|
||||
<symbol id="svg-icon-checked" viewBox="0 0 1000 1000">
|
||||
<path fill-rule="evenodd" d="M983.2,184.3L853,69.8c-4-3.5-9.3-5.3-14.5-5c-5.3,0.4-10.3,2.8-13.8,6.8L352.3,609.2L184.4,386.9c-3.2-4.2-8-7-13.2-7.8c-5.3-0.8-10.6,0.6-14.9,3.9L18,487.5c-8.8,6.7-10.6,19.3-3.9,28.1L325,927.2c3.6,4.8,9.3,7.7,15.3,8c0.2,0,0.5,0,0.7,0c5.8,0,11.3-2.5,15.1-6.8L985,212.6C992.3,204.3,991.5,191.6,983.2,184.3z"/>
|
||||
|
|
|
@ -107,7 +107,7 @@ function configDialog(style) {
|
|||
buttons.close.textContent = t(someDirty ? 'confirmCancel' : 'confirmClose');
|
||||
}
|
||||
|
||||
function save({anyChangeIsDirty = false} = {}) {
|
||||
function save({anyChangeIsDirty = false} = {}, bgStyle) {
|
||||
if (saving) {
|
||||
debounce(save, 0, ...arguments);
|
||||
return;
|
||||
|
@ -116,11 +116,18 @@ function configDialog(style) {
|
|||
!vars.some(va => va.dirty || anyChangeIsDirty && va.value !== va.savedValue)) {
|
||||
return;
|
||||
}
|
||||
if (!bgStyle) {
|
||||
API.getStyles({id: style.id, omitCode: !BG})
|
||||
.then(([bgStyle]) => save({anyChangeIsDirty}, bgStyle || {}));
|
||||
return;
|
||||
}
|
||||
style = style.sections ? Object.assign({}, style) : style;
|
||||
style.enabled = true;
|
||||
style.reason = 'config';
|
||||
style.sourceCode = null;
|
||||
style.sections = null;
|
||||
const styleVars = style.usercssData.vars;
|
||||
const bgStyle = BG.cachedStyles.byId.get(style.id);
|
||||
const bgVars = bgStyle && (bgStyle.usercssData || {}).vars || {};
|
||||
const bgVars = (bgStyle.usercssData || {}).vars || {};
|
||||
const invalid = [];
|
||||
let numValid = 0;
|
||||
for (const va of vars) {
|
||||
|
@ -164,9 +171,9 @@ function configDialog(style) {
|
|||
return;
|
||||
}
|
||||
saving = true;
|
||||
return BG.usercssHelper.save(BG.deepCopy(style))
|
||||
return API.saveUsercss(style)
|
||||
.then(saved => {
|
||||
varsInitial = getInitialValues(deepCopy(saved.usercssData.vars));
|
||||
varsInitial = getInitialValues(saved.usercssData.vars);
|
||||
vars.forEach(va => onchange({target: va.input, justSaved: true}));
|
||||
renderValues();
|
||||
updateButtons();
|
||||
|
|
|
@ -29,7 +29,7 @@ HTMLSelectElement.prototype.adjustWidth = function () {
|
|||
parent.replaceChild(this, singleSelect);
|
||||
};
|
||||
|
||||
onDOMready().then(onBackgroundReady).then(() => {
|
||||
onDOMready().then(() => {
|
||||
$('#search').oninput = searchStyles;
|
||||
if (urlFilterParam) {
|
||||
$('#search').value = 'url:' + urlFilterParam;
|
||||
|
@ -169,14 +169,17 @@ function filterAndAppend({entry, container}) {
|
|||
if (!filtersSelector.hide || !entry.matches(filtersSelector.hide)) {
|
||||
entry.classList.add('hidden');
|
||||
}
|
||||
} else if ($('#search').value.trim()) {
|
||||
searchStyles({immediately: true, container});
|
||||
}
|
||||
reapplyFilter(container);
|
||||
}
|
||||
|
||||
|
||||
function reapplyFilter(container = installed) {
|
||||
function reapplyFilter(container = installed, alreadySearched) {
|
||||
if (!alreadySearched && $('#search').value.trim()) {
|
||||
searchStyles({immediately: true, container})
|
||||
.then(() => reapplyFilter(container, true));
|
||||
return;
|
||||
}
|
||||
// A: show
|
||||
let toHide = [];
|
||||
let toUnhide = [];
|
||||
|
@ -189,9 +192,6 @@ function reapplyFilter(container = installed) {
|
|||
if (toUnhide instanceof DocumentFragment) {
|
||||
installed.appendChild(toUnhide);
|
||||
return;
|
||||
} else if (toUnhide.length && $('#search').value.trim()) {
|
||||
searchStyles({immediately: true, container: toUnhide});
|
||||
filterContainer({hide: false});
|
||||
}
|
||||
// filtering needed or a single-element job from handleUpdate()
|
||||
for (const entry of toUnhide.children || toUnhide) {
|
||||
|
@ -251,16 +251,12 @@ function reapplyFilter(container = installed) {
|
|||
|
||||
|
||||
function showFiltersStats() {
|
||||
if (!BG.cachedStyles.list) {
|
||||
debounce(showFiltersStats, 100);
|
||||
return;
|
||||
}
|
||||
const active = filtersSelector.hide !== '';
|
||||
$('#filters summary').classList.toggle('active', active);
|
||||
$('#reset-filters').disabled = !active;
|
||||
const numTotal = BG.cachedStyles.list.length;
|
||||
const numTotal = installed.children.length;
|
||||
const numHidden = installed.getElementsByClassName('entry hidden').length;
|
||||
const numShown = Math.min(numTotal - numHidden, installed.children.length);
|
||||
const numShown = numTotal - numHidden;
|
||||
if (filtersSelector.numShown !== numShown ||
|
||||
filtersSelector.numTotal !== numTotal) {
|
||||
filtersSelector.numShown = numShown;
|
||||
|
@ -273,87 +269,34 @@ function showFiltersStats() {
|
|||
|
||||
|
||||
function searchStyles({immediately, container}) {
|
||||
const searchElement = $('#search');
|
||||
const value = searchElement.value.trim();
|
||||
const urlMode = /^\s*url:/i.test(value);
|
||||
const query = urlMode
|
||||
? value.replace(/^\s*url:/i, '')
|
||||
: value.toLocaleLowerCase();
|
||||
if (query === searchElement.lastValue && !immediately && !container) {
|
||||
const el = $('#search');
|
||||
const query = el.value.trim();
|
||||
if (query === el.lastValue && !immediately && !container) {
|
||||
return;
|
||||
}
|
||||
if (!immediately) {
|
||||
debounce(searchStyles, 150, {immediately: true});
|
||||
return;
|
||||
}
|
||||
searchElement.lastValue = query;
|
||||
el.lastValue = query;
|
||||
|
||||
const rx = query.startsWith('/') && query.indexOf('/', 1) > 0 &&
|
||||
tryRegExp(...(value.match(/^\s*\/(.*?)\/([gimsuy]*)\s*$/) || []).slice(1));
|
||||
const words = rx ? null :
|
||||
query.startsWith('"') && query.endsWith('"') ? [value.trim().slice(1, -1)] :
|
||||
query.split(/\s+/).filter(s => s.length > 1);
|
||||
if (words && !words.length) {
|
||||
words.push(query);
|
||||
}
|
||||
const entries = container && container.children || container || installed.children;
|
||||
const siteStyleIds = urlMode &&
|
||||
new Set(BG.filterStyles({matchUrl: query}).map(style => style.id));
|
||||
let needsRefilter = false;
|
||||
for (const entry of entries) {
|
||||
let isMatching = !query || words && !words.length;
|
||||
if (!isMatching) {
|
||||
const style = urlMode ? siteStyleIds.has(entry.styleId) :
|
||||
BG.cachedStyles.byId.get(entry.styleId) || {};
|
||||
isMatching = Boolean(style && (
|
||||
urlMode ||
|
||||
isMatchingText(style.name) ||
|
||||
style.url && isMatchingText(style.url) ||
|
||||
style.sourceCode && isMatchingText(style.sourceCode) ||
|
||||
isMatchingStyle(style)));
|
||||
}
|
||||
if (entry.classList.contains('not-matching') !== !isMatching) {
|
||||
entry.classList.toggle('not-matching', !isMatching);
|
||||
needsRefilter = true;
|
||||
}
|
||||
}
|
||||
if (needsRefilter && !container) {
|
||||
filterOnChange({forceRefilter: true});
|
||||
}
|
||||
return;
|
||||
|
||||
function isMatchingStyle(style) {
|
||||
for (const section of style.sections) {
|
||||
for (const prop in section) {
|
||||
const value = section[prop];
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
if (isMatchingText(value)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'object':
|
||||
for (const str of value) {
|
||||
if (isMatchingText(str)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
return API.searchDB({
|
||||
query,
|
||||
ids: [...entries].map(el => el.styleId),
|
||||
}).then(ids => {
|
||||
ids = new Set(ids);
|
||||
let needsRefilter = false;
|
||||
for (const entry of entries) {
|
||||
const isMatching = ids.has(entry.styleId);
|
||||
if (entry.classList.contains('not-matching') !== !isMatching) {
|
||||
entry.classList.toggle('not-matching', !isMatching);
|
||||
needsRefilter = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isMatchingText(text) {
|
||||
if (rx) {
|
||||
return rx.test(text);
|
||||
if (needsRefilter && !container) {
|
||||
filterOnChange({forceRefilter: true});
|
||||
}
|
||||
for (let pass = 1; pass <= 2; pass++) {
|
||||
if (words.every(word => text.includes(word))) {
|
||||
return true;
|
||||
}
|
||||
text = text.toLocaleLowerCase();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return container;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global messageBox, handleUpdate, applyOnMessage */
|
||||
/* global messageBox handleUpdate applyOnMessage styleSectionsEqual */
|
||||
'use strict';
|
||||
|
||||
const STYLISH_DUMP_FILE_EXT = '.txt';
|
||||
|
@ -41,7 +41,7 @@ function importFromFile({fileTypeFilter, file} = {}) {
|
|||
importFromString(text) :
|
||||
getOwnTab().then(tab => {
|
||||
tab.url = URL.createObjectURL(new Blob([text], {type: 'text/css'}));
|
||||
return BG.usercssHelper.openInstallPage(tab, {direct: true})
|
||||
return API.installUsercss({direct: true}, {tab})
|
||||
.then(() => URL.revokeObjectURL(tab.url));
|
||||
})
|
||||
).then(numStyles => {
|
||||
|
@ -56,17 +56,17 @@ function importFromFile({fileTypeFilter, file} = {}) {
|
|||
}
|
||||
|
||||
|
||||
function importFromString(jsonString) {
|
||||
if (!BG) {
|
||||
onBackgroundReady().then(() => importFromString(jsonString));
|
||||
function importFromString(jsonString, oldStyles) {
|
||||
if (!oldStyles) {
|
||||
API.getStyles().then(styles => importFromString(jsonString, styles));
|
||||
return;
|
||||
}
|
||||
// create objects in background context
|
||||
const json = BG.tryJSONparse(jsonString) || [];
|
||||
const json = tryJSONparse(jsonString) || [];
|
||||
if (typeof json.slice !== 'function') {
|
||||
json.length = 0;
|
||||
}
|
||||
const oldStyles = json.length && BG.deepCopy(BG.cachedStyles.list || []);
|
||||
const oldStylesById = new Map(
|
||||
oldStyles.map(style => [style.id, style]));
|
||||
const oldStylesByName = json.length && new Map(
|
||||
oldStyles.map(style => [style.name.trim(), style]));
|
||||
|
||||
|
@ -94,7 +94,7 @@ function importFromString(jsonString) {
|
|||
const info = analyze(item);
|
||||
if (info) {
|
||||
// using saveStyle directly since json was parsed in background page context
|
||||
return BG.saveStyle(Object.assign(item, SAVE_OPTIONS))
|
||||
return API.saveStyle(Object.assign(item, SAVE_OPTIONS))
|
||||
.then(style => account({style, info, resolve}));
|
||||
}
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ function importFromString(jsonString) {
|
|||
return;
|
||||
}
|
||||
item.name = item.name.trim();
|
||||
const byId = BG.cachedStyles.byId.get(item.id);
|
||||
const byId = oldStylesById.get(item.id);
|
||||
const byName = oldStylesByName.get(item.name);
|
||||
oldStylesByName.delete(item.name);
|
||||
let oldStyle;
|
||||
|
@ -129,7 +129,7 @@ function importFromString(jsonString) {
|
|||
const metaEqual = oldStyleKeys &&
|
||||
oldStyleKeys.length === Object.keys(item).length &&
|
||||
oldStyleKeys.every(k => k === 'sections' || oldStyle[k] === item[k]);
|
||||
const codeEqual = oldStyle && BG.styleSectionsEqual(oldStyle, item);
|
||||
const codeEqual = oldStyle && styleSectionsEqual(oldStyle, item);
|
||||
if (metaEqual && codeEqual) {
|
||||
stats.unchanged.names.push(oldStyle.name);
|
||||
stats.unchanged.ids.push(oldStyle.id);
|
||||
|
@ -237,10 +237,10 @@ function importFromString(jsonString) {
|
|||
return;
|
||||
}
|
||||
const id = newIds[index++];
|
||||
deleteStyleSafe({id, notify: false}).then(id => {
|
||||
API.deleteStyle({id, notify: false}).then(id => {
|
||||
const oldStyle = oldStylesById.get(id);
|
||||
if (oldStyle) {
|
||||
saveStyleSafe(Object.assign(oldStyle, SAVE_OPTIONS))
|
||||
API.saveStyle(Object.assign(oldStyle, SAVE_OPTIONS))
|
||||
.then(undoNextId);
|
||||
} else {
|
||||
undoNextId();
|
||||
|
@ -293,7 +293,7 @@ function importFromString(jsonString) {
|
|||
chrome.webNavigation.getAllFrames({tabId}, frames => {
|
||||
frames = frames && frames[0] ? frames : [{frameId: 0}];
|
||||
frames.forEach(({frameId}) =>
|
||||
getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => {
|
||||
API.getStyles({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => {
|
||||
const message = {method: 'styleReplaceAll', tabId, frameId, styles};
|
||||
if (tab.id === ownTab.id) {
|
||||
applyOnMessage(message);
|
||||
|
@ -301,7 +301,7 @@ function importFromString(jsonString) {
|
|||
invokeOrPostpone(tab.active, sendMessage, message, ignoreChromeError);
|
||||
}
|
||||
if (frameId === 0) {
|
||||
setTimeout(BG.updateIcon, 0, tab, styles);
|
||||
setTimeout(API.updateIcon, 0, tab, styles);
|
||||
}
|
||||
}));
|
||||
if (resolve) {
|
||||
|
@ -314,7 +314,7 @@ function importFromString(jsonString) {
|
|||
|
||||
|
||||
$('#file-all-styles').onclick = () => {
|
||||
getStylesSafe().then(styles => {
|
||||
API.getStyles().then(styles => {
|
||||
const text = JSON.stringify(styles, null, '\t');
|
||||
const blob = new Blob([text], {type: 'application/json'});
|
||||
const objectURL = URL.createObjectURL(blob);
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
/* global messageBox, getStyleWithNoCode, retranslateCSS */
|
||||
/* global filtersSelector, filterAndAppend */
|
||||
/* global checkUpdate, handleUpdateInstalled */
|
||||
/* global objectDiff */
|
||||
/* global configDialog */
|
||||
/* global sorter */
|
||||
/*
|
||||
global messageBox getStyleWithNoCode retranslateCSS
|
||||
global filtersSelector filterAndAppend urlFilterParam
|
||||
global checkUpdate handleUpdateInstalled
|
||||
global objectDiff
|
||||
global configDialog
|
||||
global sorter
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
let installed;
|
||||
|
@ -30,14 +32,13 @@ const OWN_ICON = chrome.runtime.getManifest().icons['16'];
|
|||
const handleEvent = {};
|
||||
|
||||
Promise.all([
|
||||
getStylesSafe(),
|
||||
API.getStyles({omitCode: !BG}),
|
||||
urlFilterParam && API.searchDB({query: 'url:' + urlFilterParam}),
|
||||
onDOMready().then(initGlobalEvents),
|
||||
]).then(([styles]) => {
|
||||
showStyles(styles);
|
||||
]).then(args => {
|
||||
showStyles(...args);
|
||||
});
|
||||
|
||||
dieOnNullBackground();
|
||||
|
||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||
|
||||
function onRuntimeMessage(msg) {
|
||||
|
@ -107,7 +108,7 @@ function initGlobalEvents() {
|
|||
}
|
||||
|
||||
|
||||
function showStyles(styles = []) {
|
||||
function showStyles(styles = [], matchUrlIds) {
|
||||
const sorted = sorter.sort({
|
||||
styles: styles.map(style => ({
|
||||
style,
|
||||
|
@ -137,7 +138,13 @@ function showStyles(styles = []) {
|
|||
// eslint-disable-next-line no-unmodified-loop-condition
|
||||
(shouldRenderAll || ++rendered < 20 || performance.now() - t0 < 10)
|
||||
) {
|
||||
renderBin.appendChild(createStyleElement(sorted[index++]));
|
||||
const info = sorted[index++];
|
||||
const entry = createStyleElement(info);
|
||||
if (matchUrlIds && !matchUrlIds.includes(info.style.id)) {
|
||||
entry.classList.add('not-matching');
|
||||
rendered--;
|
||||
}
|
||||
renderBin.appendChild(entry);
|
||||
}
|
||||
filterAndAppend({container: renderBin});
|
||||
if (index < sorted.length) {
|
||||
|
@ -277,7 +284,7 @@ function createStyleTargetsElement({entry, style, iconsOnly}) {
|
|||
|
||||
|
||||
function recreateStyleTargets({styles, iconsOnly = false} = {}) {
|
||||
Promise.resolve(styles || getStylesSafe()).then(styles => {
|
||||
Promise.resolve(styles || API.getStyles()).then(styles => {
|
||||
for (const style of styles) {
|
||||
const entry = $(ENTRY_ID_PREFIX + style.id);
|
||||
if (entry) {
|
||||
|
@ -391,7 +398,7 @@ Object.assign(handleEvent, {
|
|||
},
|
||||
|
||||
toggle(event, entry) {
|
||||
saveStyleSafe({
|
||||
API.saveStyle({
|
||||
id: entry.styleId,
|
||||
enabled: this.matches('.enable') || this.checked,
|
||||
});
|
||||
|
@ -399,39 +406,30 @@ Object.assign(handleEvent, {
|
|||
|
||||
check(event, entry) {
|
||||
event.preventDefault();
|
||||
checkUpdate(entry);
|
||||
checkUpdate(entry, {single: true});
|
||||
},
|
||||
|
||||
update(event, entry) {
|
||||
event.preventDefault();
|
||||
const request = Object.assign(entry.updatedCode, {
|
||||
id: entry.styleId,
|
||||
reason: 'update',
|
||||
});
|
||||
if (entry.updatedCode.usercssData) {
|
||||
onBackgroundReady()
|
||||
.then(() => BG.usercssHelper.save(request));
|
||||
} else {
|
||||
// update everything but name
|
||||
request.name = null;
|
||||
saveStyleSafe(request);
|
||||
}
|
||||
const json = entry.updatedCode;
|
||||
json.id = entry.styleId;
|
||||
json.reason = 'update';
|
||||
API[json.usercssData ? 'saveUsercss' : 'saveStyle'](json);
|
||||
},
|
||||
|
||||
delete(event, entry) {
|
||||
event.preventDefault();
|
||||
const id = entry.styleId;
|
||||
const {name} = BG.cachedStyles.byId.get(id) || {};
|
||||
animateElement(entry);
|
||||
messageBox({
|
||||
title: t('deleteStyleConfirm'),
|
||||
contents: name,
|
||||
contents: entry.styleMeta.name,
|
||||
className: 'danger center',
|
||||
buttons: [t('confirmDelete'), t('confirmCancel')],
|
||||
})
|
||||
.then(({button}) => {
|
||||
if (button === 0) {
|
||||
deleteStyleSafe({id});
|
||||
API.deleteStyle({id});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -525,7 +523,7 @@ function handleUpdate(style, {reason, method} = {}) {
|
|||
sorter.update();
|
||||
if (!entry.matches('.hidden') && reason !== 'import') {
|
||||
animateElement(entry);
|
||||
scrollElementIntoView(entry);
|
||||
requestAnimationFrame(() => scrollElementIntoView(entry));
|
||||
}
|
||||
|
||||
function handleToggledOrCodeOnly() {
|
||||
|
@ -606,7 +604,7 @@ function switchUI({styleOnly} = {}) {
|
|||
const missingFavicons = newUI.enabled && newUI.favicons && !$('.applies-to img');
|
||||
if (changed.enabled || (missingFavicons && !createStyleElement.parts)) {
|
||||
installed.textContent = '';
|
||||
getStylesSafe().then(showStyles);
|
||||
API.getStyles().then(showStyles);
|
||||
return;
|
||||
}
|
||||
if (changed.targets) {
|
||||
|
@ -645,28 +643,3 @@ function usePrefsDuringPageLoad() {
|
|||
}
|
||||
$$('#header select').forEach(el => el.adjustWidth());
|
||||
}
|
||||
|
||||
|
||||
// TODO: remove when these bugs are fixed in FF
|
||||
function dieOnNullBackground() {
|
||||
if (!FIREFOX || BG) {
|
||||
return;
|
||||
}
|
||||
sendMessage({method: 'healthCheck'}, health => {
|
||||
if (health && !chrome.extension.getBackgroundPage()) {
|
||||
onDOMready().then(() => {
|
||||
sendMessage({method: 'getStyles'}, showStyles);
|
||||
messageBox({
|
||||
title: 'Stylus',
|
||||
className: 'danger center',
|
||||
contents: t('dysfunctionalBackgroundConnection'),
|
||||
onshow: () => {
|
||||
$('#message-box-close-icon').remove();
|
||||
window.removeEventListener('keydown', messageBox.listeners.key, true);
|
||||
}
|
||||
});
|
||||
document.documentElement.style.pointerEvents = 'none';
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -129,7 +129,7 @@ const sorter = (() => {
|
|||
styles: current.map(entry => ({
|
||||
entry,
|
||||
name: entry.styleNameLowerCase + '\n' + entry.styleMeta.name,
|
||||
style: BG.cachedStyles.byId.get(entry.styleId),
|
||||
style: entry.styleMeta,
|
||||
}))
|
||||
});
|
||||
if (current.some((entry, index) => entry !== sorted[index].entry)) {
|
||||
|
|
|
@ -29,41 +29,51 @@ function applyUpdateAll() {
|
|||
|
||||
function checkUpdateAll() {
|
||||
document.body.classList.add('update-in-progress');
|
||||
$('#check-all-updates').disabled = true;
|
||||
$('#check-all-updates-force').classList.add('hidden');
|
||||
$('#apply-all-updates').classList.add('hidden');
|
||||
$('#update-all-no-updates').classList.add('hidden');
|
||||
const btnCheck = $('#check-all-updates');
|
||||
const btnCheckForce = $('#check-all-updates-force');
|
||||
const btnApply = $('#apply-all-updates');
|
||||
const noUpdates = $('#update-all-no-updates');
|
||||
btnCheck.disabled = true;
|
||||
btnCheckForce.classList.add('hidden');
|
||||
btnApply.classList.add('hidden');
|
||||
noUpdates.classList.add('hidden');
|
||||
|
||||
const ignoreDigest = this && this.id === 'check-all-updates-force';
|
||||
$$('.updatable:not(.can-update)' + (ignoreDigest ? '' : ':not(.update-problem)'))
|
||||
.map(el => checkUpdate(el, {single: false}));
|
||||
.map(checkUpdate);
|
||||
|
||||
let total = 0;
|
||||
let checked = 0;
|
||||
let skippedEdited = 0;
|
||||
let updated = 0;
|
||||
|
||||
BG.updater.checkAllStyles({observer, save: false, ignoreDigest}).then(done);
|
||||
chrome.runtime.onConnect.addListener(function onConnect(port) {
|
||||
if (port.name !== 'updater') return;
|
||||
port.onMessage.addListener(observer);
|
||||
chrome.runtime.onConnect.removeListener(onConnect);
|
||||
});
|
||||
|
||||
function observer(state, value, details) {
|
||||
switch (state) {
|
||||
case BG.updater.COUNT:
|
||||
total = value;
|
||||
break;
|
||||
case BG.updater.UPDATED:
|
||||
if (++updated === 1) {
|
||||
$('#apply-all-updates').disabled = true;
|
||||
$('#apply-all-updates').classList.remove('hidden');
|
||||
}
|
||||
$('#apply-all-updates').dataset.value = updated;
|
||||
// fallthrough
|
||||
case BG.updater.SKIPPED:
|
||||
checked++;
|
||||
if (details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED) {
|
||||
skippedEdited++;
|
||||
}
|
||||
reportUpdateState(state, value, details);
|
||||
break;
|
||||
API.updateCheckAll({
|
||||
save: false,
|
||||
observe: true,
|
||||
ignoreDigest,
|
||||
}).then(done);
|
||||
|
||||
function observer(info) {
|
||||
if ('count' in info) {
|
||||
total = info.count;
|
||||
}
|
||||
if (info.updated) {
|
||||
if (++updated === 1) {
|
||||
btnApply.disabled = true;
|
||||
btnApply.classList.remove('hidden');
|
||||
}
|
||||
btnApply.dataset.value = updated;
|
||||
}
|
||||
if (info.updated || info.error) {
|
||||
checked++;
|
||||
skippedEdited += [info.STATES.EDITED, info.STATES.MAYBE_EDITED].includes(info.error);
|
||||
reportUpdateState(info);
|
||||
}
|
||||
const progress = $('#update-progress');
|
||||
const maxWidth = progress.parentElement.clientWidth;
|
||||
|
@ -72,35 +82,34 @@ function checkUpdateAll() {
|
|||
|
||||
function done() {
|
||||
document.body.classList.remove('update-in-progress');
|
||||
$('#check-all-updates').disabled = total === 0;
|
||||
$('#apply-all-updates').disabled = false;
|
||||
btnCheck.disabled = total === 0;
|
||||
btnApply.disabled = false;
|
||||
renderUpdatesOnlyFilter({check: updated + skippedEdited > 0});
|
||||
if (!updated) {
|
||||
$('#update-all-no-updates').dataset.skippedEdited = skippedEdited > 0;
|
||||
$('#update-all-no-updates').classList.remove('hidden');
|
||||
$('#check-all-updates-force').classList.toggle('hidden', skippedEdited === 0);
|
||||
noUpdates.dataset.skippedEdited = skippedEdited > 0;
|
||||
noUpdates.classList.remove('hidden');
|
||||
btnCheckForce.classList.toggle('hidden', skippedEdited === 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function checkUpdate(entry, {single = true} = {}) {
|
||||
function checkUpdate(entry, {single} = {}) {
|
||||
$('.update-note', entry).textContent = t('checkingForUpdate');
|
||||
$('.check-update', entry).title = '';
|
||||
if (single) {
|
||||
BG.updater.checkStyle({
|
||||
API.updateCheck({
|
||||
save: false,
|
||||
id: entry.styleId,
|
||||
ignoreDigest: entry.classList.contains('update-problem'),
|
||||
style: BG.cachedStyles.byId.get(entry.styleId),
|
||||
observer: reportUpdateState,
|
||||
});
|
||||
}).then(reportUpdateState);
|
||||
}
|
||||
entry.classList.remove('checking-update', 'no-update', 'update-problem');
|
||||
entry.classList.add('checking-update');
|
||||
}
|
||||
|
||||
|
||||
function reportUpdateState(state, style, details) {
|
||||
function reportUpdateState({updated, style, error, STATES}) {
|
||||
const entry = $(ENTRY_ID_PREFIX + style.id);
|
||||
const newClasses = new Map([
|
||||
/*
|
||||
|
@ -117,43 +126,37 @@ function reportUpdateState(state, style, details) {
|
|||
['no-update', 0],
|
||||
['update-problem', 0],
|
||||
]);
|
||||
switch (state) {
|
||||
case BG.updater.UPDATED:
|
||||
newClasses.set('can-update', true);
|
||||
entry.updatedCode = style;
|
||||
$('.update-note', entry).textContent = '';
|
||||
$('#only-updates').classList.remove('hidden');
|
||||
break;
|
||||
case BG.updater.SKIPPED: {
|
||||
if (entry.classList.contains('can-update')) {
|
||||
break;
|
||||
}
|
||||
const same = (
|
||||
details === BG.updater.SAME_MD5 ||
|
||||
details === BG.updater.SAME_CODE ||
|
||||
details === BG.updater.SAME_VERSION
|
||||
);
|
||||
const edited = details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED;
|
||||
entry.dataset.details = details;
|
||||
if (!details) {
|
||||
details = t('updateCheckFailServerUnreachable') + '\n' + style.updateUrl;
|
||||
} else if (typeof details === 'number') {
|
||||
details = t('updateCheckFailBadResponseCode', [details]) + '\n' + style.updateUrl;
|
||||
} else if (details === BG.updater.EDITED) {
|
||||
details = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
|
||||
} else if (details === BG.updater.MAYBE_EDITED) {
|
||||
details = t('updateCheckSkippedMaybeLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
|
||||
}
|
||||
const message = same ? t('updateCheckSucceededNoUpdate') : details;
|
||||
newClasses.set('no-update', true);
|
||||
newClasses.set('update-problem', !same);
|
||||
$('.update-note', entry).textContent = message;
|
||||
$('.check-update', entry).title = newUI.enabled ? message : '';
|
||||
$('.update', entry).title = t(edited ? 'updateCheckManualUpdateForce' : 'installUpdate');
|
||||
if (!document.body.classList.contains('update-in-progress')) {
|
||||
// this is a single update job so we can decide whether to hide the filter
|
||||
renderUpdatesOnlyFilter({show: $('.can-update, .update-problem')});
|
||||
}
|
||||
if (updated) {
|
||||
newClasses.set('can-update', true);
|
||||
entry.updatedCode = style;
|
||||
$('.update-note', entry).textContent = '';
|
||||
$('#only-updates').classList.remove('hidden');
|
||||
} else if (!entry.classList.contains('can-update')) {
|
||||
const same = (
|
||||
error === STATES.SAME_MD5 ||
|
||||
error === STATES.SAME_CODE ||
|
||||
error === STATES.SAME_VERSION
|
||||
);
|
||||
const edited = error === STATES.EDITED || error === STATES.MAYBE_EDITED;
|
||||
entry.dataset.error = error;
|
||||
if (!error) {
|
||||
error = t('updateCheckFailServerUnreachable') + '\n' + style.updateUrl;
|
||||
} else if (typeof error === 'number') {
|
||||
error = t('updateCheckFailBadResponseCode', [error]) + '\n' + style.updateUrl;
|
||||
} else if (error === STATES.EDITED) {
|
||||
error = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
|
||||
} else if (error === STATES.MAYBE_EDITED) {
|
||||
error = t('updateCheckSkippedMaybeLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
|
||||
}
|
||||
const message = same ? t('updateCheckSucceededNoUpdate') : error;
|
||||
newClasses.set('no-update', true);
|
||||
newClasses.set('update-problem', !same);
|
||||
$('.update-note', entry).textContent = message;
|
||||
$('.check-update', entry).title = newUI.enabled ? message : '';
|
||||
$('.update', entry).title = t(edited ? 'updateCheckManualUpdateForce' : 'installUpdate');
|
||||
if (!document.body.classList.contains('update-in-progress')) {
|
||||
// this is a single update job so we can decide whether to hide the filter
|
||||
renderUpdatesOnlyFilter({show: $('.can-update, .update-problem')});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -162,9 +165,16 @@ function reportUpdateState(state, style, details) {
|
|||
// 2. remove falsy newClasses
|
||||
// 3. keep existing classes otherwise
|
||||
const classes = new Map([...entry.classList.values()].map(cls => [cls, true]));
|
||||
[...newClasses.entries()].forEach(([cls, newState]) => classes.set(cls, newState));
|
||||
const className = [...classes.entries()].filter(([, state]) => state).map(([cls]) => cls).join(' ');
|
||||
if (className !== entry.className) entry.className = className;
|
||||
for (const [cls, newState] of newClasses.entries()) {
|
||||
classes.set(cls, newState);
|
||||
}
|
||||
const className = [...classes.entries()]
|
||||
.map(([cls, state]) => state && cls)
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
if (className !== entry.className) {
|
||||
entry.className = className;
|
||||
}
|
||||
|
||||
if (filtersSelector.hide) {
|
||||
filterAndAppend({entry});
|
||||
|
@ -200,7 +210,10 @@ function showUpdateHistory(event) {
|
|||
const log = $create('.update-history-log');
|
||||
let logText, scroller, toggler;
|
||||
let deleted = false;
|
||||
BG.chromeLocal.getValue('updateLog').then((lines = []) => {
|
||||
Promise.all([
|
||||
chromeLocal.getValue('updateLog'),
|
||||
API.getUpdaterStates(),
|
||||
]).then(([lines = [], states]) => {
|
||||
logText = lines.join('\n');
|
||||
messageBox({
|
||||
title: t('updateCheckHistory'),
|
||||
|
@ -227,6 +240,13 @@ function showUpdateHistory(event) {
|
|||
t('manageOnlyUpdates'),
|
||||
]));
|
||||
|
||||
toggler.rxRemoveNOP = new RegExp(
|
||||
'^[^#]*(' +
|
||||
Object.keys(states)
|
||||
.filter(k => k.startsWith('SAME_'))
|
||||
.map(k => states[k])
|
||||
.join('|') +
|
||||
').*\r?\n', 'gm');
|
||||
toggler.onchange();
|
||||
}),
|
||||
});
|
||||
|
@ -242,26 +262,17 @@ function showUpdateHistory(event) {
|
|||
return;
|
||||
}
|
||||
const scrollRatio = calcScrollRatio();
|
||||
const rxRemoveNOP = this.checked && new RegExp([
|
||||
'^[^#]*(',
|
||||
Object.keys(BG.updater)
|
||||
.filter(k => k.startsWith('SAME_'))
|
||||
.map(k => stringAsRegExp(BG.updater[k]))
|
||||
.map(rx => rx.source)
|
||||
.join('|'),
|
||||
').*\r?\n',
|
||||
].join(''), 'gm');
|
||||
log.textContent = !this.checked ? logText : logText.replace(rxRemoveNOP, '');
|
||||
log.textContent = !this.checked ? logText : logText.replace(this.rxRemoveNOP, '');
|
||||
if (Math.abs(scrollRatio - calcScrollRatio()) > .1) {
|
||||
scroller.scrollTop = scrollRatio * scroller.scrollHeight - scroller.clientHeight;
|
||||
}
|
||||
}
|
||||
function deleteHistory() {
|
||||
if (deleted) {
|
||||
BG.chromeLocal.setValue('updateLog', logText.split('\n'));
|
||||
chromeLocal.setValue('updateLog', logText.split('\n'));
|
||||
setTimeout(scrollToBottom);
|
||||
} else {
|
||||
BG.chromeLocal.remove('updateLog');
|
||||
chromeLocal.remove('updateLog');
|
||||
log.textContent = '';
|
||||
}
|
||||
deleted = !deleted;
|
||||
|
|
|
@ -21,17 +21,18 @@
|
|||
"background": {
|
||||
"scripts": [
|
||||
"js/messaging.js",
|
||||
"vendor/lz-string/lz-string-unsafe.js",
|
||||
"js/color-parser.js",
|
||||
"js/usercss.js",
|
||||
"js/storage-util.js",
|
||||
"background/storage.js",
|
||||
"background/usercss-helper.js",
|
||||
"js/prefs.js",
|
||||
"js/script-loader.js",
|
||||
"js/color-parser.js",
|
||||
"js/usercss.js",
|
||||
"background/background.js",
|
||||
"vendor/node-semver/semver.js",
|
||||
"background/usercss-helper.js",
|
||||
"background/style-via-api.js",
|
||||
"background/update.js"
|
||||
"background/search-db.js",
|
||||
"background/update.js",
|
||||
"vendor/node-semver/semver.js"
|
||||
]
|
||||
},
|
||||
"commands": {
|
||||
|
|
|
@ -62,23 +62,26 @@ function checkUpdates() {
|
|||
let checked = 0;
|
||||
let updated = 0;
|
||||
const maxWidth = $('#update-progress').parentElement.clientWidth;
|
||||
BG.updater.checkAllStyles({observer});
|
||||
|
||||
function observer(state, value) {
|
||||
switch (state) {
|
||||
case BG.updater.COUNT:
|
||||
total = value;
|
||||
document.body.classList.add('update-in-progress');
|
||||
break;
|
||||
case BG.updater.UPDATED:
|
||||
updated++;
|
||||
// fallthrough
|
||||
case BG.updater.SKIPPED:
|
||||
checked++;
|
||||
break;
|
||||
case BG.updater.DONE:
|
||||
document.body.classList.remove('update-in-progress');
|
||||
return;
|
||||
chrome.runtime.onConnect.addListener(function onConnect(port) {
|
||||
if (port.name !== 'updater') return;
|
||||
port.onMessage.addListener(observer);
|
||||
chrome.runtime.onConnect.removeListener(onConnect);
|
||||
});
|
||||
|
||||
API.updateCheckAll({observe: true});
|
||||
|
||||
function observer(info) {
|
||||
if ('count' in info) {
|
||||
total = info.count;
|
||||
document.body.classList.add('update-in-progress');
|
||||
} else if (info.updated) {
|
||||
updated++;
|
||||
checked++;
|
||||
} else if (info.error) {
|
||||
checked++;
|
||||
} else if (info.done) {
|
||||
document.body.classList.remove('update-in-progress');
|
||||
}
|
||||
$('#update-progress').style.width = Math.round(checked / total * maxWidth) + 'px';
|
||||
$('#updates-installed').dataset.value = updated || '';
|
||||
|
|
|
@ -161,6 +161,8 @@
|
|||
<script src="popup/popup.js"></script>
|
||||
<script src="popup/search-results.js"></script>
|
||||
<script src="popup/hotkeys.js"></script>
|
||||
<script src="js/script-loader.js" async></script>
|
||||
<script src="js/storage-util.js" async></script>
|
||||
</head>
|
||||
|
||||
<body id="stylus-popup">
|
||||
|
|
|
@ -101,11 +101,15 @@ var hotkeys = (() => {
|
|||
entry = typeof entry === 'string' ? $('#' + entry) : entry;
|
||||
if (!match && $('.checker', entry).checked !== enable || entry.classList.contains(match)) {
|
||||
results.push(entry.id);
|
||||
task = task.then(() => saveStyleSafe({
|
||||
task = task.then(() => API.saveStyle({
|
||||
id: entry.styleId,
|
||||
enabled: enable,
|
||||
notify: false,
|
||||
}));
|
||||
})).then(() => {
|
||||
entry.classList.toggle('enabled', enable);
|
||||
entry.classList.toggle('disabled', !enable);
|
||||
$('.checker', entry).checked = enable;
|
||||
});
|
||||
}
|
||||
}
|
||||
if (results.length) {
|
||||
|
@ -115,7 +119,7 @@ var hotkeys = (() => {
|
|||
}
|
||||
|
||||
function refreshAllTabs() {
|
||||
getStylesSafe({matchUrl: location.href, enabled: true, asHash: true})
|
||||
API.getStyles({matchUrl: location.href, enabled: true, asHash: true})
|
||||
.then(styles => applyOnMessage({method: 'styleReplaceAll', styles}));
|
||||
queryTabs().then(tabs =>
|
||||
tabs.forEach(tab => (!FIREFOX || tab.width) &&
|
||||
|
@ -127,11 +131,11 @@ var hotkeys = (() => {
|
|||
chrome.webNavigation.getAllFrames({tabId}, frames => {
|
||||
frames = frames && frames[0] ? frames : [{frameId: 0}];
|
||||
frames.forEach(({frameId}) =>
|
||||
getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => {
|
||||
API.getStyles({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => {
|
||||
const message = {method: 'styleReplaceAll', tabId, frameId, styles};
|
||||
invokeOrPostpone(tab.active, sendMessage, message, ignoreChromeError);
|
||||
if (frameId === 0) {
|
||||
setTimeout(BG.updateIcon, 0, tab, styles);
|
||||
setTimeout(API.updateIcon, 0, {tab, styles});
|
||||
}
|
||||
}));
|
||||
ignoreChromeError();
|
||||
|
|
190
popup/popup.js
190
popup/popup.js
|
@ -15,16 +15,15 @@ getActiveTab().then(tab =>
|
|||
FIREFOX && tab.url === 'about:blank' && tab.status === 'loading'
|
||||
? getTabRealURLFirefox(tab)
|
||||
: getTabRealURL(tab)
|
||||
).then(url => {
|
||||
tabURL = URLS.supported(url) ? url : '';
|
||||
Promise.all([
|
||||
tabURL && getStylesSafe({matchUrl: tabURL}),
|
||||
onDOMready().then(() => {
|
||||
initPopup(tabURL);
|
||||
}),
|
||||
]).then(([styles]) => {
|
||||
showStyles(styles);
|
||||
});
|
||||
).then(url => Promise.all([
|
||||
(tabURL = URLS.supported(url) ? url : '') &&
|
||||
API.getStyles({
|
||||
matchUrl: tabURL,
|
||||
omitCode: !BG,
|
||||
}),
|
||||
onDOMready().then(initPopup),
|
||||
])).then(([styles]) => {
|
||||
showStyles(styles);
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||
|
@ -33,9 +32,7 @@ function onRuntimeMessage(msg) {
|
|||
switch (msg.method) {
|
||||
case 'styleAdded':
|
||||
case 'styleUpdated':
|
||||
// notifyAllTabs sets msg.style's code to null so we have to get the actual style
|
||||
// because we analyze its code in detectSloppyRegexps
|
||||
handleUpdate(BG.cachedStyles.byId.get(msg.style.id));
|
||||
handleUpdate(msg.style);
|
||||
break;
|
||||
case 'styleDeleted':
|
||||
handleDelete(msg.id);
|
||||
|
@ -76,7 +73,7 @@ function toggleSideBorders(state = prefs.get('popup.borders')) {
|
|||
}
|
||||
|
||||
|
||||
function initPopup(url) {
|
||||
function initPopup() {
|
||||
installed = $('#installed');
|
||||
|
||||
setPopupWidth();
|
||||
|
@ -108,7 +105,7 @@ function initPopup(url) {
|
|||
installed);
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
if (!tabURL) {
|
||||
document.body.classList.add('blocked');
|
||||
document.body.insertBefore(template.unavailableInfo, document.body.firstChild);
|
||||
return;
|
||||
|
@ -153,10 +150,10 @@ function initPopup(url) {
|
|||
// For this URL
|
||||
const urlLink = template.writeStyle.cloneNode(true);
|
||||
Object.assign(urlLink, {
|
||||
href: 'edit.html?url-prefix=' + encodeURIComponent(url),
|
||||
title: `url-prefix("${url}")`,
|
||||
href: 'edit.html?url-prefix=' + encodeURIComponent(tabURL),
|
||||
title: `url-prefix("${tabURL}")`,
|
||||
textContent: prefs.get('popup.breadcrumbs.usePath')
|
||||
? new URL(url).pathname.slice(1)
|
||||
? new URL(tabURL).pathname.slice(1)
|
||||
// this URL
|
||||
: t('writeStyleForURL').replace(/ /g, '\u00a0'),
|
||||
onclick: handleEvent.openLink,
|
||||
|
@ -170,7 +167,7 @@ function initPopup(url) {
|
|||
matchTargets.appendChild(urlLink);
|
||||
|
||||
// For domain
|
||||
const domains = BG.getDomains(url);
|
||||
const domains = getDomains(tabURL);
|
||||
for (const domain of domains) {
|
||||
const numParts = domain.length - domain.replace(/\./g, '').length + 1;
|
||||
// Don't include TLD
|
||||
|
@ -193,6 +190,19 @@ function initPopup(url) {
|
|||
matchTargets.appendChild(matchTargets.removeChild(matchTargets.firstElementChild));
|
||||
}
|
||||
writeStyle.appendChild(matchWrapper);
|
||||
|
||||
function getDomains(url) {
|
||||
let d = /.*?:\/*([^/:]+)|$/.exec(url)[1];
|
||||
if (!d || url.startsWith('file:')) {
|
||||
return [];
|
||||
}
|
||||
const domains = [d];
|
||||
while (d.indexOf('.') !== -1) {
|
||||
d = d.substring(d.indexOf('.') + 1);
|
||||
domains.push(d);
|
||||
}
|
||||
return domains;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -213,34 +223,30 @@ function showStyles(styles) {
|
|||
: a.name.localeCompare(b.name)
|
||||
));
|
||||
|
||||
let postponeDetect = false;
|
||||
const t0 = performance.now();
|
||||
const container = document.createDocumentFragment();
|
||||
for (const style of styles) {
|
||||
createStyleElement({style, container, postponeDetect});
|
||||
postponeDetect = postponeDetect || performance.now() - t0 > 100;
|
||||
}
|
||||
styles.forEach(style => createStyleElement({style, container}));
|
||||
installed.appendChild(container);
|
||||
setTimeout(detectSloppyRegexps, 100, styles);
|
||||
|
||||
getStylesSafe({matchUrl: tabURL, strictRegexp: false})
|
||||
.then(unscreenedStyles => {
|
||||
for (const unscreened of unscreenedStyles) {
|
||||
if (!styles.includes(unscreened)) {
|
||||
postponeDetect = postponeDetect || performance.now() - t0 > 100;
|
||||
createStyleElement({
|
||||
style: Object.assign({appliedSections: [], postponeDetect}, unscreened),
|
||||
});
|
||||
}
|
||||
API.getStyles({
|
||||
matchUrl: tabURL,
|
||||
strictRegexp: false,
|
||||
omitCode: true,
|
||||
}).then(unscreenedStyles => {
|
||||
for (const style of unscreenedStyles) {
|
||||
if (!styles.find(({id}) => id === style.id)) {
|
||||
createStyleElement({style, check: true});
|
||||
}
|
||||
window.dispatchEvent(new Event('showStyles:done'));
|
||||
});
|
||||
}
|
||||
window.dispatchEvent(new Event('showStyles:done'));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function createStyleElement({
|
||||
style,
|
||||
check = false,
|
||||
container = installed,
|
||||
postponeDetect,
|
||||
}) {
|
||||
const entry = template.style.cloneNode(true);
|
||||
entry.setAttribute('style-id', style.id);
|
||||
|
@ -294,7 +300,7 @@ function createStyleElement({
|
|||
$('.delete', entry).onclick = handleEvent.delete;
|
||||
$('.configure', entry).onclick = handleEvent.configure;
|
||||
|
||||
invokeOrPostpone(!postponeDetect, detectSloppyRegexps, {entry, style});
|
||||
if (check) detectSloppyRegexps([style]);
|
||||
|
||||
const oldElement = $(ENTRY_ID_PREFIX + style.id);
|
||||
if (oldElement) {
|
||||
|
@ -316,23 +322,24 @@ Object.assign(handleEvent, {
|
|||
},
|
||||
|
||||
name(event) {
|
||||
this.checkbox.click();
|
||||
this.checkbox.dispatchEvent(new MouseEvent('click'));
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
toggle(event) {
|
||||
saveStyleSafe({
|
||||
API.saveStyle({
|
||||
id: handleEvent.getClickedStyleId(event),
|
||||
enabled: this.type === 'checkbox' ? this.checked : this.matches('.enable'),
|
||||
enabled: this.matches('.enable') || this.checked,
|
||||
});
|
||||
},
|
||||
|
||||
delete(event) {
|
||||
const id = handleEvent.getClickedStyleId(event);
|
||||
const entry = handleEvent.getClickedStyleElement(event);
|
||||
const id = entry.styleId;
|
||||
const box = $('#confirm');
|
||||
box.dataset.display = true;
|
||||
box.style.cssText = '';
|
||||
$('b', box).textContent = (BG.cachedStyles.byId.get(id) || {}).name;
|
||||
$('b', box).textContent = $('.style-name', entry).textContent;
|
||||
$('[data-cmd="ok"]', box).focus();
|
||||
$('[data-cmd="ok"]', box).onclick = () => confirm(true);
|
||||
$('[data-cmd="cancel"]', box).onclick = () => confirm(false);
|
||||
|
@ -350,18 +357,14 @@ Object.assign(handleEvent, {
|
|||
className: 'lights-on',
|
||||
onComplete: () => (box.dataset.display = false),
|
||||
});
|
||||
if (ok) {
|
||||
deleteStyleSafe({id}).then(() => {
|
||||
handleDelete(id);
|
||||
});
|
||||
}
|
||||
if (ok) API.deleteStyle({id});
|
||||
}
|
||||
},
|
||||
|
||||
configure(event) {
|
||||
const {styleId, styleIsUsercss} = handleEvent.getClickedStyleElement(event);
|
||||
if (styleIsUsercss) {
|
||||
getStylesSafe({id: styleId}).then(([style]) => {
|
||||
API.getStyles({id: styleId}).then(([style]) => {
|
||||
hotkeys.setState(false);
|
||||
configDialog(deepCopy(style)).then(() => {
|
||||
hotkeys.setState(true);
|
||||
|
@ -456,15 +459,22 @@ Object.assign(handleEvent, {
|
|||
|
||||
function handleUpdate(style) {
|
||||
if ($(ENTRY_ID_PREFIX + style.id)) {
|
||||
createStyleElement({style});
|
||||
createStyleElement({style, check: true});
|
||||
return;
|
||||
}
|
||||
if (!tabURL) return;
|
||||
// Add an entry when a new style for the current url is installed
|
||||
if (tabURL && BG.getApplicableSections({style, matchUrl: tabURL, stopOnFirst: true}).length) {
|
||||
document.body.classList.remove('blocked');
|
||||
$$.remove('.blocked-info, #no-styles');
|
||||
createStyleElement({style});
|
||||
}
|
||||
API.getStyles({
|
||||
matchUrl: tabURL,
|
||||
stopOnFirst: true,
|
||||
omitCode: true,
|
||||
}).then(([style]) => {
|
||||
if (style) {
|
||||
document.body.classList.remove('blocked');
|
||||
$$.remove('.blocked-info, #no-styles');
|
||||
createStyleElement({style, check: true});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -476,58 +486,28 @@ function handleDelete(id) {
|
|||
}
|
||||
|
||||
|
||||
/*
|
||||
According to CSS4 @document specification the entire URL must match.
|
||||
Stylish-for-Chrome implemented it incorrectly since the very beginning.
|
||||
We'll detect styles that abuse the bug by finding the sections that
|
||||
would have been applied by Stylish but not by us as we follow the spec.
|
||||
Additionally we'll check for invalid regexps.
|
||||
*/
|
||||
function detectSloppyRegexps({entry, style}) {
|
||||
// make sure all regexps are compiled
|
||||
const rxCache = BG.cachedStyles.regexps;
|
||||
let hasRegExp = false;
|
||||
for (const section of style.sections) {
|
||||
for (const regexp of section.regexps) {
|
||||
hasRegExp = true;
|
||||
for (let pass = 1; pass <= 2; pass++) {
|
||||
const cacheKey = pass === 1 ? regexp : BG.SLOPPY_REGEXP_PREFIX + regexp;
|
||||
if (!rxCache.has(cacheKey)) {
|
||||
// according to CSS4 @document specification the entire URL must match
|
||||
const anchored = pass === 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$';
|
||||
// create in the bg context to avoid leaking of "dead objects"
|
||||
const rx = BG.tryRegExp(anchored);
|
||||
rxCache.set(cacheKey, rx || false);
|
||||
}
|
||||
function detectSloppyRegexps(styles) {
|
||||
API.detectSloppyRegexps({
|
||||
matchUrl: tabURL,
|
||||
ids: styles.map(({id}) => id),
|
||||
}).then(results => {
|
||||
for (const {id, applied, skipped, hasInvalidRegexps} of results) {
|
||||
const entry = $(ENTRY_ID_PREFIX + id);
|
||||
if (!entry) continue;
|
||||
if (!applied) {
|
||||
entry.classList.add('not-applied');
|
||||
$('.style-name', entry).title = t('styleNotAppliedRegexpProblemTooltip');
|
||||
}
|
||||
if (skipped || hasInvalidRegexps) {
|
||||
entry.classList.toggle('regexp-partial', Boolean(skipped));
|
||||
entry.classList.toggle('regexp-invalid', Boolean(hasInvalidRegexps));
|
||||
const indicator = template.regexpProblemIndicator.cloneNode(true);
|
||||
indicator.appendChild(document.createTextNode(entry.skipped || '!'));
|
||||
indicator.onclick = handleEvent.indicator;
|
||||
$('.main-controls', entry).appendChild(indicator);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasRegExp) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
appliedSections =
|
||||
BG.getApplicableSections({style, matchUrl: tabURL}),
|
||||
wannabeSections =
|
||||
BG.getApplicableSections({style, matchUrl: tabURL, strictRegexp: false}),
|
||||
} = style;
|
||||
|
||||
entry.hasInvalidRegexps = wannabeSections.some(section =>
|
||||
section.regexps.some(rx => !rxCache.has(rx)));
|
||||
entry.sectionsSkipped = wannabeSections.length - appliedSections.length;
|
||||
|
||||
if (!appliedSections.length) {
|
||||
entry.classList.add('not-applied');
|
||||
$('.style-name', entry).title = t('styleNotAppliedRegexpProblemTooltip');
|
||||
}
|
||||
if (entry.sectionsSkipped || entry.hasInvalidRegexps) {
|
||||
entry.classList.toggle('regexp-partial', entry.sectionsSkipped);
|
||||
entry.classList.toggle('regexp-invalid', entry.hasInvalidRegexps);
|
||||
const indicator = template.regexpProblemIndicator.cloneNode(true);
|
||||
indicator.appendChild(document.createTextNode(entry.sectionsSkipped || '!'));
|
||||
indicator.onclick = handleEvent.indicator;
|
||||
$('.main-controls', entry).appendChild(indicator);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -131,7 +131,7 @@ window.addEventListener('showStyles:done', function _() {
|
|||
if (result) {
|
||||
result.installed = false;
|
||||
result.installedStyleId = -1;
|
||||
BG.clearTimeout(result.pingbackTimer);
|
||||
(BG || window).clearTimeout(result.pingbackTimer);
|
||||
renderActionButtons($('#' + RESULT_ID_PREFIX + result.id));
|
||||
}
|
||||
});
|
||||
|
@ -280,7 +280,7 @@ window.addEventListener('showStyles:done', function _() {
|
|||
return;
|
||||
}
|
||||
const md5Url = UPDATE_URL.replace('%', result.id);
|
||||
getStylesSafe({md5Url}).then(([installedStyle]) => {
|
||||
API.getStyles({md5Url}).then(([installedStyle]) => {
|
||||
if (installedStyle) {
|
||||
totalResults = Math.max(0, totalResults - 1);
|
||||
} else {
|
||||
|
@ -522,7 +522,7 @@ window.addEventListener('showStyles:done', function _() {
|
|||
event.stopPropagation();
|
||||
const entry = this.closest('.search-result');
|
||||
saveScrollPosition(entry);
|
||||
deleteStyleSafe({id: entry._result.installedStyleId})
|
||||
API.deleteStyle({id: entry._result.installedStyleId})
|
||||
.then(restoreScrollPosition);
|
||||
}
|
||||
|
||||
|
@ -550,11 +550,11 @@ window.addEventListener('showStyles:done', function _() {
|
|||
style.updateUrl += settings.length ? '?' : '';
|
||||
// show a 'style installed' tooltip in the manager
|
||||
style.reason = 'install';
|
||||
return saveStyleSafe(style);
|
||||
return API.saveStyle(style);
|
||||
})
|
||||
.catch(reason => {
|
||||
const usoId = result.id;
|
||||
console.debug('install:saveStyleSafe(usoID:', usoId, ') => [ERROR]: ', reason);
|
||||
console.debug('install:saveStyle(usoID:', usoId, ') => [ERROR]: ', reason);
|
||||
error('Error while downloading usoID:' + usoId + '\nReason: ' + reason);
|
||||
})
|
||||
.then(() => {
|
||||
|
@ -574,7 +574,8 @@ window.addEventListener('showStyles:done', function _() {
|
|||
}
|
||||
|
||||
function pingback(result) {
|
||||
result.pingbackTimer = BG.setTimeout(BG.download, PINGBACK_DELAY,
|
||||
const wnd = BG || window;
|
||||
result.pingbackTimer = wnd.setTimeout(wnd.download, PINGBACK_DELAY,
|
||||
BASE_URL + '/styles/install/' + result.id + '?source=stylish-ch');
|
||||
}
|
||||
|
||||
|
@ -721,9 +722,10 @@ window.addEventListener('showStyles:done', function _() {
|
|||
|
||||
function readCache(id) {
|
||||
const key = CACHE_PREFIX + id;
|
||||
return BG.chromeLocal.getValue(key).then(item => {
|
||||
return chromeLocal.getValue(key).then(item => {
|
||||
if (!cacheItemExpired(item)) {
|
||||
return tryJSONparse(BG.LZString.decompressFromUTF16(item.payload));
|
||||
return chromeLocal.loadLZStringScript().then(() =>
|
||||
tryJSONparse(LZString.decompressFromUTF16(item.payload)));
|
||||
} else if (item) {
|
||||
chrome.storage.local.remove(key);
|
||||
}
|
||||
|
@ -741,10 +743,11 @@ window.addEventListener('showStyles:done', function _() {
|
|||
return data;
|
||||
} else {
|
||||
debounce(cleanupCache, CACHE_CLEANUP_THROTTLE);
|
||||
return BG.chromeLocal.setValue(CACHE_PREFIX + data.id, {
|
||||
payload: BG.LZString.compressToUTF16(JSON.stringify(data)),
|
||||
date: Date.now(),
|
||||
}).then(() => data);
|
||||
return chromeLocal.loadLZStringScript().then(() =>
|
||||
chromeLocal.setValue(CACHE_PREFIX + data.id, {
|
||||
payload: LZString.compressToUTF16(JSON.stringify(data)),
|
||||
date: Date.now(),
|
||||
})).then(() => data);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user