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
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 = '<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) {

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 */
'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) {

View File

@ -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) {