mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 12:30:12 +02:00
fix lyrics components
This commit is contained in:
+1
-1
@@ -78,7 +78,6 @@
|
||||
"@tanstack/react-query-devtools": "^5.90.2",
|
||||
"@tanstack/react-query-persist-client": "^5.90.11",
|
||||
"@ts-rest/core": "^3.52.1",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@wavesurfer/react": "^1.0.11",
|
||||
"@xhayper/discord-rpc": "^1.3.0",
|
||||
"audiomotion-analyzer": "^4.5.1",
|
||||
@@ -142,6 +141,7 @@
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
|
||||
Generated
+3
-3
@@ -62,9 +62,6 @@ importers:
|
||||
'@ts-rest/core':
|
||||
specifier: ^3.52.1
|
||||
version: 3.52.1(@types/node@24.10.1)(zod@3.25.76)
|
||||
'@types/react-window':
|
||||
specifier: ^1.8.8
|
||||
version: 1.8.8
|
||||
'@wavesurfer/react':
|
||||
specifier: ^1.0.11
|
||||
version: 1.0.11(react@19.1.0)(wavesurfer.js@7.11.1)
|
||||
@@ -249,6 +246,9 @@ importers:
|
||||
'@types/react-dom':
|
||||
specifier: ^19.2.3
|
||||
version: 19.2.3(@types/react@19.2.5)
|
||||
'@types/react-window':
|
||||
specifier: ^1.8.8
|
||||
version: 1.8.8
|
||||
'@types/source-map-support':
|
||||
specifier: ^0.5.10
|
||||
version: 0.5.10
|
||||
|
||||
@@ -19,8 +19,9 @@ import {
|
||||
UnsynchronizedLyrics,
|
||||
UnsynchronizedLyricsProps,
|
||||
} from '/@/renderer/features/lyrics/unsynchronized-lyrics';
|
||||
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
||||
import { queryClient } from '/@/renderer/lib/react-query';
|
||||
import { usePlayerSong, useLyricsSettings, usePlayerStore } from '/@/renderer/store';
|
||||
import { useLyricsSettings, usePlayerSong } from '/@/renderer/store';
|
||||
import { Center } from '/@/shared/components/center/center';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
@@ -130,22 +131,17 @@ export const Lyrics = () => {
|
||||
}),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubSongChange = usePlayerStore.subscribe(
|
||||
(state) => state.current.song,
|
||||
() => {
|
||||
usePlayerEvents(
|
||||
{
|
||||
onCurrentSongChange: () => {
|
||||
setOverride(undefined);
|
||||
setIndex(0);
|
||||
setShowTranslation(false);
|
||||
setTranslatedLyrics(null);
|
||||
},
|
||||
{ equalityFn: (a, b) => a?.id === b?.id },
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubSongChange();
|
||||
};
|
||||
}, []);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (lyrics && !translatedLyrics && enableAutoTranslation) {
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
import clsx from 'clsx';
|
||||
import isElectron from 'is-electron';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import styles from './synchronized-lyrics.module.css';
|
||||
|
||||
import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
|
||||
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
||||
import { useScrobble } from '/@/renderer/features/player/hooks/use-scrobble';
|
||||
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
|
||||
import {
|
||||
useLyricsSettings,
|
||||
usePlaybackType,
|
||||
usePlayerActions,
|
||||
usePlayerData,
|
||||
usePlayerNum,
|
||||
usePlayerStatus,
|
||||
usePlayerTimestamp,
|
||||
} from '/@/renderer/store';
|
||||
import { useLyricsSettings, usePlaybackType, usePlayerActions } from '/@/renderer/store';
|
||||
import { FullLyricsMetadata, SynchronizedLyricsArray } from '/@/shared/types/domain-types';
|
||||
import { PlayerStatus, PlayerType } from '/@/shared/types/types';
|
||||
|
||||
@@ -36,31 +28,26 @@ export const SynchronizedLyrics = ({
|
||||
source,
|
||||
translatedLyrics,
|
||||
}: SynchronizedLyricsProps) => {
|
||||
const playersRef = PlayersRef;
|
||||
const status = usePlayerStatus();
|
||||
const playbackType = usePlaybackType();
|
||||
const playerData = usePlayerData();
|
||||
const now = usePlayerTimestamp();
|
||||
const settings = useLyricsSettings();
|
||||
const currentPlayer = usePlayerNum();
|
||||
const currentPlayerRef =
|
||||
currentPlayer === 1 ? playersRef.current?.player1 : playersRef.current?.player2;
|
||||
|
||||
const { mediaSeekToTimestamp } = usePlayerActions();
|
||||
const { handleScrobbleFromSeek } = useScrobble();
|
||||
|
||||
// State for player status and timestamp from events
|
||||
const [status, setStatus] = useState<PlayerStatus>(PlayerStatus.PAUSED);
|
||||
const [timestamp, setTimestamp] = useState<number>(0);
|
||||
|
||||
const handleSeek = useCallback(
|
||||
(time: number) => {
|
||||
if (playbackType === PlayerType.LOCAL && mpvPlayer) {
|
||||
mpvPlayer.seekTo(time);
|
||||
// setCurrentTime(time, true);
|
||||
} else {
|
||||
// setCurrentTime(time, true);
|
||||
handleScrobbleFromSeek(time);
|
||||
mpris?.updateSeek(time);
|
||||
currentPlayerRef?.seekTo(time);
|
||||
mediaSeekToTimestamp(time);
|
||||
}
|
||||
},
|
||||
[currentPlayerRef, handleScrobbleFromSeek, playbackType],
|
||||
[handleScrobbleFromSeek, mediaSeekToTimestamp, playbackType],
|
||||
);
|
||||
|
||||
// const seeked = useSeeked();
|
||||
@@ -95,30 +82,9 @@ export const SynchronizedLyrics = ({
|
||||
return -1;
|
||||
};
|
||||
|
||||
const getCurrentTime = useCallback(async () => {
|
||||
if (isElectron() && playbackType !== PlayerType.WEB) {
|
||||
if (mpvPlayer) {
|
||||
return mpvPlayer.getCurrentTime();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (playersRef.current === undefined) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const player =
|
||||
playerData.player.playerNum === 1
|
||||
? playersRef.current.player1
|
||||
: playersRef.current.player2;
|
||||
const underlying = player?.getInternalPlayer();
|
||||
|
||||
// If it is null, this probably means we added a new song while the lyrics tab is open
|
||||
// and the queue was previously empty
|
||||
if (!underlying) return 0;
|
||||
|
||||
return underlying.currentTime;
|
||||
}, [playbackType, playersRef, playerData]);
|
||||
const setCurrentLyricRef = useRef<
|
||||
(timeInMs: number, epoch?: number, targetIndex?: number) => void
|
||||
>(() => {});
|
||||
|
||||
const setCurrentLyric = useCallback(
|
||||
(timeInMs: number, epoch?: number, targetIndex?: number) => {
|
||||
@@ -177,7 +143,7 @@ export const SynchronizedLyrics = ({
|
||||
|
||||
lyricTimer.current = setTimeout(
|
||||
() => {
|
||||
setCurrentLyric(nextTime, nextEpoch, index + 1);
|
||||
setCurrentLyricRef.current(nextTime, nextEpoch, index + 1);
|
||||
},
|
||||
nextTime - timeInMs - elapsed,
|
||||
);
|
||||
@@ -186,6 +152,28 @@ export const SynchronizedLyrics = ({
|
||||
[],
|
||||
);
|
||||
|
||||
// Store the callback in a ref so it can be called recursively
|
||||
useEffect(() => {
|
||||
setCurrentLyricRef.current = setCurrentLyric;
|
||||
}, [setCurrentLyric]);
|
||||
|
||||
// Subscribe to player events
|
||||
usePlayerEvents(
|
||||
{
|
||||
onPlayerProgress: (properties) => {
|
||||
setTimestamp(properties.timestamp);
|
||||
},
|
||||
onPlayerSeekToTimestamp: (properties) => {
|
||||
// When seeking, update timestamp immediately
|
||||
setTimestamp(properties.timestamp);
|
||||
},
|
||||
onPlayerStatus: (properties) => {
|
||||
setStatus(properties.status);
|
||||
},
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Copy the follow settings into a ref that can be accessed in the timeout
|
||||
followRef.current = settings.follow;
|
||||
@@ -194,40 +182,21 @@ export const SynchronizedLyrics = ({
|
||||
useEffect(() => {
|
||||
// This handler is used to handle when lyrics change. It is in some sense the
|
||||
// 'primary' handler for parsing lyrics, as unlike the other callbacks, it will
|
||||
// ALSO remove listeners on close. Use the promisified getCurrentTime(), because
|
||||
// we don't want to be dependent on npw, which may not be precise
|
||||
// ALSO remove listeners on close.
|
||||
lyricRef.current = lyrics;
|
||||
|
||||
if (status === PlayerStatus.PLAYING) {
|
||||
let rejected = false;
|
||||
|
||||
getCurrentTime()
|
||||
.then((timeInSec: number) => {
|
||||
if (rejected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setCurrentLyric(timeInSec * 1000 - delayMsRef.current);
|
||||
|
||||
return true;
|
||||
})
|
||||
.catch(console.error);
|
||||
// Use the current timestamp from player events
|
||||
setCurrentLyric(timestamp * 1000 - delayMsRef.current);
|
||||
|
||||
return () => {
|
||||
// Case 1: cleanup happens before we hear back from
|
||||
// the main process. In this case, when the promise resolves, ignore the result
|
||||
rejected = true;
|
||||
|
||||
// Case 2: Cleanup happens after we hear back from main process but
|
||||
// (potentially) before the next lyric. In this case, clear the timer.
|
||||
// Do NOT do this for other cleanup functions, as it should only be done
|
||||
// when switching to a new song (or an empty one)
|
||||
// Cleanup: clear the timer when lyrics change or component unmounts
|
||||
if (lyricTimer.current) clearTimeout(lyricTimer.current);
|
||||
};
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [getCurrentTime, lyrics, playbackType, setCurrentLyric, status]);
|
||||
}, [lyrics, setCurrentLyric, status, timestamp]);
|
||||
|
||||
useEffect(() => {
|
||||
// This handler is used to deal with changes to the current delay. If the offset
|
||||
@@ -236,39 +205,22 @@ export const SynchronizedLyrics = ({
|
||||
const changed = delayMsRef.current !== settings.delayMs;
|
||||
|
||||
if (!changed) {
|
||||
return () => {};
|
||||
return;
|
||||
}
|
||||
|
||||
if (lyricTimer.current) {
|
||||
clearTimeout(lyricTimer.current);
|
||||
}
|
||||
|
||||
let rejected = false;
|
||||
|
||||
delayMsRef.current = settings.delayMs;
|
||||
|
||||
getCurrentTime()
|
||||
.then((timeInSec: number) => {
|
||||
if (rejected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setCurrentLyric(timeInSec * 1000 - delayMsRef.current);
|
||||
|
||||
return true;
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
return () => {
|
||||
// In the event this ends earlier, just kill the promise. Cleanup of
|
||||
// timeouts is otherwise handled by another handler
|
||||
rejected = true;
|
||||
};
|
||||
}, [getCurrentTime, setCurrentLyric, settings.delayMs]);
|
||||
// Use the current timestamp from player events
|
||||
setCurrentLyric(timestamp * 1000 - delayMsRef.current);
|
||||
}, [setCurrentLyric, settings.delayMs, timestamp]);
|
||||
|
||||
useEffect(() => {
|
||||
// This handler is used specifically for dealing with seeking. In this case,
|
||||
// we assume that now is the accurate time
|
||||
// This handler is used specifically for dealing with seeking and progress updates.
|
||||
// When the timestamp changes, update the current lyric position.
|
||||
if (status !== PlayerStatus.PLAYING) {
|
||||
if (lyricTimer.current) {
|
||||
clearTimeout(lyricTimer.current);
|
||||
@@ -277,20 +229,12 @@ export const SynchronizedLyrics = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// If the time goes back to 0 and we are still playing, this suggests that
|
||||
// we may be playing the same track (repeat one). In this case, we also
|
||||
// need to restart playback
|
||||
const restarted = status === PlayerStatus.PLAYING && now === 0;
|
||||
// if (!seeked && !restarted) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (lyricTimer.current) {
|
||||
clearTimeout(lyricTimer.current);
|
||||
}
|
||||
|
||||
setCurrentLyric(now * 1000 - delayMsRef.current);
|
||||
}, [now, setCurrentLyric, status]);
|
||||
setCurrentLyric(timestamp * 1000 - delayMsRef.current);
|
||||
}, [timestamp, setCurrentLyric, status]);
|
||||
|
||||
useEffect(() => {
|
||||
// Guaranteed cleanup; stop the timer, and just in case also increment
|
||||
|
||||
Reference in New Issue
Block a user