mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
184501744b
* fix(cover): build cover tiers from the full-resolution source Derive the larger tiers from the decoded download instead of re-reading the just-written smaller tier (resize never upscales, so 512/800 were stored at the small resolution). Resolve the full-res 2000 tier exactly so it is actually downloaded and cached. Bump the cache layout stamp to drop already-poisoned tiers. * fix(cover): open the cover lightbox at full resolution Require the exact requested tier for the full-res helper so a smaller warmed tier no longer pins the lightbox to a downscaled image. Race the 2000 fetch against a 500ms opening window, show the 800 tier meanwhile, and persist 2000 for the next open. Adds an opening animation (reduced-motion aware) and tests. * docs(changelog): note full-resolution cover fix (#1205) * fix(cover): keep full-res peek exact in peek_batch and the grid seeder Review follow-up: ensure-path peek alone left a hole — cover_cache_peek_batch still laddered a 2000 request down to a smaller tier, and the in-memory grid seeder wrote that smaller path under the 2000 key, so Hero/fullscreen/lightbox surfaces (which peek 2000 before ensure) still showed a downscaled cover. Share one exact-2000 rule (peek_plain_cover_tier) across ensure and peek_batch, and never seed the full-res key from a smaller tier file.
71 lines
2.8 KiB
TypeScript
71 lines
2.8 KiB
TypeScript
import { useCallback, useEffect, useState, type ReactNode } from 'react';
|
|
import CoverLightbox from '../components/CoverLightbox';
|
|
import { buildCoverArtFetchUrl } from './fetchUrl';
|
|
import { coverImgSrc } from './imgSrc';
|
|
import { getDiskSrcForGrid } from './diskSrcLookup';
|
|
import { ensureCoverTierDiskSrc } from './resolveDisk';
|
|
import type { CoverArtRef } from './types';
|
|
|
|
/** Opening window: wait this long for the full-res 2000 tier before showing 800. */
|
|
const LIGHTBOX_FULLRES_WINDOW_MS = 500;
|
|
|
|
export function useCoverLightboxSrc(
|
|
ref: CoverArtRef | null,
|
|
opts?: { alt?: string },
|
|
): { open: () => void; lightbox: ReactNode; src: string; loading: boolean } {
|
|
const [open, setOpen] = useState(false);
|
|
const [src, setSrc] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!open || !ref) return;
|
|
let cancelled = false;
|
|
// React Compiler set-state-in-effect rule: state set from an async result resolved in this effect.
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
setLoading(true);
|
|
void (async () => {
|
|
// Kick the full-res (2000) ensure — Rust downloads + stores `2000.webp`. Do
|
|
// not block the open on it: race it against a short opening window. If the
|
|
// 2000 lands in time, show it; otherwise show the warm 800 tier now and let
|
|
// the 2000 finish + persist in the background, so the next open is full-res.
|
|
const fullSrc = ensureCoverTierDiskSrc(ref, 2000);
|
|
const winner = await Promise.race([
|
|
fullSrc,
|
|
new Promise<''>(resolve => {
|
|
setTimeout(() => resolve(''), LIGHTBOX_FULLRES_WINDOW_MS);
|
|
}),
|
|
]);
|
|
if (cancelled) return;
|
|
if (winner) {
|
|
setSrc(winner);
|
|
} else {
|
|
setSrc(getDiskSrcForGrid(ref, 800) || buildCoverArtFetchUrl(ref, 2000));
|
|
}
|
|
setLoading(false);
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
// Keyed on the ref's identity fields intentionally; depending on the `ref`
|
|
// object itself would re-fetch the lightbox source on every render.
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [open, ref?.cacheEntityId, ref?.cacheKind, ref?.fetchCoverArtId, ref?.serverScope]);
|
|
|
|
useEffect(() => {
|
|
if (open) return;
|
|
// React Compiler set-state-in-effect rule: state set from an async result resolved in this effect.
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
setSrc('');
|
|
setLoading(false);
|
|
}, [open]);
|
|
|
|
const handleClose = useCallback(() => setOpen(false), []);
|
|
const handleOpen = useCallback(() => setOpen(true), []);
|
|
|
|
const lightbox = open && coverImgSrc(src) && !loading ? (
|
|
<CoverLightbox src={src} alt={opts?.alt ?? ''} onClose={handleClose} />
|
|
) : null;
|
|
|
|
return { open: handleOpen, lightbox, src, loading };
|
|
}
|