mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
persist lyrics offset per song
This commit is contained in:
@@ -2,12 +2,7 @@ import isElectron from 'is-electron';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { openLyricSearchModal } from '/@/renderer/features/lyrics/components/lyrics-search-form';
|
import { openLyricSearchModal } from '/@/renderer/features/lyrics/components/lyrics-search-form';
|
||||||
import {
|
import { useLyricsSettings, usePlayerSong } from '/@/renderer/store';
|
||||||
useLyricsSettings,
|
|
||||||
usePlayerSong,
|
|
||||||
useSettingsStore,
|
|
||||||
useSettingsStoreActions,
|
|
||||||
} from '/@/renderer/store';
|
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Button } from '/@/shared/components/button/button';
|
import { Button } from '/@/shared/components/button/button';
|
||||||
import { Center } from '/@/shared/components/center/center';
|
import { Center } from '/@/shared/components/center/center';
|
||||||
@@ -20,9 +15,11 @@ import { LyricsOverride } from '/@/shared/types/domain-types';
|
|||||||
interface LyricsActionsProps {
|
interface LyricsActionsProps {
|
||||||
index: number;
|
index: number;
|
||||||
languages: { label: string; value: string }[];
|
languages: { label: string; value: string }[];
|
||||||
|
offsetMs: number;
|
||||||
onRemoveLyric: () => void;
|
onRemoveLyric: () => void;
|
||||||
onSearchOverride: (params: LyricsOverride) => void;
|
onSearchOverride: (params: LyricsOverride) => void;
|
||||||
onTranslateLyric?: () => void;
|
onTranslateLyric?: () => void;
|
||||||
|
onUpdateOffset: (offsetMs: number) => void;
|
||||||
setIndex: (idx: number) => void;
|
setIndex: (idx: number) => void;
|
||||||
synced?: boolean;
|
synced?: boolean;
|
||||||
}
|
}
|
||||||
@@ -30,23 +27,19 @@ interface LyricsActionsProps {
|
|||||||
export const LyricsActions = ({
|
export const LyricsActions = ({
|
||||||
index,
|
index,
|
||||||
languages,
|
languages,
|
||||||
|
offsetMs,
|
||||||
onRemoveLyric,
|
onRemoveLyric,
|
||||||
onSearchOverride,
|
onSearchOverride,
|
||||||
onTranslateLyric,
|
onTranslateLyric,
|
||||||
|
onUpdateOffset,
|
||||||
setIndex,
|
setIndex,
|
||||||
}: LyricsActionsProps) => {
|
}: LyricsActionsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const currentSong = usePlayerSong();
|
const currentSong = usePlayerSong();
|
||||||
const { setSettings } = useSettingsStoreActions();
|
const { sources } = useLyricsSettings();
|
||||||
const { delayMs, sources } = useLyricsSettings();
|
|
||||||
|
|
||||||
const handleLyricOffset = (e: number | string) => {
|
const handleLyricOffset = (e: number | string) => {
|
||||||
setSettings({
|
onUpdateOffset(Number(e));
|
||||||
lyrics: {
|
|
||||||
...useSettingsStore.getState().lyrics,
|
|
||||||
delayMs: Number(e),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isActionsDisabled = !currentSong;
|
const isActionsDisabled = !currentSong;
|
||||||
@@ -86,7 +79,7 @@ export const LyricsActions = ({
|
|||||||
<ActionIcon
|
<ActionIcon
|
||||||
aria-label="Decrease lyric offset"
|
aria-label="Decrease lyric offset"
|
||||||
icon="minus"
|
icon="minus"
|
||||||
onClick={() => handleLyricOffset(delayMs - 50)}
|
onClick={() => handleLyricOffset(offsetMs - 50)}
|
||||||
tooltip={{
|
tooltip={{
|
||||||
label: t('common.slower', { postProcess: 'sentenceCase' }),
|
label: t('common.slower', { postProcess: 'sentenceCase' }),
|
||||||
openDelay: 0,
|
openDelay: 0,
|
||||||
@@ -101,14 +94,14 @@ export const LyricsActions = ({
|
|||||||
aria-label="Lyric offset"
|
aria-label="Lyric offset"
|
||||||
onChange={handleLyricOffset}
|
onChange={handleLyricOffset}
|
||||||
styles={{ input: { textAlign: 'center' } }}
|
styles={{ input: { textAlign: 'center' } }}
|
||||||
value={delayMs || 0}
|
value={offsetMs || 0}
|
||||||
width={70}
|
width={70}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
aria-label="Increase lyric offset"
|
aria-label="Increase lyric offset"
|
||||||
icon="plus"
|
icon="plus"
|
||||||
onClick={() => handleLyricOffset(delayMs + 50)}
|
onClick={() => handleLyricOffset(offsetMs + 50)}
|
||||||
tooltip={{
|
tooltip={{
|
||||||
label: t('common.faster', { postProcess: 'sentenceCase' }),
|
label: t('common.faster', { postProcess: 'sentenceCase' }),
|
||||||
openDelay: 0,
|
openDelay: 0,
|
||||||
|
|||||||
@@ -25,7 +25,12 @@ import { Center } from '/@/shared/components/center/center';
|
|||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { FullLyricsMetadata, LyricSource, LyricsOverride } from '/@/shared/types/domain-types';
|
import {
|
||||||
|
FullLyricsMetadata,
|
||||||
|
LyricSource,
|
||||||
|
LyricsOverride,
|
||||||
|
StructuredLyric,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
export const Lyrics = () => {
|
export const Lyrics = () => {
|
||||||
const currentSong = usePlayerSong();
|
const currentSong = usePlayerSong();
|
||||||
@@ -66,6 +71,19 @@ export const Lyrics = () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get the current song's offset from persisted lyrics, default to 0
|
||||||
|
const currentOffsetMs = useMemo(() => {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
if (data.length > 0) {
|
||||||
|
const selectedLyric = data[Math.min(index, data.length - 1)];
|
||||||
|
return selectedLyric.offsetMs ?? 0;
|
||||||
|
}
|
||||||
|
} else if (data?.offsetMs !== undefined) {
|
||||||
|
return data.offsetMs;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}, [data, index]);
|
||||||
|
|
||||||
const [lyrics, synced] = useMemo(() => {
|
const [lyrics, synced] = useMemo(() => {
|
||||||
// If override data is available, use it
|
// If override data is available, use it
|
||||||
if (override && overrideData) {
|
if (override && overrideData) {
|
||||||
@@ -73,6 +91,7 @@ export const Lyrics = () => {
|
|||||||
artist: override.artist,
|
artist: override.artist,
|
||||||
lyrics: overrideData,
|
lyrics: overrideData,
|
||||||
name: override.name,
|
name: override.name,
|
||||||
|
offsetMs: currentOffsetMs,
|
||||||
remote: override.remote ?? true,
|
remote: override.remote ?? true,
|
||||||
source: override.source,
|
source: override.source,
|
||||||
};
|
};
|
||||||
@@ -90,19 +109,20 @@ export const Lyrics = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [undefined, false];
|
return [undefined, false];
|
||||||
}, [data, index, override, overrideData]);
|
}, [data, index, override, overrideData, currentOffsetMs]);
|
||||||
|
|
||||||
const handleOnSearchOverride = useCallback((params: LyricsOverride) => {
|
const handleOnSearchOverride = useCallback((params: LyricsOverride) => {
|
||||||
setOverride(params);
|
setOverride(params);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Persist override lyrics to cache
|
// Persist override lyrics to cache with current offset
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (override && overrideData && currentSong) {
|
if (override && overrideData && currentSong) {
|
||||||
const persistedLyrics: FullLyricsMetadata = {
|
const persistedLyrics: FullLyricsMetadata = {
|
||||||
artist: override.artist,
|
artist: override.artist,
|
||||||
lyrics: overrideData,
|
lyrics: overrideData,
|
||||||
name: override.name,
|
name: override.name,
|
||||||
|
offsetMs: currentOffsetMs,
|
||||||
remote: override.remote ?? true,
|
remote: override.remote ?? true,
|
||||||
source: override.source,
|
source: override.source,
|
||||||
};
|
};
|
||||||
@@ -112,7 +132,42 @@ export const Lyrics = () => {
|
|||||||
persistedLyrics,
|
persistedLyrics,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [override, overrideData, currentSong]);
|
}, [override, overrideData, currentSong, currentOffsetMs]);
|
||||||
|
|
||||||
|
// Callback to update the song's persisted offset
|
||||||
|
const handleUpdateOffset = useCallback(
|
||||||
|
(offsetMs: number) => {
|
||||||
|
if (!currentSong) return;
|
||||||
|
|
||||||
|
queryClient.setQueryData(
|
||||||
|
queryKeys.songs.lyrics(currentSong._serverId, { songId: currentSong.id }),
|
||||||
|
(prev: FullLyricsMetadata | null | StructuredLyric[] | undefined) => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
|
||||||
|
// Handle array of structured lyrics
|
||||||
|
if (Array.isArray(prev)) {
|
||||||
|
if (prev.length > 0) {
|
||||||
|
const selectedIndex = Math.min(index, prev.length - 1);
|
||||||
|
const updated = [...prev];
|
||||||
|
updated[selectedIndex] = {
|
||||||
|
...updated[selectedIndex],
|
||||||
|
offsetMs,
|
||||||
|
};
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle single lyrics object
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
offsetMs,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[currentSong, index],
|
||||||
|
);
|
||||||
|
|
||||||
// const handleOnResetLyric = useCallback(() => {
|
// const handleOnResetLyric = useCallback(() => {
|
||||||
// setOverride(undefined);
|
// setOverride(undefined);
|
||||||
@@ -242,6 +297,7 @@ export const Lyrics = () => {
|
|||||||
{synced ? (
|
{synced ? (
|
||||||
<SynchronizedLyrics
|
<SynchronizedLyrics
|
||||||
{...(lyrics as SynchronizedLyricsProps)}
|
{...(lyrics as SynchronizedLyricsProps)}
|
||||||
|
offsetMs={currentOffsetMs}
|
||||||
translatedLyrics={showTranslation ? translatedLyrics : null}
|
translatedLyrics={showTranslation ? translatedLyrics : null}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -258,6 +314,7 @@ export const Lyrics = () => {
|
|||||||
<LyricsActions
|
<LyricsActions
|
||||||
index={index}
|
index={index}
|
||||||
languages={languages}
|
languages={languages}
|
||||||
|
offsetMs={currentOffsetMs}
|
||||||
onRemoveLyric={handleOnRemoveLyric}
|
onRemoveLyric={handleOnRemoveLyric}
|
||||||
onSearchOverride={handleOnSearchOverride}
|
onSearchOverride={handleOnSearchOverride}
|
||||||
onTranslateLyric={
|
onTranslateLyric={
|
||||||
@@ -265,6 +322,7 @@ export const Lyrics = () => {
|
|||||||
? handleOnTranslateLyric
|
? handleOnTranslateLyric
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
onUpdateOffset={handleUpdateOffset}
|
||||||
setIndex={setIndex}
|
setIndex={setIndex}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const mpris = isElectron() && utils?.isLinux() ? window.api.mpris : null;
|
|||||||
|
|
||||||
export interface SynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
|
export interface SynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
|
||||||
lyrics: SynchronizedLyricsArray;
|
lyrics: SynchronizedLyricsArray;
|
||||||
|
offsetMs?: number;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
translatedLyrics?: null | string;
|
translatedLyrics?: null | string;
|
||||||
}
|
}
|
||||||
@@ -29,6 +30,7 @@ export const SynchronizedLyrics = ({
|
|||||||
artist,
|
artist,
|
||||||
lyrics,
|
lyrics,
|
||||||
name,
|
name,
|
||||||
|
offsetMs,
|
||||||
remote,
|
remote,
|
||||||
source,
|
source,
|
||||||
style,
|
style,
|
||||||
@@ -40,6 +42,8 @@ export const SynchronizedLyrics = ({
|
|||||||
const status = usePlayerStatus();
|
const status = usePlayerStatus();
|
||||||
const timestamp = usePlayerTimestamp();
|
const timestamp = usePlayerTimestamp();
|
||||||
|
|
||||||
|
const effectiveOffsetMs = offsetMs ?? 0;
|
||||||
|
|
||||||
const handleSeek = useCallback(
|
const handleSeek = useCallback(
|
||||||
(time: number) => {
|
(time: number) => {
|
||||||
if (playbackType === PlayerType.LOCAL && mpvPlayer) {
|
if (playbackType === PlayerType.LOCAL && mpvPlayer) {
|
||||||
@@ -66,7 +70,7 @@ export const SynchronizedLyrics = ({
|
|||||||
// whether to proceed or stop
|
// whether to proceed or stop
|
||||||
const timerEpoch = useRef(0);
|
const timerEpoch = useRef(0);
|
||||||
|
|
||||||
const delayMsRef = useRef(settings.delayMs);
|
const delayMsRef = useRef(effectiveOffsetMs);
|
||||||
const followRef = useRef(settings.follow);
|
const followRef = useRef(settings.follow);
|
||||||
const userScrollingRef = useRef(false);
|
const userScrollingRef = useRef(false);
|
||||||
const scrollTimeoutRef = useRef<null | ReturnType<typeof setTimeout>>(null);
|
const scrollTimeoutRef = useRef<null | ReturnType<typeof setTimeout>>(null);
|
||||||
@@ -195,7 +199,8 @@ export const SynchronizedLyrics = ({
|
|||||||
// 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
|
||||||
// changes, we should immediately stop the current listening set and calculate
|
// 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
|
// the correct one using the new offset. Afterwards, timing can be calculated like normal
|
||||||
const changed = delayMsRef.current !== settings.delayMs;
|
const newOffset = offsetMs ?? 0;
|
||||||
|
const changed = delayMsRef.current !== newOffset;
|
||||||
|
|
||||||
if (!changed) {
|
if (!changed) {
|
||||||
return;
|
return;
|
||||||
@@ -205,11 +210,11 @@ export const SynchronizedLyrics = ({
|
|||||||
clearTimeout(lyricTimer.current);
|
clearTimeout(lyricTimer.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
delayMsRef.current = settings.delayMs;
|
delayMsRef.current = newOffset;
|
||||||
|
|
||||||
// Use the current timestamp from player events
|
// Use the current timestamp from player events
|
||||||
setCurrentLyric(timestamp * 1000 + delayMsRef.current);
|
setCurrentLyric(timestamp * 1000 + delayMsRef.current);
|
||||||
}, [setCurrentLyric, settings.delayMs, timestamp]);
|
}, [setCurrentLyric, offsetMs, timestamp]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// This handler is used specifically for dealing with seeking and progress updates.
|
// This handler is used specifically for dealing with seeking and progress updates.
|
||||||
|
|||||||
@@ -1060,6 +1060,7 @@ export type ArtistInfoQuery = {
|
|||||||
|
|
||||||
export type FullLyricsMetadata = Omit<InternetProviderLyricResponse, 'id' | 'lyrics' | 'source'> & {
|
export type FullLyricsMetadata = Omit<InternetProviderLyricResponse, 'id' | 'lyrics' | 'source'> & {
|
||||||
lyrics: LyricsResponse;
|
lyrics: LyricsResponse;
|
||||||
|
offsetMs?: number;
|
||||||
remote: boolean;
|
remote: boolean;
|
||||||
source: string;
|
source: string;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user