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.
90 lines
3.7 KiB
TypeScript
90 lines
3.7 KiB
TypeScript
import React, { memo, useCallback, useEffect, useRef } from 'react';
|
|
import { usePlayerStore } from '../../store/playerStore';
|
|
import { getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../../store/playbackProgress';
|
|
import { formatTime } from '../../utils/fullscreenPlayerHelpers';
|
|
|
|
// Full-width seekbar — imperative DOM updates, zero React re-renders on tick.
|
|
export const FsSeekbar = memo(function FsSeekbar({ duration }: { duration: number }) {
|
|
const seek = usePlayerStore(s => s.seek);
|
|
const timeRef = useRef<HTMLSpanElement>(null);
|
|
const playedRef = useRef<HTMLDivElement>(null);
|
|
const bufRef = useRef<HTMLDivElement>(null);
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
const isDraggingRef = useRef(false);
|
|
const pendingSeekRef = useRef<number | null>(null);
|
|
|
|
const previewSeek = useCallback((progress: number) => {
|
|
const s = usePlayerStore.getState();
|
|
const p = Math.max(0, Math.min(1, progress));
|
|
pendingSeekRef.current = p;
|
|
if (timeRef.current) {
|
|
const previewTime = duration > 0 ? p * duration : s.currentTime;
|
|
timeRef.current.textContent = formatTime(previewTime);
|
|
}
|
|
if (playedRef.current) playedRef.current.style.width = `${p * 100}%`;
|
|
if (bufRef.current) bufRef.current.style.width = `${Math.max(p * 100, s.buffered * 100)}%`;
|
|
if (inputRef.current) inputRef.current.value = String(p);
|
|
}, [duration]);
|
|
|
|
const commitSeek = useCallback(() => {
|
|
const pending = pendingSeekRef.current;
|
|
if (pending === null) return;
|
|
pendingSeekRef.current = null;
|
|
seek(pending);
|
|
}, [seek]);
|
|
|
|
useEffect(() => {
|
|
const s = getPlaybackProgressSnapshot();
|
|
const pct = s.progress * 100;
|
|
if (timeRef.current) timeRef.current.textContent = formatTime(s.currentTime);
|
|
if (playedRef.current) playedRef.current.style.width = `${pct}%`;
|
|
if (bufRef.current) bufRef.current.style.width = `${Math.max(pct, s.buffered * 100)}%`;
|
|
if (inputRef.current) inputRef.current.value = String(s.progress);
|
|
|
|
return subscribePlaybackProgress(state => {
|
|
if (isDraggingRef.current) return;
|
|
const p = state.progress * 100;
|
|
if (timeRef.current) timeRef.current.textContent = formatTime(state.currentTime);
|
|
if (playedRef.current) playedRef.current.style.width = `${p}%`;
|
|
if (bufRef.current) bufRef.current.style.width = `${Math.max(p, state.buffered * 100)}%`;
|
|
if (inputRef.current) inputRef.current.value = String(state.progress);
|
|
});
|
|
}, []);
|
|
|
|
const handleSeek = useCallback(
|
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
previewSeek(parseFloat(e.target.value));
|
|
},
|
|
[previewSeek]
|
|
);
|
|
|
|
return (
|
|
<div className="fs-seekbar-wrap">
|
|
<div className="fs-seekbar-times">
|
|
<span ref={timeRef} />
|
|
<span>{formatTime(duration)}</span>
|
|
</div>
|
|
<div className="fs-seekbar">
|
|
<div className="fs-seekbar-bg" />
|
|
<div className="fs-seekbar-buf" ref={bufRef} />
|
|
<div className="fs-seekbar-played" ref={playedRef} />
|
|
<input
|
|
ref={inputRef}
|
|
type="range" min={0} max={1} step={0.001}
|
|
defaultValue={0}
|
|
onChange={handleSeek}
|
|
onMouseDown={() => { isDraggingRef.current = true; }}
|
|
onMouseUp={() => { isDraggingRef.current = false; commitSeek(); }}
|
|
onTouchStart={() => { isDraggingRef.current = true; }}
|
|
onTouchEnd={() => { isDraggingRef.current = false; commitSeek(); }}
|
|
onPointerDown={() => { isDraggingRef.current = true; }}
|
|
onPointerUp={() => { isDraggingRef.current = false; commitSeek(); }}
|
|
onKeyUp={commitSeek}
|
|
onBlur={() => { isDraggingRef.current = false; commitSeek(); }}
|
|
aria-label="seek"
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|