Merge pull request #74 from meowarex/dev

Apply Context Aware & Bubbled lyrics to Line
This commit is contained in:
Meow Meow
2026-02-24 23:38:58 +11:00
committed by GitHub
2 changed files with 81 additions and 66 deletions
+24 -28
View File
@@ -224,34 +224,30 @@ export const Settings = () => {
} }
}} }}
/> />
{lyricsStyle >= 1 && ( <AnySwitch
<> title="Context Aware Lyrics"
<AnySwitch desc="Enables background vocal display & duet singer positioning"
title="Context Aware Lyrics" checked={contextAwareLyrics}
desc="Enables background vocal display & duet singer positioning" onChange={(_: unknown, checked: boolean) => {
checked={contextAwareLyrics} settings.contextAwareLyrics = checked;
onChange={(_: unknown, checked: boolean) => { setContextAwareLyrics(checked);
settings.contextAwareLyrics = checked; if (window.updateLyricsStyle) {
setContextAwareLyrics(checked); window.updateLyricsStyle();
if (window.updateLyricsStyle) { }
window.updateLyricsStyle(); }}
} />
}} <AnySwitch
/> title="Bubbled Lyrics"
<AnySwitch desc="Smooth bounce animation on line/word transitions"
title="Bubbled Lyrics" checked={bubbledLyrics}
desc="Smooth bounce animation on line/word transitions" onChange={(_: unknown, checked: boolean) => {
checked={bubbledLyrics} settings.bubbledLyrics = checked;
onChange={(_: unknown, checked: boolean) => { setBubbledLyrics(checked);
settings.bubbledLyrics = checked; if (window.updateLyricsStyle) {
setBubbledLyrics(checked); window.updateLyricsStyle();
if (window.updateLyricsStyle) { }
window.updateLyricsStyle(); }}
} />
}}
/>
</>
)}
<AnySwitch <AnySwitch
title="Sticky Lyrics" title="Sticky Lyrics"
desc="auto-switches to Play Queue when lyrics aren't available (mirrored in lyrics dropdown)" desc="auto-switches to Play Queue when lyrics aren't available (mirrored in lyrics dropdown)"
+57 -38
View File
@@ -1204,7 +1204,7 @@ function setupStickyLyricsObserver(): void {
observe<HTMLElement>(unloads, '[data-test="lyrics-lines"]', () => { observe<HTMLElement>(unloads, '[data-test="lyrics-lines"]', () => {
if (lyricsData) { if (lyricsData) {
reapplyWordLyrics(); reapplyWordLyrics();
} else if (settings.lyricsStyle !== 0) { } else {
onTrackChange(); onTrackChange();
} }
}); });
@@ -1823,41 +1823,64 @@ const buildWordSpans = (): {
} }
} }
for (const group of wordGroups) { if (settings.lyricsStyle === 0) {
const groupIsBg = splitBg && syllabus[group[0]].isBackground; // Line mode: one span per container (main / bg) — no word splitting
const targetContainer = groupIsBg ? bgContainer! : mainContainer; const mainSyls = syllabus.filter(s => !splitBg || !s.isBackground);
const targetWords = groupIsBg ? lineBgWords : lineWords; const bgSyls = splitBg ? syllabus.filter(s => s.isBackground) : [];
if (isSylMode) { if (mainSyls.length > 0) {
const wordStartMs = syllabus[group[0]].time; const text = mainSyls.map(s => s.text).join("").trim();
const groupSpans: HTMLSpanElement[] = []; const first = mainSyls[0];
for (const si of group) { const last = mainSyls[mainSyls.length - 1];
const syl = syllabus[si]; const span = makeSpan(text, first.time, false);
const span = makeSpan(syl.text.trimEnd(), wordStartMs, syl.isBackground); mainContainer.appendChild(span);
span.addEventListener("mouseenter", () => { lineWords.push({ el: span, start: first.time, end: last.time + last.duration, duration: (last.time + last.duration) - first.time });
for (const s of groupSpans) s.classList.add("rl-wbw-word-hover"); }
}); if (bgSyls.length > 0 && bgContainer) {
span.addEventListener("mouseleave", () => { const text = bgSyls.map(s => s.text).join("").trim().replace(/[()]/g, "");
for (const s of groupSpans) s.classList.remove("rl-wbw-word-hover"); const first = bgSyls[0];
}); const last = bgSyls[bgSyls.length - 1];
groupSpans.push(span); const span = makeSpan(text, first.time, true);
bgContainer.appendChild(span);
lineBgWords.push({ el: span, start: first.time, end: last.time + last.duration, duration: (last.time + last.duration) - first.time });
}
} else {
for (const group of wordGroups) {
const groupIsBg = splitBg && syllabus[group[0]].isBackground;
const targetContainer = groupIsBg ? bgContainer! : mainContainer;
const targetWords = groupIsBg ? lineBgWords : lineWords;
if (isSylMode) {
const wordStartMs = syllabus[group[0]].time;
const groupSpans: HTMLSpanElement[] = [];
for (const si of group) {
const syl = syllabus[si];
const span = makeSpan(syl.text.trimEnd(), wordStartMs, syl.isBackground);
span.addEventListener("mouseenter", () => {
for (const s of groupSpans) s.classList.add("rl-wbw-word-hover");
});
span.addEventListener("mouseleave", () => {
for (const s of groupSpans) s.classList.remove("rl-wbw-word-hover");
});
groupSpans.push(span);
targetContainer.appendChild(span);
const entry: WordEntry = { el: span, start: syl.time, end: syl.time + syl.duration, duration: syl.duration };
targetWords.push(entry);
}
} else {
const mergedText = group.map(si => syllabus[si].text.trimEnd()).join("");
const first = syllabus[group[0]];
const last = syllabus[group[group.length - 1]];
const start = first.time;
const end = last.time + last.duration;
const bg = first.isBackground;
const span = makeSpan(mergedText, start, bg);
targetContainer.appendChild(span); targetContainer.appendChild(span);
const entry: WordEntry = { el: span, start: syl.time, end: syl.time + syl.duration, duration: syl.duration }; const entry: WordEntry = { el: span, start, end, duration: end - start };
targetWords.push(entry); targetWords.push(entry);
} }
} else { targetContainer.appendChild(document.createTextNode(" "));
const mergedText = group.map(si => syllabus[si].text.trimEnd()).join("");
const first = syllabus[group[0]];
const last = syllabus[group[group.length - 1]];
const start = first.time;
const end = last.time + last.duration;
const bg = first.isBackground;
const span = makeSpan(mergedText, start, bg);
targetContainer.appendChild(span);
const entry: WordEntry = { el: span, start, end, duration: end - start };
targetWords.push(entry);
} }
targetContainer.appendChild(document.createTextNode(" "));
} }
wbwContainer.appendChild(lineDiv); wbwContainer.appendChild(lineDiv);
@@ -2440,8 +2463,6 @@ const startTickLoop = (): void => {
const onTrackChange = async (): Promise<void> => { const onTrackChange = async (): Promise<void> => {
teardown(); teardown();
if (settings.lyricsStyle === 0) return;
const token = ++trackChangeToken; const token = ++trackChangeToken;
const trackInfo = await getTrackInfo(); const trackInfo = await getTrackInfo();
@@ -2494,7 +2515,7 @@ const onTrackChange = async (): Promise<void> => {
// Reapply word lyrics (for tab switch back) // Reapply word lyrics (for tab switch back)
const reapplyWordLyrics = (): void => { const reapplyWordLyrics = (): void => {
if (settings.lyricsStyle === 0 || !lyricsData) return; if (!lyricsData) return;
clearTickLoop(); clearTickLoop();
clearScrollAnim(); clearScrollAnim();
@@ -2519,9 +2540,7 @@ const reapplyWordLyrics = (): void => {
// Called by Settings or dropdown // Called by Settings or dropdown
const toggle = (): void => { const toggle = (): void => {
teardown(); teardown();
if (settings.lyricsStyle !== 0) { onTrackChange();
onTrackChange();
}
}; };
const updateLyricsStyleFromSettings = (): void => { const updateLyricsStyleFromSettings = (): void => {
const segButtons = document.querySelectorAll(".rl-seg-btn"); const segButtons = document.querySelectorAll(".rl-seg-btn");
@@ -2538,7 +2557,7 @@ const updateLyricsStyleFromSettings = (): void => {
onGlobalTrackChange(() => { onGlobalTrackChange(() => {
cachedLyricsKey = null; cachedLyricsKey = null;
cachedLyricsData = null; cachedLyricsData = null;
if (settings.lyricsStyle !== 0) onTrackChange(); onTrackChange();
}); });
unloads.add(() => teardown()); unloads.add(() => teardown());