Skip to content

Commit db0f24c

Browse files
committed
refactor(synced-lyrics): unify provider switching, add utilities, and clean rendering logic
1 parent 8501f03 commit db0f24c

File tree

4 files changed

+97
-60
lines changed

4 files changed

+97
-60
lines changed

src/plugins/synced-lyrics/renderer/components/LyricsPicker.tsx

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,20 @@ export const LyricsPicker = (props: {
7979
const [starredProvider, setStarredProvider] =
8080
createSignal<ProviderName | null>(null);
8181

82+
const favoriteProviderKey = (id: string) => `ytmd-sl-starred-${id}`;
83+
const switchProvider = (provider: ProviderName, fastMs = 2500) => {
84+
requestFastScroll(fastMs);
85+
setLyricsStore('provider', provider);
86+
};
87+
8288
createEffect(() => {
8389
const id = videoId();
8490
if (id === null) {
8591
setStarredProvider(null);
8692
return;
8793
}
8894

89-
const key = `ytmd-sl-starred-${id}`;
95+
const key = favoriteProviderKey(id);
9096
const value = localStorage.getItem(key);
9197
if (!value) {
9298
setStarredProvider(null);
@@ -106,7 +112,7 @@ export const LyricsPicker = (props: {
106112
const id = videoId();
107113
if (id === null) return;
108114

109-
const key = `ytmd-sl-starred-${id}`;
115+
const key = favoriteProviderKey(id);
110116

111117
setStarredProvider((starredProvider) => {
112118
if (lyricsStore.provider === starredProvider) {
@@ -141,8 +147,7 @@ export const LyricsPicker = (props: {
141147
if (!hasManuallySwitchedProvider()) {
142148
const starred = starredProvider();
143149
if (starred !== null) {
144-
requestFastScroll(2500);
145-
setLyricsStore('provider', starred);
150+
switchProvider(starred, 2500);
146151
return;
147152
}
148153

@@ -156,29 +161,29 @@ export const LyricsPicker = (props: {
156161
force ||
157162
providerBias(lyricsStore.provider) < providerBias(provider)
158163
) {
159-
requestFastScroll(2500);
160-
setLyricsStore('provider', provider);
164+
switchProvider(provider, 2500);
161165
}
162166
}
163167
});
164168

165169
const next = () => {
166170
setHasManuallySwitchedProvider(true);
167-
requestFastScroll(2500);
168171
setLyricsStore('provider', (prevProvider) => {
169172
const idx = providerNames.indexOf(prevProvider);
170-
return providerNames[(idx + 1) % providerNames.length];
173+
const nextProvider = providerNames[(idx + 1) % providerNames.length];
174+
switchProvider(nextProvider, 2500);
175+
return nextProvider;
171176
});
172177
};
173178

174179
const previous = () => {
175180
setHasManuallySwitchedProvider(true);
176-
requestFastScroll(2500);
177181
setLyricsStore('provider', (prevProvider) => {
178182
const idx = providerNames.indexOf(prevProvider);
179-
return providerNames[
180-
(idx + providerNames.length - 1) % providerNames.length
181-
];
183+
const prev =
184+
providerNames[(idx + providerNames.length - 1) % providerNames.length];
185+
switchProvider(prev, 2500);
186+
return prev;
182187
});
183188
};
184189

@@ -317,8 +322,7 @@ export const LyricsPicker = (props: {
317322
class="lyrics-picker-dot"
318323
onClick={() => {
319324
setHasManuallySwitchedProvider(true);
320-
requestFastScroll(2500);
321-
setLyricsStore('provider', providerNames[idx()]);
325+
switchProvider(providerNames[idx()], 2500);
322326
}}
323327
style={{
324328
background: idx() === providerIdx() ? 'white' : 'black',

src/plugins/synced-lyrics/renderer/components/SyncedLine.tsx

Lines changed: 21 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@ import { type LineLyrics } from '@/plugins/synced-lyrics/types';
77
import { config, currentTime } from '../renderer';
88
import { _ytAPI } from '..';
99

10-
import { canonicalize, romanize, simplifyUnicode, getSeekTime } from '../utils';
10+
import {
11+
canonicalize,
12+
romanize,
13+
simplifyUnicode,
14+
getSeekTime,
15+
isBlank,
16+
timeCodeText,
17+
} from '../utils';
1118

1219
interface SyncedLineProps {
1320
scroller: VirtualizerHandle;
@@ -19,26 +26,6 @@ interface SyncedLineProps {
1926
isFirstEmptyLine?: boolean;
2027
}
2128

22-
function formatTime(timeInMs: number, preciseTiming: boolean): string {
23-
if (!preciseTiming) {
24-
const totalSeconds = Math.round(timeInMs / 1000);
25-
const minutes = Math.floor(totalSeconds / 60);
26-
const seconds = totalSeconds % 60;
27-
28-
return `${minutes.toString().padStart(2, '0')}:${seconds
29-
.toString()
30-
.padStart(2, '0')}`;
31-
}
32-
33-
const minutes = Math.floor(timeInMs / 60000);
34-
const seconds = Math.floor((timeInMs % 60000) / 1000);
35-
const ms = Math.floor((timeInMs % 1000) / 10);
36-
37-
return `${minutes.toString().padStart(2, '0')}:${seconds
38-
.toString()
39-
.padStart(2, '0')}.${ms.toString().padStart(2, '0')}`;
40-
}
41-
4229
const EmptyLine = (props: SyncedLineProps) => {
4330
const states = createMemo(() => {
4431
const defaultText = config()?.defaultTextString ?? '';
@@ -82,7 +69,7 @@ const EmptyLine = (props: SyncedLineProps) => {
8269
});
8370

8471
const shouldRenderPlaceholder = createMemo(() => {
85-
const isEmpty = !props.line.text?.trim();
72+
const isEmpty = isBlank(props.line.text);
8673
const showEmptySymbols = config()?.showEmptyLineSymbols ?? false;
8774

8875
return isEmpty
@@ -92,7 +79,7 @@ const EmptyLine = (props: SyncedLineProps) => {
9279

9380
const isHighlighted = createMemo(() => props.status === 'current');
9481
const isFinalEmpty = createMemo(() => {
95-
return props.isFinalLine && !props.line.text?.trim();
82+
return props.isFinalLine && isBlank(props.line.text);
9683
});
9784

9885
const shouldRemovePadding = createMemo(() => {
@@ -116,18 +103,17 @@ const EmptyLine = (props: SyncedLineProps) => {
116103
text={{
117104
runs: [
118105
{
119-
text: config()?.showTimeCodes
120-
? `[${formatTime(
121-
props.line.timeInMs,
122-
config()?.preciseTiming ?? false,
123-
)}] `
124-
: '',
106+
text: timeCodeText(
107+
props.line.timeInMs,
108+
config()?.preciseTiming ?? false,
109+
config()?.showTimeCodes ?? false,
110+
),
125111
},
126112
],
127113
}}
128114
/>
129115
<div class="text-lyrics">
130-
{props.isFinalLine && !props.line.text?.trim() ? (
116+
{props.isFinalLine && isBlank(props.line.text) ? (
131117
<span>
132118
<span class={`fade ${isHighlighted() ? 'show' : ''}`}>
133119
<yt-formatted-string text={{ runs: [{ text: '' }] }} />
@@ -185,9 +171,11 @@ export const SyncedLine = (props: SyncedLineProps) => {
185171
text={{
186172
runs: [
187173
{
188-
text: config()?.showTimeCodes
189-
? `[${formatTime(props.line.timeInMs, config()?.preciseTiming ?? false)}] `
190-
: '',
174+
text: timeCodeText(
175+
props.line.timeInMs,
176+
config()?.preciseTiming ?? false,
177+
config()?.showTimeCodes ?? false,
178+
),
191179
},
192180
],
193181
}}

src/plugins/synced-lyrics/renderer/renderer.tsx

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ import { type VirtualizerHandle, VList } from 'virtua/solid';
1111

1212
import { LyricsPicker } from './components/LyricsPicker';
1313

14-
import { selectors, getSeekTime, SFont } from './utils';
14+
import {
15+
selectors,
16+
getSeekTime,
17+
SFont,
18+
normalizePlainLyrics,
19+
isBlank,
20+
} from './utils';
1521

1622
import {
1723
ErrorDisplay,
@@ -306,7 +312,7 @@ export const LyricsRenderer = () => {
306312

307313
if (data?.lines) {
308314
const lines = data.lines;
309-
const firstEmpty = lines.findIndex((l) => !l.text?.trim());
315+
const firstEmpty = lines.findIndex((l) => isBlank(l.text));
310316
setFirstEmptyIndex(firstEmpty === -1 ? null : firstEmpty);
311317

312318
return lines.map((line) => ({
@@ -316,17 +322,7 @@ export const LyricsRenderer = () => {
316322
}
317323

318324
if (data?.lyrics) {
319-
const rawLines = data.lyrics.split('\n');
320-
321-
// preserve a single trailing empty line if provided by the provider
322-
const hasTrailingEmpty =
323-
rawLines.length > 0 && rawLines[rawLines.length - 1].trim() === '';
324-
325-
const lines = rawLines.filter((line, idx) => {
326-
if (line.trim()) return true;
327-
// keep only the final empty line (for padding) if it exists
328-
return hasTrailingEmpty && idx === rawLines.length - 1;
329-
});
325+
const lines = normalizePlainLyrics(data.lyrics);
330326

331327
return lines.map((line) => ({
332328
kind: 'PlainLine' as const,

src/plugins/synced-lyrics/renderer/utils.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ export const simplifyUnicode = (text?: string) =>
9191
.trim()
9292
: text;
9393

94+
export const isBlank = (text?: string) => {
95+
const simplified = simplifyUnicode(text);
96+
return simplified === undefined || simplified === '';
97+
};
98+
9499
// Japanese Shinjitai
95100
const shinjitai = [
96101
20055, 20081, 20120, 20124, 20175, 26469, 20341, 20206, 20253, 20605, 20385,
@@ -218,6 +223,50 @@ export const romanize = async (line: string) => {
218223
export const getSeekTime = (timeInMs: number, precise: boolean) =>
219224
precise ? timeInMs / 1000 : Math.round(timeInMs / 1000);
220225

226+
// Format a time value in ms into mm:ss or mm:ss.xx depending on preciseTiming
227+
export function formatTime(timeInMs: number, preciseTiming: boolean): string {
228+
if (!preciseTiming) {
229+
const totalSeconds = Math.round(timeInMs / 1000);
230+
const minutes = Math.floor(totalSeconds / 60);
231+
const seconds = totalSeconds % 60;
232+
233+
return `${minutes.toString().padStart(2, '0')}:${seconds
234+
.toString()
235+
.padStart(2, '0')}`;
236+
}
237+
238+
const minutes = Math.floor(timeInMs / 60000);
239+
const seconds = Math.floor((timeInMs % 60000) / 1000);
240+
const ms = Math.floor((timeInMs % 1000) / 10);
241+
242+
return `${minutes.toString().padStart(2, '0')}:${seconds
243+
.toString()
244+
.padStart(2, '0')}.${ms.toString().padStart(2, '0')}`;
245+
}
246+
247+
// Returns the time code prefix text to embed in yt-formatted-string runs
248+
export const timeCodeText = (
249+
timeInMs: number,
250+
preciseTiming: boolean,
251+
show: boolean,
252+
) => (show ? `[${formatTime(timeInMs, preciseTiming)}] ` : '');
253+
254+
// Normalizes plain-lyrics text into displayable lines, removing empty lines
255+
// while preserving a single trailing empty line if the original text ends with one.
256+
export const normalizePlainLyrics = (raw: string): string[] => {
257+
const rawLines = raw.split('\n');
258+
const hasTrailingEmpty =
259+
rawLines.length > 0 && isBlank(rawLines[rawLines.length - 1]);
260+
261+
const lines = rawLines.filter((line, idx) => {
262+
if (!isBlank(line)) return true;
263+
// keep only the final empty line (for padding) if it exists
264+
return hasTrailingEmpty && idx === rawLines.length - 1;
265+
});
266+
267+
return lines;
268+
};
269+
221270
export const SFont = () => {
222271
if (document.getElementById('satoshi-font-link')) return;
223272
const link = document.createElement('link');

0 commit comments

Comments
 (0)