mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
feat: v1.34.1 — fullscreen lyrics overlay, FsArt crossfade, lyrics toggle, contributor updates
- FullscreenPlayer: synced lyrics overlay (5-line rail, mask fade, click-to-seek) - FullscreenPlayer: FsArt crossfade component with onLoad pattern, 300px art, next-track pre-fetch - FullscreenPlayer: MicVocal lyrics toggle button next to heart (persisted in authStore) - FullscreenPlayer: idle system — only close button auto-hides, cluster+seekbar always visible - LyricsPane: refactored to use shared useLyrics hook (no double-fetch with FS overlay) - authStore: showFullscreenLyrics field (default true) - i18n: fsLyricsToggle key added to all 7 locales; ru2.ts removed (merged into ru.ts) - Settings: Contributors updated (nisrael, cucadmuh, kilyabin); logout moved to Server tab as btn-danger - theme.css: btn-danger style added Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import { fetchLyrics, parseLrc, LrcLine } from '../api/lrclib';
|
||||
import type { LrcLine } from '../api/lrclib';
|
||||
import { useLyrics } from '../hooks/useLyrics';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { Track } from '../store/playerStore';
|
||||
|
||||
@@ -8,87 +9,27 @@ interface Props {
|
||||
currentTrack: Track | null;
|
||||
}
|
||||
|
||||
interface CachedLyrics {
|
||||
syncedLines: LrcLine[] | null;
|
||||
plainLyrics: string | null;
|
||||
notFound: boolean;
|
||||
}
|
||||
|
||||
// Session-level cache — survives tab switches (component unmount/remount).
|
||||
// Cleared implicitly when the app restarts.
|
||||
const lyricsCache = new Map<string, CachedLyrics>();
|
||||
|
||||
export default function LyricsPane({ currentTrack }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const cached = currentTrack ? lyricsCache.get(currentTrack.id) : undefined;
|
||||
const { syncedLines, plainLyrics, source, loading, notFound } = useLyrics(currentTrack);
|
||||
|
||||
const [loading, setLoading] = useState(!cached && !!currentTrack);
|
||||
const [syncedLines, setSyncedLines] = useState<LrcLine[] | null>(cached?.syncedLines ?? null);
|
||||
const [plainLyrics, setPlainLyrics] = useState<string | null>(cached?.plainLyrics ?? null);
|
||||
const [notFound, setNotFound] = useState(cached?.notFound ?? false);
|
||||
|
||||
const hasSynced = syncedLines !== null && syncedLines.length > 0;
|
||||
const hasSynced = syncedLines !== null && syncedLines.length > 0;
|
||||
const currentTime = usePlayerStore(s => hasSynced ? s.currentTime : 0);
|
||||
const seek = usePlayerStore(s => s.seek);
|
||||
const duration = usePlayerStore(s => s.currentTrack?.duration ?? 0);
|
||||
const seek = usePlayerStore(s => s.seek);
|
||||
const duration = usePlayerStore(s => s.currentTrack?.duration ?? 0);
|
||||
|
||||
const lineRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const prevActive = useRef(-1);
|
||||
|
||||
// Reset refs when track changes
|
||||
useEffect(() => {
|
||||
if (!currentTrack) return;
|
||||
|
||||
// Serve from cache if available
|
||||
const hit = lyricsCache.get(currentTrack.id);
|
||||
if (hit) {
|
||||
setSyncedLines(hit.syncedLines);
|
||||
setPlainLyrics(hit.plainLyrics);
|
||||
setNotFound(hit.notFound);
|
||||
setLoading(false);
|
||||
lineRefs.current = [];
|
||||
prevActive.current = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
setSyncedLines(null);
|
||||
setPlainLyrics(null);
|
||||
setNotFound(false);
|
||||
setLoading(true);
|
||||
lineRefs.current = [];
|
||||
prevActive.current = -1;
|
||||
|
||||
fetchLyrics(
|
||||
currentTrack.artist ?? '',
|
||||
currentTrack.title,
|
||||
currentTrack.album ?? '',
|
||||
currentTrack.duration ?? 0,
|
||||
).then(result => {
|
||||
if (cancelled) return;
|
||||
setLoading(false);
|
||||
if (!result || (!result.syncedLyrics && !result.plainLyrics)) {
|
||||
lyricsCache.set(currentTrack.id, { syncedLines: null, plainLyrics: null, notFound: true });
|
||||
setNotFound(true);
|
||||
return;
|
||||
}
|
||||
const lines = result.syncedLyrics ? parseLrc(result.syncedLyrics) : null;
|
||||
const synced = lines && lines.length > 0 ? lines : null;
|
||||
lyricsCache.set(currentTrack.id, { syncedLines: synced, plainLyrics: result.plainLyrics, notFound: false });
|
||||
setSyncedLines(synced);
|
||||
setPlainLyrics(result.plainLyrics);
|
||||
}).catch(() => {
|
||||
if (!cancelled) {
|
||||
lyricsCache.set(currentTrack.id, { syncedLines: null, plainLyrics: null, notFound: true });
|
||||
setLoading(false);
|
||||
setNotFound(true);
|
||||
}
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [currentTrack?.id]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [currentTrack?.id]);
|
||||
|
||||
const activeIdx = hasSynced
|
||||
? syncedLines!.reduce((acc, line, i) => (currentTime >= line.time ? i : acc), -1)
|
||||
? (syncedLines as LrcLine[]).reduce((acc, line, i) => (currentTime >= line.time ? i : acc), -1)
|
||||
: -1;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -112,13 +53,19 @@ export default function LyricsPane({ currentTrack }: Props) {
|
||||
return `${base} active`;
|
||||
};
|
||||
|
||||
const sourceLabel = source === 'server'
|
||||
? t('player.lyricsSourceServer')
|
||||
: source === 'lrclib'
|
||||
? t('player.lyricsSourceLrclib')
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="lyrics-pane">
|
||||
{loading && <p className="lyrics-status">{t('player.lyricsLoading')}</p>}
|
||||
{notFound && !loading && <p className="lyrics-status">{t('player.lyricsNotFound')}</p>}
|
||||
{hasSynced && (
|
||||
<div className="lyrics-synced">
|
||||
{syncedLines!.map((line, i) => (
|
||||
{(syncedLines as LrcLine[]).map((line, i) => (
|
||||
<div
|
||||
key={i}
|
||||
ref={el => { lineRefs.current[i] = el; }}
|
||||
@@ -138,6 +85,9 @@ export default function LyricsPane({ currentTrack }: Props) {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{sourceLabel && !loading && !notFound && (
|
||||
<p className="lyrics-source">{sourceLabel}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user