feishin/src/renderer/features/lyrics/lyrics.tsx

205 lines
7.7 KiB
TypeScript
Raw Normal View History

import { AnimatePresence, motion } from 'motion/react';
import { useCallback, useEffect, useMemo, useState } from 'react';
2023-05-22 17:38:31 -07:00
import { ErrorBoundary } from 'react-error-boundary';
import { useTranslation } from 'react-i18next';
import styles from './lyrics.module.css';
import { queryKeys } from '/@/renderer/api/query-keys';
import { ErrorFallback } from '/@/renderer/features/action-required';
import { LyricsActions } from '/@/renderer/features/lyrics/lyrics-actions';
2025-05-20 19:23:36 -07:00
import {
useSongLyricsByRemoteId,
useSongLyricsBySong,
} from '/@/renderer/features/lyrics/queries/lyric-query';
import { translateLyrics } from '/@/renderer/features/lyrics/queries/lyric-translate';
import {
SynchronizedLyrics,
SynchronizedLyricsProps,
} from '/@/renderer/features/lyrics/synchronized-lyrics';
import {
2024-02-01 23:53:10 -08:00
UnsynchronizedLyrics,
UnsynchronizedLyricsProps,
} from '/@/renderer/features/lyrics/unsynchronized-lyrics';
import { queryClient } from '/@/renderer/lib/react-query';
import { useCurrentSong, useLyricsSettings, usePlayerStore } from '/@/renderer/store';
import { Center } from '/@/shared/components/center/center';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { Text } from '/@/shared/components/text/text';
2025-05-20 19:23:36 -07:00
import { FullLyricsMetadata, LyricSource, LyricsOverride } from '/@/shared/types/domain-types';
2023-05-22 17:38:31 -07:00
export const Lyrics = () => {
2023-07-01 19:10:05 -07:00
const currentSong = useCurrentSong();
const lyricsSettings = useLyricsSettings();
const { t } = useTranslation();
2024-02-01 23:53:10 -08:00
const [index, setIndex] = useState(0);
const [translatedLyrics, setTranslatedLyrics] = useState<null | string>(null);
const [showTranslation, setShowTranslation] = useState(false);
2023-07-01 19:10:05 -07:00
const { data, isInitialLoading } = useSongLyricsBySong(
{
query: { songId: currentSong?.id || '' },
serverId: currentSong?.serverId || '',
2023-07-01 19:10:05 -07:00
},
currentSong,
2023-06-09 03:39:38 -07:00
);
2023-07-01 19:10:05 -07:00
const [override, setOverride] = useState<LyricsOverride | undefined>(undefined);
const [lyrics, synced] = useMemo(() => {
if (Array.isArray(data)) {
if (data.length > 0) {
const selectedLyric = data[Math.min(index, data.length)];
return [selectedLyric, selectedLyric.synced];
}
} else if (data?.lyrics) {
return [data, Array.isArray(data.lyrics)];
}
return [undefined, false];
}, [data, index]);
2023-07-01 19:10:05 -07:00
const handleOnSearchOverride = useCallback((params: LyricsOverride) => {
setOverride(params);
}, []);
const handleOnResetLyric = useCallback(() => {
queryClient.invalidateQueries({
exact: true,
queryKey: queryKeys.songs.lyrics(currentSong?.serverId, { songId: currentSong?.id }),
});
}, [currentSong?.id, currentSong?.serverId]);
const handleOnRemoveLyric = useCallback(() => {
queryClient.setQueryData(
queryKeys.songs.lyrics(currentSong?.serverId, { songId: currentSong?.id }),
(prev: FullLyricsMetadata | undefined) => {
if (!prev) {
return undefined;
}
return {
...prev,
lyrics: '',
};
},
);
}, [currentSong?.id, currentSong?.serverId]);
const handleOnTranslateLyric = useCallback(async () => {
if (translatedLyrics) {
setShowTranslation(!showTranslation);
return;
}
if (!lyrics) return;
const originalLyrics = Array.isArray(lyrics.lyrics)
? lyrics.lyrics.map(([, line]) => line).join('\n')
: lyrics.lyrics;
const { translationApiKey, translationApiProvider, translationTargetLanguage } =
lyricsSettings;
const TranslatedText: null | string = await translateLyrics(
originalLyrics,
translationApiKey,
translationApiProvider,
translationTargetLanguage,
);
setTranslatedLyrics(TranslatedText);
setShowTranslation(true);
}, [lyrics, lyricsSettings, translatedLyrics, showTranslation]);
const { isInitialLoading: isOverrideLoading } = useSongLyricsByRemoteId({
2023-07-01 19:10:05 -07:00
options: {
enabled: !!override,
},
query: {
remoteSongId: override?.id,
2024-02-01 23:53:10 -08:00
remoteSource: override?.source as LyricSource | undefined,
song: currentSong,
2023-07-01 19:10:05 -07:00
},
serverId: currentSong?.serverId,
2023-07-01 19:10:05 -07:00
});
useEffect(() => {
const unsubSongChange = usePlayerStore.subscribe(
(state) => state.current.song,
() => {
setOverride(undefined);
2024-02-01 23:53:10 -08:00
setIndex(0);
2023-07-01 19:10:05 -07:00
},
{ equalityFn: (a, b) => a?.id === b?.id },
);
return () => {
unsubSongChange();
};
}, []);
2024-02-01 23:53:10 -08:00
const languages = useMemo(() => {
if (Array.isArray(data)) {
return data.map((lyric, idx) => ({ label: lyric.lang, value: idx.toString() }));
}
return [];
}, [data]);
const isLoadingLyrics = isInitialLoading || isOverrideLoading;
2023-07-01 19:10:05 -07:00
2024-02-01 23:53:10 -08:00
const hasNoLyrics = !lyrics;
2023-07-01 19:10:05 -07:00
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<div className={styles.lyricsContainer}>
2023-07-01 19:10:05 -07:00
{isLoadingLyrics ? (
2025-07-12 11:17:54 -07:00
<Spinner container size={25} />
2023-06-08 03:40:58 -07:00
) : (
2023-07-01 19:10:05 -07:00
<AnimatePresence mode="sync">
{hasNoLyrics ? (
<Center w="100%">
<Group>
<Icon icon="info" />
<Text>
{t('page.fullscreenPlayer.noLyrics', {
postProcess: 'sentenceCase',
})}
</Text>
2023-07-01 19:10:05 -07:00
</Group>
</Center>
) : (
<motion.div
2023-07-01 19:10:05 -07:00
animate={{ opacity: 1 }}
className={styles.scrollContainer}
2023-07-01 19:10:05 -07:00
initial={{ opacity: 0 }}
transition={{ duration: 0.5 }}
>
2024-02-01 23:53:10 -08:00
{synced ? (
<SynchronizedLyrics
{...(lyrics as SynchronizedLyricsProps)}
translatedLyrics={showTranslation ? translatedLyrics : null}
/>
2023-07-01 19:10:05 -07:00
) : (
<UnsynchronizedLyrics
2024-02-01 23:53:10 -08:00
{...(lyrics as UnsynchronizedLyricsProps)}
translatedLyrics={showTranslation ? translatedLyrics : null}
2023-07-01 19:10:05 -07:00
/>
)}
</motion.div>
2023-07-01 19:10:05 -07:00
)}
</AnimatePresence>
2023-06-08 03:40:58 -07:00
)}
<div className={styles.actionsContainer}>
2023-07-01 19:10:05 -07:00
<LyricsActions
2024-02-03 21:22:03 -08:00
index={index}
languages={languages}
onRemoveLyric={handleOnRemoveLyric}
onResetLyric={handleOnResetLyric}
2023-07-01 19:10:05 -07:00
onSearchOverride={handleOnSearchOverride}
onTranslateLyric={handleOnTranslateLyric}
setIndex={setIndex}
2023-07-01 19:10:05 -07:00
/>
</div>
</div>
2023-07-01 19:10:05 -07:00
</ErrorBoundary>
);
2023-05-22 17:38:31 -07:00
};