parent
e3946a9413
commit
31492fa9ef
13 changed files with 243 additions and 46 deletions
13
package-lock.json
generated
13
package-lock.json
generated
|
@ -20286,10 +20286,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/serialize-javascript": {
|
"node_modules/serialize-javascript": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
|
||||||
"integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
|
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"randombytes": "^2.1.0"
|
"randombytes": "^2.1.0"
|
||||||
}
|
}
|
||||||
|
@ -38353,9 +38354,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"serialize-javascript": {
|
"serialize-javascript": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
|
||||||
"integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
|
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"randombytes": "^2.1.0"
|
"randombytes": "^2.1.0"
|
||||||
|
|
|
@ -309,8 +309,8 @@
|
||||||
"@tanstack/react-query-persist-client": "^4.32.1",
|
"@tanstack/react-query-persist-client": "^4.32.1",
|
||||||
"@ts-rest/core": "^3.23.0",
|
"@ts-rest/core": "^3.23.0",
|
||||||
"@xhayper/discord-rpc": "^1.0.24",
|
"@xhayper/discord-rpc": "^1.0.24",
|
||||||
"auto-text-size": "^0.2.3",
|
|
||||||
"audiomotion-analyzer": "^4.5.0",
|
"audiomotion-analyzer": "^4.5.0",
|
||||||
|
"auto-text-size": "^0.2.3",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"cmdk": "^0.2.0",
|
"cmdk": "^0.2.0",
|
||||||
|
|
3
release/app/package-lock.json
generated
3
release/app/package-lock.json
generated
|
@ -2311,7 +2311,8 @@
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "8.18.0",
|
"version": "8.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="
|
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"xml2js": {
|
"xml2js": {
|
||||||
"version": "0.4.23",
|
"version": "0.4.23",
|
||||||
|
|
|
@ -109,6 +109,7 @@
|
||||||
"trackNumber": "track",
|
"trackNumber": "track",
|
||||||
"trackGain": "track gain",
|
"trackGain": "track gain",
|
||||||
"trackPeak": "track peak",
|
"trackPeak": "track peak",
|
||||||
|
"translation": "translation",
|
||||||
"unknown": "unknown",
|
"unknown": "unknown",
|
||||||
"version": "version",
|
"version": "version",
|
||||||
"year": "year",
|
"year": "year",
|
||||||
|
@ -355,7 +356,8 @@
|
||||||
},
|
},
|
||||||
"lyrics": "lyrics",
|
"lyrics": "lyrics",
|
||||||
"related": "related",
|
"related": "related",
|
||||||
"upNext": "up next"
|
"upNext": "up next",
|
||||||
|
"visualizer": "visualizer"
|
||||||
},
|
},
|
||||||
"genreList": {
|
"genreList": {
|
||||||
"showAlbums": "show $t(entity.genre_one) $t(entity.album_other)",
|
"showAlbums": "show $t(entity.genre_one) $t(entity.album_other)",
|
||||||
|
@ -653,6 +655,12 @@
|
||||||
"transcodeBitrate_description": "selects the bitrate to transcode. 0 means let the server pick",
|
"transcodeBitrate_description": "selects the bitrate to transcode. 0 means let the server pick",
|
||||||
"transcodeFormat": "format to transcode",
|
"transcodeFormat": "format to transcode",
|
||||||
"transcodeFormat_description": "selects the format to transcode. leave empty to let the server decide",
|
"transcodeFormat_description": "selects the format to transcode. leave empty to let the server decide",
|
||||||
|
"translationApiProvider": "translation api provider",
|
||||||
|
"translationApiProvider_description": "api provider for translation",
|
||||||
|
"translationApiKey": "translation api key",
|
||||||
|
"translationApiKey_description": "api key for translation (Support global service endpoint only)",
|
||||||
|
"translationTargetLanguage": "translation target language",
|
||||||
|
"translationTargetLanguage_description": "target language for translation",
|
||||||
"trayEnabled": "show tray",
|
"trayEnabled": "show tray",
|
||||||
"trayEnabled_description": "show/hide tray icon/menu. if disabled, also disables minimize/exit to tray",
|
"trayEnabled_description": "show/hide tray icon/menu. if disabled, also disables minimize/exit to tray",
|
||||||
"useSystemTheme": "use system theme",
|
"useSystemTheme": "use system theme",
|
||||||
|
|
|
@ -19,6 +19,7 @@ interface LyricsActionsProps {
|
||||||
onRemoveLyric: () => void;
|
onRemoveLyric: () => void;
|
||||||
onResetLyric: () => void;
|
onResetLyric: () => void;
|
||||||
onSearchOverride: (params: LyricsOverride) => void;
|
onSearchOverride: (params: LyricsOverride) => void;
|
||||||
|
onTranslateLyric: () => void;
|
||||||
setIndex: (idx: number) => void;
|
setIndex: (idx: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +29,7 @@ export const LyricsActions = ({
|
||||||
onRemoveLyric,
|
onRemoveLyric,
|
||||||
onResetLyric,
|
onResetLyric,
|
||||||
onSearchOverride,
|
onSearchOverride,
|
||||||
|
onTranslateLyric,
|
||||||
setIndex,
|
setIndex,
|
||||||
}: LyricsActionsProps) => {
|
}: LyricsActionsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@ -120,7 +122,6 @@ export const LyricsActions = ({
|
||||||
{isDesktop && sources.length ? (
|
{isDesktop && sources.length ? (
|
||||||
<Button
|
<Button
|
||||||
uppercase
|
uppercase
|
||||||
color="red"
|
|
||||||
disabled={isActionsDisabled}
|
disabled={isActionsDisabled}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={onRemoveLyric}
|
onClick={onRemoveLyric}
|
||||||
|
@ -129,6 +130,19 @@ export const LyricsActions = ({
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
<Box style={{ position: 'absolute', right: 0, top: -50 }}>
|
||||||
|
{isDesktop && sources.length ? (
|
||||||
|
<Button
|
||||||
|
uppercase
|
||||||
|
disabled={isActionsDisabled}
|
||||||
|
variant="subtle"
|
||||||
|
onClick={onTranslateLyric}
|
||||||
|
>
|
||||||
|
{t('common.translation', { postProcess: 'sentenceCase' })}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { RiInformationFill } from 'react-icons/ri';
|
import { RiInformationFill } from 'react-icons/ri';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { useSongLyricsByRemoteId, useSongLyricsBySong } from './queries/lyric-query';
|
import { useSongLyricsByRemoteId, useSongLyricsBySong } from './queries/lyric-query';
|
||||||
|
import { translateLyrics } from './queries/lyric-translate';
|
||||||
import { SynchronizedLyrics, SynchronizedLyricsProps } from './synchronized-lyrics';
|
import { SynchronizedLyrics, SynchronizedLyricsProps } from './synchronized-lyrics';
|
||||||
import { Spinner, TextTitle } from '/@/renderer/components';
|
import { Spinner, TextTitle } from '/@/renderer/components';
|
||||||
import { ErrorFallback } from '/@/renderer/features/action-required';
|
import { ErrorFallback } from '/@/renderer/features/action-required';
|
||||||
|
@ -12,7 +13,7 @@ import {
|
||||||
UnsynchronizedLyrics,
|
UnsynchronizedLyrics,
|
||||||
UnsynchronizedLyricsProps,
|
UnsynchronizedLyricsProps,
|
||||||
} from '/@/renderer/features/lyrics/unsynchronized-lyrics';
|
} from '/@/renderer/features/lyrics/unsynchronized-lyrics';
|
||||||
import { useCurrentSong, usePlayerStore } from '/@/renderer/store';
|
import { useCurrentSong, usePlayerStore, useLyricsSettings } from '/@/renderer/store';
|
||||||
import { FullLyricsMetadata, LyricSource, LyricsOverride } from '/@/renderer/api/types';
|
import { FullLyricsMetadata, LyricSource, LyricsOverride } from '/@/renderer/api/types';
|
||||||
import { LyricsActions } from '/@/renderer/features/lyrics/lyrics-actions';
|
import { LyricsActions } from '/@/renderer/features/lyrics/lyrics-actions';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
@ -84,7 +85,10 @@ const ScrollContainer = styled(motion.div)`
|
||||||
|
|
||||||
export const Lyrics = () => {
|
export const Lyrics = () => {
|
||||||
const currentSong = useCurrentSong();
|
const currentSong = useCurrentSong();
|
||||||
|
const lyricsSettings = useLyricsSettings();
|
||||||
const [index, setIndex] = useState(0);
|
const [index, setIndex] = useState(0);
|
||||||
|
const [translatedLyrics, setTranslatedLyrics] = useState<string | null>(null);
|
||||||
|
const [showTranslation, setShowTranslation] = useState(false);
|
||||||
|
|
||||||
const { data, isInitialLoading } = useSongLyricsBySong(
|
const { data, isInitialLoading } = useSongLyricsBySong(
|
||||||
{
|
{
|
||||||
|
@ -96,6 +100,19 @@ export const Lyrics = () => {
|
||||||
|
|
||||||
const [override, setOverride] = useState<LyricsOverride | undefined>(undefined);
|
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]);
|
||||||
|
|
||||||
const handleOnSearchOverride = useCallback((params: LyricsOverride) => {
|
const handleOnSearchOverride = useCallback((params: LyricsOverride) => {
|
||||||
setOverride(params);
|
setOverride(params);
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -123,6 +140,27 @@ export const Lyrics = () => {
|
||||||
);
|
);
|
||||||
}, [currentSong?.id, currentSong?.serverId]);
|
}, [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: string | null = await translateLyrics(
|
||||||
|
originalLyrics,
|
||||||
|
translationApiKey,
|
||||||
|
translationApiProvider,
|
||||||
|
translationTargetLanguage,
|
||||||
|
);
|
||||||
|
setTranslatedLyrics(TranslatedText);
|
||||||
|
setShowTranslation(true);
|
||||||
|
}, [lyrics, lyricsSettings, translatedLyrics, showTranslation]);
|
||||||
|
|
||||||
const { isInitialLoading: isOverrideLoading } = useSongLyricsByRemoteId({
|
const { isInitialLoading: isOverrideLoading } = useSongLyricsByRemoteId({
|
||||||
options: {
|
options: {
|
||||||
enabled: !!override,
|
enabled: !!override,
|
||||||
|
@ -150,19 +188,6 @@ export const Lyrics = () => {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
const languages = useMemo(() => {
|
const languages = useMemo(() => {
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
return data.map((lyric, idx) => ({ label: lyric.lang, value: idx.toString() }));
|
return data.map((lyric, idx) => ({ label: lyric.lang, value: idx.toString() }));
|
||||||
|
@ -203,10 +228,14 @@ export const Lyrics = () => {
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
>
|
>
|
||||||
{synced ? (
|
{synced ? (
|
||||||
<SynchronizedLyrics {...(lyrics as SynchronizedLyricsProps)} />
|
<SynchronizedLyrics
|
||||||
|
{...(lyrics as SynchronizedLyricsProps)}
|
||||||
|
translatedLyrics={showTranslation ? translatedLyrics : null}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<UnsynchronizedLyrics
|
<UnsynchronizedLyrics
|
||||||
{...(lyrics as UnsynchronizedLyricsProps)}
|
{...(lyrics as UnsynchronizedLyricsProps)}
|
||||||
|
translatedLyrics={showTranslation ? translatedLyrics : null}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ScrollContainer>
|
</ScrollContainer>
|
||||||
|
@ -221,6 +250,7 @@ export const Lyrics = () => {
|
||||||
onRemoveLyric={handleOnRemoveLyric}
|
onRemoveLyric={handleOnRemoveLyric}
|
||||||
onResetLyric={handleOnResetLyric}
|
onResetLyric={handleOnResetLyric}
|
||||||
onSearchOverride={handleOnSearchOverride}
|
onSearchOverride={handleOnSearchOverride}
|
||||||
|
onTranslateLyric={handleOnTranslateLyric}
|
||||||
/>
|
/>
|
||||||
</ActionsContainer>
|
</ActionsContainer>
|
||||||
</LyricsContainer>
|
</LyricsContainer>
|
||||||
|
|
50
src/renderer/features/lyrics/queries/lyric-translate.ts
Normal file
50
src/renderer/features/lyrics/queries/lyric-translate.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export const translateLyrics = async (
|
||||||
|
originalLyrics: string,
|
||||||
|
translationApiKey: string,
|
||||||
|
translationApiProvider: string | null,
|
||||||
|
translationTargetLanguage: string | null,
|
||||||
|
) => {
|
||||||
|
let TranslatedText = '';
|
||||||
|
if (translationApiProvider === 'Microsoft Azure') {
|
||||||
|
try {
|
||||||
|
const response = await axios({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
Text: originalLyrics,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Ocp-Apim-Subscription-Key': translationApiKey,
|
||||||
|
},
|
||||||
|
method: 'post',
|
||||||
|
url: `https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=${translationTargetLanguage as string}`,
|
||||||
|
});
|
||||||
|
TranslatedText = response.data[0].translations[0].text;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Microsoft Azure translate request got an error!', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else if (translationApiProvider === 'Google Cloud') {
|
||||||
|
try {
|
||||||
|
const response = await axios({
|
||||||
|
data: {
|
||||||
|
format: 'text',
|
||||||
|
q: originalLyrics,
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
method: 'post',
|
||||||
|
url: `https://translation.googleapis.com/language/translate/v2?target=${translationTargetLanguage as string}&key=${translationApiKey}`,
|
||||||
|
});
|
||||||
|
TranslatedText = response.data.data.translations[0].translatedText;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Google Cloud translate request got an error!', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TranslatedText;
|
||||||
|
};
|
|
@ -55,6 +55,7 @@ const SynchronizedLyricsContainer = styled.div<{ $gap: number }>`
|
||||||
|
|
||||||
export interface SynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
|
export interface SynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
|
||||||
lyrics: SynchronizedLyricsArray;
|
lyrics: SynchronizedLyricsArray;
|
||||||
|
translatedLyrics?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SynchronizedLyrics = ({
|
export const SynchronizedLyrics = ({
|
||||||
|
@ -63,6 +64,7 @@ export const SynchronizedLyrics = ({
|
||||||
name,
|
name,
|
||||||
remote,
|
remote,
|
||||||
source,
|
source,
|
||||||
|
translatedLyrics,
|
||||||
}: SynchronizedLyricsProps) => {
|
}: SynchronizedLyricsProps) => {
|
||||||
const playersRef = PlayersRef;
|
const playersRef = PlayersRef;
|
||||||
const status = useCurrentStatus();
|
const status = useCurrentStatus();
|
||||||
|
@ -364,8 +366,8 @@ export const SynchronizedLyrics = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{lyrics.map(([time, text], idx) => (
|
{lyrics.map(([time, text], idx) => (
|
||||||
|
<div key={idx}>
|
||||||
<LyricLine
|
<LyricLine
|
||||||
key={idx}
|
|
||||||
alignment={settings.alignment}
|
alignment={settings.alignment}
|
||||||
className="lyric-line synchronized"
|
className="lyric-line synchronized"
|
||||||
fontSize={settings.fontSize}
|
fontSize={settings.fontSize}
|
||||||
|
@ -373,6 +375,16 @@ export const SynchronizedLyrics = ({
|
||||||
text={text}
|
text={text}
|
||||||
onClick={() => handleSeek(time / 1000)}
|
onClick={() => handleSeek(time / 1000)}
|
||||||
/>
|
/>
|
||||||
|
{translatedLyrics && (
|
||||||
|
<LyricLine
|
||||||
|
alignment={settings.alignment}
|
||||||
|
className="lyric-line synchronized translation"
|
||||||
|
fontSize={settings.fontSize * 0.8}
|
||||||
|
text={translatedLyrics.split('\n')[idx]}
|
||||||
|
onClick={() => handleSeek(time / 1000)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</SynchronizedLyricsContainer>
|
</SynchronizedLyricsContainer>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { useLyricsSettings } from '/@/renderer/store';
|
||||||
|
|
||||||
export interface UnsynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
|
export interface UnsynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
|
||||||
lyrics: string;
|
lyrics: string;
|
||||||
|
translatedLyrics?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UnsynchronizedLyricsContainer = styled.div<{ $gap: number }>`
|
const UnsynchronizedLyricsContainer = styled.div<{ $gap: number }>`
|
||||||
|
@ -45,12 +46,17 @@ export const UnsynchronizedLyrics = ({
|
||||||
name,
|
name,
|
||||||
remote,
|
remote,
|
||||||
source,
|
source,
|
||||||
|
translatedLyrics,
|
||||||
}: UnsynchronizedLyricsProps) => {
|
}: UnsynchronizedLyricsProps) => {
|
||||||
const settings = useLyricsSettings();
|
const settings = useLyricsSettings();
|
||||||
const lines = useMemo(() => {
|
const lines = useMemo(() => {
|
||||||
return lyrics.split('\n');
|
return lyrics.split('\n');
|
||||||
}, [lyrics]);
|
}, [lyrics]);
|
||||||
|
|
||||||
|
const translatedLines = useMemo(() => {
|
||||||
|
return translatedLyrics ? translatedLyrics.split('\n') : [];
|
||||||
|
}, [translatedLyrics]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UnsynchronizedLyricsContainer
|
<UnsynchronizedLyricsContainer
|
||||||
$gap={settings.gapUnsync}
|
$gap={settings.gapUnsync}
|
||||||
|
@ -73,14 +79,23 @@ export const UnsynchronizedLyrics = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{lines.map((text, idx) => (
|
{lines.map((text, idx) => (
|
||||||
|
<div key={idx}>
|
||||||
<LyricLine
|
<LyricLine
|
||||||
key={idx}
|
|
||||||
alignment={settings.alignment}
|
alignment={settings.alignment}
|
||||||
className="lyric-line unsynchronized"
|
className="lyric-line unsynchronized"
|
||||||
fontSize={settings.fontSizeUnsync}
|
fontSize={settings.fontSizeUnsync}
|
||||||
id={`lyric-${idx}`}
|
id={`lyric-${idx}`}
|
||||||
text={text}
|
text={text}
|
||||||
/>
|
/>
|
||||||
|
{translatedLines[idx] && (
|
||||||
|
<LyricLine
|
||||||
|
alignment={settings.alignment}
|
||||||
|
className="lyric-line unsynchronized translation"
|
||||||
|
fontSize={settings.fontSizeUnsync * 0.8}
|
||||||
|
text={translatedLines[idx]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</UnsynchronizedLyricsContainer>
|
</UnsynchronizedLyricsContainer>
|
||||||
);
|
);
|
||||||
|
|
|
@ -98,7 +98,7 @@ export const FullScreenPlayerQueue = () => {
|
||||||
items.push({
|
items.push({
|
||||||
active: activeTab === 'visualizer',
|
active: activeTab === 'visualizer',
|
||||||
icon: <RiFileTextLine size="1.5rem" />,
|
icon: <RiFileTextLine size="1.5rem" />,
|
||||||
label: 'Visualizer',
|
label: t('page.fullscreenPlayer.visualizer'),
|
||||||
onClick: () => setStore({ activeTab: 'visualizer' }),
|
onClick: () => setStore({ activeTab: 'visualizer' }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,12 @@ import styled from 'styled-components';
|
||||||
import { useSettingsStore } from '/@/renderer/store';
|
import { useSettingsStore } from '/@/renderer/store';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
margin: auto;
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
margin: auto;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin: auto;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,19 @@ import {
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
} from '/@/renderer/features/settings/components/settings-section';
|
} from '/@/renderer/features/settings/components/settings-section';
|
||||||
import { useLyricsSettings, useSettingsStoreActions } from '/@/renderer/store';
|
import { useLyricsSettings, useSettingsStoreActions } from '/@/renderer/store';
|
||||||
import { MultiSelect, MultiSelectProps, NumberInput, Switch } from '/@/renderer/components';
|
import {
|
||||||
|
Select,
|
||||||
|
MultiSelect,
|
||||||
|
MultiSelectProps,
|
||||||
|
TextInput,
|
||||||
|
NumberInput,
|
||||||
|
Switch,
|
||||||
|
} from '/@/renderer/components';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { LyricSource } from '/@/renderer/api/types';
|
import { LyricSource } from '/@/renderer/api/types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { languages } from '/@/i18n/i18n';
|
||||||
|
|
||||||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||||
|
|
||||||
|
@ -116,6 +124,58 @@ export const LyricSettings = () => {
|
||||||
isHidden: !isElectron(),
|
isHidden: !isElectron(),
|
||||||
title: t('setting.lyricOffset', { postProcess: 'sentenceCase' }),
|
title: t('setting.lyricOffset', { postProcess: 'sentenceCase' }),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
data={languages}
|
||||||
|
value={settings.translationTargetLanguage}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSettings({ lyrics: { ...settings, translationTargetLanguage: value } });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.translationTargetLanguage', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
isHidden: !isElectron(),
|
||||||
|
title: t('setting.translationTargetLanguage', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
data={['Microsoft Azure', 'Google Cloud']}
|
||||||
|
value={settings.translationApiProvider}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSettings({ lyrics: { ...settings, translationApiProvider: value } });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.translationApiProvider', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
isHidden: !isElectron(),
|
||||||
|
title: t('setting.translationApiProvider', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<TextInput
|
||||||
|
value={settings.translationApiKey}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSettings({
|
||||||
|
lyrics: { ...settings, translationApiKey: e.currentTarget.value },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.translationApiKey', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
isHidden: !isElectron(),
|
||||||
|
title: t('setting.translationApiKey', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -275,6 +275,9 @@ export interface SettingsState {
|
||||||
showMatch: boolean;
|
showMatch: boolean;
|
||||||
showProvider: boolean;
|
showProvider: boolean;
|
||||||
sources: LyricSource[];
|
sources: LyricSource[];
|
||||||
|
translationApiKey: string;
|
||||||
|
translationApiProvider: string | null;
|
||||||
|
translationTargetLanguage: string | null;
|
||||||
};
|
};
|
||||||
playback: {
|
playback: {
|
||||||
audioDeviceId?: string | null;
|
audioDeviceId?: string | null;
|
||||||
|
@ -449,6 +452,9 @@ const initialState: SettingsState = {
|
||||||
showMatch: true,
|
showMatch: true,
|
||||||
showProvider: true,
|
showProvider: true,
|
||||||
sources: [],
|
sources: [],
|
||||||
|
translationApiKey: '',
|
||||||
|
translationApiProvider: '',
|
||||||
|
translationTargetLanguage: 'en',
|
||||||
},
|
},
|
||||||
playback: {
|
playback: {
|
||||||
audioDeviceId: undefined,
|
audioDeviceId: undefined,
|
||||||
|
|
Reference in a new issue