Files
youtube-music/src/plugins/synced-lyrics/renderer/components/SyncedLine.tsx
Angelos Bouklis eb50596961 feat(synced-lyrics): add new "spacer" (#3742)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-07 04:26:03 +09:00

181 lines
5.1 KiB
TypeScript

import { createEffect, For, Show, createSignal, createMemo } from 'solid-js';
import { type VirtualizerHandle } from 'virtua/solid';
import { type LineLyrics } from '@/plugins/synced-lyrics/types';
import { config, currentTime } from '../renderer';
import { _ytAPI } from '..';
import { canonicalize, romanize, simplifyUnicode } from '../utils';
interface SyncedLineProps {
scroller: VirtualizerHandle;
index: number;
line: LineLyrics;
status: 'upcoming' | 'current' | 'previous';
}
const EmptyLine = (props: SyncedLineProps) => {
const states = createMemo(() => {
const defaultText = config()?.defaultTextString ?? '';
return Array.isArray(defaultText) ? defaultText : [defaultText];
});
const index = createMemo(() => {
const progress = currentTime() - props.line.timeInMs;
const total = props.line.duration;
const percentage = Math.min(1, progress / total);
return Math.max(0, Math.floor((states().length - 1) * percentage));
});
return (
<div
class={`synced-line ${props.status}`}
onClick={() => {
_ytAPI?.seekTo((props.line.timeInMs + 10) / 1000);
}}
>
<div class="description ytmusic-description-shelf-renderer" dir="auto">
<yt-formatted-string
text={{
runs: [
{
text: config()?.showTimeCodes ? `[${props.line.time}] ` : '',
},
],
}}
/>
<div class="text-lyrics">
<span>
<span>
<Show
fallback={
<yt-formatted-string
text={{ runs: [{ text: states()[0] }] }}
/>
}
when={states().length > 1}
>
<yt-formatted-string
text={{
runs: [
{
text: states().at(
props.status === 'current' ? index() : -1,
)!,
},
],
}}
/>
</Show>
</span>
</span>
</div>
</div>
</div>
);
};
export const SyncedLine = (props: SyncedLineProps) => {
const text = createMemo(() => props.line.text.trim());
const [romanization, setRomanization] = createSignal('');
createEffect(() => {
const input = canonicalize(text());
if (!config()?.romanization) return;
romanize(input).then((result) => {
setRomanization(canonicalize(result));
});
});
return (
<Show fallback={<EmptyLine {...props} />} when={text()}>
<div
class={`synced-line ${props.status}`}
onClick={() => {
_ytAPI?.seekTo((props.line.timeInMs + 10) / 1000);
}}
>
<div class="description ytmusic-description-shelf-renderer" dir="auto">
<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>
</Show>
);
};