Polish vault icons and mobile layout

This commit is contained in:
shuaiplus
2026-05-04 04:20:23 +08:00
parent 97a3aa691d
commit 1b4d263d6e
9 changed files with 202 additions and 42 deletions
+47 -15
View File
@@ -1,8 +1,11 @@
type WebsiteIconStatus = 'idle' | 'loading' | 'loaded' | 'error';
const ICON_LOAD_TIMEOUT_MS = 5000;
interface WebsiteIconRecord {
status: WebsiteIconStatus;
promise: Promise<WebsiteIconStatus> | null;
imageUrl: string | null;
listeners: Set<(status: WebsiteIconStatus) => void>;
}
@@ -14,6 +17,7 @@ function ensureRecord(host: string): WebsiteIconRecord {
record = {
status: 'idle',
promise: null,
imageUrl: null,
listeners: new Set(),
};
iconRecords.set(host, record);
@@ -34,6 +38,11 @@ export function getWebsiteIconStatus(host: string): WebsiteIconStatus {
return ensureRecord(host).status;
}
export function getWebsiteIconImageUrl(host: string): string {
if (!host) return '';
return ensureRecord(host).imageUrl || '';
}
export function subscribeWebsiteIconStatus(host: string, listener: (status: WebsiteIconStatus) => void): () => void {
if (!host) return () => undefined;
const record = ensureRecord(host);
@@ -43,10 +52,13 @@ export function subscribeWebsiteIconStatus(host: string, listener: (status: Webs
};
}
export function markWebsiteIconLoaded(host: string): void {
export function markWebsiteIconLoaded(host: string, imageUrl?: string): void {
if (!host) return;
const record = ensureRecord(host);
record.promise = null;
if (imageUrl) {
record.imageUrl = imageUrl;
}
notifyRecord(host, 'loaded');
}
@@ -54,9 +66,19 @@ export function markWebsiteIconErrored(host: string): void {
if (!host) return;
const record = ensureRecord(host);
record.promise = null;
record.imageUrl = null;
notifyRecord(host, 'error');
}
function blobToDataUrl(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : '');
reader.onerror = () => reject(reader.error || new Error('Failed to read icon'));
reader.readAsDataURL(blob);
});
}
export function preloadWebsiteIcon(host: string, src: string): Promise<WebsiteIconStatus> {
if (!host) return Promise.resolve('error');
@@ -68,21 +90,31 @@ export function preloadWebsiteIcon(host: string, src: string): Promise<WebsiteIc
return record.promise;
}
record.status = 'loading';
record.promise = new Promise<WebsiteIconStatus>((resolve) => {
const img = new Image();
img.decoding = 'async';
img.referrerPolicy = 'no-referrer';
img.onload = () => {
markWebsiteIconLoaded(host);
resolve('loaded');
};
img.onerror = () => {
notifyRecord(host, 'loading');
record.promise = (async () => {
const controller = new AbortController();
const timeout = window.setTimeout(() => controller.abort(), ICON_LOAD_TIMEOUT_MS);
try {
const resp = await fetch(src, {
cache: 'force-cache',
signal: controller.signal,
});
if (!resp.ok) throw new Error('Icon unavailable');
const contentType = String(resp.headers.get('Content-Type') || '').toLowerCase();
if (!contentType.startsWith('image/')) throw new Error('Icon response is not an image');
const blob = await resp.blob();
if (!blob.size) throw new Error('Icon response is empty');
const imageUrl = await blobToDataUrl(blob);
if (!imageUrl) throw new Error('Icon response is empty');
markWebsiteIconLoaded(host, imageUrl);
return 'loaded';
} catch {
markWebsiteIconErrored(host);
resolve('error');
};
img.src = src;
});
return 'error';
} finally {
window.clearTimeout(timeout);
}
})();
return record.promise;
}