Jelajahi Sumber

feat: load dashboard icons instantly

tophf 3 tahun lalu
induk
melakukan
445092d1e7

+ 20 - 10
src/background/utils/db.js

@@ -2,7 +2,7 @@ import {
   compareVersion, dataUri2text, i18n, getScriptHome, isDataUri, makeDataUri,
   getFullUrl, getScriptName, getScriptUpdateUrl, isRemote, sendCmd, trueJoin,
 } from '@/common';
-import { INJECT_PAGE, INJECT_AUTO, TIMEOUT_WEEK } from '@/common/consts';
+import { ICON_PREFIX, INJECT_PAGE, INJECT_AUTO, TIMEOUT_WEEK } from '@/common/consts';
 import { forEachEntry, forEachKey, forEachValue } from '@/common/object';
 import storage from '@/common/storage';
 import pluginEvents from '../plugin/events';
@@ -137,9 +137,12 @@ preInitialize.push(async () => {
       scripts.push(script);
       // listing all known resource urls in order to remove unused mod keys
       const {
-        custom: { pathMap = {} } = {},
+        custom,
         meta = script.meta = {},
       } = script;
+      const {
+        pathMap = custom.pathMap = {},
+      } = custom;
       const {
         require = meta.require = [],
         resources = meta.resources = {},
@@ -388,14 +391,21 @@ export async function getData(ids) {
  * @param {VMScript[]} scripts
  * @return {Promise<{}>}
  */
-function getIconCache(scripts) {
-  return storage.cache.getMulti(
-    scripts.reduce((res, { custom, meta: { icon } }) => {
-      if (isRemote(icon)) res.push(custom.pathMap?.[icon] || icon);
-      return res;
-    }, []),
-    makeDataUri,
-  );
+async function getIconCache(scripts) {
+  const urls = [];
+  for (const { custom, meta: { icon } } of scripts) {
+    if (isRemote(icon)) {
+      urls.push(custom.pathMap[icon] || icon);
+    }
+  }
+  // Getting a data uri for own icon to load it instantly in Chrome when there are many images
+  const ownPath = `${ICON_PREFIX}38.png`;
+  const [res, ownUri] = await Promise.all([
+    storage.cache.getMulti(urls, makeDataUri),
+    commands.GetImageData(ownPath),
+  ]);
+  res[ownPath] = ownUri;
+  return res;
 }
 
 export async function getSizes(ids) {

+ 20 - 24
src/background/utils/icon.js

@@ -1,24 +1,20 @@
 import { i18n, noop } from '@/common';
-import { INJECTABLE_TAB_URL_RE } from '@/common/consts';
+import { ICON_PREFIX, INJECTABLE_TAB_URL_RE } from '@/common/consts';
 import { objectPick } from '@/common/object';
-import cache from './cache';
 import { postInitialize } from './init';
 import { commands, forEachTab } from './message';
 import { getOption, hookOptions } from './options';
 import { testBlacklist } from './tester';
 
-// storing in `cache` only for the duration of page load in case there are 2+ identical urls
-const CACHE_DURATION = 1000;
-
 Object.assign(commands, {
-  async GetImageData(url) {
-    const key = `GetImageData:${url}`;
-    return cache.get(key)
-      || cache.put(key, loadImageData(url, { base64: true }).catch(noop), CACHE_DURATION);
-  },
+  GetImageData: async path => (await getOwnIcon(path)).uri,
   SetBadge: setBadge,
 });
 
+/** Caching own icon to improve dashboard loading speed, as well as browserAction API
+ * (e.g. Chrome wastes 40ms in our extension's process to read 4 icons for every tab). */
+const iconCache = {};
+
 // Firefox Android does not support such APIs, use noop
 
 const browserAction = (() => {
@@ -61,10 +57,6 @@ let titleBlacklisted;
 /** @type string */
 let titleNoninjectable;
 
-// We'll cache the icon data in Chrome as it doesn't cache the data and takes up to 40ms
-// in our background page context to set the 4 icon sizes for each new tab opened
-const iconCache = !IS_FIREFOX && {};
-
 hookOptions((changes) => {
   let v;
   const jobs = [];
@@ -176,21 +168,22 @@ async function setIcon(tab = {}, data = {}) {
   const mod = data.blocked && 'b' || !isApplied && 'w' || '';
   const iconData = {};
   for (const n of [16, 19, 32, 38]) {
-    const path = `/public/images/icon${n}${mod}.png`;
-    let icon = iconCache ? iconCache[path] : path;
-    if (!icon) {
-      icon = await loadImageData(path);
-      iconCache[path] = icon;
-    }
-    iconData[n] = icon;
+    const path = `${ICON_PREFIX}${n}${mod}.png`;
+    const icon = getOwnIcon(path);
+    iconData[n] = (icon.then ? await icon : icon).img;
   }
   browserAction.setIcon({
     tabId: tab.id,
-    [iconCache ? 'imageData' : 'path']: iconData,
+    imageData: iconData,
   });
 }
 
-function loadImageData(path, { base64 } = {}) {
+function getOwnIcon(path) {
+  const icon = iconCache[path] || (iconCache[path] = loadImageData(path));
+  return icon;
+}
+
+function loadImageData(path) {
   return new Promise((resolve, reject) => {
     const img = new Image();
     img.src = path;
@@ -205,7 +198,10 @@ function loadImageData(path, { base64 } = {}) {
       canvas.width = width;
       canvas.height = height;
       ctx.drawImage(img, 0, 0, width, height);
-      resolve(base64 ? canvas.toDataURL() : ctx.getImageData(0, 0, width, height));
+      resolve(iconCache[path] = {
+        uri: canvas.toDataURL(),
+        img: ctx.getImageData(0, 0, width, height),
+      });
     };
     img.onerror = reject;
   });

+ 1 - 1
src/common/consts.js

@@ -19,7 +19,7 @@ export const INJECT_MAPPING = {
 // The SPACE must be on the same line and specifically \x20 as \s would also match \r\n\t
 // Note: when there's no valid metablock, an empty string is matched for convenience
 export const METABLOCK_RE = /(?:^|\n)\s*\/\/\x20==UserScript==([\s\S]*?\n)\s*\/\/\x20==\/UserScript==|$/;
-
+export const ICON_PREFIX = '/public/images/icon';
 export const INJECTABLE_TAB_URL_RE = /^(https?|file|ftps?):/;
 
 // `browser` is a local variable since we remove the global `chrome` and `browser` in injected*

+ 2 - 2
src/common/index.js

@@ -1,6 +1,6 @@
 // SAFETY WARNING! Exports used by `injected` must make ::safe() calls and use __proto__:null
 
-import { browser } from '@/common/consts';
+import { browser, ICON_PREFIX } from '@/common/consts';
 import { deepCopy } from './object';
 import { blob2base64, i18n, isDataUri, noop } from './util';
 
@@ -17,7 +17,7 @@ if (process.env.DEV && process.env.IS_INJECTED !== 'injected-web') {
   }
 }
 
-export const defaultImage = '/public/images/icon128.png';
+export const defaultImage = `${ICON_PREFIX}128.png`;
 
 export function initHooks() {
   const hooks = [];

+ 9 - 3
src/common/load-script-icon.js

@@ -1,18 +1,24 @@
+import { ICON_PREFIX } from '@/common/consts';
 import { isDataUri, sendCmdDirectly } from '@/common/index';
 
+// TODO: convert this into a component tag e.g. <safe-icon>
 const KEY = 'safeIcon';
 
 /**
  * Sets script's safeIcon property after the image is successfully loaded
  * @param {VMScript} script
  * @param {Object} [cache]
+ * @param {number} [defSize] - show default icon of this size, -1 = auto, falsy = no
  */
-export async function loadScriptIcon(script, cache = {}) {
+export async function loadScriptIcon(script, cache = {}, defSize) {
   const { icon } = script.meta;
-  const url = script.custom?.pathMap?.[icon] || icon;
+  const url = script.custom?.pathMap?.[icon] || icon
+    || defSize && `${ICON_PREFIX}${defSize > 0 || (script.config.removed ? 32 : 38)}.png`;
   if (!url || url !== script[KEY]) {
     // creates an observable property so Vue will see the change after `await`
-    script[KEY] = null;
+    if (!(KEY in script)) {
+      script[KEY] = null;
+    }
     if (url) {
       script[KEY] = cache[url]
         || isDataUri(url) && url

+ 9 - 10
src/options/index.js

@@ -47,11 +47,7 @@ async function initScript(script) {
   const name = script.custom.name || localeName;
   const lowerName = name.toLowerCase();
   script.$cache = { search, name, lowerName, size: '', sizes: '', sizeNum: 0 };
-  if (!await loadScriptIcon(script, store.cache)) {
-    script.safeIcon = `/public/images/icon${
-      store.HiDPI ? 128 : script.config.removed && 32 || 38
-    }.png`;
-  }
+  loadScriptIcon(script, store.cache, store.HiDPI || -1);
 }
 
 /**
@@ -90,13 +86,16 @@ export function loadData() {
 
 async function requestData(ids) {
   const getDataP = sendCmdDirectly('GetData', ids, { retry: true });
-  const getSizesP = sendCmdDirectly('GetSizes', ids, { retry: true });
   const [data] = await Promise.all([getDataP, options.ready]);
-  const { scripts } = data;
-  scripts.forEach(initScript);
-  getSizesP.then(sizes => sizes.forEach((sz, i) => initSize(sz, scripts[i])));
-  Object.assign(store, data);
+  const { scripts, ...auxData } = data;
+  Object.assign(store, auxData); // initScripts needs `cache` in store
+  scripts.forEach(initScript); // modifying scripts without triggering reactivity
+  store.scripts = scripts; // now we can render
   store.loading = false;
+  setTimeout(async () => { // sizing runs in the same thread, so we'll start it after render
+    (await sendCmdDirectly('GetSizes', ids, { retry: true }))
+    .forEach((sz, i) => initSize(sz, scripts[i]));
+  });
 }
 
 function initMain() {