feat: enhance icon error handling and loading state management in TotpCodesPage and VaultListIcon components

This commit is contained in:
shuaiplus
2026-04-25 10:20:30 +08:00
parent 481536ba24
commit 3995e01336
4 changed files with 39 additions and 14 deletions
+6 -1
View File
@@ -126,7 +126,12 @@ function buildConfigResponse(origin: string) {
} }
function normalizeIconHost(rawHost: string): string | null { 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; if (!decoded || decoded.includes('/') || decoded.includes('\\')) return null;
try { try {
const parsed = new URL(`https://${decoded}`); const parsed = new URL(`https://${decoded}`);
+14 -4
View File
@@ -74,6 +74,18 @@ function TotpListIcon({ cipher }: { cipher: Cipher }) {
const host = hostFromUri(uri); const host = hostFromUri(uri);
const [errored, setErrored] = useState(() => (host ? failedIconHosts.has(host) : false)); const [errored, setErrored] = useState(() => (host ? failedIconHosts.has(host) : false));
const [loaded, setLoaded] = useState(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(() => { useEffect(() => {
setErrored(host ? failedIconHosts.has(host) : false); setErrored(host ? failedIconHosts.has(host) : false);
setLoaded(false); setLoaded(false);
@@ -91,11 +103,9 @@ function TotpListIcon({ cipher }: { cipher: Cipher }) {
alt="" alt=""
loading="lazy" loading="lazy"
referrerPolicy="no-referrer" referrerPolicy="no-referrer"
ref={syncCachedIconState}
onLoad={() => setLoaded(true)} onLoad={() => setLoaded(true)}
onError={() => { onError={markIconError}
failedIconHosts.add(host);
setErrored(true);
}}
/> />
</span> </span>
); );
@@ -434,6 +434,18 @@ export function VaultListIcon({ cipher }: { cipher: Cipher }) {
const host = hostFromUri(uri); const host = hostFromUri(uri);
const [errored, setErrored] = useState(() => (host ? failedIconHosts.has(host) : false)); const [errored, setErrored] = useState(() => (host ? failedIconHosts.has(host) : false));
const [loaded, setLoaded] = useState(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(() => { useEffect(() => {
setErrored(host ? failedIconHosts.has(host) : false); setErrored(host ? failedIconHosts.has(host) : false);
setLoaded(false); setLoaded(false);
@@ -443,7 +455,7 @@ export function VaultListIcon({ cipher }: { cipher: Cipher }) {
return ( return (
<span className="list-icon-stack"> <span className="list-icon-stack">
<span className={`list-icon-fallback ${loaded ? 'hidden' : ''}`}> <span className={`list-icon-fallback ${loaded ? 'hidden' : ''}`}>
<TypeIcon type={Number(cipher.type || 1)} /> <Globe size={18} />
</span> </span>
<img <img
className={`list-icon ${loaded ? 'loaded' : ''}`} className={`list-icon ${loaded ? 'loaded' : ''}`}
@@ -451,11 +463,9 @@ export function VaultListIcon({ cipher }: { cipher: Cipher }) {
alt="" alt=""
loading="lazy" loading="lazy"
referrerPolicy="no-referrer" referrerPolicy="no-referrer"
ref={syncCachedIconState}
onLoad={() => setLoaded(true)} onLoad={() => setLoaded(true)}
onError={() => { onError={markIconError}
failedIconHosts.add(host);
setErrored(true);
}}
/> />
</span> </span>
); );
+4 -4
View File
@@ -376,13 +376,13 @@
opacity: 1; opacity: 1;
} }
.list-icon-fallback.hidden { .list-icon-stack > .list-icon-fallback {
opacity: 0; opacity: 1;
z-index: 1;
} }
.list-icon-stack > .list-icon-fallback { .list-icon-stack > .list-icon-fallback.hidden {
opacity: 0; opacity: 0;
z-index: 1;
} }
.list-icon-fallback { .list-icon-fallback {