diff --git a/background/background.js b/background/background.js index f2b7c3a1..9c4cd62f 100644 --- a/background/background.js +++ b/background/background.js @@ -1,6 +1,6 @@ /* global detectSloppyRegexps download prefs openURL FIREFOX CHROME VIVALDI openEditor debounce URLS ignoreChromeError queryTabs getTab - usercss styleManager db msg navigatorUtil + usercss styleManager db msg navigatorUtil iconUtil */ 'use strict'; @@ -35,7 +35,10 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, { detectSloppyRegexps, openEditor, - updateIcon, + + updateIconBadge(count) { + return updateIconBadge(this.sender.tab.id, count); + }, // exposed for stuff that requires followup sendMessage() like popup::openSettings // that would fail otherwise if another extension forced the tab to open @@ -128,37 +131,20 @@ if (chrome.commands) { chrome.commands.onCommand.addListener(command => browserCommands[command]()); } -if (!chrome.browserAction || - !['setIcon', 'setBadgeBackgroundColor', 'setBadgeText'].every(name => chrome.browserAction[name])) { - window.updateIcon = () => {}; -} - const tabIcons = new Map(); chrome.tabs.onRemoved.addListener(tabId => tabIcons.delete(tabId)); chrome.tabs.onReplaced.addListener((added, removed) => tabIcons.delete(removed)); -// ************************************************************************* -// set the default icon displayed after a tab is created until webNavigation kicks in -prefs.subscribe(['iconset'], () => - updateIcon({ - tab: {id: undefined}, - styles: {}, - })); - -navigatorUtil.onUrlChange(({url, tabId, frameId}) => { - if (frameId === 0) { - tabIcons.delete(tabId); - updateIcon({tab: {id: tabId, url}}); - } -}); - prefs.subscribe([ - 'show-badge', 'disableAll', 'badgeDisabled', 'badgeNormal', +], () => debounce(refreshIconBadgeColor)); + +prefs.subscribe([ + 'show-badge', 'iconset', -], () => debounce(updateAllTabsIcon)); +], () => debounce(refreshAllIcons)); // ************************************************************************* chrome.runtime.onInstalled.addListener(({reason}) => { @@ -250,19 +236,28 @@ if (chrome.contextMenus) { createContextMenus(keys); } -// ************************************************************************* -// [re]inject content scripts -window.addEventListener('storageReady', function _() { - window.removeEventListener('storageReady', _); +if (!FIREFOX) { + reinjectContentScripts(); +} - updateIcon({ - tab: {id: undefined}, - styles: {}, +// FIXME: implement exposeIframes in apply.js + +// register hotkeys +if (FIREFOX && browser.commands && browser.commands.update) { + const hotkeyPrefs = Object.keys(prefs.defaults).filter(k => k.startsWith('hotkey.')); + prefs.subscribe(hotkeyPrefs, (name, value) => { + try { + name = name.split('.')[1]; + if (value.trim()) { + browser.commands.update({name, shortcut: value}); + } else { + browser.commands.reset(name); + } + } catch (e) {} }); +} - // Firefox injects content script automatically - if (FIREFOX) return; - +function reinjectContentScripts() { const NTP = 'chrome://newtab/'; const ALL_URLS = ''; const contentScripts = chrome.runtime.getManifest().content_scripts; @@ -309,23 +304,6 @@ window.addEventListener('storageReady', function _() { setTimeout(pingCS, 0, cs, tab)); } })); -}); - -// FIXME: implement exposeIframes in apply.js - -// register hotkeys -if (FIREFOX && browser.commands && browser.commands.update) { - const hotkeyPrefs = Object.keys(prefs.defaults).filter(k => k.startsWith('hotkey.')); - prefs.subscribe(hotkeyPrefs, (name, value) => { - try { - name = name.split('.')[1]; - if (value.trim()) { - browser.commands.update({name, shortcut: value}); - } else { - browser.commands.reset(name); - } - } catch (e) {} - }); } function webNavUsercssInstallerFF(data) { @@ -362,98 +340,55 @@ function webNavIframeHelperFF({tabId, frameId}) { }); } - -function updateIcon({tab, styles}) { - if (tab.id < 0) { +function updateIconBadge(tabId, count) { + let tabIcon = tabIcons.get(tabId); + if (!tabIcon) tabIcons.set(tabId, (tabIcon = {})); + if (tabIcon.count === count) { return; } - if (URLS.chromeProtectsNTP && tab.url === 'chrome://newtab/') { - styles = {}; + tabIcon.count = count; + iconUtil.setBadgeText({ + text: prefs.get('show-badge') && count ? String(count) : '', + tabId + }); + if (!count) { + refreshIcon(tabId, tabIcon); } - if (styles) { - stylesReceived(styles); +} + +function refreshIcon(tabId, icon) { + const disableAll = prefs.get('disableAll'); + const iconset = prefs.get('iconset') === 1 ? 'light/' : ''; + const postfix = disableAll ? 'x' : !icon.count ? 'w' : ''; + const iconType = iconset + postfix; + + if (icon.iconType === iconType) { return; } - styleManager.countStylesByUrl(tab.url, {enabled: true}) - .then(count => stylesReceived({length: count})); + icon.iconType = iconset + postfix; + const sizes = FIREFOX || CHROME >= 2883 && !VIVALDI ? [16, 32] : [19, 38]; + iconUtil.setIcon({ + path: sizes.reduce( + (obj, size) => { + obj[size] = `/images/icon/${iconset}${size}${postfix}.png`; + return obj; + }, + {} + ), + tabId + }); +} - function stylesReceived(styles) { - const disableAll = prefs.get('disableAll'); - const postfix = disableAll ? 'x' : !styles.length ? 'w' : ''; - const color = prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal'); - const text = prefs.get('show-badge') && styles.length ? String(styles.length) : ''; - const iconset = ['', 'light/'][prefs.get('iconset')] || ''; - let tabIcon = tabIcons.get(tab.id); - if (!tabIcon) tabIcons.set(tab.id, (tabIcon = {})); +function refreshIconBadgeColor() { + const color = prefs.get(prefs.get('disableAll') ? 'badgeDisabled' : 'badgeNormal'); + iconUtil.setBadgeBackgroundColor({ + color + }); +} - if (tabIcon.iconType !== iconset + postfix) { - tabIcon.iconType = iconset + postfix; - const sizes = FIREFOX || CHROME >= 2883 && !VIVALDI ? [16, 32] : [19, 38]; - const usePath = tabIcons.get('usePath'); - Promise.all(sizes.map(size => { - const src = `/images/icon/${iconset}${size}${postfix}.png`; - return usePath ? src : tabIcons.get(src) || loadIcon(src); - })).then(data => { - const imageKey = typeof data[0] === 'string' ? 'path' : 'imageData'; - const imageData = {}; - sizes.forEach((size, i) => (imageData[size] = data[i])); - chrome.browserAction.setIcon({ - tabId: tab.id, - [imageKey]: imageData, - }, ignoreChromeError); - }); - } - if (tab.id === undefined) return; - - let defaultIcon = tabIcons.get(undefined); - if (!defaultIcon) tabIcons.set(undefined, (defaultIcon = {})); - if (defaultIcon.color !== color) { - defaultIcon.color = color; - chrome.browserAction.setBadgeBackgroundColor({color}); - } - - if (tabIcon.text === text) return; - tabIcon.text = text; - try { - // Chrome supports the callback since 67.0.3381.0, see https://crbug.com/451320 - chrome.browserAction.setBadgeText({text, tabId: tab.id}, ignoreChromeError); - } catch (e) { - setTimeout(() => { - getTab(tab.id).then(realTab => { - // skip pre-rendered tabs - if (realTab.index >= 0) { - chrome.browserAction.setBadgeText({text, tabId: tab.id}); - } - }); - }); - } - } - - function loadIcon(src, resolve) { - if (!resolve) return new Promise(resolve => loadIcon(src, resolve)); - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - const img = new Image(); - img.src = src; - img.onload = () => { - const w = canvas.width = img.width; - const h = canvas.height = img.height; - ctx.clearRect(0, 0, w, h); - ctx.drawImage(img, 0, 0, w, h); - const data = ctx.getImageData(0, 0, w, h); - // Firefox breaks Canvas when privacy.resistFingerprinting=true, https://bugzil.la/1412961 - let usePath = tabIcons.get('usePath'); - if (usePath === undefined) { - usePath = data.data.every(b => b === 255); - tabIcons.set('usePath', usePath); - } - if (usePath) { - resolve(src); - return; - } - tabIcons.set(src, data); - resolve(data); - }; +function refreshAllIcons() { + for (const [tabId, icon] of tabIcons) { + refreshIcon(tabId, icon); } } @@ -469,12 +404,6 @@ function onRuntimeMessage(msg, sender) { return fn.apply(context, msg.args); } -function updateAllTabsIcon() { - return queryTabs().then(tabs => - tabs.map(t => updateIcon({tab: t})) - ); -} - function openEditor({id}) { let url = '/edit.html'; if (id) { diff --git a/background/icon-util.js b/background/icon-util.js new file mode 100644 index 00000000..ef7b2822 --- /dev/null +++ b/background/icon-util.js @@ -0,0 +1,91 @@ +/* global ignoreChromeError */ +/* exported iconUtil */ +'use strict'; + +const iconUtil = (() => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + // https://github.com/openstyles/stylus/issues/335 + let noCanvas; + const imageDataCache = new Map(); + // test if canvas is usable + const canvasReady = loadImage('/images/icon/16.png') + .then(imageData => { + noCanvas = imageData.data.every(b => b === 255); + }); + + return extendNative({ + /* + Cache imageData for paths + */ + setIcon, + setBadgeText + }); + + function loadImage(url) { + let result = imageDataCache.get(url); + if (!result) { + result = new Promise((resolve, reject) => { + const img = new Image(); + img.src = url; + img.onload = () => { + const w = canvas.width = img.width; + const h = canvas.height = img.height; + ctx.clearRect(0, 0, w, h); + ctx.drawImage(img, 0, 0, w, h); + resolve(ctx.getImageData(0, 0, w, h)); + }; + img.onerror = reject; + }); + imageDataCache.set(url, result); + } + return result; + } + + function setIcon(data) { + canvasReady.then(() => { + if (noCanvas) { + chrome.browserAction.setIcon(data, ignoreChromeError); + return; + } + const pending = []; + data.imageData = {}; + for (const [key, url] of Object.entries(data.path)) { + pending.push(loadImage(url) + .then(imageData => { + data.imageData[key] = imageData; + })); + } + Promise.all(pending).then(() => { + delete data.path; + chrome.browserAction.setIcon(data, ignoreChromeError); + }); + }); + } + + function setBadgeText(data) { + try { + // Chrome supports the callback since 67.0.3381.0, see https://crbug.com/451320 + chrome.browserAction.setBadgeText(data, ignoreChromeError); + } catch (e) { + // FIXME: skip pre-rendered tabs? + chrome.browserAction.setBadgeText(data); + } + } + + function extendNative(target) { + return new Proxy(target, { + get: (target, prop) => { + // FIXME: do we really need this? + if (!chrome.browserAction || + !['setIcon', 'setBadgeBackgroundColor', 'setBadgeText'].every(name => chrome.browserAction[name])) { + return () => {}; + } + if (target[prop]) { + return target[prop]; + } + return chrome.browserAction[prop].bind(chrome.browserAction); + } + }); + } +})(); diff --git a/content/apply.js b/content/apply.js index fbb39e1f..a9e3773f 100644 --- a/content/apply.js +++ b/content/apply.js @@ -2,11 +2,9 @@ /* global msg API prefs */ 'use strict'; -(() => { - if (typeof window.applyOnMessage === 'function') { - // some weird bug in new Chrome: the content script gets injected multiple times - return; - } +// some weird bug in new Chrome: the content script gets injected multiple times +// define a constant so it throws when redefined +const APPLY = (() => { const CHROME = chrome.app ? parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]) : NaN; var ID_PREFIX = 'stylus-'; var ROOT = document.documentElement; @@ -42,7 +40,6 @@ }); } msg.onTab(applyOnMessage); - window.applyOnMessage = applyOnMessage; if (!isOwnPage) { window.dispatchEvent(new CustomEvent(chrome.runtime.id)); @@ -139,10 +136,6 @@ } break; - case 'styleApply': - applyStyles(request.styles); - break; - case 'urlChanged': API.getSectionsByUrl(getMatchUrl(), {enabled: true}) .then(buildSections) @@ -187,6 +180,25 @@ } } + function updateCount() { + if (window !== parent) { + // we don't care about iframes + return; + } + let count = 0; + for (const id of styleElements.keys()) { + if (!disabledElements.has(id)) { + count++; + } + } + // we have to send the tabId so we can't use `sendBg` that is used by `API` + msg.send({ + method: 'invokeAPI', + name: 'updateIconBadge', + args: [count] + }); + } + function applyStyleState({id, enabled}) { const inCache = disabledElements.get(id) || styleElements.get(id); const inDoc = document.getElementById(ID_PREFIX + id); @@ -197,7 +209,7 @@ addStyleElement(inCache); disabledElements.delete(id); } else { - API.getSectionsByUrl(getMatchUrl(), {id}) + return API.getSectionsByUrl(getMatchUrl(), {id}) .then(buildSections) .then(applyStyles); } @@ -207,6 +219,7 @@ docRootObserver.evade(() => inDoc.remove()); } } + updateCount(); } function removeStyle({id, retire = false}) { @@ -224,12 +237,18 @@ docRootObserver.evade(() => el.remove()); } } - styleElements.delete(ID_PREFIX + id); disabledElements.delete(id); retiredStyleTimers.delete(id); + if (styleElements.delete(ID_PREFIX + id)) { + updateCount(); + } } function applyStyles(styles) { + if (!styles.length) { + return; + } + if (!document.documentElement) { new MutationObserver((mutations, observer) => { if (document.documentElement) { @@ -240,16 +259,12 @@ return; } - const gotNewStyles = styles.length || styles.needTransitionPatch; - if (gotNewStyles) { + if (styles.length) { if (docRootObserver) { docRootObserver.stop(); } else { initDocRootObserver(); } - } - - if (gotNewStyles) { for (const section of styles) { applySections(section.id, section.code); } @@ -275,6 +290,9 @@ } updateExposeIframes(); + if (styles.length) { + updateCount(); + } } function applySections(styleId, code) { diff --git a/js/messaging.js b/js/messaging.js index e8335bd9..1ab433b5 100644 --- a/js/messaging.js +++ b/js/messaging.js @@ -78,12 +78,6 @@ if (!IS_BG) { } else { if (VIVALDI) document.documentElement.classList.add('vivaldi'); } - // TODO: remove once our manifest's minimum_chrome_version is 50+ - // Chrome 49 doesn't report own extension pages in webNavigation apparently - if (CHROME && CHROME < 2661) { - getActiveTab().then(tab => - window.API.updateIcon({tab})); - } } if (IS_BG) {