diff --git a/_locales/en/messages.json b/_locales/en/messages.json index f8c8aa3a..73b20638 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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", diff --git a/background/background.js b/background/background.js index 8b141d36..6cfc6f07 100644 --- a/background/background.js +++ b/background/background.js @@ -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" diff --git a/background/icon-manager.js b/background/icon-manager.js index f5d84f3b..71a8f29d 100644 --- a/background/icon-manager.js +++ b/background/icon-manager.js @@ -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 diff --git a/background/tab-manager.js b/background/tab-manager.js index 30179ac0..bcd7901a 100644 --- a/background/tab-manager.js +++ b/background/tab-manager.js @@ -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(); + }, + }; })(); diff --git a/background/usercss-helper.js b/background/usercss-helper.js index f353bb47..ea083ae9 100644 --- a/background/usercss-helper.js +++ b/background/usercss-helper.js @@ -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}); + } }, }; diff --git a/content/download-self.js b/content/download-self.js deleted file mode 100644 index 16c8552a..00000000 --- a/content/download-self.js +++ /dev/null @@ -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; diff --git a/content/install-hook-usercss.js b/content/install-hook-usercss.js new file mode 100644 index 00000000..80e837ea --- /dev/null +++ b/content/install-hook-usercss.js @@ -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 diff --git a/install-usercss.html b/install-usercss.html index 762b7ac4..d168a346 100644 --- a/install-usercss.html +++ b/install-usercss.html @@ -58,8 +58,7 @@

- - +