feat: add romaji lyrics display (#2180)

This commit is contained in:
York
2026-06-27 12:07:58 +08:00
committed by GitHub
parent 26eea7422d
commit f8ca8861fc
13 changed files with 139 additions and 9 deletions
@@ -307,6 +307,20 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
isHidden: !isElectron(),
title: t('setting.enableFurigana'),
},
{
control: (
<Switch
aria-label="Enable romaji"
defaultChecked={lyricsSettings.enableRomaji}
onChange={(e) => updateLyricsSetting({ enableRomaji: e.currentTarget.checked })}
/>
),
description: t('setting.enableRomaji', {
context: 'description',
}),
isHidden: !isElectron(),
title: t('setting.enableRomaji'),
},
{
control: (
<Switch
@@ -28,3 +28,27 @@ export const useFuriganaLyrics = (lyrics: LyricsResponse | null | undefined, ena
staleTime: Infinity,
});
};
export const useRomajiLyrics = (lyrics: LyricsResponse | null | undefined, enabled: boolean) => {
return useQuery({
enabled: enabled && !!lyrics && !!lyricsApi,
queryFn: async () => {
if (!lyrics || !lyricsApi || !enabled) return lyrics;
if (typeof lyrics === 'string') {
return await lyricsApi.convertRomaji(lyrics);
} else if (Array.isArray(lyrics)) {
const text = lyrics.map(([, line]) => line).join('\n');
const converted = await lyricsApi.convertRomaji(text);
const convertedLines = converted.split('\n');
return lyrics.map(([time], i) => [
time,
convertedLines[i] ?? lyrics[i][1],
]) as SynchronizedLyricsArray;
}
return lyrics;
},
queryKey: ['romaji', lyrics],
staleTime: Infinity,
});
};
@@ -25,3 +25,8 @@
.lyric-line:global(.synchronized) {
cursor: pointer;
}
.romaji-line {
font-size: 0.8em;
font-weight: 600;
}
+20 -1
View File
@@ -10,11 +10,21 @@ import { Stack } from '/@/shared/components/stack/stack';
interface LyricLineProps extends ComponentPropsWithoutRef<'div'> {
alignment: 'center' | 'left' | 'right';
fontSize: number;
romajiText?: null | string;
text: string;
translatedText?: null | string;
}
export const LyricLine = memo(
({ alignment, className, fontSize, text, ...props }: LyricLineProps) => {
({
alignment,
className,
fontSize,
romajiText,
text,
translatedText,
...props
}: LyricLineProps) => {
const lines = useMemo(() => text.split('_BREAK_'), [text]);
const style = useMemo(
@@ -31,6 +41,15 @@ export const LyricLine = memo(
{lines.map((line, index) => (
<span dangerouslySetInnerHTML={{ __html: sanitize(line) }} key={index} />
))}
{romajiText && (
<span
className={styles.romajiLine}
dangerouslySetInnerHTML={{ __html: sanitize(romajiText) }}
/>
)}
{translatedText && (
<span dangerouslySetInnerHTML={{ __html: sanitize(translatedText) }} />
)}
</Stack>
</Box>
);
+16 -1
View File
@@ -14,7 +14,10 @@ import {
type LyricsQueryResult,
} from '/@/renderer/features/lyrics/api/lyrics-api';
import { openLyricsExportModal } from '/@/renderer/features/lyrics/components/lyrics-export-form';
import { useFuriganaLyrics } from '/@/renderer/features/lyrics/hooks/use-furigana-lyrics';
import {
useFuriganaLyrics,
useRomajiLyrics,
} from '/@/renderer/features/lyrics/hooks/use-furigana-lyrics';
import { LyricsActions } from '/@/renderer/features/lyrics/lyrics-actions';
import {
SynchronizedLyrics,
@@ -51,6 +54,7 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
const {
enableAutoTranslation,
enableFurigana,
enableRomaji,
preferLocalLyrics,
translationApiKey,
translationApiProvider,
@@ -119,6 +123,7 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
}, [data, indexToUse, preferLocalLyrics]);
const { data: furiganaConvertedLyrics } = useFuriganaLyrics(lyrics?.lyrics, !!enableFurigana);
const { data: romajiConvertedLyrics } = useRomajiLyrics(lyrics?.lyrics, !!enableRomaji);
const displayLyrics = useMemo(() => {
if (isLyricsDisabled || !lyrics) return null;
@@ -344,12 +349,22 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
<SynchronizedLyrics
{...(displayLyrics as SynchronizedLyricsProps)}
offsetMs={displayOffsetMs}
romajiLyrics={
enableRomaji
? (romajiConvertedLyrics as SynchronizedLyricsProps['romajiLyrics'])
: null
}
settingsKey={settingsKey}
translatedLyrics={showTranslation ? translatedLyrics : null}
/>
) : (
<UnsynchronizedLyrics
{...(displayLyrics as UnsynchronizedLyricsProps)}
romajiLyrics={
enableRomaji
? (romajiConvertedLyrics as UnsynchronizedLyricsProps['romajiLyrics'])
: null
}
settingsKey={settingsKey}
translatedLyrics={showTranslation ? translatedLyrics : null}
/>
@@ -23,6 +23,7 @@ const mpris = isElectron() && utils?.isLinux() ? window.api.mpris : null;
export interface SynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
lyrics: SynchronizedLyricsArray;
offsetMs?: number;
romajiLyrics?: null | SynchronizedLyricsArray;
settingsKey?: string;
style?: React.CSSProperties;
translatedLyrics?: null | string;
@@ -34,6 +35,7 @@ export const SynchronizedLyrics = ({
name,
offsetMs,
remote,
romajiLyrics,
settingsKey = 'default',
source,
style,
@@ -368,10 +370,9 @@ export const SynchronizedLyrics = ({
handleSeek(time / 1000);
}
}}
text={
text +
(translatedLyrics ? `_BREAK_${translatedLyrics.split('\n')[idx]}` : '')
}
romajiText={romajiLyrics?.[idx]?.[1]}
text={text}
translatedText={translatedLyrics?.split('\n')[idx]}
/>
))}
</div>
@@ -8,6 +8,7 @@ import { FullLyricsMetadata } from '/@/shared/types/domain-types';
export interface UnsynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
lyrics: string;
romajiLyrics?: null | string;
settingsKey?: string;
translatedLyrics?: null | string;
}
@@ -17,6 +18,7 @@ export const UnsynchronizedLyrics = ({
lyrics,
name,
remote,
romajiLyrics,
settingsKey = 'default',
source,
translatedLyrics,
@@ -42,6 +44,10 @@ export const UnsynchronizedLyrics = ({
return translatedLyrics ? translatedLyrics.split('\n') : [];
}, [translatedLyrics]);
const romajiLines = useMemo(() => {
return romajiLyrics ? romajiLyrics.split('\n') : [];
}, [romajiLyrics]);
return (
<div className={styles.container} style={{ gap: `${settings.gapUnsync}px` }}>
{settings.showProvider && source && (
@@ -67,7 +73,9 @@ export const UnsynchronizedLyrics = ({
fontSize={settings.fontSizeUnsync}
id={`lyric-${idx}`}
key={idx}
text={text + (translatedLines[idx] ? `_BREAK_${translatedLines[idx]}` : '')}
romajiText={romajiLines[idx]}
text={text}
translatedText={translatedLines[idx]}
/>
))}
</div>
@@ -107,6 +107,20 @@ export const LyricSettings = memo(() => {
isHidden: !isElectron(),
title: t('setting.enableFurigana'),
},
{
control: (
<Switch
aria-label="Enable romaji generation"
defaultChecked={settings.enableRomaji}
onChange={(e) => updateSetting({ enableRomaji: e.currentTarget.checked })}
/>
),
description: t('setting.enableRomaji', {
context: 'description',
}),
isHidden: !isElectron(),
title: t('setting.enableRomaji'),
},
{
control: (
<Switch
+2
View File
@@ -578,6 +578,7 @@ const LyricsSettingsSchema = z.object({
enableAutoTranslation: z.boolean(),
enableFurigana: z.boolean().optional(),
enableNeteaseTranslation: z.boolean(),
enableRomaji: z.boolean().optional(),
fetch: z.boolean(),
follow: z.boolean(),
preferLocalLyrics: z.boolean(),
@@ -1848,6 +1849,7 @@ const initialState: SettingsState = {
enableAutoTranslation: false,
enableFurigana: false,
enableNeteaseTranslation: false,
enableRomaji: false,
fetch: true,
follow: true,
preferLocalLyrics: true,