"use strict"; // License: MIT import { downloads, CHROME } from "./browser"; import { EventEmitter } from "../uikit/lib/events"; import { PromiseSerializer } from "./pserializer"; const VERSION = 1; const STORE = "iconcache"; // eslint-disable-next-line no-magic-numbers const CACHE_SIZES = CHROME ? [16, 32] : [16, 32, 64, 127]; const BLACKLISTED = Object.freeze(new Set([ "", "ext", "ico", "pif", "scr", "ani", "cur", "ttf", "otf", "woff", "woff2", "cpl", "desktop", "app", ])); async function getIcon(size: number, manId: number) { const raw = await downloads.getFileIcon(manId, {size}); const icon = new URL(raw); if (icon.protocol === "data:") { const res = await fetch(icon.toString()); const blob = await res.blob(); return {size, icon: blob}; } return {size, icon}; } const SYNONYMS = Object.freeze(new Map([ ["jpe", "jpg"], ["jpeg", "jpg"], ["jfif", "jpg"], ["mpe", "mpg"], ["mpeg", "mpg"], ["m4v", "mp4"], ])); export const IconCache = new class IconCache extends EventEmitter { private db: Promise; private cache: Map; constructor() { super(); this.db = this.init(); this.cache = new Map(); this.get = PromiseSerializer.wrapNew(8, this, this.get); this.set = PromiseSerializer.wrapNew(1, this, this.set); } private async init() { return await new Promise((resolve, reject) => { const req = indexedDB.open(STORE, VERSION); req.onupgradeneeded = evt => { const db = req.result; switch (evt.oldVersion) { case 0: { db.createObjectStore(STORE); break; } } }; req.onerror = ex => reject(ex); req.onsuccess = () => { resolve(req.result); }; }); } private normalize(ext: string) { ext = ext.toLocaleLowerCase(); return SYNONYMS.get(ext) || ext; } // eslint-disable-next-line no-magic-numbers async get(ext: string, size = 16) { ext = this.normalize(ext); if (BLACKLISTED.has(ext)) { return undefined; } const sext = `${ext}-${size}`; let rv = this.cache.get(sext); if (rv) { return rv; } const db = await this.db; rv = this.cache.get(sext); if (rv) { return rv; } return await new Promise(resolve => { const trans = db.transaction(STORE, "readonly"); trans.onerror = () => resolve(undefined); const store = trans.objectStore(STORE); const req = store.get(sext); req.onerror = () => resolve(undefined); req.onsuccess = () => { const rv = this.cache.get(sext); if (rv) { resolve(rv); return; } let {result} = req; if (!result) { resolve(undefined); return; } if (typeof req.result !== "string") { result = URL.createObjectURL(result).toString(); } this.cache.set(sext, result); this.cache.set(ext, ""); resolve(result); }; }); } async set(ext: string, manId: number) { ext = this.normalize(ext); if (BLACKLISTED.has(ext)) { return; } if (this.cache.has(ext)) { // already processed in this session return; } // eslint-disable-next-line no-magic-numbers const urls = await Promise.all(CACHE_SIZES.map( size => getIcon(size, manId))); if (this.cache.has(ext)) { // already processed in this session return; } for (const {size, icon} of urls) { this.cache.set(`${ext}-${size}`, URL.createObjectURL(icon)); } this.cache.set(ext, ""); const db = await this.db; await new Promise((resolve, reject) => { const trans = db.transaction(STORE, "readwrite"); trans.onerror = reject; trans.oncomplete = resolve; const store = trans.objectStore(STORE); for (const {size, icon} of urls) { store.put(icon, `${ext}-${size}`); } }); this.emit("cached", ext); } }();