mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
d0a270d90a
Three-cut cluster pulling tightly-scoped state + memo islands out of PlaylistDetail.tsx as custom hooks. 659 → 565 LOC (−94). Each hook returns the same names the page already used, so call sites stay identical. usePlaylistCovers(songs, customCoverId) — the 2×2 cover quad memo, the four cover-quad URLs (with their stable cache keys to avoid the buildCoverArtUrl-salt re-render loop documented in the comment), the customCover fetch URL + cache key, and the blurred background URL going through useCachedUrl. Returns coverQuadUrls / customCoverFetchUrl / customCoverCacheKey / resolvedBgUrl. The page no longer imports buildCoverArtUrl / coverArtCacheKey / useCachedUrl directly. usePlaylistSelection(songs, setSongs, savePlaylist) — selectedIds / lastSelectedIdx state + toggleSelect (with shift-range support) + allSelected + toggleAll + bulkRemove (savePlaylist + setSongs + clears selection). Returns the same shape the tracklist props already destructured. savePlaylist moves a few lines up in PlaylistDetail so the hook can be called with it. usePlaylistSuggestions(songs, playlist.id) — suggestions state + loadSuggestions (genre-weighted random pull, top 10) + the auto-load useEffect that fires on playlist change. Returns setSuggestions too so addSong on the page can still filter the just-added song out of the suggestions strip. The page drops the getRandomSongs import. Pure code move otherwise — no behaviour change.
55 lines
2.1 KiB
TypeScript
55 lines
2.1 KiB
TypeScript
import { useMemo } from 'react';
|
||
import { buildCoverArtUrl, coverArtCacheKey } from '../api/subsonicStreamUrl';
|
||
import type { SubsonicSong } from '../api/subsonicTypes';
|
||
import { useCachedUrl } from '../components/CachedImage';
|
||
|
||
export interface PlaylistCovers {
|
||
coverQuadUrls: ({ src: string; cacheKey: string } | null)[];
|
||
customCoverFetchUrl: string | null;
|
||
customCoverCacheKey: string | null;
|
||
resolvedBgUrl: string | null;
|
||
}
|
||
|
||
export function usePlaylistCovers(songs: SubsonicSong[], customCoverId: string | null): PlaylistCovers {
|
||
const coverQuad = useMemo(() => {
|
||
const seen = new Set<string>();
|
||
const result: string[] = [];
|
||
for (const s of songs) {
|
||
if (s.coverArt && !seen.has(s.coverArt)) {
|
||
seen.add(s.coverArt);
|
||
result.push(s.coverArt);
|
||
if (result.length === 4) break;
|
||
}
|
||
}
|
||
return result;
|
||
}, [songs]);
|
||
|
||
// Stable fetch URLs + cache keys for the 2×2 grid and blurred background.
|
||
// buildCoverArtUrl generates a new crypto salt on every call, so these MUST
|
||
// be memoized — otherwise every render produces new URLs, useCachedUrl
|
||
// re-triggers, state updates, another render → infinite flicker loop.
|
||
const coverQuadUrls = useMemo(() =>
|
||
Array.from({ length: 4 }, (_, i) => {
|
||
const coverId = coverQuad[i % Math.max(1, coverQuad.length)];
|
||
if (!coverId) return null;
|
||
return { src: buildCoverArtUrl(coverId, 200), cacheKey: coverArtCacheKey(coverId, 200) };
|
||
}),
|
||
[coverQuad]);
|
||
|
||
const effectiveBgId = customCoverId ?? coverQuad[0] ?? '';
|
||
const bgFetchUrl = useMemo(() => buildCoverArtUrl(effectiveBgId, 300), [effectiveBgId]);
|
||
const bgCacheKey = useMemo(() => coverArtCacheKey(effectiveBgId, 300), [effectiveBgId]);
|
||
const resolvedBgUrl = useCachedUrl(bgFetchUrl, bgCacheKey);
|
||
|
||
const customCoverFetchUrl = useMemo(
|
||
() => customCoverId ? buildCoverArtUrl(customCoverId, 300) : null,
|
||
[customCoverId],
|
||
);
|
||
const customCoverCacheKey = useMemo(
|
||
() => customCoverId ? coverArtCacheKey(customCoverId, 300) : null,
|
||
[customCoverId],
|
||
);
|
||
|
||
return { coverQuadUrls, customCoverFetchUrl, customCoverCacheKey, resolvedBgUrl };
|
||
}
|