mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-15 15:10:01 +02:00
performance refactor for synchronized lyrics timestamp/status subscribers
This commit is contained in:
@@ -6,13 +6,14 @@ import styles from './synchronized-lyrics.module.css';
|
|||||||
|
|
||||||
import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
|
import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
|
||||||
import {
|
import {
|
||||||
|
subscribePlayerStatus,
|
||||||
useLyricsDisplaySettings,
|
useLyricsDisplaySettings,
|
||||||
useLyricsSettings,
|
useLyricsSettings,
|
||||||
usePlaybackType,
|
usePlaybackType,
|
||||||
usePlayerActions,
|
usePlayerActions,
|
||||||
usePlayerStatus,
|
usePlayerStoreBase,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { usePlayerTimestamp } from '/@/renderer/store/timestamp.store';
|
import { subscribePlayerProgress, useTimestampStoreBase } from '/@/renderer/store/timestamp.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';
|
||||||
|
|
||||||
@@ -58,8 +59,6 @@ export const SynchronizedLyrics = ({
|
|||||||
: 0.95,
|
: 0.95,
|
||||||
};
|
};
|
||||||
const { mediaSeekToTimestamp } = usePlayerActions();
|
const { mediaSeekToTimestamp } = usePlayerActions();
|
||||||
const status = usePlayerStatus();
|
|
||||||
const timestamp = usePlayerTimestamp();
|
|
||||||
|
|
||||||
const effectiveOffsetMs = offsetMs ?? 0;
|
const effectiveOffsetMs = offsetMs ?? 0;
|
||||||
|
|
||||||
@@ -75,7 +74,20 @@ export const SynchronizedLyrics = ({
|
|||||||
[mediaSeekToTimestamp, playbackType],
|
[mediaSeekToTimestamp, playbackType],
|
||||||
);
|
);
|
||||||
|
|
||||||
// const seeked = useSeeked();
|
const handleContainerClick = useCallback(
|
||||||
|
(event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
const target = (event.target as HTMLElement).closest('[data-lyric-time]');
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const time = Number((target as HTMLElement).dataset.lyricTime);
|
||||||
|
if (time > 0 && Number.isFinite(time)) {
|
||||||
|
handleSeek(time / 1000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleSeek],
|
||||||
|
);
|
||||||
|
|
||||||
// A reference to the timeout handler
|
// A reference to the timeout handler
|
||||||
const lyricTimer = useRef<null | ReturnType<typeof setTimeout>>(null);
|
const lyricTimer = useRef<null | ReturnType<typeof setTimeout>>(null);
|
||||||
@@ -91,6 +103,7 @@ export const SynchronizedLyrics = ({
|
|||||||
|
|
||||||
const delayMsRef = useRef(effectiveOffsetMs);
|
const delayMsRef = useRef(effectiveOffsetMs);
|
||||||
const followRef = useRef(settings.follow);
|
const followRef = useRef(settings.follow);
|
||||||
|
const statusRef = useRef(usePlayerStoreBase.getState().player.status);
|
||||||
const userScrollingRef = useRef(false);
|
const userScrollingRef = useRef(false);
|
||||||
const scrollTimeoutRef = useRef<null | ReturnType<typeof setTimeout>>(null);
|
const scrollTimeoutRef = useRef<null | ReturnType<typeof setTimeout>>(null);
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -197,6 +210,11 @@ export const SynchronizedLyrics = ({
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const syncFromCurrentTimestamp = useCallback(() => {
|
||||||
|
const timestamp = useTimestampStoreBase.getState().timestamp;
|
||||||
|
setCurrentLyric(timestamp * 1000 + delayMsRef.current);
|
||||||
|
}, [setCurrentLyric]);
|
||||||
|
|
||||||
// Store the callback in a ref so it can be called recursively
|
// Store the callback in a ref so it can be called recursively
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentLyricRef.current = setCurrentLyric;
|
setCurrentLyricRef.current = setCurrentLyric;
|
||||||
@@ -208,32 +226,22 @@ export const SynchronizedLyrics = ({
|
|||||||
}, [settings.follow]);
|
}, [settings.follow]);
|
||||||
|
|
||||||
useEffect(() => {
|
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.
|
|
||||||
lyricRef.current = lyrics;
|
lyricRef.current = lyrics;
|
||||||
|
|
||||||
if (status === PlayerStatus.PLAYING) {
|
if (statusRef.current === PlayerStatus.PLAYING) {
|
||||||
// Use the current timestamp from player events
|
syncFromCurrentTimestamp();
|
||||||
setCurrentLyric(timestamp * 1000 + delayMsRef.current);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
// Cleanup: clear the timer when lyrics change or component unmounts
|
|
||||||
if (lyricTimer.current) clearTimeout(lyricTimer.current);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {};
|
return () => {
|
||||||
}, [lyrics, setCurrentLyric, status, timestamp]);
|
if (lyricTimer.current) {
|
||||||
|
clearTimeout(lyricTimer.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [lyrics, syncFromCurrentTimestamp]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// This handler is used to deal with changes to the current delay. If the offset
|
|
||||||
// changes, we should immediately stop the current listening set and calculate
|
|
||||||
// the correct one using the new offset. Afterwards, timing can be calculated like normal
|
|
||||||
const newOffset = offsetMs ?? 0;
|
const newOffset = offsetMs ?? 0;
|
||||||
const changed = delayMsRef.current !== newOffset;
|
if (delayMsRef.current === newOffset) {
|
||||||
|
|
||||||
if (!changed) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,14 +250,31 @@ export const SynchronizedLyrics = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
delayMsRef.current = newOffset;
|
delayMsRef.current = newOffset;
|
||||||
|
syncFromCurrentTimestamp();
|
||||||
// Use the current timestamp from player events
|
}, [offsetMs, syncFromCurrentTimestamp]);
|
||||||
setCurrentLyric(timestamp * 1000 + delayMsRef.current);
|
|
||||||
}, [setCurrentLyric, offsetMs, timestamp]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// This handler is used specifically for dealing with seeking and progress updates.
|
const unsubscribe = subscribePlayerProgress(({ timestamp }) => {
|
||||||
// When the timestamp changes, update the current lyric position.
|
if (statusRef.current !== PlayerStatus.PLAYING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lyricTimer.current) {
|
||||||
|
clearTimeout(lyricTimer.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentLyric(timestamp * 1000 + delayMsRef.current);
|
||||||
|
});
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
}, [setCurrentLyric]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
statusRef.current = usePlayerStoreBase.getState().player.status;
|
||||||
|
|
||||||
|
const unsubscribe = subscribePlayerStatus(({ status }) => {
|
||||||
|
statusRef.current = status;
|
||||||
|
|
||||||
if (status !== PlayerStatus.PLAYING) {
|
if (status !== PlayerStatus.PLAYING) {
|
||||||
if (lyricTimer.current) {
|
if (lyricTimer.current) {
|
||||||
clearTimeout(lyricTimer.current);
|
clearTimeout(lyricTimer.current);
|
||||||
@@ -262,8 +287,11 @@ export const SynchronizedLyrics = ({
|
|||||||
clearTimeout(lyricTimer.current);
|
clearTimeout(lyricTimer.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentLyric(timestamp * 1000 + delayMsRef.current);
|
syncFromCurrentTimestamp();
|
||||||
}, [timestamp, setCurrentLyric, status]);
|
});
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
}, [syncFromCurrentTimestamp]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Guaranteed cleanup; stop the timer, and just in case also increment
|
// Guaranteed cleanup; stop the timer, and just in case also increment
|
||||||
@@ -336,6 +364,7 @@ export const SynchronizedLyrics = ({
|
|||||||
<div
|
<div
|
||||||
className={clsx(styles.container, 'synchronized-lyrics overlay-scrollbar')}
|
className={clsx(styles.container, 'synchronized-lyrics overlay-scrollbar')}
|
||||||
id="sychronized-lyrics-scroll-container"
|
id="sychronized-lyrics-scroll-container"
|
||||||
|
onClick={handleContainerClick}
|
||||||
onMouseEnter={showScrollbar}
|
onMouseEnter={showScrollbar}
|
||||||
onMouseLeave={hideScrollbar}
|
onMouseLeave={hideScrollbar}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
@@ -372,14 +401,10 @@ export const SynchronizedLyrics = ({
|
|||||||
<LyricLine
|
<LyricLine
|
||||||
alignment={settings.alignment}
|
alignment={settings.alignment}
|
||||||
className="lyric-line synchronized"
|
className="lyric-line synchronized"
|
||||||
|
data-lyric-time={time}
|
||||||
fontSize={settings.fontSize}
|
fontSize={settings.fontSize}
|
||||||
id={`lyric-${idx}`}
|
id={`lyric-${idx}`}
|
||||||
key={idx}
|
key={idx}
|
||||||
onClick={() => {
|
|
||||||
if (time > 0 && Number.isFinite(time)) {
|
|
||||||
handleSeek(time / 1000);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
romajiText={romajiLyrics?.[idx]?.[1]}
|
romajiText={romajiLyrics?.[idx]?.[1]}
|
||||||
text={text}
|
text={text}
|
||||||
translatedText={translatedLyrics?.split('\n')[idx]}
|
translatedText={translatedLyrics?.split('\n')[idx]}
|
||||||
|
|||||||
Reference in New Issue
Block a user