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;
}
// 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) {
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<string | null>(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<string>('');