mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-05 10:09:58 +02:00
Compare commits
2 Commits
21d788226c
...
f8ca8861fc
| Author | SHA1 | Date | |
|---|---|---|---|
| f8ca8861fc | |||
| 26eea7422d |
@@ -845,6 +845,8 @@
|
|||||||
"enableAutoTranslation": "Enable auto translation",
|
"enableAutoTranslation": "Enable auto translation",
|
||||||
"enableFurigana_description": "Display pronunciation guides (furigana) over Japanese kanji lyrics.",
|
"enableFurigana_description": "Display pronunciation guides (furigana) over Japanese kanji lyrics.",
|
||||||
"enableFurigana": "Enable furigana generation",
|
"enableFurigana": "Enable furigana generation",
|
||||||
|
"enableRomaji_description": "Display a romaji pronunciation line under Japanese lyrics.",
|
||||||
|
"enableRomaji": "Enable romaji generation",
|
||||||
"equalizer_descriptionMpv": "Parametric equalizer via FFmpeg lavfi (MPV)",
|
"equalizer_descriptionMpv": "Parametric equalizer via FFmpeg lavfi (MPV)",
|
||||||
"equalizer_descriptionWebAudio": "Parametric equalizer via Web Audio API",
|
"equalizer_descriptionWebAudio": "Parametric equalizer via Web Audio API",
|
||||||
"equalizer": "Equalizer",
|
"equalizer": "Equalizer",
|
||||||
|
|||||||
@@ -7,16 +7,19 @@ let kuroshiroInstance: any = null;
|
|||||||
let initPromise: null | Promise<void> = null;
|
let initPromise: null | Promise<void> = null;
|
||||||
|
|
||||||
const getKuroshiro = async () => {
|
const getKuroshiro = async () => {
|
||||||
if (kuroshiroInstance) return kuroshiroInstance;
|
|
||||||
if (initPromise) {
|
if (initPromise) {
|
||||||
await initPromise;
|
await initPromise;
|
||||||
return kuroshiroInstance;
|
return kuroshiroInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (kuroshiroInstance) return kuroshiroInstance;
|
||||||
|
|
||||||
const KuroshiroClass = (Kuroshiro as any).default || Kuroshiro;
|
const KuroshiroClass = (Kuroshiro as any).default || Kuroshiro;
|
||||||
kuroshiroInstance = new KuroshiroClass();
|
kuroshiroInstance = new KuroshiroClass();
|
||||||
initPromise = kuroshiroInstance.init(new KuromojiAnalyzer());
|
initPromise = kuroshiroInstance.init(new KuromojiAnalyzer());
|
||||||
await initPromise;
|
await initPromise;
|
||||||
|
|
||||||
|
initPromise = null;
|
||||||
return kuroshiroInstance;
|
return kuroshiroInstance;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,3 +38,17 @@ export const convertFurigana = async (text: string): Promise<string> => {
|
|||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const convertRomaji = async (text: string): Promise<string> => {
|
||||||
|
const KuroshiroClass = (Kuroshiro as any).default || Kuroshiro;
|
||||||
|
|
||||||
|
if (!KuroshiroClass.Util.hasKana(text)) return text;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const kuroshiro = await getKuroshiro();
|
||||||
|
return await kuroshiro.convert(text, { mode: 'spaced', to: 'romaji' });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Romaji conversion error: ', e);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ipcMain } from 'electron';
|
import { ipcMain } from 'electron';
|
||||||
|
|
||||||
import { store } from '../settings';
|
import { store } from '../settings';
|
||||||
import { convertFurigana } from './furigana';
|
import { convertFurigana, convertRomaji } from './furigana';
|
||||||
import { getLyricsBySongId as getGenius, getSearchResults as searchGenius } from './genius';
|
import { getLyricsBySongId as getGenius, getSearchResults as searchGenius } from './genius';
|
||||||
import { getLyricsBySongId as getLrcLib, getSearchResults as searchLrcLib } from './lrclib';
|
import { getLyricsBySongId as getLrcLib, getSearchResults as searchLrcLib } from './lrclib';
|
||||||
import { getLyricsBySongId as getNetease, getSearchResults as searchNetease } from './netease';
|
import { getLyricsBySongId as getNetease, getSearchResults as searchNetease } from './netease';
|
||||||
@@ -236,3 +236,7 @@ ipcMain.handle('lyric-by-remote-id', async (_event, params: LyricGetQuery) => {
|
|||||||
ipcMain.handle('lyric-convert-furigana', async (_event, text: string) => {
|
ipcMain.handle('lyric-convert-furigana', async (_event, text: string) => {
|
||||||
return await convertFurigana(text);
|
return await convertFurigana(text);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('lyric-convert-romaji', async (_event, text: string) => {
|
||||||
|
return await convertRomaji(text);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import console from 'console';
|
import console from 'console';
|
||||||
import { app, ipcMain } from 'electron';
|
import { app, ipcMain, powerMonitor } from 'electron';
|
||||||
import { access, rm } from 'fs/promises';
|
import { access, rm } from 'fs/promises';
|
||||||
import uniq from 'lodash/uniq';
|
import uniq from 'lodash/uniq';
|
||||||
import MpvAPI from 'node-mpv';
|
import MpvAPI from 'node-mpv';
|
||||||
@@ -85,6 +85,19 @@ const DEFAULT_MPV_PARAMETERS = (extraParameters?: string[]) => {
|
|||||||
parameters.push('--prefetch-playlist=yes');
|
parameters.push('--prefetch-playlist=yes');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Without these, mpv/ffmpeg will block indefinitely on a dead TCP connection
|
||||||
|
// instead of failing or reconnecting. This commonly happens when the OS network
|
||||||
|
// adapter resets after the system wakes from sleep while a stream is open.
|
||||||
|
if (!extraParameters?.some((param) => param.startsWith('--network-timeout'))) {
|
||||||
|
parameters.push('--network-timeout=10');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!extraParameters?.some((param) => param.startsWith('--stream-lavf-o'))) {
|
||||||
|
parameters.push(
|
||||||
|
'--stream-lavf-o=reconnect=1,reconnect_streamed=1,reconnect_at_eof=1,reconnect_delay_max=5',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return parameters;
|
return parameters;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -191,21 +204,44 @@ export const getMpvInstance = () => {
|
|||||||
return mpvInstance;
|
return mpvInstance;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const QUIT_TIMEOUT_MS = 3000;
|
||||||
|
|
||||||
|
const killMpvProcess = (mpv: MpvAPI) => {
|
||||||
|
const mpvProcess = (mpv as any).process || (mpv as any).mpvProcess;
|
||||||
|
if (mpvProcess && typeof mpvProcess.kill === 'function') {
|
||||||
|
try {
|
||||||
|
mpvProcess.kill('SIGTERM');
|
||||||
|
} catch (killErr) {
|
||||||
|
mpvLog({ action: 'Failed to kill mpv process' }, killErr as NodeMpvError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const quit = async (instance?: MpvAPI | null) => {
|
const quit = async (instance?: MpvAPI | null) => {
|
||||||
const mpv = instance || getMpvInstance();
|
const mpv = instance || getMpvInstance();
|
||||||
if (mpv) {
|
if (mpv) {
|
||||||
try {
|
try {
|
||||||
await mpv.quit();
|
// mpv.quit() resolves only when mpv replies over IPC. If mpv's command queue
|
||||||
|
// is wedged (e.g. blocked on a dead network stream after the system resumes
|
||||||
|
// from sleep), that reply never arrives, so this must not be allowed to hang
|
||||||
|
// forever - fall back to killing the process directly.
|
||||||
|
let timedOut = false;
|
||||||
|
await Promise.race([
|
||||||
|
mpv.quit(),
|
||||||
|
new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
timedOut = true;
|
||||||
|
resolve(undefined);
|
||||||
|
}, QUIT_TIMEOUT_MS);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (timedOut) {
|
||||||
|
killMpvProcess(mpv);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// If quit() fails, try to kill the process directly
|
// If quit() fails, try to kill the process directly
|
||||||
const mpvProcess = (mpv as any).process || (mpv as any).mpvProcess;
|
killMpvProcess(mpv);
|
||||||
if (mpvProcess && typeof mpvProcess.kill === 'function') {
|
|
||||||
try {
|
|
||||||
mpvProcess.kill('SIGTERM');
|
|
||||||
} catch (killErr) {
|
|
||||||
mpvLog({ action: 'Failed to kill mpv process' }, killErr as NodeMpvError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!isWindows()) {
|
if (!isWindows()) {
|
||||||
try {
|
try {
|
||||||
@@ -666,6 +702,17 @@ const cleanupMpv = async (force = false) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// When the OS resumes from sleep, any network stream mpv had open is likely dead
|
||||||
|
// (the connection silently dropped while the network adapter was suspended). Tell
|
||||||
|
// the renderer to reload mpv so it reconnects with a fresh stream instead of staying
|
||||||
|
// stuck on the old, now-dead connection until the app is manually restarted.
|
||||||
|
powerMonitor.on('resume', () => {
|
||||||
|
if (getMpvInstance()) {
|
||||||
|
mpvLog({ action: 'System resumed from sleep, reloading mpv' });
|
||||||
|
getMainWindow()?.webContents.send('renderer-mpv-reconnect');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.on('before-quit', async (event) => {
|
app.on('before-quit', async (event) => {
|
||||||
switch (mpvState) {
|
switch (mpvState) {
|
||||||
case MpvState.DONE:
|
case MpvState.DONE:
|
||||||
|
|||||||
@@ -30,8 +30,13 @@ const convertFurigana = (text: string): Promise<string> => {
|
|||||||
return ipcRenderer.invoke('lyric-convert-furigana', text);
|
return ipcRenderer.invoke('lyric-convert-furigana', text);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const convertRomaji = (text: string): Promise<string> => {
|
||||||
|
return ipcRenderer.invoke('lyric-convert-romaji', text);
|
||||||
|
};
|
||||||
|
|
||||||
export const lyrics = {
|
export const lyrics = {
|
||||||
convertFurigana,
|
convertFurigana,
|
||||||
|
convertRomaji,
|
||||||
getRemoteLyricsByRemoteId,
|
getRemoteLyricsByRemoteId,
|
||||||
getRemoteLyricsBySong,
|
getRemoteLyricsBySong,
|
||||||
searchRemoteLyrics,
|
searchRemoteLyrics,
|
||||||
|
|||||||
@@ -174,6 +174,10 @@ const rendererPlayerFallback = (cb: (data: boolean) => void) => {
|
|||||||
ipcRenderer.on('renderer-player-fallback', (_, data) => cb(data));
|
ipcRenderer.on('renderer-player-fallback', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rendererMpvReconnect = (cb: () => void) => {
|
||||||
|
ipcRenderer.on('renderer-mpv-reconnect', () => cb());
|
||||||
|
};
|
||||||
|
|
||||||
export const mpvPlayer = {
|
export const mpvPlayer = {
|
||||||
autoNext,
|
autoNext,
|
||||||
cleanup,
|
cleanup,
|
||||||
@@ -205,6 +209,7 @@ export const mpvPlayerListener = {
|
|||||||
rendererAutoNext,
|
rendererAutoNext,
|
||||||
rendererCurrentTime,
|
rendererCurrentTime,
|
||||||
rendererError,
|
rendererError,
|
||||||
|
rendererMpvReconnect,
|
||||||
rendererNext,
|
rendererNext,
|
||||||
rendererPause,
|
rendererPause,
|
||||||
rendererPlay,
|
rendererPlay,
|
||||||
|
|||||||
@@ -307,6 +307,20 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
|
|||||||
isHidden: !isElectron(),
|
isHidden: !isElectron(),
|
||||||
title: t('setting.enableFurigana'),
|
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: (
|
control: (
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -28,3 +28,27 @@ export const useFuriganaLyrics = (lyrics: LyricsResponse | null | undefined, ena
|
|||||||
staleTime: Infinity,
|
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) {
|
.lyric-line:global(.synchronized) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.romaji-line {
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,11 +10,21 @@ import { Stack } from '/@/shared/components/stack/stack';
|
|||||||
interface LyricLineProps extends ComponentPropsWithoutRef<'div'> {
|
interface LyricLineProps extends ComponentPropsWithoutRef<'div'> {
|
||||||
alignment: 'center' | 'left' | 'right';
|
alignment: 'center' | 'left' | 'right';
|
||||||
fontSize: number;
|
fontSize: number;
|
||||||
|
romajiText?: null | string;
|
||||||
text: string;
|
text: string;
|
||||||
|
translatedText?: null | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LyricLine = memo(
|
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 lines = useMemo(() => text.split('_BREAK_'), [text]);
|
||||||
|
|
||||||
const style = useMemo(
|
const style = useMemo(
|
||||||
@@ -31,6 +41,15 @@ export const LyricLine = memo(
|
|||||||
{lines.map((line, index) => (
|
{lines.map((line, index) => (
|
||||||
<span dangerouslySetInnerHTML={{ __html: sanitize(line) }} key={index} />
|
<span dangerouslySetInnerHTML={{ __html: sanitize(line) }} key={index} />
|
||||||
))}
|
))}
|
||||||
|
{romajiText && (
|
||||||
|
<span
|
||||||
|
className={styles.romajiLine}
|
||||||
|
dangerouslySetInnerHTML={{ __html: sanitize(romajiText) }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{translatedText && (
|
||||||
|
<span dangerouslySetInnerHTML={{ __html: sanitize(translatedText) }} />
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ import {
|
|||||||
type LyricsQueryResult,
|
type LyricsQueryResult,
|
||||||
} from '/@/renderer/features/lyrics/api/lyrics-api';
|
} from '/@/renderer/features/lyrics/api/lyrics-api';
|
||||||
import { openLyricsExportModal } from '/@/renderer/features/lyrics/components/lyrics-export-form';
|
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 { LyricsActions } from '/@/renderer/features/lyrics/lyrics-actions';
|
||||||
import {
|
import {
|
||||||
SynchronizedLyrics,
|
SynchronizedLyrics,
|
||||||
@@ -51,6 +54,7 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
|||||||
const {
|
const {
|
||||||
enableAutoTranslation,
|
enableAutoTranslation,
|
||||||
enableFurigana,
|
enableFurigana,
|
||||||
|
enableRomaji,
|
||||||
preferLocalLyrics,
|
preferLocalLyrics,
|
||||||
translationApiKey,
|
translationApiKey,
|
||||||
translationApiProvider,
|
translationApiProvider,
|
||||||
@@ -119,6 +123,7 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
|||||||
}, [data, indexToUse, preferLocalLyrics]);
|
}, [data, indexToUse, preferLocalLyrics]);
|
||||||
|
|
||||||
const { data: furiganaConvertedLyrics } = useFuriganaLyrics(lyrics?.lyrics, !!enableFurigana);
|
const { data: furiganaConvertedLyrics } = useFuriganaLyrics(lyrics?.lyrics, !!enableFurigana);
|
||||||
|
const { data: romajiConvertedLyrics } = useRomajiLyrics(lyrics?.lyrics, !!enableRomaji);
|
||||||
|
|
||||||
const displayLyrics = useMemo(() => {
|
const displayLyrics = useMemo(() => {
|
||||||
if (isLyricsDisabled || !lyrics) return null;
|
if (isLyricsDisabled || !lyrics) return null;
|
||||||
@@ -344,12 +349,22 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
|||||||
<SynchronizedLyrics
|
<SynchronizedLyrics
|
||||||
{...(displayLyrics as SynchronizedLyricsProps)}
|
{...(displayLyrics as SynchronizedLyricsProps)}
|
||||||
offsetMs={displayOffsetMs}
|
offsetMs={displayOffsetMs}
|
||||||
|
romajiLyrics={
|
||||||
|
enableRomaji
|
||||||
|
? (romajiConvertedLyrics as SynchronizedLyricsProps['romajiLyrics'])
|
||||||
|
: null
|
||||||
|
}
|
||||||
settingsKey={settingsKey}
|
settingsKey={settingsKey}
|
||||||
translatedLyrics={showTranslation ? translatedLyrics : null}
|
translatedLyrics={showTranslation ? translatedLyrics : null}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<UnsynchronizedLyrics
|
<UnsynchronizedLyrics
|
||||||
{...(displayLyrics as UnsynchronizedLyricsProps)}
|
{...(displayLyrics as UnsynchronizedLyricsProps)}
|
||||||
|
romajiLyrics={
|
||||||
|
enableRomaji
|
||||||
|
? (romajiConvertedLyrics as UnsynchronizedLyricsProps['romajiLyrics'])
|
||||||
|
: null
|
||||||
|
}
|
||||||
settingsKey={settingsKey}
|
settingsKey={settingsKey}
|
||||||
translatedLyrics={showTranslation ? translatedLyrics : null}
|
translatedLyrics={showTranslation ? translatedLyrics : null}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const mpris = isElectron() && utils?.isLinux() ? window.api.mpris : null;
|
|||||||
export interface SynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
|
export interface SynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
|
||||||
lyrics: SynchronizedLyricsArray;
|
lyrics: SynchronizedLyricsArray;
|
||||||
offsetMs?: number;
|
offsetMs?: number;
|
||||||
|
romajiLyrics?: null | SynchronizedLyricsArray;
|
||||||
settingsKey?: string;
|
settingsKey?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
translatedLyrics?: null | string;
|
translatedLyrics?: null | string;
|
||||||
@@ -34,6 +35,7 @@ export const SynchronizedLyrics = ({
|
|||||||
name,
|
name,
|
||||||
offsetMs,
|
offsetMs,
|
||||||
remote,
|
remote,
|
||||||
|
romajiLyrics,
|
||||||
settingsKey = 'default',
|
settingsKey = 'default',
|
||||||
source,
|
source,
|
||||||
style,
|
style,
|
||||||
@@ -368,10 +370,9 @@ export const SynchronizedLyrics = ({
|
|||||||
handleSeek(time / 1000);
|
handleSeek(time / 1000);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
text={
|
romajiText={romajiLyrics?.[idx]?.[1]}
|
||||||
text +
|
text={text}
|
||||||
(translatedLyrics ? `_BREAK_${translatedLyrics.split('\n')[idx]}` : '')
|
translatedText={translatedLyrics?.split('\n')[idx]}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { FullLyricsMetadata } from '/@/shared/types/domain-types';
|
|||||||
|
|
||||||
export interface UnsynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
|
export interface UnsynchronizedLyricsProps extends Omit<FullLyricsMetadata, 'lyrics'> {
|
||||||
lyrics: string;
|
lyrics: string;
|
||||||
|
romajiLyrics?: null | string;
|
||||||
settingsKey?: string;
|
settingsKey?: string;
|
||||||
translatedLyrics?: null | string;
|
translatedLyrics?: null | string;
|
||||||
}
|
}
|
||||||
@@ -17,6 +18,7 @@ export const UnsynchronizedLyrics = ({
|
|||||||
lyrics,
|
lyrics,
|
||||||
name,
|
name,
|
||||||
remote,
|
remote,
|
||||||
|
romajiLyrics,
|
||||||
settingsKey = 'default',
|
settingsKey = 'default',
|
||||||
source,
|
source,
|
||||||
translatedLyrics,
|
translatedLyrics,
|
||||||
@@ -42,6 +44,10 @@ export const UnsynchronizedLyrics = ({
|
|||||||
return translatedLyrics ? translatedLyrics.split('\n') : [];
|
return translatedLyrics ? translatedLyrics.split('\n') : [];
|
||||||
}, [translatedLyrics]);
|
}, [translatedLyrics]);
|
||||||
|
|
||||||
|
const romajiLines = useMemo(() => {
|
||||||
|
return romajiLyrics ? romajiLyrics.split('\n') : [];
|
||||||
|
}, [romajiLyrics]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container} style={{ gap: `${settings.gapUnsync}px` }}>
|
<div className={styles.container} style={{ gap: `${settings.gapUnsync}px` }}>
|
||||||
{settings.showProvider && source && (
|
{settings.showProvider && source && (
|
||||||
@@ -67,7 +73,9 @@ export const UnsynchronizedLyrics = ({
|
|||||||
fontSize={settings.fontSizeUnsync}
|
fontSize={settings.fontSizeUnsync}
|
||||||
id={`lyric-${idx}`}
|
id={`lyric-${idx}`}
|
||||||
key={idx}
|
key={idx}
|
||||||
text={text + (translatedLines[idx] ? `_BREAK_${translatedLines[idx]}` : '')}
|
romajiText={romajiLines[idx]}
|
||||||
|
text={text}
|
||||||
|
translatedText={translatedLines[idx]}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -68,9 +68,13 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
eventEmitter.on('MPV_RELOAD', handleMpvReload);
|
eventEmitter.on('MPV_RELOAD', handleMpvReload);
|
||||||
|
// The main process notifies us after the OS resumes from sleep, since the
|
||||||
|
// stream mpv had open is likely on a now-dead connection.
|
||||||
|
mpvPlayerListener?.rendererMpvReconnect(handleMpvReload);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
eventEmitter.off('MPV_RELOAD', handleMpvReload);
|
eventEmitter.off('MPV_RELOAD', handleMpvReload);
|
||||||
|
ipc?.removeAllListeners('renderer-mpv-reconnect');
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,20 @@ export const LyricSettings = memo(() => {
|
|||||||
isHidden: !isElectron(),
|
isHidden: !isElectron(),
|
||||||
title: t('setting.enableFurigana'),
|
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: (
|
control: (
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -578,6 +578,7 @@ const LyricsSettingsSchema = z.object({
|
|||||||
enableAutoTranslation: z.boolean(),
|
enableAutoTranslation: z.boolean(),
|
||||||
enableFurigana: z.boolean().optional(),
|
enableFurigana: z.boolean().optional(),
|
||||||
enableNeteaseTranslation: z.boolean(),
|
enableNeteaseTranslation: z.boolean(),
|
||||||
|
enableRomaji: z.boolean().optional(),
|
||||||
fetch: z.boolean(),
|
fetch: z.boolean(),
|
||||||
follow: z.boolean(),
|
follow: z.boolean(),
|
||||||
preferLocalLyrics: z.boolean(),
|
preferLocalLyrics: z.boolean(),
|
||||||
@@ -1848,6 +1849,7 @@ const initialState: SettingsState = {
|
|||||||
enableAutoTranslation: false,
|
enableAutoTranslation: false,
|
||||||
enableFurigana: false,
|
enableFurigana: false,
|
||||||
enableNeteaseTranslation: false,
|
enableNeteaseTranslation: false,
|
||||||
|
enableRomaji: false,
|
||||||
fetch: true,
|
fetch: true,
|
||||||
follow: true,
|
follow: true,
|
||||||
preferLocalLyrics: true,
|
preferLocalLyrics: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user