parent
0bf9e76441
commit
0a9155dcec
161
lib/iconcache.ts
Normal file
161
lib/iconcache.ts
Normal file
@ -0,0 +1,161 @@
|
||||
"use strict";
|
||||
// License: MIT
|
||||
|
||||
import { downloads } 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 = [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 icon = new URL(await downloads.getFileIcon(manId, {size}));
|
||||
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<string, string>([
|
||||
["jpe", "jpg"],
|
||||
["jpeg", "jpg"],
|
||||
["jfif", "jpg"],
|
||||
["mpe", "mpg"],
|
||||
["mpeg", "mpg"],
|
||||
["m4v", "mp4"],
|
||||
]));
|
||||
|
||||
export const IconCache = new class IconCache extends EventEmitter {
|
||||
private db: Promise<IDBDatabase>;
|
||||
|
||||
private cache: Map<string, string>;
|
||||
|
||||
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<IDBDatabase>((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<string | undefined>(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);
|
||||
}
|
||||
}();
|
@ -153,6 +153,7 @@ export class BaseDownload {
|
||||
rv.destPath = dest.path;
|
||||
rv.destFull = dest.full;
|
||||
rv.error = this.error;
|
||||
rv.ext = this.renamer.p_ext;
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import { PromiseSerializer } from "../pserializer";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { Manager } from "./man";
|
||||
import { downloads } from "../browser";
|
||||
import { IconCache } from "../iconcache";
|
||||
|
||||
|
||||
const setShelfEnabled = downloads.setShelfEnabled || function() {
|
||||
@ -134,6 +135,9 @@ export class Download extends BaseDownload {
|
||||
this.manager.addManId(
|
||||
this.manId = await downloads.download(options), this);
|
||||
}
|
||||
await IconCache.
|
||||
set(this.renamer.p_ext, this.manId).
|
||||
catch(console.error);
|
||||
}
|
||||
finally {
|
||||
setShelfEnabled(true);
|
||||
|
@ -5,6 +5,7 @@ import { donate, openPrefs } from "../windowutils";
|
||||
import { API } from "../api";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { BaseDownload } from "./basedownload";
|
||||
import { IconCache } from "../iconcache";
|
||||
|
||||
type SID = {sid: number};
|
||||
type SIDS = {
|
||||
@ -61,6 +62,11 @@ export class ManagerPort {
|
||||
delete this.manager;
|
||||
delete this.port;
|
||||
});
|
||||
|
||||
IconCache.on("cached", ext => {
|
||||
this.port.post("icon-cached", ext);
|
||||
});
|
||||
|
||||
this.port.post("active", this.manager.active);
|
||||
this.sendAll();
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
"default_locale": "en",
|
||||
|
||||
"content_security_policy": "script-src 'self'; style-src 'self' 'unsafe-inline'; default-src 'self'",
|
||||
"content_security_policy": "script-src 'self'; style-src 'self' 'unsafe-inline'; img-src data: blob: 'self'; connect-src data: blob: 'self'; default-src 'self'",
|
||||
|
||||
"icons": {
|
||||
"16": "style/icon16.png",
|
||||
|
@ -442,8 +442,13 @@ body > * {
|
||||
}
|
||||
|
||||
#tooltip-icon {
|
||||
font-size: 48px;
|
||||
line-height: 48px;
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
background-size: 64px 64px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
font-size: 64px;
|
||||
line-height: 64px;
|
||||
padding: 6px;
|
||||
text-align: center;
|
||||
grid-row: 1/-1;
|
||||
|
@ -19,7 +19,7 @@ export class Icons extends Map {
|
||||
}
|
||||
let cls = super.get(url);
|
||||
if (!cls) {
|
||||
cls = `icon-${++this.running}`;
|
||||
cls = `iconcache-${++this.running}`;
|
||||
const rule = `.${cls} { background-image: url(${url}); }`;
|
||||
this.sheet.insertRule(rule);
|
||||
super.set(url, cls);
|
||||
|
@ -93,6 +93,10 @@ addEventListener("DOMContentLoaded", function dom() {
|
||||
Table.setItems(items);
|
||||
});
|
||||
});
|
||||
PORT.on("icon-cached", async () => {
|
||||
await fullyloaded;
|
||||
Table.onIconCached();
|
||||
});
|
||||
|
||||
// Updates
|
||||
const serializer = new PromiseSerializer(1);
|
||||
|
@ -678,6 +678,11 @@ export class FilteredCollection extends EventEmitter {
|
||||
this.emit("sorted");
|
||||
}
|
||||
}
|
||||
|
||||
invalidateIcons() {
|
||||
this.items.forEach(item => item.clearFontIcons());
|
||||
this.recalculate();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@ -37,6 +37,7 @@ import { downloads } from "../../lib/browser";
|
||||
import { $ } from "../winutil";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { TableConfig } from "../../uikit/lib/config";
|
||||
import { IconCache } from "../../lib/iconcache";
|
||||
|
||||
const TREE_CONFIG_VERSION = 2;
|
||||
const RUNNING_TIMEOUT = 1000;
|
||||
@ -52,7 +53,14 @@ const COL_SPEED = 6;
|
||||
const COL_MASK = 7;
|
||||
const COL_SEGS = 8;
|
||||
|
||||
const HIDPI = window.matchMedia &&
|
||||
window.matchMedia("(min-resolution: 2dppx)").matches;
|
||||
|
||||
const ICON_BASE_SIZE = 16;
|
||||
const ICON_REAL_SIZE = HIDPI ? ICON_BASE_SIZE * 2 : ICON_BASE_SIZE;
|
||||
const LARGE_ICON_BASE_SIZE = 64;
|
||||
const MAX_ICON_BASE_SIZE = 127;
|
||||
const LARGE_ICON_REAL_SIZE = HIDPI ? MAX_ICON_BASE_SIZE : LARGE_ICON_BASE_SIZE;
|
||||
|
||||
let TEXT_SIZE_UNKNOWM = "unknown";
|
||||
let REAL_STATE_TEXTS = Object.freeze(new Map<number, string>());
|
||||
@ -108,6 +116,8 @@ export class DownloadItem extends EventEmitter {
|
||||
|
||||
public finalName: string;
|
||||
|
||||
public ext?: string;
|
||||
|
||||
public position: number;
|
||||
|
||||
public filteredPosition: number;
|
||||
@ -128,6 +138,10 @@ export class DownloadItem extends EventEmitter {
|
||||
|
||||
public mask: string;
|
||||
|
||||
private iconField?: string;
|
||||
|
||||
private largeIconField?: string;
|
||||
|
||||
constructor(owner: DownloadTable, raw: any, stats?: Stats) {
|
||||
super();
|
||||
Object.assign(this, raw);
|
||||
@ -138,6 +152,42 @@ export class DownloadItem extends EventEmitter {
|
||||
this.lastWritten = 0;
|
||||
}
|
||||
|
||||
get icon() {
|
||||
if (this.iconField) {
|
||||
return this.iconField;
|
||||
}
|
||||
this.iconField = this.owner.icons.get(
|
||||
iconForPath(this.finalName, ICON_BASE_SIZE));
|
||||
if (this.ext) {
|
||||
IconCache.get(this.ext, ICON_REAL_SIZE).then(icon => {
|
||||
if (icon) {
|
||||
this.iconField = this.owner.icons.get(icon);
|
||||
if (typeof this.filteredPosition !== undefined) {
|
||||
this.owner.invalidateCell(this.filteredPosition, COL_URL);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.iconField || "";
|
||||
}
|
||||
|
||||
get largeIcon() {
|
||||
if (this.largeIconField) {
|
||||
return this.largeIconField;
|
||||
}
|
||||
this.largeIconField = this.owner.icons.get(
|
||||
iconForPath(this.finalName, LARGE_ICON_BASE_SIZE));
|
||||
if (this.ext) {
|
||||
IconCache.get(this.ext, LARGE_ICON_REAL_SIZE).then(icon => {
|
||||
if (icon) {
|
||||
this.largeIconField = this.owner.icons.get(icon);
|
||||
}
|
||||
this.emit("largeIcon");
|
||||
});
|
||||
}
|
||||
return this.largeIconField || "";
|
||||
}
|
||||
|
||||
get eta() {
|
||||
const {avg} = this.stats;
|
||||
if (!this.totalSize || !avg) {
|
||||
@ -215,6 +265,9 @@ export class DownloadItem extends EventEmitter {
|
||||
PORT.post("all");
|
||||
return;
|
||||
}
|
||||
if (("ext" in raw) && raw.ext !== this.ext) {
|
||||
this.clearIcons();
|
||||
}
|
||||
delete raw.position;
|
||||
delete raw.owner;
|
||||
const oldState = this.state;
|
||||
@ -297,6 +350,20 @@ export class DownloadItem extends EventEmitter {
|
||||
this.domain = this.uURL.domain;
|
||||
this.emit("url");
|
||||
}
|
||||
|
||||
clearIcons() {
|
||||
this.iconField = undefined;
|
||||
this.largeIconField = undefined;
|
||||
}
|
||||
|
||||
clearFontIcons() {
|
||||
if (this.iconField && this.iconField.startsWith("icon-")) {
|
||||
this.iconField = undefined;
|
||||
}
|
||||
if (this.largeIconField && this.largeIconField.startsWith("icon-")) {
|
||||
this.largeIconField = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -323,7 +390,7 @@ export class DownloadTable extends VirtualTable {
|
||||
|
||||
private readonly sids: Map<number, DownloadItem>;
|
||||
|
||||
private readonly icons: Icons;
|
||||
public readonly icons: Icons;
|
||||
|
||||
private readonly contextMenu: ContextMenu;
|
||||
|
||||
@ -357,6 +424,7 @@ export class DownloadTable extends VirtualTable {
|
||||
this.showUrls = new ShowUrlsWatcher(this);
|
||||
|
||||
this.updateCounts = debounce(this.updateCounts.bind(this), 100);
|
||||
this.onIconCached = debounce(this.onIconCached.bind(this), 1000);
|
||||
|
||||
this.downloads = new FilteredCollection(this);
|
||||
this.downloads.on("changed", () => this.updateCounts());
|
||||
@ -1043,10 +1111,10 @@ export class DownloadTable extends VirtualTable {
|
||||
}
|
||||
const item = this.downloads.filtered[rowid];
|
||||
if (colid === COL_URL) {
|
||||
return this.icons.get(iconForPath(item.finalName, ICON_BASE_SIZE));
|
||||
return item.icon;
|
||||
}
|
||||
if (colid === COL_PROGRESS) {
|
||||
return StateIcons.get(item.state);
|
||||
return StateIcons.get(item.state) || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -1119,4 +1187,8 @@ export class DownloadTable extends VirtualTable {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
onIconCached() {
|
||||
this.downloads.invalidateIcons();
|
||||
}
|
||||
}
|
||||
|
@ -146,6 +146,8 @@ export class Tooltip {
|
||||
constructor(item: DownloadItem, pos: number) {
|
||||
this.update = this.update.bind(this);
|
||||
this.item = item;
|
||||
this.item.on("largeIcon", this.update);
|
||||
|
||||
const tmpl = (
|
||||
document.querySelector<HTMLTemplateElement>("#tooltip-template"));
|
||||
if (!tmpl) {
|
||||
@ -178,7 +180,7 @@ export class Tooltip {
|
||||
this.dismiss();
|
||||
return;
|
||||
}
|
||||
const icon = item.owner.getCellIcon(item.filteredPosition, 0);
|
||||
const icon = item.largeIcon;
|
||||
this.icon.className = icon;
|
||||
this.name.textContent = item.destFull;
|
||||
this.from.textContent = item.usable;
|
||||
@ -318,5 +320,6 @@ export class Tooltip {
|
||||
}
|
||||
this.item.off("stats", this.update);
|
||||
this.item.off("update", this.update);
|
||||
this.item.off("largeIcon", this.update);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user