From 3995e0133622fb0107fb39db96e5c4ea7c4b60a7 Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Sat, 25 Apr 2026 10:20:30 +0800 Subject: [PATCH] feat: enhance icon error handling and loading state management in TotpCodesPage and VaultListIcon components --- src/router-public.ts | 7 ++++++- webapp/src/components/TotpCodesPage.tsx | 18 +++++++++++++---- .../components/vault/vault-page-helpers.tsx | 20 ++++++++++++++----- webapp/src/styles/vault.css | 8 ++++---- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/router-public.ts b/src/router-public.ts index 0d4235c..65aa4ca 100644 --- a/src/router-public.ts +++ b/src/router-public.ts @@ -126,7 +126,12 @@ function buildConfigResponse(origin: string) { } function normalizeIconHost(rawHost: string): string | null { - const decoded = decodeURIComponent(String(rawHost || '').trim()).toLowerCase().replace(/\.+$/, ''); + let decoded: string; + try { + decoded = decodeURIComponent(String(rawHost || '').trim()).toLowerCase().replace(/\.+$/, ''); + } catch { + return null; + } if (!decoded || decoded.includes('/') || decoded.includes('\\')) return null; try { const parsed = new URL(`https://${decoded}`); diff --git a/webapp/src/components/TotpCodesPage.tsx b/webapp/src/components/TotpCodesPage.tsx index 603b9be..bd4e9ca 100644 --- a/webapp/src/components/TotpCodesPage.tsx +++ b/webapp/src/components/TotpCodesPage.tsx @@ -74,6 +74,18 @@ function TotpListIcon({ cipher }: { cipher: Cipher }) { const host = hostFromUri(uri); const [errored, setErrored] = useState(() => (host ? failedIconHosts.has(host) : false)); const [loaded, setLoaded] = useState(false); + const markIconError = () => { + if (host) failedIconHosts.add(host); + setErrored(true); + }; + const syncCachedIconState = (img: HTMLImageElement | null) => { + if (!img || !img.complete) return; + if (img.naturalWidth > 0) { + setLoaded(true); + return; + } + markIconError(); + }; useEffect(() => { setErrored(host ? failedIconHosts.has(host) : false); setLoaded(false); @@ -91,11 +103,9 @@ function TotpListIcon({ cipher }: { cipher: Cipher }) { alt="" loading="lazy" referrerPolicy="no-referrer" + ref={syncCachedIconState} onLoad={() => setLoaded(true)} - onError={() => { - failedIconHosts.add(host); - setErrored(true); - }} + onError={markIconError} /> ); diff --git a/webapp/src/components/vault/vault-page-helpers.tsx b/webapp/src/components/vault/vault-page-helpers.tsx index f4e3e62..baccc4a 100644 --- a/webapp/src/components/vault/vault-page-helpers.tsx +++ b/webapp/src/components/vault/vault-page-helpers.tsx @@ -434,6 +434,18 @@ export function VaultListIcon({ cipher }: { cipher: Cipher }) { const host = hostFromUri(uri); const [errored, setErrored] = useState(() => (host ? failedIconHosts.has(host) : false)); const [loaded, setLoaded] = useState(false); + const markIconError = () => { + if (host) failedIconHosts.add(host); + setErrored(true); + }; + const syncCachedIconState = (img: HTMLImageElement | null) => { + if (!img || !img.complete) return; + if (img.naturalWidth > 0) { + setLoaded(true); + return; + } + markIconError(); + }; useEffect(() => { setErrored(host ? failedIconHosts.has(host) : false); setLoaded(false); @@ -443,7 +455,7 @@ export function VaultListIcon({ cipher }: { cipher: Cipher }) { return ( - + setLoaded(true)} - onError={() => { - failedIconHosts.add(host); - setErrored(true); - }} + onError={markIconError} /> ); diff --git a/webapp/src/styles/vault.css b/webapp/src/styles/vault.css index d22630b..c668253 100644 --- a/webapp/src/styles/vault.css +++ b/webapp/src/styles/vault.css @@ -376,13 +376,13 @@ opacity: 1; } -.list-icon-fallback.hidden { - opacity: 0; +.list-icon-stack > .list-icon-fallback { + opacity: 1; + z-index: 1; } -.list-icon-stack > .list-icon-fallback { +.list-icon-stack > .list-icon-fallback.hidden { opacity: 0; - z-index: 1; } .list-icon-fallback {