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