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 {
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}`);
+14 -4
View File
@@ -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}
/>
</span>
);
@@ -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 (
<span className="list-icon-stack">
<span className={`list-icon-fallback ${loaded ? 'hidden' : ''}`}>
<TypeIcon type={Number(cipher.type || 1)} />
<Globe size={18} />
</span>
<img
className={`list-icon ${loaded ? 'loaded' : ''}`}
@@ -451,11 +463,9 @@ export function VaultListIcon({ cipher }: { cipher: Cipher }) {
alt=""
loading="lazy"
referrerPolicy="no-referrer"
ref={syncCachedIconState}
onLoad={() => setLoaded(true)}
onError={() => {
failedIconHosts.add(host);
setErrored(true);
}}
onError={markIconError}
/>
</span>
);
+4 -4
View File
@@ -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 {