icon-manager.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. /* global API */// msg.js
  2. /* global addAPI bgReady */// common.js
  3. /* global prefs */
  4. /* global tabMan */
  5. /* global CHROME FIREFOX VIVALDI debounce ignoreChromeError */// toolbox.js
  6. 'use strict';
  7. /* exported iconMan */
  8. const iconMan = (() => {
  9. const ICON_SIZES = FIREFOX || CHROME >= 55 && !VIVALDI ? [16, 32] : [19, 38];
  10. const staleBadges = new Set();
  11. const imageDataCache = new Map();
  12. const badgeOvr = {color: '', text: ''};
  13. // https://github.com/openstyles/stylus/issues/1287 Fenix can't use custom ImageData
  14. const FIREFOX_ANDROID = FIREFOX && navigator.userAgent.includes('Android');
  15. // https://github.com/openstyles/stylus/issues/335
  16. let hasCanvas = FIREFOX_ANDROID ? false : loadImage(`/images/icon/${ICON_SIZES[0]}.png`)
  17. .then(({data}) => (hasCanvas = data.some(b => b !== 255)));
  18. addAPI(/** @namespace API */ {
  19. /**
  20. * @param {(number|string)[]} styleIds
  21. * @param {boolean} [lazyBadge=false] preventing flicker during page load
  22. */
  23. updateIconBadge(styleIds, {lazyBadge} = {}) {
  24. // FIXME: in some cases, we only have to redraw the badge. is it worth a optimization?
  25. const {frameId, tab: {id: tabId}} = this.sender;
  26. const value = styleIds.length ? styleIds.map(Number) : undefined;
  27. tabMan.set(tabId, 'styleIds', frameId, value);
  28. debounce(refreshStaleBadges, frameId && lazyBadge ? 250 : 0);
  29. staleBadges.add(tabId);
  30. if (!frameId) refreshIcon(tabId, true);
  31. },
  32. });
  33. chrome.webNavigation.onCommitted.addListener(({tabId, frameId}) => {
  34. if (!frameId) tabMan.set(tabId, 'styleIds', undefined);
  35. });
  36. chrome.runtime.onConnect.addListener(port => {
  37. if (port.name === 'iframe') {
  38. port.onDisconnect.addListener(onPortDisconnected);
  39. }
  40. });
  41. bgReady.all.then(() => {
  42. prefs.subscribe([
  43. 'disableAll',
  44. 'badgeDisabled',
  45. 'badgeNormal',
  46. ], () => debounce(refreshIconBadgeColor), {runNow: true});
  47. prefs.subscribe([
  48. 'show-badge',
  49. ], () => debounce(refreshAllIconsBadgeText), {runNow: true});
  50. prefs.subscribe([
  51. 'disableAll',
  52. 'iconset',
  53. ], () => debounce(refreshAllIcons), {runNow: true});
  54. });
  55. return {
  56. /** Calling with no params clears the override */
  57. overrideBadge({text = '', color = '', title = ''} = {}) {
  58. if (badgeOvr.text === text) {
  59. return;
  60. }
  61. badgeOvr.text = text;
  62. badgeOvr.color = color;
  63. refreshIconBadgeColor();
  64. setBadgeText({text});
  65. for (const tabId of tabMan.list()) {
  66. if (text) {
  67. setBadgeText({tabId, text});
  68. } else {
  69. refreshIconBadgeText(tabId);
  70. }
  71. }
  72. chrome.browserAction.setTitle({
  73. title: title && chrome.i18n.getMessage(title) || title || '',
  74. });
  75. },
  76. };
  77. function onPortDisconnected({sender}) {
  78. if (tabMan.get(sender.tab.id, 'styleIds')) {
  79. API.updateIconBadge.call({sender}, [], {lazyBadge: true});
  80. }
  81. }
  82. function refreshIconBadgeText(tabId) {
  83. if (badgeOvr.text) return;
  84. const text = prefs.get('show-badge') ? `${getStyleCount(tabId)}` : '';
  85. setBadgeText({tabId, text});
  86. }
  87. function getIconName(hasStyles = false) {
  88. const iconset = prefs.get('iconset') === 1 ? 'light/' : '';
  89. const postfix = prefs.get('disableAll') ? 'x' : !hasStyles ? 'w' : '';
  90. return `${iconset}$SIZE$${postfix}`;
  91. }
  92. function refreshIcon(tabId, force = false) {
  93. const oldIcon = tabMan.get(tabId, 'icon');
  94. const newIcon = getIconName(tabMan.get(tabId, 'styleIds', 0));
  95. // (changing the icon only for the main page, frameId = 0)
  96. if (!force && oldIcon === newIcon) {
  97. return;
  98. }
  99. tabMan.set(tabId, 'icon', newIcon);
  100. setIcon({
  101. path: getIconPath(newIcon),
  102. tabId,
  103. });
  104. }
  105. function getIconPath(icon) {
  106. return ICON_SIZES.reduce(
  107. (obj, size) => {
  108. obj[size] = `/images/icon/${icon.replace('$SIZE$', size)}.png`;
  109. return obj;
  110. },
  111. {}
  112. );
  113. }
  114. /** @return {number | ''} */
  115. function getStyleCount(tabId) {
  116. const allIds = new Set();
  117. const data = tabMan.get(tabId, 'styleIds') || {};
  118. Object.values(data).forEach(frameIds => frameIds.forEach(id => allIds.add(id)));
  119. return allIds.size || '';
  120. }
  121. // Caches imageData for icon paths
  122. async function loadImage(url) {
  123. const {OffscreenCanvas} = !FIREFOX && self.createImageBitmap && self || {};
  124. const img = OffscreenCanvas
  125. ? await createImageBitmap(await (await fetch(url)).blob())
  126. : await new Promise((resolve, reject) =>
  127. Object.assign(new Image(), {
  128. src: url,
  129. onload: e => resolve(e.target),
  130. onerror: reject,
  131. }));
  132. const {width: w, height: h} = img;
  133. const canvas = OffscreenCanvas
  134. ? new OffscreenCanvas(w, h)
  135. : Object.assign(document.createElement('canvas'), {width: w, height: h});
  136. const ctx = canvas.getContext('2d');
  137. ctx.drawImage(img, 0, 0, w, h);
  138. const result = ctx.getImageData(0, 0, w, h);
  139. imageDataCache.set(url, result);
  140. return result;
  141. }
  142. function refreshGlobalIcon() {
  143. setIcon({
  144. path: getIconPath(getIconName()),
  145. });
  146. }
  147. function refreshIconBadgeColor() {
  148. setBadgeBackgroundColor({
  149. color: badgeOvr.color ||
  150. prefs.get(prefs.get('disableAll') ? 'badgeDisabled' : 'badgeNormal'),
  151. });
  152. }
  153. function refreshAllIcons() {
  154. for (const tabId of tabMan.list()) {
  155. refreshIcon(tabId);
  156. }
  157. refreshGlobalIcon();
  158. }
  159. function refreshAllIconsBadgeText() {
  160. for (const tabId of tabMan.list()) {
  161. refreshIconBadgeText(tabId);
  162. }
  163. }
  164. function refreshStaleBadges() {
  165. for (const tabId of staleBadges) {
  166. refreshIconBadgeText(tabId);
  167. }
  168. staleBadges.clear();
  169. }
  170. function safeCall(method, data) {
  171. const {browserAction = {}} = chrome;
  172. const fn = browserAction[method];
  173. if (fn) {
  174. try {
  175. // Chrome supports the callback since 67.0.3381.0, see https://crbug.com/451320
  176. fn.call(browserAction, data, ignoreChromeError);
  177. } catch (e) {
  178. // FIXME: skip pre-rendered tabs?
  179. fn.call(browserAction, data);
  180. }
  181. }
  182. }
  183. /** @param {chrome.browserAction.TabIconDetails} data */
  184. async function setIcon(data) {
  185. if (hasCanvas === true || await hasCanvas) {
  186. data.imageData = {};
  187. for (const [key, url] of Object.entries(data.path)) {
  188. data.imageData[key] = imageDataCache.get(url) || await loadImage(url);
  189. }
  190. delete data.path;
  191. }
  192. safeCall('setIcon', data);
  193. }
  194. /** @param {chrome.browserAction.BadgeTextDetails} data */
  195. function setBadgeText(data) {
  196. safeCall('setBadgeText', data);
  197. }
  198. /** @param {chrome.browserAction.BadgeBackgroundColorDetails} data */
  199. function setBadgeBackgroundColor(data) {
  200. safeCall('setBadgeBackgroundColor', data);
  201. }
  202. })();