Files
youtube-music/src/plugins/synced-lyrics/renderer/components/SyncedLine.tsx
Angelos Bouklis 4b35a96778 feat(synced-lyrics): romanization (#2790)
* feat(synced-lyrics): init romanization!

* remove debug logs and add TODO

* feat(synced-lyrics/romanization): Mandarin!

* feat(synced-lyrics/romanization): improve japanese detection

* feat(synced-lyrics/romanization): Korean!

* qol(synced-lyrics/romanization): canonicalize punctuation and symbols

* feat(synced-lyrics/romanization): handle japanese+korean and korean+chinese lyrics

* revert formatting on electron.vite.config.mts

* feat(synced-lyrics/romanization): romanize plain lyrics

* apply fix by @kimjammer

* fix lockfile due to rebase

* feat(synced-lyrics): improve lyric processing and formatting;

* feat(synced-lyrics/romanization): add option to enable/disable romanization

* chore: move default value for --lyrics-duration to the declaration

* update lockfile

* fix: improvement

1. improved language detection logic
2. changed code to work in the renderer process

* fix: fix regression (canonicalize)

---------

Co-authored-by: JellyBrick <shlee1503@naver.com>
2025-03-26 20:29:43 +09:00

158 lines
4.1 KiB
TypeScript

import { createEffect, createMemo, For, Show, createSignal } from 'solid-js';
import { currentTime } from './LyricsContainer';
import { config } from '../renderer';
import { _ytAPI } from '..';
import {
canonicalize,
romanizeChinese,
romanizeHangul,
romanizeJapanese,
romanizeJapaneseOrHangul,
simplifyUnicode,
} from '../utils';
import type { LineLyrics } from '../../types';
interface SyncedLineProps {
line: LineLyrics;
hasJapanese: boolean;
hasKorean: boolean;
}
export const SyncedLine = (props: SyncedLineProps) => {
const status = createMemo(() => {
const current = currentTime();
if (props.line.timeInMs >= current) return 'upcoming';
if (current - props.line.timeInMs >= props.line.duration) return 'previous';
return 'current';
});
let ref: HTMLDivElement | undefined;
createEffect(() => {
if (status() === 'current') {
ref?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
const text = createMemo(() => {
if (!props.line.text.trim()) {
return config()?.defaultTextString ?? '';
}
return props.line.text;
});
const [romanization, setRomanization] = createSignal('');
createEffect(async () => {
if (!config()?.romanization) return;
const input = canonicalize(text());
let result: string;
if (props.hasJapanese) {
if (props.hasKorean) result = await romanizeJapaneseOrHangul(input);
else result = await romanizeJapanese(input);
} else if (props.hasKorean) result = romanizeHangul(input);
else result = romanizeChinese(input);
setRomanization(canonicalize(result));
});
if (!text()) {
return (
<yt-formatted-string
text={{
runs: [{ text: '' }],
}}
/>
);
}
return (
<div
ref={ref}
class={`synced-line ${status()}`}
onClick={() => {
_ytAPI?.seekTo(props.line.timeInMs / 1000);
}}
>
<div dir="auto" class="description ytmusic-description-shelf-renderer">
<yt-formatted-string
text={{
runs: [
{ text: config()?.showTimeCodes ? `[${props.line.time}] ` : '' },
],
}}
/>
<div
class="text-lyrics"
ref={(div: HTMLDivElement) => {
// TODO: Investigate the animation, even though the duration is properly set, all lines have the same animation duration
div.style.setProperty(
'--lyrics-duration',
`${props.line.duration / 1000}s`,
'important',
);
}}
style={{ 'display': 'flex', 'flex-direction': 'column' }}
>
<span>
<For each={text().split(' ')}>
{(word, index) => {
return (
<span
style={{
'transition-delay': `${index() * 0.05}s`,
'animation-delay': `${index() * 0.05}s`,
}}
>
<yt-formatted-string
text={{
runs: [{ text: `${word} ` }],
}}
/>
</span>
);
}}
</For>
</span>
<Show
when={
config()?.romanization &&
simplifyUnicode(text()) !== simplifyUnicode(romanization())
}
>
<span class="romaji">
<For each={romanization().split(' ')}>
{(word, index) => {
return (
<span
style={{
'transition-delay': `${index() * 0.05}s`,
'animation-delay': `${index() * 0.05}s`,
}}
>
<yt-formatted-string
text={{
runs: [{ text: `${word} ` }],
}}
/>
</span>
);
}}
</For>
</span>
</Show>
</div>
</div>
</div>
);
};