mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +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-devtools": "^5.90.2",
|
||||||
"@tanstack/react-query-persist-client": "^5.90.11",
|
"@tanstack/react-query-persist-client": "^5.90.11",
|
||||||
"@ts-rest/core": "^3.52.1",
|
"@ts-rest/core": "^3.52.1",
|
||||||
"@types/react-window": "^1.8.8",
|
|
||||||
"@wavesurfer/react": "^1.0.11",
|
"@wavesurfer/react": "^1.0.11",
|
||||||
"@xhayper/discord-rpc": "^1.3.0",
|
"@xhayper/discord-rpc": "^1.3.0",
|
||||||
"audiomotion-analyzer": "^4.5.1",
|
"audiomotion-analyzer": "^4.5.1",
|
||||||
@@ -142,6 +141,7 @@
|
|||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.5",
|
"@types/react": "^19.2.5",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@types/react-window": "^1.8.8",
|
||||||
"@types/source-map-support": "^0.5.10",
|
"@types/source-map-support": "^0.5.10",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
|
|||||||
Generated
+3
-3
@@ -62,9 +62,6 @@ importers:
|
|||||||
'@ts-rest/core':
|
'@ts-rest/core':
|
||||||
specifier: ^3.52.1
|
specifier: ^3.52.1
|
||||||
version: 3.52.1(@types/node@24.10.1)(zod@3.25.76)
|
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':
|
'@wavesurfer/react':
|
||||||
specifier: ^1.0.11
|
specifier: ^1.0.11
|
||||||
version: 1.0.11(react@19.1.0)(wavesurfer.js@7.11.1)
|
version: 1.0.11(react@19.1.0)(wavesurfer.js@7.11.1)
|
||||||
@@ -249,6 +246,9 @@ importers:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
specifier: ^19.2.3
|
specifier: ^19.2.3
|
||||||
version: 19.2.3(@types/react@19.2.5)
|
version: 19.2.3(@types/react@19.2.5)
|
||||||
|
'@types/react-window':
|
||||||
|
specifier: ^1.8.8
|
||||||
|
version: 1.8.8
|
||||||
'@types/source-map-support':
|
'@types/source-map-support':
|
||||||
specifier: ^0.5.10
|
specifier: ^0.5.10
|
||||||
version: 0.5.10
|
version: 0.5.10
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ import {
|
|||||||
UnsynchronizedLyrics,
|
UnsynchronizedLyrics,
|
||||||
UnsynchronizedLyricsProps,
|
UnsynchronizedLyricsProps,
|
||||||
} from '/@/renderer/features/lyrics/unsynchronized-lyrics';
|
} 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 { 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 { Center } from '/@/shared/components/center/center';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
@@ -130,22 +131,17 @@ export const Lyrics = () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
usePlayerEvents(
|
||||||
const unsubSongChange = usePlayerStore.subscribe(
|
{
|
||||||
(state) => state.current.song,
|
onCurrentSongChange: () => {
|
||||||
() => {
|
|
||||||
setOverride(undefined);
|
setOverride(undefined);
|
||||||
setIndex(0);
|
setIndex(0);
|
||||||
setShowTranslation(false);
|
setShowTranslation(false);
|
||||||
setTranslatedLyrics(null);
|
setTranslatedLyrics(null);
|
||||||
},
|
},
|
||||||
{ equalityFn: (a, b) => a?.id === b?.id },
|
},
|
||||||
);
|
[],
|
||||||
|
);
|
||||||
return () => {
|
|
||||||
unsubSongChange();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lyrics && !translatedLyrics && enableAutoTranslation) {
|
if (lyrics && !translatedLyrics && enableAutoTranslation) {
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import isElectron from 'is-electron';
|
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 styles from './synchronized-lyrics.module.css';
|
||||||
|
|
||||||
import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
|
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 { useScrobble } from '/@/renderer/features/player/hooks/use-scrobble';
|
||||||
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
|
import { useLyricsSettings, usePlaybackType, usePlayerActions } from '/@/renderer/store';
|
||||||
import {
|
|
||||||
useLyricsSettings,
|
|
||||||
usePlaybackType,
|
|
||||||
usePlayerActions,
|
|
||||||
usePlayerData,
|
|
||||||
usePlayerNum,
|
|
||||||
usePlayerStatus,
|
|
||||||
usePlayerTimestamp,
|
|
||||||
} from '/@/renderer/store';
|
|
||||||
import { FullLyricsMetadata, SynchronizedLyricsArray } from '/@/shared/types/domain-types';
|
import { FullLyricsMetadata, SynchronizedLyricsArray } from '/@/shared/types/domain-types';
|
||||||
import { PlayerStatus, PlayerType } from '/@/shared/types/types';
|
import { PlayerStatus, PlayerType } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -36,31 +28,26 @@ export const SynchronizedLyrics = ({
|
|||||||
source,
|
source,
|
||||||
translatedLyrics,
|
translatedLyrics,
|
||||||
}: SynchronizedLyricsProps) => {
|
}: SynchronizedLyricsProps) => {
|
||||||
const playersRef = PlayersRef;
|
|
||||||
const status = usePlayerStatus();
|
|
||||||
const playbackType = usePlaybackType();
|
const playbackType = usePlaybackType();
|
||||||
const playerData = usePlayerData();
|
|
||||||
const now = usePlayerTimestamp();
|
|
||||||
const settings = useLyricsSettings();
|
const settings = useLyricsSettings();
|
||||||
const currentPlayer = usePlayerNum();
|
const { mediaSeekToTimestamp } = usePlayerActions();
|
||||||
const currentPlayerRef =
|
|
||||||
currentPlayer === 1 ? playersRef.current?.player1 : playersRef.current?.player2;
|
|
||||||
|
|
||||||
const { handleScrobbleFromSeek } = useScrobble();
|
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(
|
const handleSeek = useCallback(
|
||||||
(time: number) => {
|
(time: number) => {
|
||||||
if (playbackType === PlayerType.LOCAL && mpvPlayer) {
|
if (playbackType === PlayerType.LOCAL && mpvPlayer) {
|
||||||
mpvPlayer.seekTo(time);
|
mpvPlayer.seekTo(time);
|
||||||
// setCurrentTime(time, true);
|
|
||||||
} else {
|
} else {
|
||||||
// setCurrentTime(time, true);
|
|
||||||
handleScrobbleFromSeek(time);
|
handleScrobbleFromSeek(time);
|
||||||
mpris?.updateSeek(time);
|
mpris?.updateSeek(time);
|
||||||
currentPlayerRef?.seekTo(time);
|
mediaSeekToTimestamp(time);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[currentPlayerRef, handleScrobbleFromSeek, playbackType],
|
[handleScrobbleFromSeek, mediaSeekToTimestamp, playbackType],
|
||||||
);
|
);
|
||||||
|
|
||||||
// const seeked = useSeeked();
|
// const seeked = useSeeked();
|
||||||
@@ -95,30 +82,9 @@ export const SynchronizedLyrics = ({
|
|||||||
return -1;
|
return -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCurrentTime = useCallback(async () => {
|
const setCurrentLyricRef = useRef<
|
||||||
if (isElectron() && playbackType !== PlayerType.WEB) {
|
(timeInMs: number, epoch?: number, targetIndex?: number) => void
|
||||||
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 setCurrentLyric = useCallback(
|
const setCurrentLyric = useCallback(
|
||||||
(timeInMs: number, epoch?: number, targetIndex?: number) => {
|
(timeInMs: number, epoch?: number, targetIndex?: number) => {
|
||||||
@@ -177,7 +143,7 @@ export const SynchronizedLyrics = ({
|
|||||||
|
|
||||||
lyricTimer.current = setTimeout(
|
lyricTimer.current = setTimeout(
|
||||||
() => {
|
() => {
|
||||||
setCurrentLyric(nextTime, nextEpoch, index + 1);
|
setCurrentLyricRef.current(nextTime, nextEpoch, index + 1);
|
||||||
},
|
},
|
||||||
nextTime - timeInMs - elapsed,
|
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(() => {
|
useEffect(() => {
|
||||||
// Copy the follow settings into a ref that can be accessed in the timeout
|
// Copy the follow settings into a ref that can be accessed in the timeout
|
||||||
followRef.current = settings.follow;
|
followRef.current = settings.follow;
|
||||||
@@ -194,40 +182,21 @@ export const SynchronizedLyrics = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// This handler is used to handle when lyrics change. It is in some sense the
|
// 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
|
// 'primary' handler for parsing lyrics, as unlike the other callbacks, it will
|
||||||
// ALSO remove listeners on close. Use the promisified getCurrentTime(), because
|
// ALSO remove listeners on close.
|
||||||
// we don't want to be dependent on npw, which may not be precise
|
|
||||||
lyricRef.current = lyrics;
|
lyricRef.current = lyrics;
|
||||||
|
|
||||||
if (status === PlayerStatus.PLAYING) {
|
if (status === PlayerStatus.PLAYING) {
|
||||||
let rejected = false;
|
// Use the current timestamp from player events
|
||||||
|
setCurrentLyric(timestamp * 1000 - delayMsRef.current);
|
||||||
getCurrentTime()
|
|
||||||
.then((timeInSec: number) => {
|
|
||||||
if (rejected) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentLyric(timeInSec * 1000 - delayMsRef.current);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// Case 1: cleanup happens before we hear back from
|
// Cleanup: clear the timer when lyrics change or component unmounts
|
||||||
// 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)
|
|
||||||
if (lyricTimer.current) clearTimeout(lyricTimer.current);
|
if (lyricTimer.current) clearTimeout(lyricTimer.current);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {};
|
return () => {};
|
||||||
}, [getCurrentTime, lyrics, playbackType, setCurrentLyric, status]);
|
}, [lyrics, setCurrentLyric, status, timestamp]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// This handler is used to deal with changes to the current delay. If the offset
|
// 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;
|
const changed = delayMsRef.current !== settings.delayMs;
|
||||||
|
|
||||||
if (!changed) {
|
if (!changed) {
|
||||||
return () => {};
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lyricTimer.current) {
|
if (lyricTimer.current) {
|
||||||
clearTimeout(lyricTimer.current);
|
clearTimeout(lyricTimer.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
let rejected = false;
|
|
||||||
|
|
||||||
delayMsRef.current = settings.delayMs;
|
delayMsRef.current = settings.delayMs;
|
||||||
|
|
||||||
getCurrentTime()
|
// Use the current timestamp from player events
|
||||||
.then((timeInSec: number) => {
|
setCurrentLyric(timestamp * 1000 - delayMsRef.current);
|
||||||
if (rejected) {
|
}, [setCurrentLyric, settings.delayMs, timestamp]);
|
||||||
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]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// This handler is used specifically for dealing with seeking. In this case,
|
// This handler is used specifically for dealing with seeking and progress updates.
|
||||||
// we assume that now is the accurate time
|
// When the timestamp changes, update the current lyric position.
|
||||||
if (status !== PlayerStatus.PLAYING) {
|
if (status !== PlayerStatus.PLAYING) {
|
||||||
if (lyricTimer.current) {
|
if (lyricTimer.current) {
|
||||||
clearTimeout(lyricTimer.current);
|
clearTimeout(lyricTimer.current);
|
||||||
@@ -277,20 +229,12 @@ export const SynchronizedLyrics = ({
|
|||||||
return;
|
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) {
|
if (lyricTimer.current) {
|
||||||
clearTimeout(lyricTimer.current);
|
clearTimeout(lyricTimer.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentLyric(now * 1000 - delayMsRef.current);
|
setCurrentLyric(timestamp * 1000 - delayMsRef.current);
|
||||||
}, [now, setCurrentLyric, status]);
|
}, [timestamp, setCurrentLyric, status]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Guaranteed cleanup; stop the timer, and just in case also increment
|
// Guaranteed cleanup; stop the timer, and just in case also increment
|
||||||
|
|||||||
Reference in New Issue
Block a user