mirror of
https://github.com/antebudimir/feishin.git
synced 2025-12-31 18:13:31 +00:00
parent
e3946a9413
commit
31492fa9ef
13 changed files with 243 additions and 46 deletions
|
|
@ -19,6 +19,7 @@ interface LyricsActionsProps {
|
|||
onRemoveLyric: () => void;
|
||||
onResetLyric: () => void;
|
||||
onSearchOverride: (params: LyricsOverride) => void;
|
||||
onTranslateLyric: () => void;
|
||||
setIndex: (idx: number) => void;
|
||||
}
|
||||
|
||||
|
|
@ -28,6 +29,7 @@ export const LyricsActions = ({
|
|||
onRemoveLyric,
|
||||
onResetLyric,
|
||||
onSearchOverride,
|
||||
onTranslateLyric,
|
||||
setIndex,
|
||||
}: LyricsActionsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -120,7 +122,6 @@ export const LyricsActions = ({
|
|||
{isDesktop && sources.length ? (
|
||||
<Button
|
||||
uppercase
|
||||
color="red"
|
||||
disabled={isActionsDisabled}
|
||||
variant="subtle"
|
||||
onClick={onRemoveLyric}
|
||||
|
|
@ -129,6 +130,19 @@ export const LyricsActions = ({
|
|||
</Button>
|
||||
) : null}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { ErrorBoundary } from 'react-error-boundary';
|
|||
import { RiInformationFill } from 'react-icons/ri';
|
||||
import styled from 'styled-components';
|
||||
import { useSongLyricsByRemoteId, useSongLyricsBySong } from './queries/lyric-query';
|
||||
import { translateLyrics } from './queries/lyric-translate';
|
||||
import { SynchronizedLyrics, SynchronizedLyricsProps } from './synchronized-lyrics';
|
||||
import { Spinner, TextTitle } from '/@/renderer/components';
|
||||
import { ErrorFallback } from '/@/renderer/features/action-required';
|
||||
|
|
@ -12,7 +13,7 @@ import {
|
|||
UnsynchronizedLyrics,
|
||||
UnsynchronizedLyricsProps,
|
||||
} 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 { LyricsActions } from '/@/renderer/features/lyrics/lyrics-actions';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
|
|
@ -84,7 +85,10 @@ const ScrollContainer = styled(motion.div)`
|
|||
|
||||
export const Lyrics = () => {
|
||||
const currentSong = useCurrentSong();
|
||||
const lyricsSettings = useLyricsSettings();
|
||||
const [index, setIndex] = useState(0);
|
||||
const [translatedLyrics, setTranslatedLyrics] = useState<string | null>(null);
|
||||
const [showTranslation, setShowTranslation] = useState(false);
|
||||
|
||||
const { data, isInitialLoading } = useSongLyricsBySong(
|
||||
{
|
||||
|
|
@ -96,6 +100,19 @@ export const Lyrics = () => {
|
|||
|
||||
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) => {
|
||||
setOverride(params);
|
||||
}, []);
|
||||
|
|
@ -123,6 +140,27 @@ export const 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: string | null = await translateLyrics(
|
||||
originalLyrics,
|
||||
translationApiKey,
|
||||
translationApiProvider,
|
||||
translationTargetLanguage,
|
||||
);
|
||||
setTranslatedLyrics(TranslatedText);
|
||||
setShowTranslation(true);
|
||||
}, [lyrics, lyricsSettings, translatedLyrics, showTranslation]);
|
||||
|
||||
const { isInitialLoading: isOverrideLoading } = useSongLyricsByRemoteId({
|
||||
options: {
|
||||
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(() => {
|
||||
if (Array.isArray(data)) {
|
||||
return data.map((lyric, idx) => ({ label: lyric.lang, value: idx.toString() }));
|
||||
|
|
@ -203,10 +228,14 @@ export const Lyrics = () => {
|
|||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{synced ? (
|
||||
<SynchronizedLyrics {...(lyrics as SynchronizedLyricsProps)} />
|
||||
<SynchronizedLyrics
|
||||
{...(lyrics as SynchronizedLyricsProps)}
|
||||
translatedLyrics={showTranslation ? translatedLyrics : null}
|
||||
/>
|
||||
) : (
|
||||
<UnsynchronizedLyrics
|
||||
{...(lyrics as UnsynchronizedLyricsProps)}
|
||||
translatedLyrics={showTranslation ? translatedLyrics : null}
|
||||
/>
|
||||
)}
|
||||
</ScrollContainer>
|
||||
|
|
@ -221,6 +250,7 @@ export const Lyrics = () => {
|
|||
onRemoveLyric={handleOnRemoveLyric}
|
||||
onResetLyric={handleOnResetLyric}
|
||||
onSearchOverride={handleOnSearchOverride}
|
||||
onTranslateLyric={handleOnTranslateLyric}
|
||||
/>
|
||||
</ActionsContainer>
|
||||
</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'> {
|
||||
lyrics: SynchronizedLyricsArray;
|
||||
translatedLyrics?: string | null;
|
||||
}
|
||||
|
||||
export const SynchronizedLyrics = ({
|
||||
|
|
@ -63,6 +64,7 @@ export const SynchronizedLyrics = ({
|
|||
name,
|
||||
remote,
|
||||
source,
|
||||
translatedLyrics,
|
||||
}: SynchronizedLyricsProps) => {
|
||||
const playersRef = PlayersRef;
|
||||
const status = useCurrentStatus();
|
||||
|
|
@ -364,15 +366,25 @@ export const SynchronizedLyrics = ({
|
|||
/>
|
||||
)}
|
||||
{lyrics.map(([time, text], idx) => (
|
||||
<LyricLine
|
||||
key={idx}
|
||||
alignment={settings.alignment}
|
||||
className="lyric-line synchronized"
|
||||
fontSize={settings.fontSize}
|
||||
id={`lyric-${idx}`}
|
||||
text={text}
|
||||
onClick={() => handleSeek(time / 1000)}
|
||||
/>
|
||||
<div key={idx}>
|
||||
<LyricLine
|
||||
alignment={settings.alignment}
|
||||
className="lyric-line synchronized"
|
||||
fontSize={settings.fontSize}
|
||||
id={`lyric-${idx}`}
|
||||
text={text}
|
||||
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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { useLyricsSettings } from '/@/renderer/store';
|
|||
|
||||
export interface UnsynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
|
||||
lyrics: string;
|
||||
translatedLyrics?: string | null;
|
||||
}
|
||||
|
||||
const UnsynchronizedLyricsContainer = styled.div<{ $gap: number }>`
|
||||
|
|
@ -45,12 +46,17 @@ export const UnsynchronizedLyrics = ({
|
|||
name,
|
||||
remote,
|
||||
source,
|
||||
translatedLyrics,
|
||||
}: UnsynchronizedLyricsProps) => {
|
||||
const settings = useLyricsSettings();
|
||||
const lines = useMemo(() => {
|
||||
return lyrics.split('\n');
|
||||
}, [lyrics]);
|
||||
|
||||
const translatedLines = useMemo(() => {
|
||||
return translatedLyrics ? translatedLyrics.split('\n') : [];
|
||||
}, [translatedLyrics]);
|
||||
|
||||
return (
|
||||
<UnsynchronizedLyricsContainer
|
||||
$gap={settings.gapUnsync}
|
||||
|
|
@ -73,14 +79,23 @@ export const UnsynchronizedLyrics = ({
|
|||
/>
|
||||
)}
|
||||
{lines.map((text, idx) => (
|
||||
<LyricLine
|
||||
key={idx}
|
||||
alignment={settings.alignment}
|
||||
className="lyric-line unsynchronized"
|
||||
fontSize={settings.fontSizeUnsync}
|
||||
id={`lyric-${idx}`}
|
||||
text={text}
|
||||
/>
|
||||
<div key={idx}>
|
||||
<LyricLine
|
||||
alignment={settings.alignment}
|
||||
className="lyric-line unsynchronized"
|
||||
fontSize={settings.fontSizeUnsync}
|
||||
id={`lyric-${idx}`}
|
||||
text={text}
|
||||
/>
|
||||
{translatedLines[idx] && (
|
||||
<LyricLine
|
||||
alignment={settings.alignment}
|
||||
className="lyric-line unsynchronized translation"
|
||||
fontSize={settings.fontSizeUnsync * 0.8}
|
||||
text={translatedLines[idx]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</UnsynchronizedLyricsContainer>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue