perf(fullscreen): fix dynamic accent color delay via direct fetch + cache

- 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 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-04-08 16:13:48 +02:00
parent 9047a44480
commit 099516121e
+30 -9
View File
@@ -245,6 +245,10 @@ interface FullscreenPlayerProps {
onClose: () => void; 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<string, string>();
export default function FullscreenPlayer({ onClose }: FullscreenPlayerProps) { export default function FullscreenPlayer({ onClose }: FullscreenPlayerProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const currentTrack = usePlayerStore(s => s.currentTrack); 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 // Reset to null on track change so the previous color doesn't linger while
// the new one is being extracted. // the new one is being extracted.
const [dynamicAccent, setDynamicAccent] = useState<string | null>(null); const [dynamicAccent, setDynamicAccent] = useState<string | null>(null);
// Reset immediately on track change so the previous color doesn't linger.
useEffect(() => { setDynamicAccent(null); }, [artKey]); // On cover change: hit cache for instant result, or fetch → extract → cache.
// Extract as soon as the display blob is ready — reuses resolvedCoverUrl so // Cache hit avoids re-fetching for same-album tracks. Reset only when uncached.
// no redundant network request for a separate cover size.
useEffect(() => { 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; let cancelled = false;
extractCoverColors(resolvedCoverUrl).then(colors => { let blobUrl = '';
if (!cancelled && colors.accent) setDynamicAccent(colors.accent); (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; }; return () => { cancelled = true; };
}, [resolvedCoverUrl]); }, [artKey]);
// Artist image → portrait on right. Falls back to cover art. // Artist image → portrait on right. Falls back to cover art.
const [artistBgUrl, setArtistBgUrl] = useState<string>(''); const [artistBgUrl, setArtistBgUrl] = useState<string>('');