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.
63 lines
2.5 KiB
TypeScript
63 lines
2.5 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import { getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../store/playbackProgress';
|
|
import type { Track } from '../store/playerStoreTypes';
|
|
import type { WordLyricsLine } from './useLyrics';
|
|
|
|
interface Args {
|
|
enabled: boolean;
|
|
wordLines: WordLyricsLine[] | null;
|
|
currentTrack: Track | null;
|
|
/** CSS class prefix — `fsa` for Apple Music view, `fsr` for rail view. */
|
|
classPrefix: 'fsa' | 'fsr';
|
|
}
|
|
|
|
/** Imperative word-sync DOM updates — toggles the per-word `<span>` classes
|
|
* (`<prefix>-lyric-word`, ` played`, ` active`) from a single playback-progress
|
|
* subscription without re-rendering React on every tick. Returns the ref array
|
|
* the consumer attaches to each word span. */
|
|
export function useWordLyricsSync({ enabled, wordLines, currentTrack, classPrefix }: Args) {
|
|
const wordRefs = useRef<HTMLSpanElement[][]>([]);
|
|
const prevWord = useRef<{ line: number; word: number }>({ line: -1, word: -1 });
|
|
|
|
useEffect(() => {
|
|
wordRefs.current = [];
|
|
prevWord.current = { line: -1, word: -1 };
|
|
}, [currentTrack?.id, enabled]);
|
|
|
|
useEffect(() => {
|
|
if (!enabled || !wordLines) return;
|
|
const lines = wordLines;
|
|
const baseClass = `${classPrefix}-lyric-word`;
|
|
const apply = (time: number) => {
|
|
let li = -1;
|
|
for (let i = 0; i < lines.length; i++) { if (time >= lines[i].time) li = i; else break; }
|
|
let wi = -1;
|
|
if (li >= 0) {
|
|
const ws = lines[li].words;
|
|
for (let j = 0; j < ws.length; j++) { if (time >= ws[j].time) wi = j; else break; }
|
|
}
|
|
const prev = prevWord.current;
|
|
if (prev.line === li && prev.word === wi) return;
|
|
if (prev.line !== li && prev.line >= 0 && wordRefs.current[prev.line]) {
|
|
for (const w of wordRefs.current[prev.line]) w.className = baseClass;
|
|
}
|
|
if (li >= 0 && wordRefs.current[li]) {
|
|
const ws = wordRefs.current[li];
|
|
for (let j = 0; j < ws.length; j++) {
|
|
ws[j].className = j < wi ? `${baseClass} played` : j === wi ? `${baseClass} active` : baseClass;
|
|
}
|
|
}
|
|
prevWord.current = { line: li, word: wi };
|
|
};
|
|
apply(getPlaybackProgressSnapshot().currentTime);
|
|
return subscribePlaybackProgress(s => apply(s.currentTime));
|
|
}, [enabled, wordLines, classPrefix]);
|
|
|
|
const setWordRef = (lineIdx: number, wordIdx: number) => (el: HTMLSpanElement | null) => {
|
|
if (!wordRefs.current[lineIdx]) wordRefs.current[lineIdx] = [];
|
|
if (el) wordRefs.current[lineIdx][wordIdx] = el;
|
|
};
|
|
|
|
return { setWordRef };
|
|
}
|