From 099516121eb2903f99b32831a3e8bc6ebda19b40 Mon Sep 17 00:00:00 2001 From: Psychotoxical Date: Wed, 8 Apr 2026 16:13:48 +0200 Subject: [PATCH] perf(fullscreen): fix dynamic accent color delay via direct fetch + cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bypass ImageCache 5-slot queue: fetch cover art directly, create blob URL for canvas extraction, revoke immediately after use - Module-level cache (artKey → accent) avoids re-fetching within the same album — same-cover tracks get the color instantly - Keep previous color visible on cache miss instead of flashing to theme color Co-Authored-By: Claude Sonnet 4.6 --- src/components/FullscreenPlayer.tsx | 39 ++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/components/FullscreenPlayer.tsx b/src/components/FullscreenPlayer.tsx index dd6fd206..983a8958 100644 --- a/src/components/FullscreenPlayer.tsx +++ b/src/components/FullscreenPlayer.tsx @@ -245,6 +245,10 @@ interface FullscreenPlayerProps { onClose: () => void; } +// Module-level cache: artKey → accent color string. +// Survives track changes so same-album songs reuse the extracted color instantly. +const coverAccentCache = new Map(); + export default function FullscreenPlayer({ onClose }: FullscreenPlayerProps) { const { t } = useTranslation(); const currentTrack = usePlayerStore(s => s.currentTrack); @@ -291,18 +295,35 @@ export default function FullscreenPlayer({ onClose }: FullscreenPlayerProps) { // Reset to null on track change so the previous color doesn't linger while // the new one is being extracted. const [dynamicAccent, setDynamicAccent] = useState(null); - // Reset immediately on track change so the previous color doesn't linger. - useEffect(() => { setDynamicAccent(null); }, [artKey]); - // Extract as soon as the display blob is ready — reuses resolvedCoverUrl so - // no redundant network request for a separate cover size. + + // On cover change: hit cache for instant result, or fetch → extract → cache. + // Cache hit avoids re-fetching for same-album tracks. Reset only when uncached. useEffect(() => { - if (!resolvedCoverUrl) return; + if (!artKey || !artUrl) { setDynamicAccent(null); return; } + const cached = coverAccentCache.get(artKey); + if (cached) { setDynamicAccent(cached); return; } + // No cache hit — keep the previous color visible until extraction completes. let cancelled = false; - extractCoverColors(resolvedCoverUrl).then(colors => { - if (!cancelled && colors.accent) setDynamicAccent(colors.accent); - }); + let blobUrl = ''; + (async () => { + try { + const resp = await fetch(artUrl); + if (cancelled) return; + const blob = await resp.blob(); + if (cancelled) return; + blobUrl = URL.createObjectURL(blob); + const colors = await extractCoverColors(blobUrl); + if (cancelled) return; + if (colors.accent) { + coverAccentCache.set(artKey, colors.accent); + setDynamicAccent(colors.accent); + } + } catch { /* ignore */ } finally { + if (blobUrl) URL.revokeObjectURL(blobUrl); + } + })(); return () => { cancelled = true; }; - }, [resolvedCoverUrl]); + }, [artKey]); // Artist image → portrait on right. Falls back to cover art. const [artistBgUrl, setArtistBgUrl] = useState('');