diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 47714891..1fc75fe2 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -507,7 +507,6 @@ "artistBackgroundBlur_description": "adjusts the amount of blur applied to the artist background image", "artistConfiguration": "album artist page configuration", "artistConfiguration_description": "configure what items are shown, and in what order, on the album artist page", - "artistConfiguration": "album artist page configuration", "audioDevice_description": "select the audio device to use for playback (web player only)", "audioDevice": "audio device", "audioExclusiveMode_description": "enable exclusive output mode. In this mode, the system is usually locked out, and only mpv will be able to output audio", @@ -560,9 +559,10 @@ "discordServeImage_description": "share cover art for {{discord}} rich presence from server itself, only available for Jellyfin and Navidrome. {{discord}} uses a bot to fetch images, so your server must be reachable from the public internet", "discordUpdateInterval": "{{discord}} rich presence update interval", "discordUpdateInterval_description": "the time in seconds between each update (minimum 15 seconds)", - "discordUpdateInterval": "{{discord}} rich presence update interval", "doubleClickBehavior_description": "if true, all matching tracks in a track search will be queued. otherwise, only the clicked one will be queued", "doubleClickBehavior": "queue all searched tracks when double clicking", + "enableAutoTranslation_description": "enable translation automatically when lyrics are loaded", + "enableAutoTranslation": "enable auto translation", "enableRemote_description": "enables the remote control server to allow other devices to control the application", "enableRemote": "enable remote control server", "exitToTray_description": "exit the application to the system tray", diff --git a/src/renderer/features/lyrics/lyrics.tsx b/src/renderer/features/lyrics/lyrics.tsx index d3f37975..1f56d6d0 100644 --- a/src/renderer/features/lyrics/lyrics.tsx +++ b/src/renderer/features/lyrics/lyrics.tsx @@ -32,8 +32,12 @@ import { FullLyricsMetadata, LyricSource, LyricsOverride } from '/@/shared/types export const Lyrics = () => { const currentSong = useCurrentSong(); - const { translationApiKey, translationApiProvider, translationTargetLanguage } = - useLyricsSettings(); + const { + enableAutoTranslation, + translationApiKey, + translationApiProvider, + translationTargetLanguage, + } = useLyricsSettings(); const { t } = useTranslation(); const [index, setIndex] = useState(0); const [translatedLyrics, setTranslatedLyrics] = useState(null); @@ -52,7 +56,7 @@ export const Lyrics = () => { const [lyrics, synced] = useMemo(() => { if (Array.isArray(data)) { if (data.length > 0) { - const selectedLyric = data[Math.min(index, data.length)]; + const selectedLyric = data[Math.min(index, data.length - 1)]; return [selectedLyric, selectedLyric.synced]; } } else if (data?.lyrics) { @@ -89,11 +93,7 @@ export const Lyrics = () => { ); }, [currentSong?.id, currentSong?.serverId]); - const handleOnTranslateLyric = useCallback(async () => { - if (translatedLyrics) { - setShowTranslation(!showTranslation); - return; - } + const fetchTranslation = useCallback(async () => { if (!lyrics) return; const originalLyrics = Array.isArray(lyrics.lyrics) ? lyrics.lyrics.map(([, line]) => line).join('\n') @@ -106,14 +106,15 @@ export const Lyrics = () => { ); setTranslatedLyrics(TranslatedText); setShowTranslation(true); - }, [ - translatedLyrics, - lyrics, - translationApiKey, - translationApiProvider, - translationTargetLanguage, - showTranslation, - ]); + }, [lyrics, translationApiKey, translationApiProvider, translationTargetLanguage]); + + const handleOnTranslateLyric = useCallback(async () => { + if (translatedLyrics) { + setShowTranslation(!showTranslation); + return; + } + await fetchTranslation(); + }, [translatedLyrics, showTranslation, fetchTranslation]); const { isInitialLoading: isOverrideLoading } = useSongLyricsByRemoteId({ options: { @@ -133,6 +134,8 @@ export const Lyrics = () => { () => { setOverride(undefined); setIndex(0); + setShowTranslation(false); + setTranslatedLyrics(null); }, { equalityFn: (a, b) => a?.id === b?.id }, ); @@ -142,6 +145,12 @@ export const Lyrics = () => { }; }, []); + useEffect(() => { + if (lyrics && !translatedLyrics && enableAutoTranslation) { + fetchTranslation(); + } + }, [lyrics, translatedLyrics, enableAutoTranslation, fetchTranslation]); + const languages = useMemo(() => { if (Array.isArray(data)) { return data.map((lyric, idx) => ({ label: lyric.lang, value: idx.toString() })); diff --git a/src/renderer/features/settings/components/playback/lyric-settings.tsx b/src/renderer/features/settings/components/playback/lyric-settings.tsx index 8c1886aa..ec16fcd6 100644 --- a/src/renderer/features/settings/components/playback/lyric-settings.tsx +++ b/src/renderer/features/settings/components/playback/lyric-settings.tsx @@ -214,6 +214,28 @@ export const LyricSettings = () => { isHidden: !isElectron(), title: t('setting.translationApiKey', { postProcess: 'sentenceCase' }), }, + { + control: ( + { + setSettings({ + lyrics: { + ...settings, + enableAutoTranslation: e.currentTarget.checked, + }, + }); + }} + /> + ), + description: t('setting.enableAutoTranslation', { + context: 'description', + postProcess: 'sentenceCase', + }), + isHidden: !isElectron(), + title: t('setting.enableAutoTranslation', { postProcess: 'sentenceCase' }), + }, ]; return ; diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index af485cb7..00a814e0 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -225,6 +225,7 @@ const HotkeysSettingsSchema = z.object({ const LyricsSettingsSchema = z.object({ alignment: z.enum(['center', 'left', 'right']), delayMs: z.number(), + enableAutoTranslation: z.boolean(), enableNeteaseTranslation: z.boolean(), fetch: z.boolean(), follow: z.boolean(), @@ -612,6 +613,7 @@ const initialState: SettingsState = { lyrics: { alignment: 'center', delayMs: 0, + enableAutoTranslation: false, enableNeteaseTranslation: false, fetch: false, follow: true,