FF: support private/container tabs

This commit is contained in:
tophf 2018-01-01 20:02:49 +03:00
parent 62e333a0ba
commit 3418ac9cb9
33 changed files with 1054 additions and 859 deletions

View File

@ -13,9 +13,11 @@ globals:
KEEP_CHANNEL_OPEN: false KEEP_CHANNEL_OPEN: false
CHROME: false CHROME: false
FIREFOX: false FIREFOX: false
VIVALDI: false
OPERA: false OPERA: false
URLS: false URLS: false
BG: false BG: false
API: false
notifyAllTabs: false notifyAllTabs: false
sendMessage: false sendMessage: false
queryTabs: false queryTabs: false
@ -33,10 +35,6 @@ globals:
tryJSONparse: false tryJSONparse: false
debounce: false debounce: false
deepCopy: false deepCopy: false
onBackgroundReady: false
deleteStyleSafe: false
getStylesSafe: false
saveStyleSafe: false
sessionStorageHash: false sessionStorageHash: false
download: false download: false
invokeOrPostpone: false invokeOrPostpone: false
@ -63,6 +61,10 @@ globals:
# prefs.js # prefs.js
prefs: false prefs: false
setupLivePrefs: false setupLivePrefs: false
# storage-util.js
chromeLocal: false
chromeSync: false
LZString: false
rules: rules:
accessor-pairs: [2] accessor-pairs: [2]

View File

@ -1,9 +1,38 @@
/* global dbExec, getStyles, saveStyle */ /*
/* global handleCssTransitionBug */ global dbExec getStyles saveStyle deleteStyle
/* global usercssHelper openEditor */ global handleCssTransitionBug detectSloppyRegexps
/* global styleViaAPI */ global openEditor
global styleViaAPI
global loadScript
global updater
*/
'use strict'; '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 // eslint-disable-next-line no-var
var browserCommands, contextMenus; var browserCommands, contextMenus;
@ -55,9 +84,17 @@ if (!chrome.browserAction ||
window.updateIcon = () => {}; 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 // 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.addEventListener('storageReady', function _() {
window.removeEventListener('storageReady', _); window.removeEventListener('storageReady', _);
updateIcon({id: undefined}, {}); updateIcon({
tab: {id: undefined},
styles: {},
});
const NTP = 'chrome://newtab/'; const NTP = 'chrome://newtab/';
const ALL_URLS = '<all_urls>'; const ALL_URLS = '<all_urls>';
@ -223,7 +263,8 @@ function webNavigationListener(method, {url, tabId, frameId}) {
} }
// main page frame id is 0 // main page frame id is 0
if (frameId === 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), getTab(tabId),
]).then(([pong, tab]) => { ]).then(([pong, tab]) => {
if (pong !== true && tab.url !== 'about:blank') { 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) { if (tab.id < 0) {
return; return;
} }
@ -277,38 +318,44 @@ function updateIcon(tab, styles) {
.then(url => getStyles({matchUrl: url, enabled: true, asHash: true})) .then(url => getStyles({matchUrl: url, enabled: true, asHash: true}))
.then(stylesReceived); .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) { function stylesReceived(styles) {
let numStyles = styles.length; const numStyles = countStyles(styles);
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 disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll'); const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll');
const postfix = disableAll ? 'x' : numStyles === 0 ? 'w' : ''; const postfix = disableAll ? 'x' : numStyles === 0 ? 'w' : '';
const color = prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal'); const color = prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal');
const text = prefs.get('show-badge') && numStyles ? String(numStyles) : ''; const text = prefs.get('show-badge') && numStyles ? String(numStyles) : '';
const iconset = ['', 'light/'][prefs.get('iconset')] || ''; const iconset = ['', 'light/'][prefs.get('iconset')] || '';
const path = 'images/icon/' + iconset; const path = 'images/icon/' + iconset;
chrome.browserAction.setIcon({ const tabIcon = tabIcons.get(tab.id) || {};
tabId: tab.id, if (tabIcon.iconType !== iconset + postfix) {
path: { tabIcons.set(tab.id, tabIcon);
tabIcon.iconType = iconset + postfix;
const paths = {};
if (FIREFOX || CHROME >= 2883 && !VIVALDI) {
// Material Design 2016 new size is 16px // Material Design 2016 new size is 16px
16: `${path}16${postfix}.png`, paths['16'] = `${path}16${postfix}.png`;
32: `${path}32${postfix}.png`, paths['32'] = `${path}32${postfix}.png`;
} else {
// Chromium forks or non-chromium browsers may still use the traditional 19px // Chromium forks or non-chromium browsers may still use the traditional 19px
19: `${path}19${postfix}.png`, paths['19'] = `${path}19${postfix}.png`;
38: `${path}38${postfix}.png`, paths['38'] = `${path}38${postfix}.png`;
// TODO: add Edge preferred sizes: 20, 25, 30, 40
},
}, () => {
if (chrome.runtime.lastError || tab.id === undefined) {
return;
} }
// 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}); chrome.browserAction.setBadgeBackgroundColor({color});
}
if (tabIcon.text !== text) {
tabIcon.text = text;
setTimeout(() => { setTimeout(() => {
getTab(tab.id).then(realTab => { getTab(tab.id).then(realTab => {
// skip pre-rendered tabs // 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': function onRuntimeMessage(msg, sender, sendResponse) {
getStyles(request).then(sendResponse); const fn = API_METHODS[msg.method];
return KEEP_CHANNEL_OPEN; if (!fn) return;
case 'saveStyle': // wrap 'Error' object instance as {__ERROR__: message},
saveStyle(request).then(sendResponse); // which will be unwrapped by sendMessage,
return KEEP_CHANNEL_OPEN; // and prevent exceptions on sending to a closed tab
const respond = data =>
case 'saveUsercss': tryCatch(sendResponse,
usercssHelper.save(request, true).then(sendResponse); data instanceof Error ? {__ERROR__: data.message} : data);
return KEEP_CHANNEL_OPEN;
const result = fn(msg, sender, respond);
case 'buildUsercss': if (result instanceof Promise) {
usercssHelper.build(request, true).then(sendResponse); result
return KEEP_CHANNEL_OPEN; .catch(e => ({__ERROR__: e instanceof Error ? e.message : e}))
.then(respond);
case 'healthCheck': return KEEP_CHANNEL_OPEN;
dbExec() } else if (result === KEEP_CHANNEL_OPEN) {
.then(() => sendResponse(true)) return KEEP_CHANNEL_OPEN;
.catch(() => sendResponse(false)); } else if (result !== undefined) {
return KEEP_CHANNEL_OPEN; respond(result);
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;
} }
} }

100
background/search-db.js Normal file
View 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();
}
})();

View File

@ -1,4 +1,4 @@
/* global LZString */ /* global getStyleWithNoCode */
'use strict'; 'use strict';
const RX_NAMESPACE = new RegExp([/[\s\r\n]*/, 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 // eslint-disable-next-line no-var
var dbExec = dbExecIndexedDB; var dbExec = dbExecIndexedDB;
dbExec.initialized = false; dbExec.initialized = false;
@ -247,6 +199,7 @@ function filterStyles({
matchUrl = null, matchUrl = null,
md5Url = null, md5Url = null,
asHash = null, asHash = null,
omitCode,
strictRegexp = true, // used by the popup to detect bad regexps strictRegexp = true, // used by the popup to detect bad regexps
} = {}) { } = {}) {
enabled = enabled === null || typeof enabled === 'boolean' ? enabled : enabled = enabled === null || typeof enabled === 'boolean' ? enabled :
@ -274,24 +227,34 @@ function filterStyles({
const cacheKey = [enabled, id, matchUrl, md5Url, asHash, strictRegexp].join('\t'); const cacheKey = [enabled, id, matchUrl, md5Url, asHash, strictRegexp].join('\t');
const cached = cachedStyles.filters.get(cacheKey); const cached = cachedStyles.filters.get(cacheKey);
let styles;
if (cached) { if (cached) {
cached.hits++; cached.hits++;
cached.lastHit = Date.now(); cached.lastHit = Date.now();
return asHash styles = asHash
? Object.assign(blankHash, cached.styles) ? Object.assign(blankHash, cached.styles)
: cached.styles; : cached.styles.slice();
} else {
styles = filterStylesInternal({
enabled,
id,
matchUrl,
md5Url,
asHash,
strictRegexp,
blankHash,
cacheKey,
});
} }
if (!omitCode) return styles;
return filterStylesInternal({ if (!asHash) return styles.map(getStyleWithNoCode);
enabled, for (const id in styles) {
id, const style = styles[id];
matchUrl, if (style && style.sections) {
md5Url, styles[id] = getStyleWithNoCode(style);
asHash, }
strictRegexp, }
blankHash, return styles;
cacheKey,
});
} }
@ -427,6 +390,7 @@ function saveStyle(style) {
md5Url: null, md5Url: null,
url: null, url: null,
originalMd5: null, originalMd5: null,
installDate: Date.now(),
}, style); }, style);
return write(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)); 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;
}

View File

@ -1,7 +1,7 @@
/* global getStyles */ /* global getStyles API_METHODS */
'use strict'; 'use strict';
const styleViaAPI = !CHROME && (() => { API_METHODS.styleViaAPI = !CHROME && (() => {
const ACTIONS = { const ACTIONS = {
styleApply, styleApply,
styleDeleted, styleDeleted,

View File

@ -1,47 +1,74 @@
/* global getStyles, saveStyle, styleSectionsEqual, chromeLocal */ /*
/* global calcStyleDigest */ global getStyles saveStyle styleSectionsEqual
/* global usercss semverCompare usercssHelper */ global calcStyleDigest cachedStyles getStyleWithNoCode
global usercss semverCompare
global API_METHODS
*/
'use strict'; 'use strict';
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
var updater = { var updater = (() => {
COUNT: 'count', const STATES = {
UPDATED: 'updated', UPDATED: 'updated',
SKIPPED: 'skipped', SKIPPED: 'skipped',
DONE: 'done',
// details for SKIPPED status // details for SKIPPED status
EDITED: 'locally edited', EDITED: 'locally edited',
MAYBE_EDITED: 'may be locally edited', MAYBE_EDITED: 'may be locally edited',
SAME_MD5: 'up-to-date: MD5 is unchanged', SAME_MD5: 'up-to-date: MD5 is unchanged',
SAME_CODE: 'up-to-date: code sections are unchanged', SAME_CODE: 'up-to-date: code sections are unchanged',
SAME_VERSION: 'up-to-date: version is unchanged', SAME_VERSION: 'up-to-date: version is unchanged',
ERROR_MD5: 'error: MD5 is invalid', ERROR_MD5: 'error: MD5 is invalid',
ERROR_JSON: 'error: JSON is invalid', ERROR_JSON: 'error: JSON is invalid',
ERROR_VERSION: 'error: version is older than installed style', 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} = {}) { API_METHODS.updateCheckAll = checkAllStyles;
updater.resetInterval(); API_METHODS.updateCheck = checkStyle;
updater.checkAllStyles.running = true; 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 => { return getStyles({}).then(styles => {
styles = styles.filter(style => style.updateUrl); styles = styles.filter(style => style.updateUrl);
observer(updater.COUNT, styles.length); if (port) port.postMessage({count: styles.length});
updater.log(''); log('');
updater.log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`); log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`);
return Promise.all( return Promise.all(
styles.map(style => styles.map(style =>
updater.checkStyle({style, observer, save, ignoreDigest}))); checkStyle({style, port, save, ignoreDigest})));
}).then(() => { }).then(() => {
observer(updater.DONE); if (port) port.postMessage({done: true});
updater.log(''); if (port) port.disconnect();
updater.checkAllStyles.running = false; 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: Original style digests are calculated in these cases:
* style is installed or updated from server * style is installed or updated from server
@ -65,29 +92,33 @@ var updater = {
.catch(reportFailure); .catch(reportFailure);
function reportSuccess(saved) { function reportSuccess(saved) {
observer(updater.UPDATED, saved); log(STATES.UPDATED + ` #${style.id} ${style.name}`);
updater.log(updater.UPDATED + ` #${style.id} ${style.name}`); const info = {updated: true, style: saved};
if (port) port.postMessage(info);
return info;
} }
function reportFailure(err) { function reportFailure(error) {
observer(updater.SKIPPED, style, err); error = error === 0 ? 'server unreachable' : error;
err = err === 0 ? 'server unreachable' : err; log(STATES.SKIPPED + ` (${error}) #${style.id} ${style.name}`);
updater.log(updater.SKIPPED + ` (${err}) #${style.id} ${style.name}`); const info = {error, STATES, style: getStyleWithNoCode(style)};
if (port) port.postMessage(info);
return info;
} }
function checkIfEdited(digest) { function checkIfEdited(digest) {
if (style.originalDigest && style.originalDigest !== digest) { if (style.originalDigest && style.originalDigest !== digest) {
return Promise.reject(updater.EDITED); return Promise.reject(STATES.EDITED);
} }
} }
function maybeUpdateUSO() { function maybeUpdateUSO() {
return download(style.md5Url).then(md5 => { return download(style.md5Url).then(md5 => {
if (!md5 || md5.length !== 32) { if (!md5 || md5.length !== 32) {
return Promise.reject(updater.ERROR_MD5); return Promise.reject(STATES.ERROR_MD5);
} }
if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) { if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
return Promise.reject(updater.SAME_MD5); return Promise.reject(STATES.SAME_MD5);
} }
return download(style.updateUrl) return download(style.updateUrl)
.then(text => tryJSONparse(text)); .then(text => tryJSONparse(text));
@ -104,14 +135,14 @@ var updater = {
case 0: case 0:
// re-install is invalid in a soft upgrade // re-install is invalid in a soft upgrade
if (!ignoreDigest) { if (!ignoreDigest) {
return Promise.reject(updater.SAME_VERSION); return Promise.reject(STATES.SAME_VERSION);
} else if (text === style.sourceCode) { } else if (text === style.sourceCode) {
return Promise.reject(updater.SAME_CODE); return Promise.reject(STATES.SAME_CODE);
} }
break; break;
case 1: case 1:
// downgrade is always invalid // downgrade is always invalid
return Promise.reject(updater.ERROR_VERSION); return Promise.reject(STATES.ERROR_VERSION);
} }
return usercss.buildCode(json); return usercss.buildCode(json);
}); });
@ -120,8 +151,9 @@ var updater = {
function maybeSave(json = {}) { function maybeSave(json = {}) {
// usercss is already validated while building // usercss is already validated while building
if (!json.usercssData && !styleJSONseemsValid(json)) { if (!json.usercssData && !styleJSONseemsValid(json)) {
return Promise.reject(updater.ERROR_JSON); return Promise.reject(STATES.ERROR_JSON);
} }
json.id = style.id; json.id = style.id;
json.updateDate = Date.now(); json.updateDate = Date.now();
json.reason = 'update'; json.reason = 'update';
@ -139,15 +171,16 @@ var updater = {
if (styleSectionsEqual(json, style)) { if (styleSectionsEqual(json, style)) {
// update digest even if save === false as there might be just a space added etc. // update digest even if save === false as there might be just a space added etc.
saveStyle(Object.assign(json, {reason: 'update-digest'})); saveStyle(Object.assign(json, {reason: 'update-digest'}));
return Promise.reject(updater.SAME_CODE); return Promise.reject(STATES.SAME_CODE);
} else if (!style.originalDigest && !ignoreDigest) {
return Promise.reject(updater.MAYBE_EDITED);
} }
return !save ? json : if (!style.originalDigest && !ignoreDigest) {
json.usercssData return Promise.reject(STATES.MAYBE_EDITED);
? usercssHelper.save(json) }
: saveStyle(json);
return save ?
API_METHODS[json.usercssData ? 'saveUsercss' : 'saveStyle'](json) :
json;
} }
function styleJSONseemsValid(json) { function styleJSONseemsValid(json) {
@ -157,49 +190,47 @@ var updater = {
&& typeof json.sections.every === 'function' && typeof json.sections.every === 'function'
&& typeof json.sections[0].code === 'string'; && typeof json.sections[0].code === 'string';
} }
}, }
schedule() { function schedule() {
const interval = prefs.get('updateInterval') * 60 * 60 * 1000; const interval = prefs.get('updateInterval') * 60 * 60 * 1000;
if (interval) { if (interval) {
const elapsed = Math.max(0, Date.now() - updater.lastUpdateTime); const elapsed = Math.max(0, Date.now() - lastUpdateTime);
debounce(updater.checkAllStyles, Math.max(10e3, interval - elapsed)); debounce(checkAllStyles, Math.max(10e3, interval - elapsed));
} else { } else {
debounce.unregister(updater.checkAllStyles); debounce.unregister(checkAllStyles);
} }
}, }
resetInterval() { function resetInterval() {
localStorage.lastUpdateTime = updater.lastUpdateTime = Date.now(); localStorage.lastUpdateTime = lastUpdateTime = Date.now();
updater.schedule(); schedule();
}, }
log: (() => { function log(text) {
let queue = []; logQueue.push({text, time: new Date().toLocaleString()});
let lastWriteTime = 0; debounce(flushQueue, text && checkingAll ? 1000 : 0);
return text => { }
queue.push({text, time: new Date().toLocaleString()});
debounce(flushQueue, text && updater.checkAllStyles.running ? 1000 : 0); function flushQueue(stored) {
}; if (!stored) {
function flushQueue() { chrome.storage.local.get('updateLog', flushQueue);
chromeLocal.getValue('updateLog').then((lines = []) => { return;
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 = [];
});
} }
})(), 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(); chrome.storage.local.set({updateLog: lines});
prefs.subscribe(['updateInterval'], updater.schedule); logLastWriteTime = Date.now();
logQueue = [];
}
})();

View File

@ -1,8 +1,11 @@
/* global usercss saveStyle getStyles chromeLocal */ /* global API_METHODS usercss saveStyle getStyles chromeLocal cachedStyles */
'use strict'; '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_PREFIX = 'tempUsercssCode';
const TEMP_CODE_CLEANUP_DELAY = 60e3; const TEMP_CODE_CLEANUP_DELAY = 60e3;
@ -48,31 +51,25 @@ var usercssHelper = (() => {
return usercss.buildCode(style); 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 // Parse the source and find the duplication
function build({sourceCode, checkDup = false}, noReject) { function build({sourceCode, checkDup = false}) {
const pending = buildMeta({sourceCode}) return buildMeta({sourceCode})
.then(style => Promise.all([ .then(style => Promise.all([
buildCode(style), buildCode(style),
checkDup && findDup(style) checkDup && findDup(style)
])) ]))
.then(([style, dup]) => ({style, dup})); .then(([style, dup]) => ({style, dup}));
return noReject ? wrapReject(pending) : pending;
} }
function save(style, noReject) { function save(style) {
const pending = buildMeta(style) if (!style.sourceCode) {
style.sourceCode = cachedStyles.byId.get(style.id).sourceCode;
}
return buildMeta(style)
.then(assignVars) .then(assignVars)
.then(buildCode) .then(buildCode)
.then(saveStyle); .then(saveStyle);
return noReject ? wrapReject(pending) : pending;
function assignVars(style) { function assignVars(style) {
if (style.reason === 'config' && style.id) { if (style.reason === 'config' && style.id) {
return style; 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) { if (direct && !downloaded) {
prefetchCodeForInstallation(tab.id, url); prefetchCodeForInstallation(tab.id, url);
} }
return wrapReject(openURL({ return openURL({
url: '/install-usercss.html' + url: '/install-usercss.html' +
'?updateUrl=' + encodeURIComponent(url) + '?updateUrl=' + encodeURIComponent(url) +
'&tabId=' + tab.id + '&tabId=' + tab.id +
@ -117,7 +115,7 @@ var usercssHelper = (() => {
index: tab.index + 1, index: tab.index + 1,
openerTabId: tab.id, openerTabId: tab.id,
currentWindow: null, currentWindow: null,
})); });
} }
function prefetchCodeForInstallation(tabId, url) { function prefetchCodeForInstallation(tabId, url) {
@ -131,6 +129,4 @@ var usercssHelper = (() => {
setTimeout(() => chromeLocal.remove(key), TEMP_CODE_CLEANUP_DELAY); setTimeout(() => chromeLocal.remove(key), TEMP_CODE_CLEANUP_DELAY);
}); });
} }
return {build, save, findDup, openInstallPage};
})(); })();

View File

@ -48,8 +48,8 @@
asHash: true, asHash: true,
}, options); }, options);
// On own pages we request the styles directly to minimize delay and flicker // On own pages we request the styles directly to minimize delay and flicker
if (typeof getStylesSafe === 'function') { if (typeof API === 'function') {
getStylesSafe(request).then(callback); API.getStyles(request).then(callback);
} else { } else {
chrome.runtime.sendMessage(request, callback); chrome.runtime.sendMessage(request, callback);
} }

View File

@ -97,7 +97,7 @@ function initUsercssInstall() {
}); });
}); });
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
method: 'openUsercssInstallPage', method: 'installUsercss',
url: location.href, url: location.href,
}, r => r && r.__ERROR__ && alert(r.__ERROR__)); }, r => r && r.__ERROR__ && alert(r.__ERROR__));
} }

View File

@ -198,7 +198,14 @@
if (url.startsWith('#')) { if (url.startsWith('#')) {
resolve(document.getElementById(url.slice(1)).textContent); resolve(document.getElementById(url.slice(1)).textContent);
} else { } 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);
}
});
} }
}); });
} }

View File

@ -23,6 +23,7 @@
<script src="js/localization.js"></script> <script src="js/localization.js"></script>
<script src="js/script-loader.js"></script> <script src="js/script-loader.js"></script>
<script src="js/moz-parser.js"></script> <script src="js/moz-parser.js"></script>
<script src="js/storage-util.js"></script>
<script src="content/apply.js"></script> <script src="content/apply.js"></script>
<script src="edit/lint.js"></script> <script src="edit/lint.js"></script>
<script src="edit/util.js"></script> <script src="edit/util.js"></script>

View File

@ -44,7 +44,7 @@ Promise.all([
if (usercss) { if (usercss) {
editor = createSourceEditor(style); editor = createSourceEditor(style);
} else { } else {
initWithSectionStyle({style}); initWithSectionStyle(style);
document.addEventListener('wheel', scrollEntirePageOnCtrlShift); document.addEventListener('wheel', scrollEntirePageOnCtrlShift);
} }
}); });
@ -155,14 +155,17 @@ function onRuntimeMessage(request) {
request.reason !== 'editSave' && request.reason !== 'editSave' &&
request.reason !== 'config') { request.reason !== 'config') {
// code-less style from notifyAllTabs // code-less style from notifyAllTabs
if ((request.style.sections[0] || {}).code === null) { const {sections, id} = request.style;
request.style = BG.cachedStyles.byId.get(request.style.id); ((sections[0] || {}).code === null
} ? API.getStyles({id})
if (isUsercss(request.style)) { : Promise.resolve([request.style])
editor.replaceStyle(request.style, request.codeIsUpdated); ).then(([style]) => {
} else { if (isUsercss(style)) {
initWithSectionStyle(request); editor.replaceStyle(style, request.codeIsUpdated);
} } else {
initWithSectionStyle(style, request.codeIsUpdated);
}
});
} }
break; break;
case 'styleDeleted': case 'styleDeleted':
@ -228,7 +231,7 @@ function initStyleData() {
) )
], ],
}); });
return getStylesSafe({id: id || -1}) return API.getStyles({id: id || -1})
.then(([style = createEmptyStyle()]) => { .then(([style = createEmptyStyle()]) => {
styleId = style.id; styleId = style.id;
if (styleId) sessionStorage.justEditedStyleId = styleId; if (styleId) sessionStorage.justEditedStyleId = styleId;
@ -344,7 +347,7 @@ function save() {
return; return;
} }
saveStyleSafe({ API.saveStyle({
id: styleId, id: styleId,
name: $('#name').value.trim(), name: $('#name').value.trim(),
enabled: $('#enabled').checked, enabled: $('#enabled').checked,

View File

@ -121,12 +121,12 @@ var linterConfig = {
config = this.fallbackToDefaults(config); config = this.fallbackToDefaults(config);
const linter = linterConfig.getName(); const linter = linterConfig.getName();
this[linter] = config; this[linter] = config;
BG.chromeSync.setLZValue(this.storageName[linter], config); chromeSync.setLZValue(this.storageName[linter], config);
return config; return config;
}, },
loadAll() { loadAll() {
return BG.chromeSync.getLZValues([ return chromeSync.getLZValues([
'editorCSSLintConfig', 'editorCSSLintConfig',
'editorStylelintConfig', 'editorStylelintConfig',
]).then(data => { ]).then(data => {
@ -167,10 +167,8 @@ var linterConfig = {
}, },
init() { init() {
if (!linterConfig.init.pending) { if (!this.init.pending) this.init.pending = this.loadAll();
linterConfig.init.pending = linterConfig.loadAll(); return this.init.pending;
}
return linterConfig.init.pending;
} }
}; };

View File

@ -8,7 +8,7 @@ global showAppliesToHelp beautify regExpTester setGlobalProgress setCleanSection
*/ */
'use strict'; 'use strict';
function initWithSectionStyle({style, codeIsUpdated}) { function initWithSectionStyle(style, codeIsUpdated) {
$('#name').value = style.name || ''; $('#name').value = style.name || '';
$('#enabled').checked = style.enabled !== false; $('#enabled').checked = style.enabled !== false;
$('#url').href = style.url || ''; $('#url').href = style.url || '';

View File

@ -106,7 +106,7 @@ function createSourceEditor(style) {
`.replace(/^\s+/gm, ''); `.replace(/^\s+/gm, '');
dirty.clear('sourceGeneration'); dirty.clear('sourceGeneration');
style.sourceCode = ''; style.sourceCode = '';
BG.chromeSync.getLZValue('usercssTemplate').then(code => { chromeSync.getLZValue('usercssTemplate').then(code => {
style.sourceCode = code || DEFAULT_CODE; style.sourceCode = code || DEFAULT_CODE;
cm.startOperation(); cm.startOperation();
cm.setValue(style.sourceCode); cm.setValue(style.sourceCode);
@ -216,8 +216,8 @@ function createSourceEditor(style) {
return; return;
} }
const code = cm.getValue(); const code = cm.getValue();
return onBackgroundReady() return (
.then(() => BG.usercssHelper.save({ API.saveUsercss({
reason: 'editSave', reason: 'editSave',
id: style.id, id: style.id,
enabled: style.enabled, enabled: style.enabled,
@ -228,8 +228,8 @@ function createSourceEditor(style) {
.catch(err => { .catch(err => {
if (err.message === t('styleMissingMeta', 'name')) { if (err.message === t('styleMissingMeta', 'name')) {
messageBox.confirm(t('usercssReplaceTemplateConfirmation')).then(ok => ok && messageBox.confirm(t('usercssReplaceTemplateConfirmation')).then(ok => ok &&
BG.chromeSync.setLZValue('usercssTemplate', code) chromeSync.setLZValue('usercssTemplate', code)
.then(() => BG.chromeSync.getLZValue('usercssTemplate')) .then(() => chromeSync.getLZValue('usercssTemplate'))
.then(saved => saved !== code && messageBox.alert(t('syncStorageErrorSaving')))); .then(saved => saved !== code && messageBox.alert(t('syncStorageErrorSaving'))));
return; return;
} }

View File

@ -200,7 +200,7 @@
if (!liveReload && !prefs.get('openEditInWindow')) { if (!liveReload && !prefs.get('openEditInWindow')) {
chrome.tabs.update({url: '/edit.html?id=' + style.id}); chrome.tabs.update({url: '/edit.html?id=' + style.id});
} else { } else {
BG.openEditor(style.id); API.openEditor({id: style.id});
if (!liveReload) { if (!liveReload) {
closeCurrentTab(); closeCurrentTab();
} }
@ -212,8 +212,8 @@
function initSourceCode(sourceCode) { function initSourceCode(sourceCode) {
cm.setValue(sourceCode); cm.setValue(sourceCode);
cm.refresh(); cm.refresh();
BG.usercssHelper.build(BG.deepCopy({sourceCode, checkDup: true})) API.buildUsercss({sourceCode, checkDup: true})
.then(r => init(deepCopy(r))) .then(r => init(r instanceof Object ? r : deepCopy(r)))
.catch(err => { .catch(err => {
$('.header').classList.add('meta-init-error'); $('.header').classList.add('meta-init-error');
showError(err); showError(err);
@ -222,7 +222,7 @@
function buildWarning(err) { function buildWarning(err) {
const contents = Array.isArray(err) ? const contents = Array.isArray(err) ?
$create('pre', err.join('\n')) : [$create('pre', err.join('\n'))] :
[err && err.message || err || 'Unknown error']; [err && err.message || err || 'Unknown error'];
if (Number.isInteger(err.index)) { if (Number.isInteger(err.index)) {
const pos = cm.posFromIndex(err.index); const pos = cm.posFromIndex(err.index);
@ -283,8 +283,8 @@
data.version, data.version,
])) ]))
).then(ok => ok && ).then(ok => ok &&
BG.usercssHelper.save(BG.deepCopy(Object.assign(style, dup && {reason: 'update'}))) API.saveUsercss(Object.assign(style, dup && {reason: 'update'}))
.then(r => install(deepCopy(r))) .then(r => install(r instanceof Object ? r : deepCopy(r)))
.catch(err => messageBox.alert(t('styleInstallFailed', err))) .catch(err => messageBox.alert(t('styleInstallFailed', err)))
); );
}; };

View File

@ -64,7 +64,8 @@ onDOMready().then(() => {
if (!chrome.app && chrome.windows) { if (!chrome.app && chrome.windows) {
// die if unable to access BG directly // die if unable to access BG directly
chrome.windows.getCurrent(wnd => { chrome.windows.getCurrent(wnd => {
if (!BG && wnd.incognito) { if (!BG && wnd.incognito &&
!location.pathname.includes('popup.html')) {
// private windows can't get bg page // private windows can't get bg page
location.href = '/msgbox/dysfunctional.html'; location.href = '/msgbox/dysfunctional.html';
throw 0; throw 0;

View File

@ -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'; 'use strict';
// keep message channel open for sendResponse in chrome.runtime.onMessage listener // keep message channel open for sendResponse in chrome.runtime.onMessage listener
const KEEP_CHANNEL_OPEN = true; const KEEP_CHANNEL_OPEN = true;
const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]); 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; 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) { if (!CHROME && !chrome.browserAction.openPopup) {
// in FF pre-57 legacy addons can override useragent so we assume the worst // 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'); document.documentElement.classList.add('firefox');
} else if (OPERA) { } else if (OPERA) {
document.documentElement.classList.add('opera'); document.documentElement.classList.add('opera');
} else if (chrome.app && navigator.userAgent.includes('Vivaldi')) { } else {
document.documentElement.classList.add('vivaldi'); if (VIVALDI) document.documentElement.classList.add('vivaldi');
} }
// TODO: remove once our manifest's minimum_chrome_version is 50+ // TODO: remove once our manifest's minimum_chrome_version is 50+
// Chrome 49 doesn't report own extension pages in webNavigation apparently // Chrome 49 doesn't report own extension pages in webNavigation apparently
if (CHROME && CHROME < 2661) { 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: {}}); 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) { function notifyAllTabs(msg) {
const originalMessage = msg; const originalMessage = msg;
@ -99,6 +158,12 @@ function notifyAllTabs(msg) {
const affectsIcon = affectsAll || msg.affects.icon; const affectsIcon = affectsAll || msg.affects.icon;
const affectsPopup = affectsAll || msg.affects.popup; const affectsPopup = affectsAll || msg.affects.popup;
const affectsSelf = affectsPopup || msg.prefs; 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) { if (affectsTabs || affectsIcon) {
const notifyTab = tab => { const notifyTab = tab => {
// own pages will be notified via runtime.sendMessage later // own pages will be notified via runtime.sendMessage later
@ -109,8 +174,9 @@ function notifyAllTabs(msg) {
msg.tabId = tab.id; msg.tabId = tab.id;
sendMessage(msg, ignoreChromeError); sendMessage(msg, ignoreChromeError);
} }
if (affectsIcon && BG) { if (affectsIcon) {
BG.updateIcon(tab); // eslint-disable-next-line no-use-before-define
debounce(API.updateIcon, 0, {tab});
} }
}; };
// list all tabs including chrome-extension:// which can be ours // list all tabs including chrome-extension:// which can be ours
@ -132,11 +198,6 @@ function notifyAllTabs(msg) {
if (typeof applyOnMessage !== 'undefined') { if (typeof applyOnMessage !== 'undefined') {
applyOnMessage(originalMessage); 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) { function getStyleWithNoCode(style) {
const stripped = Object.assign({}, style, {sections: []}); const stripped = deepCopy(style);
for (const section of style.sections) { for (const section of stripped.sections) section.code = null;
stripped.sections.push(Object.assign({}, section, {code: null})); stripped.sourceCode = null;
}
return stripped; return stripped;
} }
@ -343,31 +403,23 @@ const debounce = Object.assign((fn, delay, ...args) => {
function deepCopy(obj) { function deepCopy(obj) {
return obj !== null && obj !== undefined && typeof obj === 'object' if (!obj || typeof obj !== 'object') return obj;
? deepMerge(typeof obj.slice === 'function' ? [] : {}, obj) // N.B. a copy should be an explicitly literal
: obj; if (Array.isArray(obj)) {
} const copy = [];
for (const v of obj) {
copy.push(!v || typeof v !== 'object' ? v : deepCopy(v));
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);
}
} }
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, { function download(url, {
method = url.includes('?') ? 'POST' : 'GET', method = url.includes('?') ? 'POST' : 'GET',
body = url.includes('?') ? url.slice(url.indexOf('?')) : null, 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'; let url = '/edit.html';
if (id) { if (id) {
url += `?id=${id}`; url += `?id=${id}`;

View File

@ -148,17 +148,13 @@ var prefs = new function Prefs() {
values[key] = value; values[key] = value;
defineReadonlyProperty(this.readOnlyValues, key, value); defineReadonlyProperty(this.readOnlyValues, key, value);
const hasChanged = !equal(value, oldValue); const hasChanged = !equal(value, oldValue);
if (!fromBroadcast) { if (!fromBroadcast || FIREFOX_NO_DOM_STORAGE) {
if (BG && BG !== window) { localStorage[key] = typeof defaults[key] === 'object'
BG.prefs.set(key, BG.deepCopy(value), {broadcast, sync}); ? JSON.stringify(value)
} else { : value;
localStorage[key] = typeof defaults[key] === 'object' }
? JSON.stringify(value) if (!fromBroadcast && broadcast && hasChanged) {
: value; this.broadcast(key, value, {sync});
if (broadcast && hasChanged) {
this.broadcast(key, value, {sync});
}
}
} }
if (hasChanged) { if (hasChanged) {
const specific = onChange.specific.get(key); 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])), reset: key => this.set(key, deepCopy(defaults[key])),
broadcast(key, value, {sync = true} = {}) { 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 const importFromBG = () =>
// during the startup phase API.getPrefs().then(prefs => {
for (const key in defaults) { const props = {};
const defaultValue = defaults[key]; for (const id in prefs) {
let value = localStorage[key]; const value = prefs[id];
if (typeof value === 'string') { values[id] = value;
switch (typeof defaultValue) { props[id] = {value: deepCopy(value)};
case 'boolean': }
value = value.toLowerCase() === 'true'; Object.defineProperties(this.readOnlyValues, props);
break; });
case 'number': // Unlike chrome.storage or messaging, HTML5 localStorage is synchronous and always ready,
value |= 0; // so we'll mirror the prefs to avoid using the wrong defaults during the startup phase
break; const importFromLocalStorage = () => {
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 = {}) => {
for (const key in defaults) { for (const key in defaults) {
if (key in synced) { const defaultValue = defaults[key];
this.set(key, synced[key], {sync: false}); let value = localStorage[key];
} if (typeof value === 'string') {
} switch (typeof defaultValue) {
}; case 'boolean':
value = value.toLowerCase() === 'true';
getSync().get('settings', ({settings} = {}) => importFromSync(settings)); break;
case 'number':
chrome.storage.onChanged.addListener((changes, area) => { value |= 0;
if (area === 'sync' && 'settings' in changes) { break;
const synced = changes.settings.newValue; case 'object':
if (synced) { value = tryJSONparse(value) || defaultValue;
importFromSync(synced); break;
}
} else if (FIREFOX_NO_DOM_STORAGE && BG) {
value = BG.localStorage[key];
value = value === undefined ? defaultValue : value;
localStorage[key] = value;
} else { } else {
// user manually deleted our settings, we'll recreate them value = defaultValue;
getSync().set({'settings': values}); }
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) { function defineReadonlyProperty(obj, key, value) {
const copy = deepCopy(value); const copy = deepCopy(value);
if (typeof copy === 'object') { if (typeof copy === 'object') {

99
js/storage-util.js Normal file
View 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))
);
}
}

View File

@ -150,13 +150,18 @@
<script src="js/prefs.js"></script> <script src="js/prefs.js"></script>
<script src="content/apply.js"></script> <script src="content/apply.js"></script>
<script src="js/localization.js"></script> <script src="js/localization.js"></script>
<script src="js/storage-util.js"></script>
<script src="manage/filters.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/sort.js"></script>
<script src="manage/manage.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> </head>
<body id="stylus-manage" i18n-dragndrop-hint="dragDropMessage"> <body id="stylus-manage" i18n-dragndrop-hint="dragDropMessage">
@ -358,10 +363,6 @@
<div id="installed"></div> <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;"> <svg xmlns="http://www.w3.org/2000/svg" style="display: none !important;">
<symbol id="svg-icon-checked" viewBox="0 0 1000 1000"> <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"/> <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"/>

View File

@ -107,7 +107,7 @@ function configDialog(style) {
buttons.close.textContent = t(someDirty ? 'confirmCancel' : 'confirmClose'); buttons.close.textContent = t(someDirty ? 'confirmCancel' : 'confirmClose');
} }
function save({anyChangeIsDirty = false} = {}) { function save({anyChangeIsDirty = false} = {}, bgStyle) {
if (saving) { if (saving) {
debounce(save, 0, ...arguments); debounce(save, 0, ...arguments);
return; return;
@ -116,11 +116,18 @@ function configDialog(style) {
!vars.some(va => va.dirty || anyChangeIsDirty && va.value !== va.savedValue)) { !vars.some(va => va.dirty || anyChangeIsDirty && va.value !== va.savedValue)) {
return; 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.enabled = true;
style.reason = 'config'; style.reason = 'config';
style.sourceCode = null;
style.sections = null;
const styleVars = style.usercssData.vars; const styleVars = style.usercssData.vars;
const bgStyle = BG.cachedStyles.byId.get(style.id); const bgVars = (bgStyle.usercssData || {}).vars || {};
const bgVars = bgStyle && (bgStyle.usercssData || {}).vars || {};
const invalid = []; const invalid = [];
let numValid = 0; let numValid = 0;
for (const va of vars) { for (const va of vars) {
@ -164,9 +171,9 @@ function configDialog(style) {
return; return;
} }
saving = true; saving = true;
return BG.usercssHelper.save(BG.deepCopy(style)) return API.saveUsercss(style)
.then(saved => { .then(saved => {
varsInitial = getInitialValues(deepCopy(saved.usercssData.vars)); varsInitial = getInitialValues(saved.usercssData.vars);
vars.forEach(va => onchange({target: va.input, justSaved: true})); vars.forEach(va => onchange({target: va.input, justSaved: true}));
renderValues(); renderValues();
updateButtons(); updateButtons();

View File

@ -29,7 +29,7 @@ HTMLSelectElement.prototype.adjustWidth = function () {
parent.replaceChild(this, singleSelect); parent.replaceChild(this, singleSelect);
}; };
onDOMready().then(onBackgroundReady).then(() => { onDOMready().then(() => {
$('#search').oninput = searchStyles; $('#search').oninput = searchStyles;
if (urlFilterParam) { if (urlFilterParam) {
$('#search').value = 'url:' + urlFilterParam; $('#search').value = 'url:' + urlFilterParam;
@ -169,14 +169,17 @@ function filterAndAppend({entry, container}) {
if (!filtersSelector.hide || !entry.matches(filtersSelector.hide)) { if (!filtersSelector.hide || !entry.matches(filtersSelector.hide)) {
entry.classList.add('hidden'); entry.classList.add('hidden');
} }
} else if ($('#search').value.trim()) {
searchStyles({immediately: true, container});
} }
reapplyFilter(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 // A: show
let toHide = []; let toHide = [];
let toUnhide = []; let toUnhide = [];
@ -189,9 +192,6 @@ function reapplyFilter(container = installed) {
if (toUnhide instanceof DocumentFragment) { if (toUnhide instanceof DocumentFragment) {
installed.appendChild(toUnhide); installed.appendChild(toUnhide);
return; 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() // filtering needed or a single-element job from handleUpdate()
for (const entry of toUnhide.children || toUnhide) { for (const entry of toUnhide.children || toUnhide) {
@ -251,16 +251,12 @@ function reapplyFilter(container = installed) {
function showFiltersStats() { function showFiltersStats() {
if (!BG.cachedStyles.list) {
debounce(showFiltersStats, 100);
return;
}
const active = filtersSelector.hide !== ''; const active = filtersSelector.hide !== '';
$('#filters summary').classList.toggle('active', active); $('#filters summary').classList.toggle('active', active);
$('#reset-filters').disabled = !active; $('#reset-filters').disabled = !active;
const numTotal = BG.cachedStyles.list.length; const numTotal = installed.children.length;
const numHidden = installed.getElementsByClassName('entry hidden').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 || if (filtersSelector.numShown !== numShown ||
filtersSelector.numTotal !== numTotal) { filtersSelector.numTotal !== numTotal) {
filtersSelector.numShown = numShown; filtersSelector.numShown = numShown;
@ -273,87 +269,34 @@ function showFiltersStats() {
function searchStyles({immediately, container}) { function searchStyles({immediately, container}) {
const searchElement = $('#search'); const el = $('#search');
const value = searchElement.value.trim(); const query = el.value.trim();
const urlMode = /^\s*url:/i.test(value); if (query === el.lastValue && !immediately && !container) {
const query = urlMode
? value.replace(/^\s*url:/i, '')
: value.toLocaleLowerCase();
if (query === searchElement.lastValue && !immediately && !container) {
return; return;
} }
if (!immediately) { if (!immediately) {
debounce(searchStyles, 150, {immediately: true}); debounce(searchStyles, 150, {immediately: true});
return; 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 entries = container && container.children || container || installed.children;
const siteStyleIds = urlMode && return API.searchDB({
new Set(BG.filterStyles({matchUrl: query}).map(style => style.id)); query,
let needsRefilter = false; ids: [...entries].map(el => el.styleId),
for (const entry of entries) { }).then(ids => {
let isMatching = !query || words && !words.length; ids = new Set(ids);
if (!isMatching) { let needsRefilter = false;
const style = urlMode ? siteStyleIds.has(entry.styleId) : for (const entry of entries) {
BG.cachedStyles.byId.get(entry.styleId) || {}; const isMatching = ids.has(entry.styleId);
isMatching = Boolean(style && ( if (entry.classList.contains('not-matching') !== !isMatching) {
urlMode || entry.classList.toggle('not-matching', !isMatching);
isMatchingText(style.name) || needsRefilter = true;
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;
}
} }
} }
} if (needsRefilter && !container) {
filterOnChange({forceRefilter: true});
function isMatchingText(text) {
if (rx) {
return rx.test(text);
} }
for (let pass = 1; pass <= 2; pass++) { return container;
if (words.every(word => text.includes(word))) { });
return true;
}
text = text.toLocaleLowerCase();
}
return false;
}
} }

View File

@ -1,4 +1,4 @@
/* global messageBox, handleUpdate, applyOnMessage */ /* global messageBox handleUpdate applyOnMessage styleSectionsEqual */
'use strict'; 'use strict';
const STYLISH_DUMP_FILE_EXT = '.txt'; const STYLISH_DUMP_FILE_EXT = '.txt';
@ -41,7 +41,7 @@ function importFromFile({fileTypeFilter, file} = {}) {
importFromString(text) : importFromString(text) :
getOwnTab().then(tab => { getOwnTab().then(tab => {
tab.url = URL.createObjectURL(new Blob([text], {type: 'text/css'})); 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(() => URL.revokeObjectURL(tab.url));
}) })
).then(numStyles => { ).then(numStyles => {
@ -56,17 +56,17 @@ function importFromFile({fileTypeFilter, file} = {}) {
} }
function importFromString(jsonString) { function importFromString(jsonString, oldStyles) {
if (!BG) { if (!oldStyles) {
onBackgroundReady().then(() => importFromString(jsonString)); API.getStyles().then(styles => importFromString(jsonString, styles));
return; return;
} }
// create objects in background context const json = tryJSONparse(jsonString) || [];
const json = BG.tryJSONparse(jsonString) || [];
if (typeof json.slice !== 'function') { if (typeof json.slice !== 'function') {
json.length = 0; 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( const oldStylesByName = json.length && new Map(
oldStyles.map(style => [style.name.trim(), style])); oldStyles.map(style => [style.name.trim(), style]));
@ -94,7 +94,7 @@ function importFromString(jsonString) {
const info = analyze(item); const info = analyze(item);
if (info) { if (info) {
// using saveStyle directly since json was parsed in background page context // 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})); .then(style => account({style, info, resolve}));
} }
} }
@ -110,7 +110,7 @@ function importFromString(jsonString) {
return; return;
} }
item.name = item.name.trim(); 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); const byName = oldStylesByName.get(item.name);
oldStylesByName.delete(item.name); oldStylesByName.delete(item.name);
let oldStyle; let oldStyle;
@ -129,7 +129,7 @@ function importFromString(jsonString) {
const metaEqual = oldStyleKeys && const metaEqual = oldStyleKeys &&
oldStyleKeys.length === Object.keys(item).length && oldStyleKeys.length === Object.keys(item).length &&
oldStyleKeys.every(k => k === 'sections' || oldStyle[k] === item[k]); 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) { if (metaEqual && codeEqual) {
stats.unchanged.names.push(oldStyle.name); stats.unchanged.names.push(oldStyle.name);
stats.unchanged.ids.push(oldStyle.id); stats.unchanged.ids.push(oldStyle.id);
@ -237,10 +237,10 @@ function importFromString(jsonString) {
return; return;
} }
const id = newIds[index++]; const id = newIds[index++];
deleteStyleSafe({id, notify: false}).then(id => { API.deleteStyle({id, notify: false}).then(id => {
const oldStyle = oldStylesById.get(id); const oldStyle = oldStylesById.get(id);
if (oldStyle) { if (oldStyle) {
saveStyleSafe(Object.assign(oldStyle, SAVE_OPTIONS)) API.saveStyle(Object.assign(oldStyle, SAVE_OPTIONS))
.then(undoNextId); .then(undoNextId);
} else { } else {
undoNextId(); undoNextId();
@ -293,7 +293,7 @@ function importFromString(jsonString) {
chrome.webNavigation.getAllFrames({tabId}, frames => { chrome.webNavigation.getAllFrames({tabId}, frames => {
frames = frames && frames[0] ? frames : [{frameId: 0}]; frames = frames && frames[0] ? frames : [{frameId: 0}];
frames.forEach(({frameId}) => 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}; const message = {method: 'styleReplaceAll', tabId, frameId, styles};
if (tab.id === ownTab.id) { if (tab.id === ownTab.id) {
applyOnMessage(message); applyOnMessage(message);
@ -301,7 +301,7 @@ function importFromString(jsonString) {
invokeOrPostpone(tab.active, sendMessage, message, ignoreChromeError); invokeOrPostpone(tab.active, sendMessage, message, ignoreChromeError);
} }
if (frameId === 0) { if (frameId === 0) {
setTimeout(BG.updateIcon, 0, tab, styles); setTimeout(API.updateIcon, 0, tab, styles);
} }
})); }));
if (resolve) { if (resolve) {
@ -314,7 +314,7 @@ function importFromString(jsonString) {
$('#file-all-styles').onclick = () => { $('#file-all-styles').onclick = () => {
getStylesSafe().then(styles => { API.getStyles().then(styles => {
const text = JSON.stringify(styles, null, '\t'); const text = JSON.stringify(styles, null, '\t');
const blob = new Blob([text], {type: 'application/json'}); const blob = new Blob([text], {type: 'application/json'});
const objectURL = URL.createObjectURL(blob); const objectURL = URL.createObjectURL(blob);

View File

@ -1,9 +1,11 @@
/* global messageBox, getStyleWithNoCode, retranslateCSS */ /*
/* global filtersSelector, filterAndAppend */ global messageBox getStyleWithNoCode retranslateCSS
/* global checkUpdate, handleUpdateInstalled */ global filtersSelector filterAndAppend urlFilterParam
/* global objectDiff */ global checkUpdate handleUpdateInstalled
/* global configDialog */ global objectDiff
/* global sorter */ global configDialog
global sorter
*/
'use strict'; 'use strict';
let installed; let installed;
@ -30,14 +32,13 @@ const OWN_ICON = chrome.runtime.getManifest().icons['16'];
const handleEvent = {}; const handleEvent = {};
Promise.all([ Promise.all([
getStylesSafe(), API.getStyles({omitCode: !BG}),
urlFilterParam && API.searchDB({query: 'url:' + urlFilterParam}),
onDOMready().then(initGlobalEvents), onDOMready().then(initGlobalEvents),
]).then(([styles]) => { ]).then(args => {
showStyles(styles); showStyles(...args);
}); });
dieOnNullBackground();
chrome.runtime.onMessage.addListener(onRuntimeMessage); chrome.runtime.onMessage.addListener(onRuntimeMessage);
function onRuntimeMessage(msg) { function onRuntimeMessage(msg) {
@ -107,7 +108,7 @@ function initGlobalEvents() {
} }
function showStyles(styles = []) { function showStyles(styles = [], matchUrlIds) {
const sorted = sorter.sort({ const sorted = sorter.sort({
styles: styles.map(style => ({ styles: styles.map(style => ({
style, style,
@ -137,7 +138,13 @@ function showStyles(styles = []) {
// eslint-disable-next-line no-unmodified-loop-condition // eslint-disable-next-line no-unmodified-loop-condition
(shouldRenderAll || ++rendered < 20 || performance.now() - t0 < 10) (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}); filterAndAppend({container: renderBin});
if (index < sorted.length) { if (index < sorted.length) {
@ -277,7 +284,7 @@ function createStyleTargetsElement({entry, style, iconsOnly}) {
function recreateStyleTargets({styles, iconsOnly = false} = {}) { function recreateStyleTargets({styles, iconsOnly = false} = {}) {
Promise.resolve(styles || getStylesSafe()).then(styles => { Promise.resolve(styles || API.getStyles()).then(styles => {
for (const style of styles) { for (const style of styles) {
const entry = $(ENTRY_ID_PREFIX + style.id); const entry = $(ENTRY_ID_PREFIX + style.id);
if (entry) { if (entry) {
@ -391,7 +398,7 @@ Object.assign(handleEvent, {
}, },
toggle(event, entry) { toggle(event, entry) {
saveStyleSafe({ API.saveStyle({
id: entry.styleId, id: entry.styleId,
enabled: this.matches('.enable') || this.checked, enabled: this.matches('.enable') || this.checked,
}); });
@ -399,39 +406,30 @@ Object.assign(handleEvent, {
check(event, entry) { check(event, entry) {
event.preventDefault(); event.preventDefault();
checkUpdate(entry); checkUpdate(entry, {single: true});
}, },
update(event, entry) { update(event, entry) {
event.preventDefault(); event.preventDefault();
const request = Object.assign(entry.updatedCode, { const json = entry.updatedCode;
id: entry.styleId, json.id = entry.styleId;
reason: 'update', json.reason = 'update';
}); API[json.usercssData ? 'saveUsercss' : 'saveStyle'](json);
if (entry.updatedCode.usercssData) {
onBackgroundReady()
.then(() => BG.usercssHelper.save(request));
} else {
// update everything but name
request.name = null;
saveStyleSafe(request);
}
}, },
delete(event, entry) { delete(event, entry) {
event.preventDefault(); event.preventDefault();
const id = entry.styleId; const id = entry.styleId;
const {name} = BG.cachedStyles.byId.get(id) || {};
animateElement(entry); animateElement(entry);
messageBox({ messageBox({
title: t('deleteStyleConfirm'), title: t('deleteStyleConfirm'),
contents: name, contents: entry.styleMeta.name,
className: 'danger center', className: 'danger center',
buttons: [t('confirmDelete'), t('confirmCancel')], buttons: [t('confirmDelete'), t('confirmCancel')],
}) })
.then(({button}) => { .then(({button}) => {
if (button === 0) { if (button === 0) {
deleteStyleSafe({id}); API.deleteStyle({id});
} }
}); });
}, },
@ -525,7 +523,7 @@ function handleUpdate(style, {reason, method} = {}) {
sorter.update(); sorter.update();
if (!entry.matches('.hidden') && reason !== 'import') { if (!entry.matches('.hidden') && reason !== 'import') {
animateElement(entry); animateElement(entry);
scrollElementIntoView(entry); requestAnimationFrame(() => scrollElementIntoView(entry));
} }
function handleToggledOrCodeOnly() { function handleToggledOrCodeOnly() {
@ -606,7 +604,7 @@ function switchUI({styleOnly} = {}) {
const missingFavicons = newUI.enabled && newUI.favicons && !$('.applies-to img'); const missingFavicons = newUI.enabled && newUI.favicons && !$('.applies-to img');
if (changed.enabled || (missingFavicons && !createStyleElement.parts)) { if (changed.enabled || (missingFavicons && !createStyleElement.parts)) {
installed.textContent = ''; installed.textContent = '';
getStylesSafe().then(showStyles); API.getStyles().then(showStyles);
return; return;
} }
if (changed.targets) { if (changed.targets) {
@ -645,28 +643,3 @@ function usePrefsDuringPageLoad() {
} }
$$('#header select').forEach(el => el.adjustWidth()); $$('#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';
});
}
});
}

View File

@ -129,7 +129,7 @@ const sorter = (() => {
styles: current.map(entry => ({ styles: current.map(entry => ({
entry, entry,
name: entry.styleNameLowerCase + '\n' + entry.styleMeta.name, 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)) { if (current.some((entry, index) => entry !== sorted[index].entry)) {

View File

@ -29,41 +29,51 @@ function applyUpdateAll() {
function checkUpdateAll() { function checkUpdateAll() {
document.body.classList.add('update-in-progress'); document.body.classList.add('update-in-progress');
$('#check-all-updates').disabled = true; const btnCheck = $('#check-all-updates');
$('#check-all-updates-force').classList.add('hidden'); const btnCheckForce = $('#check-all-updates-force');
$('#apply-all-updates').classList.add('hidden'); const btnApply = $('#apply-all-updates');
$('#update-all-no-updates').classList.add('hidden'); 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'; const ignoreDigest = this && this.id === 'check-all-updates-force';
$$('.updatable:not(.can-update)' + (ignoreDigest ? '' : ':not(.update-problem)')) $$('.updatable:not(.can-update)' + (ignoreDigest ? '' : ':not(.update-problem)'))
.map(el => checkUpdate(el, {single: false})); .map(checkUpdate);
let total = 0; let total = 0;
let checked = 0; let checked = 0;
let skippedEdited = 0; let skippedEdited = 0;
let updated = 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) { API.updateCheckAll({
switch (state) { save: false,
case BG.updater.COUNT: observe: true,
total = value; ignoreDigest,
break; }).then(done);
case BG.updater.UPDATED:
if (++updated === 1) { function observer(info) {
$('#apply-all-updates').disabled = true; if ('count' in info) {
$('#apply-all-updates').classList.remove('hidden'); total = info.count;
} }
$('#apply-all-updates').dataset.value = updated; if (info.updated) {
// fallthrough if (++updated === 1) {
case BG.updater.SKIPPED: btnApply.disabled = true;
checked++; btnApply.classList.remove('hidden');
if (details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED) { }
skippedEdited++; btnApply.dataset.value = updated;
} }
reportUpdateState(state, value, details); if (info.updated || info.error) {
break; checked++;
skippedEdited += [info.STATES.EDITED, info.STATES.MAYBE_EDITED].includes(info.error);
reportUpdateState(info);
} }
const progress = $('#update-progress'); const progress = $('#update-progress');
const maxWidth = progress.parentElement.clientWidth; const maxWidth = progress.parentElement.clientWidth;
@ -72,35 +82,34 @@ function checkUpdateAll() {
function done() { function done() {
document.body.classList.remove('update-in-progress'); document.body.classList.remove('update-in-progress');
$('#check-all-updates').disabled = total === 0; btnCheck.disabled = total === 0;
$('#apply-all-updates').disabled = false; btnApply.disabled = false;
renderUpdatesOnlyFilter({check: updated + skippedEdited > 0}); renderUpdatesOnlyFilter({check: updated + skippedEdited > 0});
if (!updated) { if (!updated) {
$('#update-all-no-updates').dataset.skippedEdited = skippedEdited > 0; noUpdates.dataset.skippedEdited = skippedEdited > 0;
$('#update-all-no-updates').classList.remove('hidden'); noUpdates.classList.remove('hidden');
$('#check-all-updates-force').classList.toggle('hidden', skippedEdited === 0); btnCheckForce.classList.toggle('hidden', skippedEdited === 0);
} }
} }
} }
function checkUpdate(entry, {single = true} = {}) { function checkUpdate(entry, {single} = {}) {
$('.update-note', entry).textContent = t('checkingForUpdate'); $('.update-note', entry).textContent = t('checkingForUpdate');
$('.check-update', entry).title = ''; $('.check-update', entry).title = '';
if (single) { if (single) {
BG.updater.checkStyle({ API.updateCheck({
save: false, save: false,
id: entry.styleId,
ignoreDigest: entry.classList.contains('update-problem'), ignoreDigest: entry.classList.contains('update-problem'),
style: BG.cachedStyles.byId.get(entry.styleId), }).then(reportUpdateState);
observer: reportUpdateState,
});
} }
entry.classList.remove('checking-update', 'no-update', 'update-problem'); entry.classList.remove('checking-update', 'no-update', 'update-problem');
entry.classList.add('checking-update'); entry.classList.add('checking-update');
} }
function reportUpdateState(state, style, details) { function reportUpdateState({updated, style, error, STATES}) {
const entry = $(ENTRY_ID_PREFIX + style.id); const entry = $(ENTRY_ID_PREFIX + style.id);
const newClasses = new Map([ const newClasses = new Map([
/* /*
@ -117,43 +126,37 @@ function reportUpdateState(state, style, details) {
['no-update', 0], ['no-update', 0],
['update-problem', 0], ['update-problem', 0],
]); ]);
switch (state) { if (updated) {
case BG.updater.UPDATED: newClasses.set('can-update', true);
newClasses.set('can-update', true); entry.updatedCode = style;
entry.updatedCode = style; $('.update-note', entry).textContent = '';
$('.update-note', entry).textContent = ''; $('#only-updates').classList.remove('hidden');
$('#only-updates').classList.remove('hidden'); } else if (!entry.classList.contains('can-update')) {
break; const same = (
case BG.updater.SKIPPED: { error === STATES.SAME_MD5 ||
if (entry.classList.contains('can-update')) { error === STATES.SAME_CODE ||
break; error === STATES.SAME_VERSION
} );
const same = ( const edited = error === STATES.EDITED || error === STATES.MAYBE_EDITED;
details === BG.updater.SAME_MD5 || entry.dataset.error = error;
details === BG.updater.SAME_CODE || if (!error) {
details === BG.updater.SAME_VERSION error = t('updateCheckFailServerUnreachable') + '\n' + style.updateUrl;
); } else if (typeof error === 'number') {
const edited = details === BG.updater.EDITED || details === BG.updater.MAYBE_EDITED; error = t('updateCheckFailBadResponseCode', [error]) + '\n' + style.updateUrl;
entry.dataset.details = details; } else if (error === STATES.EDITED) {
if (!details) { error = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
details = t('updateCheckFailServerUnreachable') + '\n' + style.updateUrl; } else if (error === STATES.MAYBE_EDITED) {
} else if (typeof details === 'number') { error = t('updateCheckSkippedMaybeLocallyEdited') + '\n' + t('updateCheckManualUpdateHint');
details = t('updateCheckFailBadResponseCode', [details]) + '\n' + style.updateUrl; }
} else if (details === BG.updater.EDITED) { const message = same ? t('updateCheckSucceededNoUpdate') : error;
details = t('updateCheckSkippedLocallyEdited') + '\n' + t('updateCheckManualUpdateHint'); newClasses.set('no-update', true);
} else if (details === BG.updater.MAYBE_EDITED) { newClasses.set('update-problem', !same);
details = t('updateCheckSkippedMaybeLocallyEdited') + '\n' + t('updateCheckManualUpdateHint'); $('.update-note', entry).textContent = message;
} $('.check-update', entry).title = newUI.enabled ? message : '';
const message = same ? t('updateCheckSucceededNoUpdate') : details; $('.update', entry).title = t(edited ? 'updateCheckManualUpdateForce' : 'installUpdate');
newClasses.set('no-update', true); if (!document.body.classList.contains('update-in-progress')) {
newClasses.set('update-problem', !same); // this is a single update job so we can decide whether to hide the filter
$('.update-note', entry).textContent = message; renderUpdatesOnlyFilter({show: $('.can-update, .update-problem')});
$('.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 // 2. remove falsy newClasses
// 3. keep existing classes otherwise // 3. keep existing classes otherwise
const classes = new Map([...entry.classList.values()].map(cls => [cls, true])); const classes = new Map([...entry.classList.values()].map(cls => [cls, true]));
[...newClasses.entries()].forEach(([cls, newState]) => classes.set(cls, newState)); for (const [cls, newState] of newClasses.entries()) {
const className = [...classes.entries()].filter(([, state]) => state).map(([cls]) => cls).join(' '); classes.set(cls, newState);
if (className !== entry.className) entry.className = className; }
const className = [...classes.entries()]
.map(([cls, state]) => state && cls)
.filter(Boolean)
.join(' ');
if (className !== entry.className) {
entry.className = className;
}
if (filtersSelector.hide) { if (filtersSelector.hide) {
filterAndAppend({entry}); filterAndAppend({entry});
@ -200,7 +210,10 @@ function showUpdateHistory(event) {
const log = $create('.update-history-log'); const log = $create('.update-history-log');
let logText, scroller, toggler; let logText, scroller, toggler;
let deleted = false; let deleted = false;
BG.chromeLocal.getValue('updateLog').then((lines = []) => { Promise.all([
chromeLocal.getValue('updateLog'),
API.getUpdaterStates(),
]).then(([lines = [], states]) => {
logText = lines.join('\n'); logText = lines.join('\n');
messageBox({ messageBox({
title: t('updateCheckHistory'), title: t('updateCheckHistory'),
@ -227,6 +240,13 @@ function showUpdateHistory(event) {
t('manageOnlyUpdates'), t('manageOnlyUpdates'),
])); ]));
toggler.rxRemoveNOP = new RegExp(
'^[^#]*(' +
Object.keys(states)
.filter(k => k.startsWith('SAME_'))
.map(k => states[k])
.join('|') +
').*\r?\n', 'gm');
toggler.onchange(); toggler.onchange();
}), }),
}); });
@ -242,26 +262,17 @@ function showUpdateHistory(event) {
return; return;
} }
const scrollRatio = calcScrollRatio(); const scrollRatio = calcScrollRatio();
const rxRemoveNOP = this.checked && new RegExp([ log.textContent = !this.checked ? logText : logText.replace(this.rxRemoveNOP, '');
'^[^#]*(',
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, '');
if (Math.abs(scrollRatio - calcScrollRatio()) > .1) { if (Math.abs(scrollRatio - calcScrollRatio()) > .1) {
scroller.scrollTop = scrollRatio * scroller.scrollHeight - scroller.clientHeight; scroller.scrollTop = scrollRatio * scroller.scrollHeight - scroller.clientHeight;
} }
} }
function deleteHistory() { function deleteHistory() {
if (deleted) { if (deleted) {
BG.chromeLocal.setValue('updateLog', logText.split('\n')); chromeLocal.setValue('updateLog', logText.split('\n'));
setTimeout(scrollToBottom); setTimeout(scrollToBottom);
} else { } else {
BG.chromeLocal.remove('updateLog'); chromeLocal.remove('updateLog');
log.textContent = ''; log.textContent = '';
} }
deleted = !deleted; deleted = !deleted;

View File

@ -21,17 +21,18 @@
"background": { "background": {
"scripts": [ "scripts": [
"js/messaging.js", "js/messaging.js",
"vendor/lz-string/lz-string-unsafe.js", "js/storage-util.js",
"js/color-parser.js",
"js/usercss.js",
"background/storage.js", "background/storage.js",
"background/usercss-helper.js",
"js/prefs.js", "js/prefs.js",
"js/script-loader.js", "js/script-loader.js",
"js/color-parser.js",
"js/usercss.js",
"background/background.js", "background/background.js",
"vendor/node-semver/semver.js", "background/usercss-helper.js",
"background/style-via-api.js", "background/style-via-api.js",
"background/update.js" "background/search-db.js",
"background/update.js",
"vendor/node-semver/semver.js"
] ]
}, },
"commands": { "commands": {

View File

@ -62,23 +62,26 @@ function checkUpdates() {
let checked = 0; let checked = 0;
let updated = 0; let updated = 0;
const maxWidth = $('#update-progress').parentElement.clientWidth; const maxWidth = $('#update-progress').parentElement.clientWidth;
BG.updater.checkAllStyles({observer});
function observer(state, value) { chrome.runtime.onConnect.addListener(function onConnect(port) {
switch (state) { if (port.name !== 'updater') return;
case BG.updater.COUNT: port.onMessage.addListener(observer);
total = value; chrome.runtime.onConnect.removeListener(onConnect);
document.body.classList.add('update-in-progress'); });
break;
case BG.updater.UPDATED: API.updateCheckAll({observe: true});
updated++;
// fallthrough function observer(info) {
case BG.updater.SKIPPED: if ('count' in info) {
checked++; total = info.count;
break; document.body.classList.add('update-in-progress');
case BG.updater.DONE: } else if (info.updated) {
document.body.classList.remove('update-in-progress'); updated++;
return; 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'; $('#update-progress').style.width = Math.round(checked / total * maxWidth) + 'px';
$('#updates-installed').dataset.value = updated || ''; $('#updates-installed').dataset.value = updated || '';

View File

@ -161,6 +161,8 @@
<script src="popup/popup.js"></script> <script src="popup/popup.js"></script>
<script src="popup/search-results.js"></script> <script src="popup/search-results.js"></script>
<script src="popup/hotkeys.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> </head>
<body id="stylus-popup"> <body id="stylus-popup">

View File

@ -101,11 +101,15 @@ var hotkeys = (() => {
entry = typeof entry === 'string' ? $('#' + entry) : entry; entry = typeof entry === 'string' ? $('#' + entry) : entry;
if (!match && $('.checker', entry).checked !== enable || entry.classList.contains(match)) { if (!match && $('.checker', entry).checked !== enable || entry.classList.contains(match)) {
results.push(entry.id); results.push(entry.id);
task = task.then(() => saveStyleSafe({ task = task.then(() => API.saveStyle({
id: entry.styleId, id: entry.styleId,
enabled: enable, enabled: enable,
notify: false, notify: false,
})); })).then(() => {
entry.classList.toggle('enabled', enable);
entry.classList.toggle('disabled', !enable);
$('.checker', entry).checked = enable;
});
} }
} }
if (results.length) { if (results.length) {
@ -115,7 +119,7 @@ var hotkeys = (() => {
} }
function refreshAllTabs() { 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})); .then(styles => applyOnMessage({method: 'styleReplaceAll', styles}));
queryTabs().then(tabs => queryTabs().then(tabs =>
tabs.forEach(tab => (!FIREFOX || tab.width) && tabs.forEach(tab => (!FIREFOX || tab.width) &&
@ -127,11 +131,11 @@ var hotkeys = (() => {
chrome.webNavigation.getAllFrames({tabId}, frames => { chrome.webNavigation.getAllFrames({tabId}, frames => {
frames = frames && frames[0] ? frames : [{frameId: 0}]; frames = frames && frames[0] ? frames : [{frameId: 0}];
frames.forEach(({frameId}) => 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}; const message = {method: 'styleReplaceAll', tabId, frameId, styles};
invokeOrPostpone(tab.active, sendMessage, message, ignoreChromeError); invokeOrPostpone(tab.active, sendMessage, message, ignoreChromeError);
if (frameId === 0) { if (frameId === 0) {
setTimeout(BG.updateIcon, 0, tab, styles); setTimeout(API.updateIcon, 0, {tab, styles});
} }
})); }));
ignoreChromeError(); ignoreChromeError();

View File

@ -15,16 +15,15 @@ getActiveTab().then(tab =>
FIREFOX && tab.url === 'about:blank' && tab.status === 'loading' FIREFOX && tab.url === 'about:blank' && tab.status === 'loading'
? getTabRealURLFirefox(tab) ? getTabRealURLFirefox(tab)
: getTabRealURL(tab) : getTabRealURL(tab)
).then(url => { ).then(url => Promise.all([
tabURL = URLS.supported(url) ? url : ''; (tabURL = URLS.supported(url) ? url : '') &&
Promise.all([ API.getStyles({
tabURL && getStylesSafe({matchUrl: tabURL}), matchUrl: tabURL,
onDOMready().then(() => { omitCode: !BG,
initPopup(tabURL); }),
}), onDOMready().then(initPopup),
]).then(([styles]) => { ])).then(([styles]) => {
showStyles(styles); showStyles(styles);
});
}); });
chrome.runtime.onMessage.addListener(onRuntimeMessage); chrome.runtime.onMessage.addListener(onRuntimeMessage);
@ -33,9 +32,7 @@ function onRuntimeMessage(msg) {
switch (msg.method) { switch (msg.method) {
case 'styleAdded': case 'styleAdded':
case 'styleUpdated': case 'styleUpdated':
// notifyAllTabs sets msg.style's code to null so we have to get the actual style handleUpdate(msg.style);
// because we analyze its code in detectSloppyRegexps
handleUpdate(BG.cachedStyles.byId.get(msg.style.id));
break; break;
case 'styleDeleted': case 'styleDeleted':
handleDelete(msg.id); handleDelete(msg.id);
@ -76,7 +73,7 @@ function toggleSideBorders(state = prefs.get('popup.borders')) {
} }
function initPopup(url) { function initPopup() {
installed = $('#installed'); installed = $('#installed');
setPopupWidth(); setPopupWidth();
@ -108,7 +105,7 @@ function initPopup(url) {
installed); installed);
} }
if (!url) { if (!tabURL) {
document.body.classList.add('blocked'); document.body.classList.add('blocked');
document.body.insertBefore(template.unavailableInfo, document.body.firstChild); document.body.insertBefore(template.unavailableInfo, document.body.firstChild);
return; return;
@ -153,10 +150,10 @@ function initPopup(url) {
// For this URL // For this URL
const urlLink = template.writeStyle.cloneNode(true); const urlLink = template.writeStyle.cloneNode(true);
Object.assign(urlLink, { Object.assign(urlLink, {
href: 'edit.html?url-prefix=' + encodeURIComponent(url), href: 'edit.html?url-prefix=' + encodeURIComponent(tabURL),
title: `url-prefix("${url}")`, title: `url-prefix("${tabURL}")`,
textContent: prefs.get('popup.breadcrumbs.usePath') textContent: prefs.get('popup.breadcrumbs.usePath')
? new URL(url).pathname.slice(1) ? new URL(tabURL).pathname.slice(1)
// this&nbsp;URL // this&nbsp;URL
: t('writeStyleForURL').replace(/ /g, '\u00a0'), : t('writeStyleForURL').replace(/ /g, '\u00a0'),
onclick: handleEvent.openLink, onclick: handleEvent.openLink,
@ -170,7 +167,7 @@ function initPopup(url) {
matchTargets.appendChild(urlLink); matchTargets.appendChild(urlLink);
// For domain // For domain
const domains = BG.getDomains(url); const domains = getDomains(tabURL);
for (const domain of domains) { for (const domain of domains) {
const numParts = domain.length - domain.replace(/\./g, '').length + 1; const numParts = domain.length - domain.replace(/\./g, '').length + 1;
// Don't include TLD // Don't include TLD
@ -193,6 +190,19 @@ function initPopup(url) {
matchTargets.appendChild(matchTargets.removeChild(matchTargets.firstElementChild)); matchTargets.appendChild(matchTargets.removeChild(matchTargets.firstElementChild));
} }
writeStyle.appendChild(matchWrapper); 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) : a.name.localeCompare(b.name)
)); ));
let postponeDetect = false;
const t0 = performance.now();
const container = document.createDocumentFragment(); const container = document.createDocumentFragment();
for (const style of styles) { styles.forEach(style => createStyleElement({style, container}));
createStyleElement({style, container, postponeDetect});
postponeDetect = postponeDetect || performance.now() - t0 > 100;
}
installed.appendChild(container); installed.appendChild(container);
setTimeout(detectSloppyRegexps, 100, styles);
getStylesSafe({matchUrl: tabURL, strictRegexp: false}) API.getStyles({
.then(unscreenedStyles => { matchUrl: tabURL,
for (const unscreened of unscreenedStyles) { strictRegexp: false,
if (!styles.includes(unscreened)) { omitCode: true,
postponeDetect = postponeDetect || performance.now() - t0 > 100; }).then(unscreenedStyles => {
createStyleElement({ for (const style of unscreenedStyles) {
style: Object.assign({appliedSections: [], postponeDetect}, unscreened), 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({ function createStyleElement({
style, style,
check = false,
container = installed, container = installed,
postponeDetect,
}) { }) {
const entry = template.style.cloneNode(true); const entry = template.style.cloneNode(true);
entry.setAttribute('style-id', style.id); entry.setAttribute('style-id', style.id);
@ -294,7 +300,7 @@ function createStyleElement({
$('.delete', entry).onclick = handleEvent.delete; $('.delete', entry).onclick = handleEvent.delete;
$('.configure', entry).onclick = handleEvent.configure; $('.configure', entry).onclick = handleEvent.configure;
invokeOrPostpone(!postponeDetect, detectSloppyRegexps, {entry, style}); if (check) detectSloppyRegexps([style]);
const oldElement = $(ENTRY_ID_PREFIX + style.id); const oldElement = $(ENTRY_ID_PREFIX + style.id);
if (oldElement) { if (oldElement) {
@ -316,23 +322,24 @@ Object.assign(handleEvent, {
}, },
name(event) { name(event) {
this.checkbox.click(); this.checkbox.dispatchEvent(new MouseEvent('click'));
event.preventDefault(); event.preventDefault();
}, },
toggle(event) { toggle(event) {
saveStyleSafe({ API.saveStyle({
id: handleEvent.getClickedStyleId(event), id: handleEvent.getClickedStyleId(event),
enabled: this.type === 'checkbox' ? this.checked : this.matches('.enable'), enabled: this.matches('.enable') || this.checked,
}); });
}, },
delete(event) { delete(event) {
const id = handleEvent.getClickedStyleId(event); const entry = handleEvent.getClickedStyleElement(event);
const id = entry.styleId;
const box = $('#confirm'); const box = $('#confirm');
box.dataset.display = true; box.dataset.display = true;
box.style.cssText = ''; 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).focus();
$('[data-cmd="ok"]', box).onclick = () => confirm(true); $('[data-cmd="ok"]', box).onclick = () => confirm(true);
$('[data-cmd="cancel"]', box).onclick = () => confirm(false); $('[data-cmd="cancel"]', box).onclick = () => confirm(false);
@ -350,18 +357,14 @@ Object.assign(handleEvent, {
className: 'lights-on', className: 'lights-on',
onComplete: () => (box.dataset.display = false), onComplete: () => (box.dataset.display = false),
}); });
if (ok) { if (ok) API.deleteStyle({id});
deleteStyleSafe({id}).then(() => {
handleDelete(id);
});
}
} }
}, },
configure(event) { configure(event) {
const {styleId, styleIsUsercss} = handleEvent.getClickedStyleElement(event); const {styleId, styleIsUsercss} = handleEvent.getClickedStyleElement(event);
if (styleIsUsercss) { if (styleIsUsercss) {
getStylesSafe({id: styleId}).then(([style]) => { API.getStyles({id: styleId}).then(([style]) => {
hotkeys.setState(false); hotkeys.setState(false);
configDialog(deepCopy(style)).then(() => { configDialog(deepCopy(style)).then(() => {
hotkeys.setState(true); hotkeys.setState(true);
@ -456,15 +459,22 @@ Object.assign(handleEvent, {
function handleUpdate(style) { function handleUpdate(style) {
if ($(ENTRY_ID_PREFIX + style.id)) { if ($(ENTRY_ID_PREFIX + style.id)) {
createStyleElement({style}); createStyleElement({style, check: true});
return; return;
} }
if (!tabURL) return;
// Add an entry when a new style for the current url is installed // Add an entry when a new style for the current url is installed
if (tabURL && BG.getApplicableSections({style, matchUrl: tabURL, stopOnFirst: true}).length) { API.getStyles({
document.body.classList.remove('blocked'); matchUrl: tabURL,
$$.remove('.blocked-info, #no-styles'); stopOnFirst: true,
createStyleElement({style}); 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) {
} }
/* function detectSloppyRegexps(styles) {
According to CSS4 @document specification the entire URL must match. API.detectSloppyRegexps({
Stylish-for-Chrome implemented it incorrectly since the very beginning. matchUrl: tabURL,
We'll detect styles that abuse the bug by finding the sections that ids: styles.map(({id}) => id),
would have been applied by Stylish but not by us as we follow the spec. }).then(results => {
Additionally we'll check for invalid regexps. for (const {id, applied, skipped, hasInvalidRegexps} of results) {
*/ const entry = $(ENTRY_ID_PREFIX + id);
function detectSloppyRegexps({entry, style}) { if (!entry) continue;
// make sure all regexps are compiled if (!applied) {
const rxCache = BG.cachedStyles.regexps; entry.classList.add('not-applied');
let hasRegExp = false; $('.style-name', entry).title = t('styleNotAppliedRegexpProblemTooltip');
for (const section of style.sections) { }
for (const regexp of section.regexps) { if (skipped || hasInvalidRegexps) {
hasRegExp = true; entry.classList.toggle('regexp-partial', Boolean(skipped));
for (let pass = 1; pass <= 2; pass++) { entry.classList.toggle('regexp-invalid', Boolean(hasInvalidRegexps));
const cacheKey = pass === 1 ? regexp : BG.SLOPPY_REGEXP_PREFIX + regexp; const indicator = template.regexpProblemIndicator.cloneNode(true);
if (!rxCache.has(cacheKey)) { indicator.appendChild(document.createTextNode(entry.skipped || '!'));
// according to CSS4 @document specification the entire URL must match indicator.onclick = handleEvent.indicator;
const anchored = pass === 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$'; $('.main-controls', entry).appendChild(indicator);
// create in the bg context to avoid leaking of "dead objects"
const rx = BG.tryRegExp(anchored);
rxCache.set(cacheKey, rx || false);
}
} }
} }
} });
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);
}
} }

View File

@ -131,7 +131,7 @@ window.addEventListener('showStyles:done', function _() {
if (result) { if (result) {
result.installed = false; result.installed = false;
result.installedStyleId = -1; result.installedStyleId = -1;
BG.clearTimeout(result.pingbackTimer); (BG || window).clearTimeout(result.pingbackTimer);
renderActionButtons($('#' + RESULT_ID_PREFIX + result.id)); renderActionButtons($('#' + RESULT_ID_PREFIX + result.id));
} }
}); });
@ -280,7 +280,7 @@ window.addEventListener('showStyles:done', function _() {
return; return;
} }
const md5Url = UPDATE_URL.replace('%', result.id); const md5Url = UPDATE_URL.replace('%', result.id);
getStylesSafe({md5Url}).then(([installedStyle]) => { API.getStyles({md5Url}).then(([installedStyle]) => {
if (installedStyle) { if (installedStyle) {
totalResults = Math.max(0, totalResults - 1); totalResults = Math.max(0, totalResults - 1);
} else { } else {
@ -522,7 +522,7 @@ window.addEventListener('showStyles:done', function _() {
event.stopPropagation(); event.stopPropagation();
const entry = this.closest('.search-result'); const entry = this.closest('.search-result');
saveScrollPosition(entry); saveScrollPosition(entry);
deleteStyleSafe({id: entry._result.installedStyleId}) API.deleteStyle({id: entry._result.installedStyleId})
.then(restoreScrollPosition); .then(restoreScrollPosition);
} }
@ -550,11 +550,11 @@ window.addEventListener('showStyles:done', function _() {
style.updateUrl += settings.length ? '?' : ''; style.updateUrl += settings.length ? '?' : '';
// show a 'style installed' tooltip in the manager // show a 'style installed' tooltip in the manager
style.reason = 'install'; style.reason = 'install';
return saveStyleSafe(style); return API.saveStyle(style);
}) })
.catch(reason => { .catch(reason => {
const usoId = result.id; 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); error('Error while downloading usoID:' + usoId + '\nReason: ' + reason);
}) })
.then(() => { .then(() => {
@ -574,7 +574,8 @@ window.addEventListener('showStyles:done', function _() {
} }
function pingback(result) { 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'); BASE_URL + '/styles/install/' + result.id + '?source=stylish-ch');
} }
@ -721,9 +722,10 @@ window.addEventListener('showStyles:done', function _() {
function readCache(id) { function readCache(id) {
const key = CACHE_PREFIX + id; const key = CACHE_PREFIX + id;
return BG.chromeLocal.getValue(key).then(item => { return chromeLocal.getValue(key).then(item => {
if (!cacheItemExpired(item)) { if (!cacheItemExpired(item)) {
return tryJSONparse(BG.LZString.decompressFromUTF16(item.payload)); return chromeLocal.loadLZStringScript().then(() =>
tryJSONparse(LZString.decompressFromUTF16(item.payload)));
} else if (item) { } else if (item) {
chrome.storage.local.remove(key); chrome.storage.local.remove(key);
} }
@ -741,10 +743,11 @@ window.addEventListener('showStyles:done', function _() {
return data; return data;
} else { } else {
debounce(cleanupCache, CACHE_CLEANUP_THROTTLE); debounce(cleanupCache, CACHE_CLEANUP_THROTTLE);
return BG.chromeLocal.setValue(CACHE_PREFIX + data.id, { return chromeLocal.loadLZStringScript().then(() =>
payload: BG.LZString.compressToUTF16(JSON.stringify(data)), chromeLocal.setValue(CACHE_PREFIX + data.id, {
date: Date.now(), payload: LZString.compressToUTF16(JSON.stringify(data)),
}).then(() => data); date: Date.now(),
})).then(() => data);
} }
} }