mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
463d3e0c5b
* refactor(fullscreen-player): H5 — extract FsLyricsApple + FsLyricsRail + useWordLyricsSync
The two lyrics views become own files under components/fullscreenPlayer/.
Their identical word-sync imperative DOM-update useEffect (only differing in
the .fsa-/.fsr- class prefix) collapses into the shared useWordLyricsSync
hook with a classPrefix arg.
FullscreenPlayer.tsx: 911 → 613 LOC.
* refactor(fullscreen-player): H5 — extract FsArt + FsPortrait + FsSeekbar
The three visual subcomponents move into own files. formatTime moves into
utils/fullscreenPlayerHelpers.ts so FsSeekbar keeps using it.
FullscreenPlayer.tsx: 613 → 421 LOC.
* refactor(fullscreen-player): H5 — extract FsLyricsMenu + FsPlayBtn
The lyrics-settings popover and the isolated play/pause button move into
own files. FullscreenPlayer drops the now-unused lucide-react icons
(Play/Pause/Moon/Sunrise/Music) and the PlaybackDelayModal +
PlaybackScheduleBadge imports — both used only by FsPlayBtn now.
FullscreenPlayer.tsx: 421 → 304 LOC.
* refactor(fullscreen-player): H5 — extract useFsDynamicAccent + useFsArtistPortrait + useFsIdleFade
Pulls three state+effect islands out of FullscreenPlayer:
- useFsDynamicAccent owns the cover-blob fetch + extractCoverColors call
plus the module-level artKey → accent cache that makes same-album song
switches instant.
- useFsArtistPortrait fetches getArtistInfo().largeImageUrl for the right-
side portrait, returning '' until resolved (or when no artistId).
- useFsIdleFade flips isIdle true after 3 s of inactivity, exposes a
throttled mousemove handler, and binds Escape to the provided callback.
FullscreenPlayer.tsx: 304 → 228 LOC.
55 lines
2.1 KiB
TypeScript
55 lines
2.1 KiB
TypeScript
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
|
import { Music } from 'lucide-react';
|
|
import { useCachedUrl } from '../CachedImage';
|
|
|
|
// Album art box — crossfades layers so old art stays visible while new loads.
|
|
// Uses 300px thumbnails (portrait fallback uses 500px separately).
|
|
//
|
|
// Why onLoad instead of new Image() preload:
|
|
// React batches setLayers(add invisible) + rAF setLayers(make visible) into one
|
|
// commit, so the browser never sees opacity:0 and the CSS transition never fires.
|
|
// Using the DOM img's own onLoad guarantees the element was painted at opacity:0
|
|
// before we flip it to 1.
|
|
export const FsArt = memo(function FsArt({ fetchUrl, cacheKey }: { fetchUrl: string; cacheKey: string }) {
|
|
// true = show raw fetchUrl immediately as fallback while blob resolves.
|
|
// PlayerBar uses 128px; FS player uses 300px — different cache keys, no warm hit.
|
|
// Showing the URL directly avoids the multi-second blank wait.
|
|
const blobUrl = useCachedUrl(fetchUrl, cacheKey, true);
|
|
|
|
const [layers, setLayers] = useState<Array<{ src: string; id: number; vis: boolean }>>([]);
|
|
const counter = useRef(0);
|
|
const cleanupTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!blobUrl) return;
|
|
const id = ++counter.current;
|
|
setLayers(prev => [...prev, { src: blobUrl, id, vis: false }]);
|
|
}, [blobUrl]);
|
|
|
|
const handleLoad = useCallback((id: number) => {
|
|
if (cleanupTimer.current) clearTimeout(cleanupTimer.current);
|
|
setLayers(prev => prev.map(l => ({ ...l, vis: l.id === id })));
|
|
cleanupTimer.current = setTimeout(() => setLayers(prev => prev.filter(l => l.id === id)), 400);
|
|
}, []);
|
|
|
|
if (layers.length === 0) {
|
|
return <div className="fs-art fs-art-placeholder"><Music size={40} /></div>;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{layers.map(l => (
|
|
<img
|
|
key={l.id}
|
|
src={l.src}
|
|
className="fs-art"
|
|
style={{ opacity: l.vis ? 1 : 0 }}
|
|
onLoad={() => handleLoad(l.id)}
|
|
alt=""
|
|
decoding="async"
|
|
/>
|
|
))}
|
|
</>
|
|
);
|
|
});
|