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"
},
"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"
},
"liveReloadInstallHintFF": {
"message": "Keep the original tab open too as it's needed for local file:// URLs in Firefox 68 and newer.",
"description": "The extra hint of live-reload feature shown only for file:// URLs in Firefox 68+"
"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"
},
"liveReloadLabel": {
"message": "Live reload",

View File

@ -2,7 +2,7 @@
URLS ignoreChromeError usercssHelper
styleManager msg navigatorUtil workerUtil contentScripts sync
findExistingTab createTab activateTab isTabReplaceable getActiveTab
iconManager */
iconManager tabManager */
'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) {
// FF misses some about:blank iframes so we inject our content script explicitly
navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, {
@ -108,13 +116,6 @@ if (chrome.commands) {
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}) => {
// 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?
function updateIconBadge(tabId, count, force = true) {
tabManager.setMeta(tabId, 'count', count);
tabManager.set(tabId, 'count', count);
refreshIconBadgeText(tabId);
refreshIcon(tabId, force);
}
function refreshIconBadgeText(tabId) {
const count = tabManager.getMeta(tabId, 'count');
const count = tabManager.get(tabId, 'count');
iconUtil.setBadgeText({
text: prefs.get('show-badge') && count ? String(count) : '',
tabId
@ -50,13 +50,13 @@ const iconManager = (() => {
}
function refreshIcon(tabId, force = false) {
const oldIcon = tabManager.getMeta(tabId, 'icon');
const newIcon = getIconName(tabManager.getMeta(tabId, 'count'));
const oldIcon = tabManager.get(tabId, 'icon');
const newIcon = getIconName(tabManager.get(tabId, 'count'));
if (!force && oldIcon === newIcon) {
return;
}
tabManager.setMeta(tabId, 'icon', newIcon);
tabManager.set(tabId, 'icon', newIcon);
iconUtil.setIcon({
path: getIconPath(newIcon),
tabId

View File

@ -9,40 +9,35 @@ const tabManager = (() => {
chrome.tabs.onReplaced.addListener((added, removed) => cache.delete(removed));
navigatorUtil.onUrlChange(({tabId, frameId, url}) => {
if (frameId) return;
setMeta(tabId, 'url', url);
emitUpdate({tabId, url});
});
return {onUpdate, setMeta, getMeta, list};
function list() {
return cache.keys();
}
function onUpdate(callback) {
listeners.push(callback);
}
function emitUpdate(e) {
for (const callback of listeners) {
const oldUrl = tabManager.get(tabId, 'url');
tabManager.set(tabId, 'url', url);
for (const fn of listeners) {
try {
callback(e);
fn({tabId, url, oldUrl});
} catch (err) {
console.error(err);
}
}
}
});
function setMeta(tabId, key, value) {
let meta = cache.get(tabId);
if (!meta) {
meta = new Map();
cache.set(tabId, meta);
}
meta.set(key, value);
}
function getMeta(tabId, key) {
return cache.get(tabId).get(key);
}
return {
onUpdate(fn) {
listeners.push(fn);
},
get(tabId, key) {
const meta = cache.get(tabId);
return meta && meta[key];
},
set(tabId, key, value) {
let meta = cache.get(tabId);
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 */
'use strict';
// eslint-disable-next-line no-unused-vars
const usercssHelper = (() => {
// detecting FF68 by the added feature as navigator.ua may be spoofed via about:config or devtools
const tabExec = !chrome.app && chrome.storage.managed && promisify(chrome.tabs.executeScript.bind(chrome.tabs));
const downloadSelf = tabExec && {file: '/content/download-self.js'};
const installCodeCache = new Map();
const clearInstallCode = url => installCodeCache.delete(url);
const isPlainCssResponse = r => /^text\/(css|plain)(;.*?)?$/i.test(r.headers.get('content-type'));
const installCodeCache = {};
const clearInstallCode = url => delete installCodeCache[url];
const isResponseText = r => /^text\/(css|plain)(;.*?)?$/i.test(r.headers.get('content-type'));
// in Firefox we have to use a content script to read file://
const fileLoader = !chrome.app && // not relying on navigator.ua which can be spoofed
(tabId => browser.tabs.executeScript(tabId, {file: '/content/install-hook-usercss.js'}).then(r => r[0]));
API_METHODS.installUsercss = installUsercss;
API_METHODS.editSaveUsercss = editSaveUsercss;
@ -19,10 +19,11 @@ const usercssHelper = (() => {
API_METHODS.findUsercss = find;
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);
clearTimeout(timer);
return code || '';
return code;
};
return {
@ -30,35 +31,34 @@ const usercssHelper = (() => {
testUrl(url) {
return url.includes('.user.') &&
/^(https?|file|ftps?):/.test(url) &&
/\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]) &&
!url.startsWith(URLS.installUsercss);
/\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]);
},
openInstallerPage(tabId, url) {
/** @return {Promise<{ code:string, inTab:boolean } | false>} */
testContents(tabId, url) {
const isFile = url.startsWith('file:');
const isFileFF = isFile && tabExec;
return Promise.resolve(isFile || fetch(url, {method: 'HEAD'}).then(isPlainCssResponse))
.then(ok => ok && (isFileFF ? tabExec(tabId, downloadSelf) : download(url)))
.then(code => {
if (Array.isArray(code)) code = code[0];
if (!/==userstyle==/i.test(code)) return;
const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`;
if (isFileFF) {
getTab(tabId).then(tab =>
openURL({
url: `${newUrl}&tabId=${tabId}`,
active: tab.active,
index: tab.index + 1,
openerTabId: tabId,
currentWindow: null,
}));
} else {
const timer = setTimeout(clearInstallCode, 10e3, url);
installCodeCache.set(url, {code, timer});
chrome.tabs.update(tabId, {url: newUrl});
return newUrl;
}
});
const inTab = isFile && Boolean(fileLoader);
return Promise.resolve(isFile || fetch(url, {method: 'HEAD'}).then(isResponseText))
.then(ok => ok && (inTab ? fileLoader(tabId) : download(url)))
.then(code => /==userstyle==/i.test(code) && {code, inTab});
},
openInstallerPage(tabId, url, {code, inTab} = {}) {
const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`;
if (inTab) {
getTab(tabId).then(tab =>
openURL({
url: `${newUrl}&tabId=${tabId}`,
active: tab.active,
index: tab.index + 1,
openerTabId: tabId,
currentWindow: null,
}));
} else {
const timer = setTimeout(clearInstallCode, 10e3, url);
installCodeCache[url] = {code, timer};
chrome.tabs.update(tabId, {url: newUrl});
}
},
};

View File

@ -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">
<h2 class="installed" i18n-text="installButtonInstalled"></h2>
<button class="install" i18n-text="installButton"></button>
<p id="live-reload-install-hint" i18n-text="liveReloadInstallHint" hidden></p>
<p id="live-reload-install-hint-ff" i18n-text="liveReloadInstallHintFF" hidden></p>
<p id="live-reload-install-hint" hidden></p>
<label class="set-update-url">
<input type="checkbox">
<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 tabId = params.has('tabId') ? Number(params.get('tabId')) : -1;
const initialUrl = params.get('updateUrl');
if (!initialUrl) throw 'No updateUrl parameter';
let installed = null;
let installedDup = null;
let initialized = false;
let filePort;
const liveReload = initLiveReload();
// 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));
liveReload.ready.then(initSourceCode, error => messageBox.alert(error, 'pre'));
const theme = prefs.get('editor.theme');
const cm = CodeMirror($('.main'), {
@ -172,7 +165,7 @@
} else {
API.openEditor({id: style.id});
if (!liveReload.enabled) {
if (!filePort && history.length > 1) {
if (tabId < 0 && history.length > 1) {
history.back();
} else {
closeCurrentTab();
@ -234,8 +227,6 @@
}
function init({style, dup}) {
initialized = true;
const data = style.usercssData;
const dupData = dup && dup.usercssData;
const versionTest = dup && semverCompare(data.version, dupData.version);
@ -321,66 +312,55 @@
const DELAY = 500;
let isEnabled = false;
let timer = 0;
let sequence = Promise.resolve();
if (tabId >= 0) {
filePort = chrome.tabs.connect(tabId, {name: 'downloadSelf'});
filePort.postMessage('init');
filePort.onMessage.addListener(onPortMessage);
filePort.onDisconnect.addListener(onPortDisconnect);
/** @type function(?options):Promise<string|null> */
let getData = null;
/** @type Promise */
let sequence = null;
if (tabId < 0) {
getData = DirectDownloader();
sequence = API.getUsercssInstallCode(initialUrl).catch(getData);
} else {
getData = PortDownloader();
sequence = getData({timer: false});
}
return {
get enabled() {
return isEnabled;
},
ready: sequence,
onToggled(e) {
if (e) isEnabled = e.target.checked;
if (installed || installedDup) {
(isEnabled ? start : stop)();
$('.install').disabled = isEnabled;
$('#live-reload-install-hint').hidden = !isEnabled;
$('#live-reload-install-hint-ff').hidden = !isEnabled || !filePort;
Object.assign($('#live-reload-install-hint'), {
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() {
start(true);
if (filePort) {
filePort.postMessage('timer');
} else {
download(initialUrl).then(update, logError);
}
getData()
.then(update, logError)
.then(() => {
timer = 0;
start();
});
}
function logError(error) {
console.warn(t('liveReloadError', error));
}
function start(reset) {
timer = !reset && timer || setTimeout(check, DELAY);
function start() {
timer = timer || setTimeout(check, DELAY);
}
function stop() {
clearTimeout(check);
clearTimeout(timer);
timer = 0;
}
function update(code) {
if (!code) return logError('EMPTY');
sequence = sequence.then(() => {
if (code == null) return;
sequence = sequence.catch(console.error).then(() => {
const {id} = installed || installedDup;
const scrollInfo = cm.getScrollInfo();
const cursor = cm.getCursor();
@ -392,5 +372,41 @@
.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,
// unlike `sendBg` which invokes the background page directly in our own extension tabs
getTabUrlPrefix: true,
getUsercssInstallCode: true,
updateIconBadge: true,
styleViaAPI: true,
}, {