Add: icon-util

This commit is contained in:
eight 2018-10-11 15:42:23 +08:00
parent 510a886e14
commit 30e494eda9
4 changed files with 197 additions and 165 deletions

View File

@ -1,6 +1,6 @@
/* global detectSloppyRegexps download prefs openURL FIREFOX CHROME VIVALDI /* global detectSloppyRegexps download prefs openURL FIREFOX CHROME VIVALDI
openEditor debounce URLS ignoreChromeError queryTabs getTab openEditor debounce URLS ignoreChromeError queryTabs getTab
usercss styleManager db msg navigatorUtil usercss styleManager db msg navigatorUtil iconUtil
*/ */
'use strict'; 'use strict';
@ -35,7 +35,10 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
detectSloppyRegexps, detectSloppyRegexps,
openEditor, openEditor,
updateIcon,
updateIconBadge(count) {
return updateIconBadge(this.sender.tab.id, count);
},
// exposed for stuff that requires followup sendMessage() like popup::openSettings // exposed for stuff that requires followup sendMessage() like popup::openSettings
// that would fail otherwise if another extension forced the tab to open // 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]()); chrome.commands.onCommand.addListener(command => browserCommands[command]());
} }
if (!chrome.browserAction ||
!['setIcon', 'setBadgeBackgroundColor', 'setBadgeText'].every(name => chrome.browserAction[name])) {
window.updateIcon = () => {};
}
const tabIcons = new Map(); const tabIcons = new Map();
chrome.tabs.onRemoved.addListener(tabId => tabIcons.delete(tabId)); chrome.tabs.onRemoved.addListener(tabId => tabIcons.delete(tabId));
chrome.tabs.onReplaced.addListener((added, removed) => tabIcons.delete(removed)); 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([ prefs.subscribe([
'show-badge',
'disableAll', 'disableAll',
'badgeDisabled', 'badgeDisabled',
'badgeNormal', 'badgeNormal',
], () => debounce(refreshIconBadgeColor));
prefs.subscribe([
'show-badge',
'iconset', 'iconset',
], () => debounce(updateAllTabsIcon)); ], () => debounce(refreshAllIcons));
// ************************************************************************* // *************************************************************************
chrome.runtime.onInstalled.addListener(({reason}) => { chrome.runtime.onInstalled.addListener(({reason}) => {
@ -250,19 +236,28 @@ if (chrome.contextMenus) {
createContextMenus(keys); createContextMenus(keys);
} }
// ************************************************************************* if (!FIREFOX) {
// [re]inject content scripts reinjectContentScripts();
window.addEventListener('storageReady', function _() { }
window.removeEventListener('storageReady', _);
updateIcon({ // FIXME: implement exposeIframes in apply.js
tab: {id: undefined},
styles: {}, // 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 function reinjectContentScripts() {
if (FIREFOX) return;
const NTP = 'chrome://newtab/'; const NTP = 'chrome://newtab/';
const ALL_URLS = '<all_urls>'; const ALL_URLS = '<all_urls>';
const contentScripts = chrome.runtime.getManifest().content_scripts; const contentScripts = chrome.runtime.getManifest().content_scripts;
@ -309,23 +304,6 @@ window.addEventListener('storageReady', function _() {
setTimeout(pingCS, 0, cs, tab)); 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) { function webNavUsercssInstallerFF(data) {
@ -362,98 +340,55 @@ function webNavIframeHelperFF({tabId, frameId}) {
}); });
} }
function updateIconBadge(tabId, count) {
function updateIcon({tab, styles}) { let tabIcon = tabIcons.get(tabId);
if (tab.id < 0) { if (!tabIcon) tabIcons.set(tabId, (tabIcon = {}));
if (tabIcon.count === count) {
return; return;
} }
if (URLS.chromeProtectsNTP && tab.url === 'chrome://newtab/') { tabIcon.count = count;
styles = {}; 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; return;
} }
styleManager.countStylesByUrl(tab.url, {enabled: true}) icon.iconType = iconset + postfix;
.then(count => stylesReceived({length: count})); 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) { function refreshIconBadgeColor() {
const disableAll = prefs.get('disableAll'); const color = prefs.get(prefs.get('disableAll') ? 'badgeDisabled' : 'badgeNormal');
const postfix = disableAll ? 'x' : !styles.length ? 'w' : ''; iconUtil.setBadgeBackgroundColor({
const color = prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal'); color
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 = {}));
if (tabIcon.iconType !== iconset + postfix) { function refreshAllIcons() {
tabIcon.iconType = iconset + postfix; for (const [tabId, icon] of tabIcons) {
const sizes = FIREFOX || CHROME >= 2883 && !VIVALDI ? [16, 32] : [19, 38]; refreshIcon(tabId, icon);
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);
};
} }
} }
@ -469,12 +404,6 @@ function onRuntimeMessage(msg, sender) {
return fn.apply(context, msg.args); return fn.apply(context, msg.args);
} }
function updateAllTabsIcon() {
return queryTabs().then(tabs =>
tabs.map(t => updateIcon({tab: t}))
);
}
function openEditor({id}) { function openEditor({id}) {
let url = '/edit.html'; let url = '/edit.html';
if (id) { if (id) {

91
background/icon-util.js Normal file
View File

@ -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);
}
});
}
})();

View File

@ -2,11 +2,9 @@
/* global msg API prefs */ /* global msg API prefs */
'use strict'; 'use strict';
(() => { // some weird bug in new Chrome: the content script gets injected multiple times
if (typeof window.applyOnMessage === 'function') { // define a constant so it throws when redefined
// some weird bug in new Chrome: the content script gets injected multiple times const APPLY = (() => {
return;
}
const CHROME = chrome.app ? parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]) : NaN; const CHROME = chrome.app ? parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]) : NaN;
var ID_PREFIX = 'stylus-'; var ID_PREFIX = 'stylus-';
var ROOT = document.documentElement; var ROOT = document.documentElement;
@ -42,7 +40,6 @@
}); });
} }
msg.onTab(applyOnMessage); msg.onTab(applyOnMessage);
window.applyOnMessage = applyOnMessage;
if (!isOwnPage) { if (!isOwnPage) {
window.dispatchEvent(new CustomEvent(chrome.runtime.id)); window.dispatchEvent(new CustomEvent(chrome.runtime.id));
@ -139,10 +136,6 @@
} }
break; break;
case 'styleApply':
applyStyles(request.styles);
break;
case 'urlChanged': case 'urlChanged':
API.getSectionsByUrl(getMatchUrl(), {enabled: true}) API.getSectionsByUrl(getMatchUrl(), {enabled: true})
.then(buildSections) .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}) { function applyStyleState({id, enabled}) {
const inCache = disabledElements.get(id) || styleElements.get(id); const inCache = disabledElements.get(id) || styleElements.get(id);
const inDoc = document.getElementById(ID_PREFIX + id); const inDoc = document.getElementById(ID_PREFIX + id);
@ -197,7 +209,7 @@
addStyleElement(inCache); addStyleElement(inCache);
disabledElements.delete(id); disabledElements.delete(id);
} else { } else {
API.getSectionsByUrl(getMatchUrl(), {id}) return API.getSectionsByUrl(getMatchUrl(), {id})
.then(buildSections) .then(buildSections)
.then(applyStyles); .then(applyStyles);
} }
@ -207,6 +219,7 @@
docRootObserver.evade(() => inDoc.remove()); docRootObserver.evade(() => inDoc.remove());
} }
} }
updateCount();
} }
function removeStyle({id, retire = false}) { function removeStyle({id, retire = false}) {
@ -224,12 +237,18 @@
docRootObserver.evade(() => el.remove()); docRootObserver.evade(() => el.remove());
} }
} }
styleElements.delete(ID_PREFIX + id);
disabledElements.delete(id); disabledElements.delete(id);
retiredStyleTimers.delete(id); retiredStyleTimers.delete(id);
if (styleElements.delete(ID_PREFIX + id)) {
updateCount();
}
} }
function applyStyles(styles) { function applyStyles(styles) {
if (!styles.length) {
return;
}
if (!document.documentElement) { if (!document.documentElement) {
new MutationObserver((mutations, observer) => { new MutationObserver((mutations, observer) => {
if (document.documentElement) { if (document.documentElement) {
@ -240,16 +259,12 @@
return; return;
} }
const gotNewStyles = styles.length || styles.needTransitionPatch; if (styles.length) {
if (gotNewStyles) {
if (docRootObserver) { if (docRootObserver) {
docRootObserver.stop(); docRootObserver.stop();
} else { } else {
initDocRootObserver(); initDocRootObserver();
} }
}
if (gotNewStyles) {
for (const section of styles) { for (const section of styles) {
applySections(section.id, section.code); applySections(section.id, section.code);
} }
@ -275,6 +290,9 @@
} }
updateExposeIframes(); updateExposeIframes();
if (styles.length) {
updateCount();
}
} }
function applySections(styleId, code) { function applySections(styleId, code) {

View File

@ -78,12 +78,6 @@ if (!IS_BG) {
} else { } else {
if (VIVALDI) document.documentElement.classList.add('vivaldi'); 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) { if (IS_BG) {