Files
psysonic/src/hooks/useWordLyricsSync.ts
T
Frank Stellmacher 463d3e0c5b refactor(fullscreen-player): H5 — split FullscreenPlayer.tsx 911 → 228 LOC across 11 files (#668)
* 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.
2026-05-13 22:52:08 +02:00

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 };
}