fixes/cosmetics

* usercss installer url check
* extract downloaders
* simplify tabManager
* rework/split openInstallerPage
* use a simple object instead of map
* trivial bugfixes
* cosmetics
This commit is contained in:
tophf 2020-02-19 08:48:00 +03:00
parent 73f6e8a964
commit b01f03babf
10 changed files with 164 additions and 155 deletions

View File

@ -626,12 +626,12 @@
"description": "The label of live-reload error" "description": "The label of live-reload error"
}, },
"liveReloadInstallHint": { "liveReloadInstallHint": {
"message": "Keep this tab open to auto-update the installed style on external changes.", "message": "Keep this tab open to auto-update the style on external changes.",
"description": "The label of live-reload feature" "description": "The label of live-reload feature"
}, },
"liveReloadInstallHintFF": { "liveReloadInstallHintFF": {
"message": "Keep the original tab open too as it's needed for local file:// URLs in Firefox 68 and newer.", "message": "Keep both this tab and the original tab open to auto-update the style on external changes.",
"description": "The extra hint of live-reload feature shown only for file:// URLs in Firefox 68+" "description": "The extra hint of live-reload feature shown only for file:// URLs in Firefox"
}, },
"liveReloadLabel": { "liveReloadLabel": {
"message": "Live reload", "message": "Live reload",

View File

@ -2,7 +2,7 @@
URLS ignoreChromeError usercssHelper URLS ignoreChromeError usercssHelper
styleManager msg navigatorUtil workerUtil contentScripts sync styleManager msg navigatorUtil workerUtil contentScripts sync
findExistingTab createTab activateTab isTabReplaceable getActiveTab findExistingTab createTab activateTab isTabReplaceable getActiveTab
iconManager */ iconManager tabManager */
'use strict'; 'use strict';
@ -89,6 +89,14 @@ navigatorUtil.onUrlChange(({tabId, frameId}, type) => {
} }
}); });
tabManager.onUpdate(({tabId, url, oldUrl = ''}) => {
if (usercssHelper.testUrl(url) && !oldUrl.startsWith(URLS.installUsercss)) {
usercssHelper.testContents(tabId, url).then(data => {
if (data.code) usercssHelper.openInstallerPage(tabId, url, data);
});
}
});
if (FIREFOX) { if (FIREFOX) {
// FF misses some about:blank iframes so we inject our content script explicitly // FF misses some about:blank iframes so we inject our content script explicitly
navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, { navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, {
@ -108,13 +116,6 @@ if (chrome.commands) {
chrome.commands.onCommand.addListener(command => browserCommands[command]()); chrome.commands.onCommand.addListener(command => browserCommands[command]());
} }
// detect usercss and open the installer page
navigatorUtil.onCommitted(({tabId, frameId, url}) => {
if (!frameId && usercssHelper.testUrl(url)) {
usercssHelper.openInstallerPage(tabId, url);
}
});
// ************************************************************************* // *************************************************************************
chrome.runtime.onInstalled.addListener(({reason}) => { chrome.runtime.onInstalled.addListener(({reason}) => {
// save install type: "admin", "development", "normal", "sideload" or "other" // save install type: "admin", "development", "normal", "sideload" or "other"

View File

@ -30,13 +30,13 @@ const iconManager = (() => {
// FIXME: in some cases, we only have to redraw the badge. is it worth a optimization? // FIXME: in some cases, we only have to redraw the badge. is it worth a optimization?
function updateIconBadge(tabId, count, force = true) { function updateIconBadge(tabId, count, force = true) {
tabManager.setMeta(tabId, 'count', count); tabManager.set(tabId, 'count', count);
refreshIconBadgeText(tabId); refreshIconBadgeText(tabId);
refreshIcon(tabId, force); refreshIcon(tabId, force);
} }
function refreshIconBadgeText(tabId) { function refreshIconBadgeText(tabId) {
const count = tabManager.getMeta(tabId, 'count'); const count = tabManager.get(tabId, 'count');
iconUtil.setBadgeText({ iconUtil.setBadgeText({
text: prefs.get('show-badge') && count ? String(count) : '', text: prefs.get('show-badge') && count ? String(count) : '',
tabId tabId
@ -50,13 +50,13 @@ const iconManager = (() => {
} }
function refreshIcon(tabId, force = false) { function refreshIcon(tabId, force = false) {
const oldIcon = tabManager.getMeta(tabId, 'icon'); const oldIcon = tabManager.get(tabId, 'icon');
const newIcon = getIconName(tabManager.getMeta(tabId, 'count')); const newIcon = getIconName(tabManager.get(tabId, 'count'));
if (!force && oldIcon === newIcon) { if (!force && oldIcon === newIcon) {
return; return;
} }
tabManager.setMeta(tabId, 'icon', newIcon); tabManager.set(tabId, 'icon', newIcon);
iconUtil.setIcon({ iconUtil.setIcon({
path: getIconPath(newIcon), path: getIconPath(newIcon),
tabId tabId

View File

@ -9,40 +9,35 @@ const tabManager = (() => {
chrome.tabs.onReplaced.addListener((added, removed) => cache.delete(removed)); chrome.tabs.onReplaced.addListener((added, removed) => cache.delete(removed));
navigatorUtil.onUrlChange(({tabId, frameId, url}) => { navigatorUtil.onUrlChange(({tabId, frameId, url}) => {
if (frameId) return; if (frameId) return;
setMeta(tabId, 'url', url); const oldUrl = tabManager.get(tabId, 'url');
emitUpdate({tabId, url}); tabManager.set(tabId, 'url', url);
}); for (const fn of listeners) {
return {onUpdate, setMeta, getMeta, list};
function list() {
return cache.keys();
}
function onUpdate(callback) {
listeners.push(callback);
}
function emitUpdate(e) {
for (const callback of listeners) {
try { try {
callback(e); fn({tabId, url, oldUrl});
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
} }
} });
function setMeta(tabId, key, value) { return {
let meta = cache.get(tabId); onUpdate(fn) {
if (!meta) { listeners.push(fn);
meta = new Map(); },
cache.set(tabId, meta); get(tabId, key) {
} const meta = cache.get(tabId);
meta.set(key, value); return meta && meta[key];
} },
set(tabId, key, value) {
function getMeta(tabId, key) { let meta = cache.get(tabId);
return cache.get(tabId).get(key); if (!meta) {
} meta = {};
cache.set(tabId, meta);
}
meta[key] = value;
},
list() {
return cache.keys();
},
};
})(); })();

View File

@ -1,15 +1,15 @@
/* global API_METHODS usercss styleManager deepCopy openURL download URLS getTab promisify */ /* global API_METHODS usercss styleManager deepCopy openURL download URLS getTab */
/* exports usercssHelper */ /* exports usercssHelper */
'use strict'; 'use strict';
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const usercssHelper = (() => { const usercssHelper = (() => {
// detecting FF68 by the added feature as navigator.ua may be spoofed via about:config or devtools const installCodeCache = {};
const tabExec = !chrome.app && chrome.storage.managed && promisify(chrome.tabs.executeScript.bind(chrome.tabs)); const clearInstallCode = url => delete installCodeCache[url];
const downloadSelf = tabExec && {file: '/content/download-self.js'}; const isResponseText = r => /^text\/(css|plain)(;.*?)?$/i.test(r.headers.get('content-type'));
const installCodeCache = new Map(); // in Firefox we have to use a content script to read file://
const clearInstallCode = url => installCodeCache.delete(url); const fileLoader = !chrome.app && // not relying on navigator.ua which can be spoofed
const isPlainCssResponse = r => /^text\/(css|plain)(;.*?)?$/i.test(r.headers.get('content-type')); (tabId => browser.tabs.executeScript(tabId, {file: '/content/install-hook-usercss.js'}).then(r => r[0]));
API_METHODS.installUsercss = installUsercss; API_METHODS.installUsercss = installUsercss;
API_METHODS.editSaveUsercss = editSaveUsercss; API_METHODS.editSaveUsercss = editSaveUsercss;
@ -19,10 +19,11 @@ const usercssHelper = (() => {
API_METHODS.findUsercss = find; API_METHODS.findUsercss = find;
API_METHODS.getUsercssInstallCode = url => { API_METHODS.getUsercssInstallCode = url => {
const {code, timer} = installCodeCache.get(url) || {}; // when the installer tab is reloaded after the cache is expired, this will throw intentionally
const {code, timer} = installCodeCache[url];
clearInstallCode(url); clearInstallCode(url);
clearTimeout(timer); clearTimeout(timer);
return code || ''; return code;
}; };
return { return {
@ -30,35 +31,34 @@ const usercssHelper = (() => {
testUrl(url) { testUrl(url) {
return url.includes('.user.') && return url.includes('.user.') &&
/^(https?|file|ftps?):/.test(url) && /^(https?|file|ftps?):/.test(url) &&
/\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]) && /\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]);
!url.startsWith(URLS.installUsercss);
}, },
openInstallerPage(tabId, url) { /** @return {Promise<{ code:string, inTab:boolean } | false>} */
testContents(tabId, url) {
const isFile = url.startsWith('file:'); const isFile = url.startsWith('file:');
const isFileFF = isFile && tabExec; const inTab = isFile && Boolean(fileLoader);
return Promise.resolve(isFile || fetch(url, {method: 'HEAD'}).then(isPlainCssResponse)) return Promise.resolve(isFile || fetch(url, {method: 'HEAD'}).then(isResponseText))
.then(ok => ok && (isFileFF ? tabExec(tabId, downloadSelf) : download(url))) .then(ok => ok && (inTab ? fileLoader(tabId) : download(url)))
.then(code => { .then(code => /==userstyle==/i.test(code) && {code, inTab});
if (Array.isArray(code)) code = code[0]; },
if (!/==userstyle==/i.test(code)) return;
const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`; openInstallerPage(tabId, url, {code, inTab} = {}) {
if (isFileFF) { const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`;
getTab(tabId).then(tab => if (inTab) {
openURL({ getTab(tabId).then(tab =>
url: `${newUrl}&tabId=${tabId}`, openURL({
active: tab.active, url: `${newUrl}&tabId=${tabId}`,
index: tab.index + 1, active: tab.active,
openerTabId: tabId, index: tab.index + 1,
currentWindow: null, openerTabId: tabId,
})); currentWindow: null,
} else { }));
const timer = setTimeout(clearInstallCode, 10e3, url); } else {
installCodeCache.set(url, {code, timer}); const timer = setTimeout(clearInstallCode, 10e3, url);
chrome.tabs.update(tabId, {url: newUrl}); installCodeCache[url] = {code, timer};
return newUrl; chrome.tabs.update(tabId, {url: newUrl});
} }
});
}, },
}; };

View File

@ -1,23 +0,0 @@
'use strict';
// preventing reinjection by tabs.executeScript, just in case
typeof self.oldCode !== 'string' && // eslint-disable-line no-unused-expressions
chrome.runtime.onConnect.addListener(port => {
if (port.name !== 'downloadSelf') return;
const read = r => r.status === 200 ? r.text() : Promise.reject(r.status);
const wrapError = error => ({error});
const postBack = msg => {
port.postMessage(msg);
self.oldCode = msg.code;
};
port.onMessage.addListener(cmd => {
const oldCode = cmd === 'timer' ? self.oldCode : '';
fetch(location.href, {mode: 'same-origin'})
.then(read)
.then(code => ({code: code === oldCode ? '' : code}), wrapError)
.then(postBack);
});
});
// this assignment also passes the result to tabs.executeScript
self.oldCode = (document.querySelector('body > pre') || document.body).textContent;

View File

@ -0,0 +1,22 @@
'use strict';
// preventing reregistration if reinjected by tabs.executeScript for whatever reason, just in case
if (typeof self.oldCode !== 'string') {
self.oldCode = (document.querySelector('body > pre') || document.body).textContent;
chrome.runtime.onConnect.addListener(port => {
if (port.name !== 'downloadSelf') return;
port.onMessage.addListener(({id, timer}) => {
fetch(location.href, {mode: 'same-origin'})
.then(r => r.text())
.then(code => ({id, code: timer && code === self.oldCode ? null : code}))
.catch(error => ({id, error: error.message || `${error}`}))
.then(msg => {
port.postMessage(msg);
if (msg.code != null) self.oldCode = msg.code;
});
});
});
}
// passing the result to tabs.executeScript
self.oldCode; // eslint-disable-line no-unused-expressions

View File

@ -58,8 +58,7 @@
<div class="actions"> <div class="actions">
<h2 class="installed" i18n-text="installButtonInstalled"></h2> <h2 class="installed" i18n-text="installButtonInstalled"></h2>
<button class="install" i18n-text="installButton"></button> <button class="install" i18n-text="installButton"></button>
<p id="live-reload-install-hint" i18n-text="liveReloadInstallHint" hidden></p> <p id="live-reload-install-hint" hidden></p>
<p id="live-reload-install-hint-ff" i18n-text="liveReloadInstallHintFF" hidden></p>
<label class="set-update-url"> <label class="set-update-url">
<input type="checkbox"> <input type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg> <svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>

View File

@ -7,19 +7,12 @@
const params = new URLSearchParams(location.search.replace(/^\?/, '')); const params = new URLSearchParams(location.search.replace(/^\?/, ''));
const tabId = params.has('tabId') ? Number(params.get('tabId')) : -1; const tabId = params.has('tabId') ? Number(params.get('tabId')) : -1;
const initialUrl = params.get('updateUrl'); const initialUrl = params.get('updateUrl');
if (!initialUrl) throw 'No updateUrl parameter';
let installed = null; let installed = null;
let installedDup = null; let installedDup = null;
let initialized = false;
let filePort;
const liveReload = initLiveReload(); const liveReload = initLiveReload();
liveReload.ready.then(initSourceCode, error => messageBox.alert(error, 'pre'));
// when this tab is reloaded, bg may no longer have the code as it's kept only for a few seconds
API.getUsercssInstallCode(initialUrl)
.then(code => code || !filePort && download(initialUrl))
.then(code => (code || !filePort) && initSourceCode(code));
const theme = prefs.get('editor.theme'); const theme = prefs.get('editor.theme');
const cm = CodeMirror($('.main'), { const cm = CodeMirror($('.main'), {
@ -172,7 +165,7 @@
} else { } else {
API.openEditor({id: style.id}); API.openEditor({id: style.id});
if (!liveReload.enabled) { if (!liveReload.enabled) {
if (!filePort && history.length > 1) { if (tabId < 0 && history.length > 1) {
history.back(); history.back();
} else { } else {
closeCurrentTab(); closeCurrentTab();
@ -234,8 +227,6 @@
} }
function init({style, dup}) { function init({style, dup}) {
initialized = true;
const data = style.usercssData; const data = style.usercssData;
const dupData = dup && dup.usercssData; const dupData = dup && dup.usercssData;
const versionTest = dup && semverCompare(data.version, dupData.version); const versionTest = dup && semverCompare(data.version, dupData.version);
@ -321,66 +312,55 @@
const DELAY = 500; const DELAY = 500;
let isEnabled = false; let isEnabled = false;
let timer = 0; let timer = 0;
let sequence = Promise.resolve(); /** @type function(?options):Promise<string|null> */
if (tabId >= 0) { let getData = null;
filePort = chrome.tabs.connect(tabId, {name: 'downloadSelf'}); /** @type Promise */
filePort.postMessage('init'); let sequence = null;
filePort.onMessage.addListener(onPortMessage); if (tabId < 0) {
filePort.onDisconnect.addListener(onPortDisconnect); getData = DirectDownloader();
sequence = API.getUsercssInstallCode(initialUrl).catch(getData);
} else {
getData = PortDownloader();
sequence = getData({timer: false});
} }
return { return {
get enabled() { get enabled() {
return isEnabled; return isEnabled;
}, },
ready: sequence,
onToggled(e) { onToggled(e) {
if (e) isEnabled = e.target.checked; if (e) isEnabled = e.target.checked;
if (installed || installedDup) { if (installed || installedDup) {
(isEnabled ? start : stop)(); (isEnabled ? start : stop)();
$('.install').disabled = isEnabled; $('.install').disabled = isEnabled;
$('#live-reload-install-hint').hidden = !isEnabled; Object.assign($('#live-reload-install-hint'), {
$('#live-reload-install-hint-ff').hidden = !isEnabled || !filePort; hidden: !isEnabled,
textContent: t(`liveReloadInstallHint${tabId >= 0 ? 'FF' : ''}`),
});
} }
}, },
}; };
function onPortMessage({code, error}) {
if (error) {
messageBox.alert(error, 'pre');
} else if (!initialized) {
initSourceCode(code);
} else {
update(code);
}
}
function onPortDisconnect() {
chrome.tabs.get(tabId, tab => {
if (chrome.runtime.lastError) {
closeCurrentTab();
} else if (tab.url === initialUrl) {
location.reload();
}
});
}
function check() { function check() {
start(true); getData()
if (filePort) { .then(update, logError)
filePort.postMessage('timer'); .then(() => {
} else { timer = 0;
download(initialUrl).then(update, logError); start();
} });
} }
function logError(error) { function logError(error) {
console.warn(t('liveReloadError', error)); console.warn(t('liveReloadError', error));
} }
function start(reset) { function start() {
timer = !reset && timer || setTimeout(check, DELAY); timer = timer || setTimeout(check, DELAY);
} }
function stop() { function stop() {
clearTimeout(check); clearTimeout(timer);
timer = 0; timer = 0;
} }
function update(code) { function update(code) {
if (!code) return logError('EMPTY'); if (code == null) return;
sequence = sequence.then(() => { sequence = sequence.catch(console.error).then(() => {
const {id} = installed || installedDup; const {id} = installed || installedDup;
const scrollInfo = cm.getScrollInfo(); const scrollInfo = cm.getScrollInfo();
const cursor = cm.getCursor(); const cursor = cm.getCursor();
@ -392,5 +372,41 @@
.catch(showError); .catch(showError);
}); });
} }
function DirectDownloader() {
let oldCode = null;
const passChangedCode = code => {
const isSame = code === oldCode;
oldCode = code;
return isSame ? null : code;
};
return () => download(initialUrl).then(passChangedCode);
}
function PortDownloader() {
const resolvers = new Map();
const port = chrome.tabs.connect(tabId, {name: 'downloadSelf'});
port.onMessage.addListener(({id, code, error}) => {
const r = resolvers.get(id);
resolvers.delete(id);
if (error) {
r.reject(error);
} else {
r.resolve(code);
}
});
port.onDisconnect.addListener(() => {
chrome.tabs.get(tabId, tab => {
if (chrome.runtime.lastError) {
closeCurrentTab();
} else if (tab.url === initialUrl) {
location.reload();
}
});
});
return ({timer = true} = {}) => new Promise((resolve, reject) => {
const id = performance.now();
resolvers.set(id, {resolve, reject});
port.postMessage({id, timer});
});
}
} }
})(); })();

View File

@ -242,7 +242,6 @@ self.API = self.INJECTED === 1 ? self.API : new Proxy({
// Handlers for these methods need sender.tab.id which is set by `send` as it uses messaging, // Handlers for these methods need sender.tab.id which is set by `send` as it uses messaging,
// unlike `sendBg` which invokes the background page directly in our own extension tabs // unlike `sendBg` which invokes the background page directly in our own extension tabs
getTabUrlPrefix: true, getTabUrlPrefix: true,
getUsercssInstallCode: true,
updateIconBadge: true, updateIconBadge: true,
styleViaAPI: true, styleViaAPI: true,
}, { }, {