Merge pull request #82 from meowarex/dev

Add Lyrics to Tracks, Tidal doesn't have <3
This commit is contained in:
Meow Meow
2026-02-28 19:12:32 +11:00
committed by GitHub
2 changed files with 483 additions and 75 deletions
+406 -52
View File
@@ -1092,7 +1092,7 @@ const sylTrace = (...args: unknown[]) => {
const container = document.querySelector(".rl-wbw-container"); const container = document.querySelector(".rl-wbw-container");
if (container) { if (container) {
container.classList.remove("rl-syl-pop", "rl-syl-jump"); container.classList.remove("rl-syl-pop", "rl-syl-jump");
if (isWordTimingMode()) { if (isWordMode()) {
if (clamped === 1) container.classList.add("rl-syl-pop"); if (clamped === 1) container.classList.add("rl-syl-pop");
else if (clamped === 2) container.classList.add("rl-syl-jump"); else if (clamped === 2) container.classList.add("rl-syl-jump");
} }
@@ -1260,6 +1260,43 @@ const createStickyLyricsDropdown = (): void => {
}); });
}; };
// Sticky Lyrics nav for injected lyrics tab
const tryActivateStickyLyricsTab = (): boolean => {
if (!settings.stickyLyrics) return false;
const lyricsTab = document.querySelector(
'[data-test="tabs-lyrics"]',
) as HTMLElement;
const playQueueTab = document.querySelector(
'[data-test="tabs-play-queue"]',
) as HTMLElement;
if (!lyricsTab) return false;
if (lyricsTab.getAttribute("data-rl-injected") === "true") {
showInjectedLyricsTab();
} else {
lyricsTab.click();
}
// Verify we actually stayed on lyrics after a short delay
safeTimeout(
unloads,
() => {
if (!settings.stickyLyrics) return;
const onLyrics = document.querySelector(
'[data-test="tabs-lyrics"][aria-selected="true"]',
);
if (!onLyrics && playQueueTab) {
playQueueTab.click();
}
},
800,
);
return true;
};
// Handle switching tabs on track change // Handle switching tabs on track change
const handleStickyLyricsTrackChange = (): void => { const handleStickyLyricsTrackChange = (): void => {
if (!settings.stickyLyrics) return; if (!settings.stickyLyrics) return;
@@ -1271,40 +1308,303 @@ const handleStickyLyricsTrackChange = (): void => {
() => { () => {
if (!settings.stickyLyrics) return; if (!settings.stickyLyrics) return;
const lyricsTab = document.querySelector( if (!tryActivateStickyLyricsTab()) {
'[data-test="tabs-lyrics"]', const playQueueTab = document.querySelector(
) as HTMLElement; '[data-test="tabs-play-queue"]',
const playQueueTab = document.querySelector( ) as HTMLElement;
'[data-test="tabs-play-queue"]',
) as HTMLElement;
if (!lyricsTab) {
if (playQueueTab) playQueueTab.click(); if (playQueueTab) playQueueTab.click();
return;
} }
lyricsTab.click();
// Verify we actually stayed on lyrics after a short delay
// TODO: Make not shitty (one day maybe)
safeTimeout(
unloads,
() => {
if (!settings.stickyLyrics) return;
const onLyrics = document.querySelector(
'[data-test="tabs-lyrics"][aria-selected="true"]',
);
if (!onLyrics && playQueueTab) {
playQueueTab.click();
}
},
800,
);
}, },
1200, 1200,
); );
}; };
// MARKER: Injected API Lyrics (for non tidal lyric tracks)
let injectedTablistClickCleanup: (() => void) | null = null;
const getTabsRoot = (): HTMLElement | null => {
const roots = Array.from(
document.querySelectorAll('.react-tabs[data-rttabs="true"]'),
) as HTMLElement[];
for (const root of roots) {
if (root.querySelector('[data-test="tabs-play-queue"]')) return root;
}
return null;
};
const hideInjectedLyricsTab = (): void => {
if (!injectedTabEl || !injectedPanelEl) return;
const root = getTabsRoot();
if (root) {
const nativePanels = Array.from(
root.querySelectorAll('div[role="tabpanel"]'),
) as HTMLElement[];
for (const panel of nativePanels) {
if (panel === injectedPanelEl) continue;
panel.removeAttribute("aria-hidden");
panel.style.removeProperty("display");
}
}
injectedTabEl.setAttribute("aria-selected", "false");
injectedTabEl.setAttribute("tabindex", "-1");
injectedTabEl.setAttribute("aria-expanded", "false");
injectedTabEl.classList.remove("react-tabs__tab--selected");
if (activeTabClass) {
injectedTabEl.classList.remove(activeTabClass);
}
injectedPanelEl.classList.remove("react-tabs__tab-panel--selected");
if (activePanelClass) {
injectedPanelEl.classList.remove(activePanelClass);
}
injectedPanelEl.setAttribute("aria-hidden", "true");
injectedPanelEl.style.display = "none";
};
const showInjectedLyricsTab = (): void => {
if (!injectedTabEl || !injectedPanelEl) return;
const root = getTabsRoot();
if (!root) return;
const tabs = Array.from(
root.querySelectorAll('ul[role="tablist"] > li[role="tab"]'),
) as HTMLElement[];
const panels = Array.from(
root.querySelectorAll('div[role="tabpanel"]'),
) as HTMLElement[];
if (!activeTabClass) {
for (const tab of tabs) {
const cls = Array.from(tab.classList).find((c) =>
c.includes("_activeTab_"),
);
if (cls) {
activeTabClass = cls;
break;
}
}
}
if (!activePanelClass) {
for (const panel of panels) {
const cls = Array.from(panel.classList).find((c) =>
c.includes("_isActive_"),
);
if (cls) {
activePanelClass = cls;
break;
}
}
}
const nativePanels = Array.from(
root.querySelectorAll('div[role="tabpanel"]'),
) as HTMLElement[];
for (const tab of tabs) {
if (tab === injectedTabEl) continue;
if (activeTabClass) tab.classList.remove(activeTabClass);
}
for (const panel of nativePanels) {
if (panel === injectedPanelEl) continue;
panel.setAttribute("aria-hidden", "true");
panel.style.display = "none";
}
injectedTabEl.setAttribute("aria-selected", "true");
injectedTabEl.setAttribute("tabindex", "0");
injectedTabEl.setAttribute("aria-expanded", "true");
injectedTabEl.classList.add("react-tabs__tab--selected");
if (activeTabClass) injectedTabEl.classList.add(activeTabClass);
injectedPanelEl.classList.add("react-tabs__tab-panel--selected");
if (activePanelClass) {
injectedPanelEl.classList.add(activePanelClass);
}
injectedPanelEl.removeAttribute("aria-hidden");
injectedPanelEl.style.removeProperty("display");
};
const clearInjectedLyricsTab = (): void => {
hideInjectedLyricsTab();
if (creditsTabEl) {
if (creditsPrevOrder) {
creditsTabEl.style.setProperty("order", creditsPrevOrder);
} else {
creditsTabEl.style.removeProperty("order");
}
}
if (injectedTablistClickCleanup) {
injectedTablistClickCleanup();
injectedTablistClickCleanup = null;
}
if (injectedTabEl) injectedTabEl.remove();
if (injectedPanelEl) injectedPanelEl.remove();
injectedTabEl = null;
injectedPanelEl = null;
activeTabClass = "";
activePanelClass = "";
creditsTabEl = null;
creditsPrevOrder = "";
};
const buildInjectedLyricsShell = (panel: HTMLElement): void => {
if (panel.querySelector('[data-test="lyrics-lines"]')) return;
const trackLyrics = document.createElement("div");
trackLyrics.setAttribute("data-test", "track-lyrics");
const lyricsContainer = document.createElement("div");
lyricsContainer.className = "_lyricsContainer_fa37c08 _smoothScroll_05ef096";
lyricsContainer.setAttribute("data-test", "lyrics");
const lyricsLines = document.createElement("div");
lyricsLines.className =
"_lyricsText_bf0080e _lyrics_0537465 _hasCues_76b4841";
lyricsLines.setAttribute("data-test", "lyrics-lines");
const placeholder = document.createElement("span");
placeholder.textContent = "...";
const linesInner = document.createElement("div");
lyricsLines.appendChild(placeholder);
lyricsLines.appendChild(linesInner);
lyricsContainer.appendChild(lyricsLines);
trackLyrics.appendChild(lyricsContainer);
panel.replaceChildren(trackLyrics);
};
const ensureLyricsTab = (): boolean => {
const existingLyricsTab = document.querySelector(
'[data-test="tabs-lyrics"]',
) as HTMLElement;
if (existingLyricsTab && existingLyricsTab !== injectedTabEl) {
clearInjectedLyricsTab();
return true;
}
if (injectedTabEl && injectedPanelEl) {
buildInjectedLyricsShell(injectedPanelEl);
return true;
}
const root = getTabsRoot();
if (!root) return false;
const tabList = root.querySelector('ul[role="tablist"]') as HTMLElement;
if (!tabList) return false;
const sampleTab = tabList.querySelector('li[role="tab"]') as HTMLElement;
const tabItemClass =
Array.from(sampleTab?.classList ?? []).find((c) =>
c.includes("_tabItem_"),
) ?? "";
const samplePanel = root.querySelector('div[role="tabpanel"]') as HTMLElement;
const panelBaseClass =
Array.from(samplePanel?.classList ?? []).find((c) =>
c.includes("_tabPanelStyles_"),
) ?? "";
const panelId = `panel:rl:${Date.now().toString(36)}`;
const tabId = `tab:rl:${Date.now().toString(36)}`;
const tabEl = document.createElement("li");
tabEl.setAttribute("data-test", "tabs-lyrics");
tabEl.setAttribute("data-rttab", "true");
tabEl.setAttribute("data-rl-injected", "true");
tabEl.setAttribute("role", "tab");
tabEl.setAttribute("id", tabId);
tabEl.setAttribute("aria-selected", "false");
tabEl.setAttribute("aria-disabled", "false");
tabEl.setAttribute("aria-controls", panelId);
tabEl.setAttribute("tabindex", "-1");
if (tabItemClass) tabEl.classList.add(tabItemClass);
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
icon.setAttribute("class", "_icon_77f3f89");
icon.setAttribute("viewBox", "0 0 20 20");
const use = document.createElementNS("http://www.w3.org/2000/svg", "use");
use.setAttribute("href", "#general__lyrics");
icon.appendChild(use);
const label = document.createElement("span");
label.className = "wave-text-description-demi";
label.setAttribute("data-wave-color", "textDefault");
label.textContent = "Lyrics";
tabEl.appendChild(icon);
tabEl.appendChild(label);
const panelEl = document.createElement("div");
panelEl.setAttribute("data-rl-injected", "true");
panelEl.setAttribute("role", "tabpanel");
panelEl.setAttribute("id", panelId);
panelEl.setAttribute("aria-labelledby", tabId);
if (panelBaseClass) panelEl.classList.add(panelBaseClass);
buildInjectedLyricsShell(panelEl);
const creditsTab = tabList.querySelector(
'[data-test="tabs-credits"]',
) as HTMLElement | null;
if (creditsTab) {
creditsTabEl = creditsTab;
creditsPrevOrder = creditsTab.style.getPropertyValue("order") || "";
creditsTab.style.setProperty("order", "1000");
tabEl.style.setProperty("order", "999");
}
tabList.appendChild(tabEl);
root.appendChild(panelEl);
tabEl.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
(
e as unknown as { stopImmediatePropagation?: () => void }
).stopImmediatePropagation?.();
showInjectedLyricsTab();
});
const handleTabListClick = (e: Event): void => {
const target = e.target as Node;
const clickedTab = (target as HTMLElement)?.closest?.(
'li[role="tab"]',
) as HTMLElement;
if (!clickedTab || clickedTab === tabEl) return;
const allTabs = Array.from(
tabList.querySelectorAll('li[role="tab"]'),
) as HTMLElement[];
for (const tab of allTabs) {
if (tab === tabEl) continue;
if (activeTabClass) tab.classList.remove(activeTabClass);
tab.classList.remove("react-tabs__tab--selected");
}
if (activeTabClass) clickedTab.classList.add(activeTabClass);
clickedTab.classList.add("react-tabs__tab--selected");
clickedTab.setAttribute("aria-selected", "true");
clickedTab.setAttribute("tabindex", "0");
window.setTimeout(() => {
hideInjectedLyricsTab();
}, 0);
};
tabList.addEventListener("click", handleTabListClick);
injectedTablistClickCleanup = () => {
tabList.removeEventListener("click", handleTabListClick);
};
injectedTabEl = tabEl;
injectedPanelEl = panelEl;
return true;
};
// Observer: create dropdown when lyrics tab appears & detect track changes // Observer: create dropdown when lyrics tab appears & detect track changes
function setupStickyLyricsObserver(): void { function setupStickyLyricsObserver(): void {
// Create dropdown if lyrics tab already exists // Create dropdown if lyrics tab already exists
@@ -1319,12 +1619,15 @@ function setupStickyLyricsObserver(): void {
if (tab && !tab.querySelector(".sticky-lyrics-trigger")) { if (tab && !tab.querySelector(".sticky-lyrics-trigger")) {
createStickyLyricsDropdown(); createStickyLyricsDropdown();
} }
if (settings.stickyLyrics) {
tryActivateStickyLyricsTab();
}
}); });
// Apply word lyrics when lyrics container appears or reappears // Apply word lyrics when lyrics container appears or reappears
observe<HTMLElement>(unloads, '[data-test="lyrics-lines"]', () => { observe<HTMLElement>(unloads, '[data-test="lyrics-lines"]', () => {
if (lyricsMode === "line-tidal") { if (lyricsMode === "line-tidal") {
void reapplyTidalLineLyrics(); void reapplyTidalLines();
} else if (lyricsData) { } else if (lyricsData) {
reapplyWordLyrics(); reapplyWordLyrics();
} else { } else {
@@ -1428,10 +1731,15 @@ let tickLoopUnload: LunaUnload | null = null;
let isActive = false; let isActive = false;
let savedTidalClasses: string[] | null = null; let savedTidalClasses: string[] | null = null;
let tidalFollowObserver: MutationObserver | null = null; let tidalFollowObserver: MutationObserver | null = null;
let injectedTabEl: HTMLElement | null = null;
let injectedPanelEl: HTMLElement | null = null;
let activeTabClass = "";
let activePanelClass = "";
let creditsTabEl: HTMLElement | null = null;
let creditsPrevOrder = "";
const isWordTimingMode = (): boolean => lyricsMode === "word"; const isWordMode = (): boolean => lyricsMode === "word";
const getEffectiveLyricsStyle = (): number => const getLyricsStyle = (): number => (isWordMode() ? settings.lyricsStyle : 0);
isWordTimingMode() ? settings.lyricsStyle : 0;
interface WordEntry { interface WordEntry {
el: HTMLSpanElement; el: HTMLSpanElement;
@@ -1457,6 +1765,22 @@ const activeWordEls = new Map<number, HTMLSpanElement | null>();
const activeBgWordEls = new Map<number, HTMLSpanElement | null>(); const activeBgWordEls = new Map<number, HTMLSpanElement | null>();
let activeLineIdxs = new Set<number>(); let activeLineIdxs = new Set<number>();
let primaryLineIdx = -1; let primaryLineIdx = -1;
const lineSlideTimers = new Map<number, number>();
const clearLineSlideTimer = (idx: number): void => {
const timer = lineSlideTimers.get(idx);
if (timer !== undefined) {
window.clearTimeout(timer);
lineSlideTimers.delete(idx);
}
};
const clearLineSlideTimers = (): void => {
for (const timer of lineSlideTimers.values()) {
window.clearTimeout(timer);
}
lineSlideTimers.clear();
};
// Scroll sync (unhook on user scroll) // Scroll sync (unhook on user scroll)
let scrollSynced = true; let scrollSynced = true;
@@ -1768,7 +2092,7 @@ const fetchLyrics = async (
return null; return null;
}; };
const normalizeLineLyricsData = (data: ApiLine[]): WordLine[] => { const normalizeLineData = (data: ApiLine[]): WordLine[] => {
return data return data
.filter((line) => typeof line.text === "string") .filter((line) => typeof line.text === "string")
.map((line, idx) => { .map((line, idx) => {
@@ -1811,7 +2135,7 @@ const normalizeLineLyricsData = (data: ApiLine[]): WordLine[] => {
}; };
// Scrapes Tidal Line Texts (For Romanization) // Scrapes Tidal Line Texts (For Romanization)
const getTidalLineTexts = (): string[] => { const getTidalLines = (): string[] => {
const lyricsContainer = document.querySelector( const lyricsContainer = document.querySelector(
'[data-test="lyrics-lines"]', '[data-test="lyrics-lines"]',
) as HTMLElement; ) as HTMLElement;
@@ -1827,9 +2151,7 @@ const getTidalLineTexts = (): string[] => {
.filter((text) => text.trim().length > 0); .filter((text) => text.trim().length > 0);
}; };
const romanizeLinePayload = async ( const romanizeLines = async (lineTexts: string[]): Promise<string[] | null> => {
lineTexts: string[],
): Promise<string[] | null> => {
if (!settings.romanizeLyrics || lineTexts.length === 0) return null; if (!settings.romanizeLyrics || lineTexts.length === 0) return null;
const cacheKey = `${lineTexts.join("\n")}\0r`; const cacheKey = `${lineTexts.join("\n")}\0r`;
@@ -2056,8 +2378,8 @@ const buildWordSpans = (): {
wbwContainer.className = "rl-wbw-container"; wbwContainer.className = "rl-wbw-container";
if (settings.blurInactive) wbwContainer.classList.add("rl-blur-active"); if (settings.blurInactive) wbwContainer.classList.add("rl-blur-active");
if (settings.bubbledLyrics) wbwContainer.classList.add("rl-bubbled"); if (settings.bubbledLyrics) wbwContainer.classList.add("rl-bubbled");
const effectiveStyle = getEffectiveLyricsStyle(); const effectiveStyle = getLyricsStyle();
const allowWordSylStyles = isWordTimingMode(); const allowWordSylStyles = isWordMode();
// MARKER: Syllable animations (WIP coming soon) // MARKER: Syllable animations (WIP coming soon)
if (allowWordSylStyles && settings.syllableStyle === 1) if (allowWordSylStyles && settings.syllableStyle === 1)
wbwContainer.classList.add("rl-syl-pop"); wbwContainer.classList.add("rl-syl-pop");
@@ -2361,7 +2683,7 @@ const buildWordSpans = (): {
}; };
// Scrapes & Builds Tidal Line Spans (no lines found in API) // Scrapes & Builds Tidal Line Spans (no lines found in API)
const buildTidalLineSpans = ( const buildTidalLines = (
romanizedLines: string[] | null = null, romanizedLines: string[] | null = null,
): { lines: LineEntry[] } => { ): { lines: LineEntry[] } => {
const lines: LineEntry[] = []; const lines: LineEntry[] = [];
@@ -2608,7 +2930,7 @@ const watchForRerender = (): void => {
sylTrace("Lyrics overlay: re-applying after Tidal re-render"); sylTrace("Lyrics overlay: re-applying after Tidal re-render");
hideTidalLyrics(); hideTidalLyrics();
if (lyricsMode === "line-tidal") { if (lyricsMode === "line-tidal") {
const result = buildTidalLineSpans(cachedTidalRomanizedLines); const result = buildTidalLines(cachedTidalRomanizedLines);
lines = result.lines; lines = result.lines;
startTidalFollowLoop(); startTidalFollowLoop();
} else if (lyricsData) { } else if (lyricsData) {
@@ -2648,6 +2970,7 @@ const teardown = (): void => {
trackChangeToken++; trackChangeToken++;
clearTickLoop(); clearTickLoop();
stopTidalFollowLoop(); stopTidalFollowLoop();
clearInjectedLyricsTab();
clearScrollAnim(); clearScrollAnim();
unwatchRerender(); unwatchRerender();
unhookUserScroll(); unhookUserScroll();
@@ -2663,6 +2986,8 @@ const teardown = (): void => {
activeBgWordEls.clear(); activeBgWordEls.clear();
activeLineIdxs.clear(); activeLineIdxs.clear();
primaryLineIdx = -1; primaryLineIdx = -1;
clearLineSlideTimers();
clearLineSlideTimers();
restoreTidalLyrics(); restoreTidalLyrics();
}; };
@@ -2847,7 +3172,7 @@ const startTickLoop = (): void => {
if (!isActive || lines.length === 0) return; if (!isActive || lines.length === 0) return;
const nowMs = getPlaybackMs(); const nowMs = getPlaybackMs();
const effectiveStyle = getEffectiveLyricsStyle(); const effectiveStyle = getLyricsStyle();
const isSyl = effectiveStyle === 2; const isSyl = effectiveStyle === 2;
const isLineStyle = effectiveStyle === 0; const isLineStyle = effectiveStyle === 0;
const CLS_ACTIVE = isSyl ? "rl-syl-active" : "rl-wbw-active"; const CLS_ACTIVE = isSyl ? "rl-syl-active" : "rl-wbw-active";
@@ -2896,6 +3221,7 @@ const startTickLoop = (): void => {
// single pass to set correct state for all words (scrub or seek) // single pass to set correct state for all words (scrub or seek)
if (didScrub) { if (didScrub) {
for (let li = 0; li < lines.length; li++) { for (let li = 0; li < lines.length; li++) {
lines[li].el.classList.remove("rl-line-slide");
const allEntries = const allEntries =
lines[li].bgWords.length > 0 lines[li].bgWords.length > 0
? [...lines[li].words, ...lines[li].bgWords] ? [...lines[li].words, ...lines[li].bgWords]
@@ -2922,6 +3248,7 @@ const startTickLoop = (): void => {
} }
activeLineIdxs.clear(); activeLineIdxs.clear();
primaryLineIdx = -1; primaryLineIdx = -1;
clearLineSlideTimers();
const held = document.querySelector(".rl-gap-hold"); const held = document.querySelector(".rl-gap-hold");
if (held) held.classList.remove("rl-gap-hold"); if (held) held.classList.remove("rl-gap-hold");
sylLog( sylLog(
@@ -2933,6 +3260,8 @@ const startTickLoop = (): void => {
for (const idx of activeLineIdxs) { for (const idx of activeLineIdxs) {
if (!newActiveSet.has(idx) && idx < lines.length) { if (!newActiveSet.has(idx) && idx < lines.length) {
lines[idx].el.classList.remove("rl-wbw-line-active"); lines[idx].el.classList.remove("rl-wbw-line-active");
lines[idx].el.classList.remove("rl-line-slide");
clearLineSlideTimer(idx);
lines[idx].el.removeAttribute("data-current"); lines[idx].el.removeAttribute("data-current");
const lastWord = activeWordEls.get(idx); const lastWord = activeWordEls.get(idx);
if (lastWord) { if (lastWord) {
@@ -2957,6 +3286,16 @@ const startTickLoop = (): void => {
lines[idx].el.classList.add("rl-wbw-line-active"); lines[idx].el.classList.add("rl-wbw-line-active");
lines[idx].el.classList.remove("rl-pos-1", "rl-pos-2", "rl-pos-3"); lines[idx].el.classList.remove("rl-pos-1", "rl-pos-2", "rl-pos-3");
lines[idx].el.setAttribute("data-current", "true"); lines[idx].el.setAttribute("data-current", "true");
if (isLineStyle) {
lines[idx].el.classList.add("rl-line-slide");
clearLineSlideTimer(idx);
const t = window.setTimeout(() => {
if (idx < lines.length)
lines[idx].el.classList.remove("rl-line-slide");
lineSlideTimers.delete(idx);
}, 360);
lineSlideTimers.set(idx, t);
}
sylLog( sylLog(
`[RL-Syllable] Line ${idx} Active "${lines[idx].el.textContent?.slice(0, 40)}" | ${lines[idx].startMs} ms - ${lines[idx].endMs} ms [${nowMs.toFixed(0)} ms]`, `[RL-Syllable] Line ${idx} Active "${lines[idx].el.textContent?.slice(0, 40)}" | ${lines[idx].startMs} ms - ${lines[idx].endMs} ms [${nowMs.toFixed(0)} ms]`,
); );
@@ -3192,9 +3531,9 @@ const onTrackChange = async (): Promise<void> => {
if (token !== trackChangeToken) return; if (token !== trackChangeToken) return;
if (!response) { if (!response) {
trace.log("RL API: no API lyrics available, falling back to TIDAL lines"); trace.log("RL API: no API lyrics available, falling back to TIDAL lines");
const tidalTexts = getTidalLineTexts(); const tidalTexts = getTidalLines();
const romanized = settings.romanizeLyrics const romanized = settings.romanizeLyrics
? await romanizeLinePayload(tidalTexts) ? await romanizeLines(tidalTexts)
: null; : null;
if (token !== trackChangeToken) return; if (token !== trackChangeToken) return;
cachedTidalRomanizedLines = romanized; cachedTidalRomanizedLines = romanized;
@@ -3204,7 +3543,7 @@ const onTrackChange = async (): Promise<void> => {
isActive = true; isActive = true;
lyricsMode = "line-tidal"; lyricsMode = "line-tidal";
hideTidalLyrics(); hideTidalLyrics();
const tidalResult = buildTidalLineSpans(romanized); const tidalResult = buildTidalLines(romanized);
lines = tidalResult.lines; lines = tidalResult.lines;
if (lines.length === 0) { if (lines.length === 0) {
trace.log("No TIDAL lines available yet"); trace.log("No TIDAL lines available yet");
@@ -3224,10 +3563,24 @@ const onTrackChange = async (): Promise<void> => {
); );
lyricsMode = response.type === "Word" ? "word" : "line-api"; lyricsMode = response.type === "Word" ? "word" : "line-api";
if (!ensureLyricsTab()) {
trace.log("Could not create/find lyrics tab container");
teardown();
return;
}
if (injectedTabEl && settings.stickyLyrics) {
showInjectedLyricsTab();
safeTimeout(
unloads,
() => {
if (!settings.stickyLyrics || token !== trackChangeToken) return;
showInjectedLyricsTab();
},
180,
);
}
lyricsData = lyricsData =
response.type === "Word" response.type === "Word" ? response.data : normalizeLineData(response.data);
? response.data
: normalizeLineLyricsData(response.data);
lyricsResponse = response; lyricsResponse = response;
isActive = true; isActive = true;
if (!lyricsData || lyricsData.length === 0) { if (!lyricsData || lyricsData.length === 0) {
@@ -3264,6 +3617,7 @@ const reapplyWordLyrics = (): void => {
activeBgWordEls.clear(); activeBgWordEls.clear();
activeLineIdxs.clear(); activeLineIdxs.clear();
primaryLineIdx = -1; primaryLineIdx = -1;
clearLineSlideTimers();
isActive = true; isActive = true;
lyricsMode = lyricsMode === "line-api" ? "line-api" : "word"; lyricsMode = lyricsMode === "line-api" ? "line-api" : "word";
@@ -3275,7 +3629,7 @@ const reapplyWordLyrics = (): void => {
sylLog("[RL-Syllable] Reapplied word/syllable lyrics (cached)"); sylLog("[RL-Syllable] Reapplied word/syllable lyrics (cached)");
}; };
const reapplyTidalLineLyrics = async (): Promise<void> => { const reapplyTidalLines = async (): Promise<void> => {
clearTickLoop(); clearTickLoop();
stopTidalFollowLoop(); stopTidalFollowLoop();
clearScrollAnim(); clearScrollAnim();
@@ -3290,12 +3644,12 @@ const reapplyTidalLineLyrics = async (): Promise<void> => {
isActive = true; isActive = true;
lyricsMode = "line-tidal"; lyricsMode = "line-tidal";
const tidalTexts = getTidalLineTexts(); const tidalTexts = getTidalLines();
const romanized = settings.romanizeLyrics const romanized = settings.romanizeLyrics
? await romanizeLinePayload(tidalTexts) ? await romanizeLines(tidalTexts)
: null; : null;
hideTidalLyrics(); hideTidalLyrics();
const result = buildTidalLineSpans(romanized); const result = buildTidalLines(romanized);
lines = result.lines; lines = result.lines;
if (lines.length === 0) return; if (lines.length === 0) return;
watchForRerender(); watchForRerender();
+76 -22
View File
@@ -9,19 +9,22 @@
@font-face { @font-face {
font-family: "AbyssFont"; font-family: "AbyssFont";
font-weight: 500; font-weight: 500;
src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2") format("woff2"); src: url("https://excel.lexploits.top/extra/tidal/LyricsMedium.woff2")
format("woff2");
} }
@font-face { @font-face {
font-family: "AbyssFont"; font-family: "AbyssFont";
font-weight: 600; font-weight: 600;
src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2") format("woff2"); src: url("https://excel.lexploits.top/extra/tidal/LyricsSemibold.woff2")
format("woff2");
} }
@font-face { @font-face {
font-family: "AbyssFont"; font-family: "AbyssFont";
font-weight: 700; font-weight: 700;
src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2") format("woff2"); src: url("https://excel.lexploits.top/extra/tidal/LyricsBold.woff2")
format("woff2");
} }
/* Enhanced lyrics styling with glow effects */ /* Enhanced lyrics styling with glow effects */
@@ -223,7 +226,9 @@
.rl-wbw-line:not(.rl-wbw-line-active) > .rl-wbw-word:hover, .rl-wbw-line:not(.rl-wbw-line-active) > .rl-wbw-word:hover,
.rl-wbw-line:not(.rl-wbw-line-active) > .rl-wbw-word.rl-wbw-word-hover, .rl-wbw-line:not(.rl-wbw-line-active) > .rl-wbw-word.rl-wbw-word-hover,
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-main .rl-wbw-word:hover, .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-main .rl-wbw-word:hover,
.rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-main .rl-wbw-word.rl-wbw-word-hover { .rl-wbw-line:not(.rl-wbw-line-active)
.rl-wbw-main
.rl-wbw-word.rl-wbw-word-hover {
text-shadow: text-shadow:
0 0 var(--rl-glow-inner, 2px) lightgray, 0 0 var(--rl-glow-inner, 2px) lightgray,
/* biome-ignore lint: Hover glow should override defaults */ /* biome-ignore lint: Hover glow should override defaults */
@@ -243,16 +248,33 @@
color: var(--cl-glow1, #fff) !important; color: var(--cl-glow1, #fff) !important;
} }
/* Line mode: suppress glow during slide-in only */
.rl-wbw-line.rl-line-slide .rl-wbw-word.rl-wbw-active {
text-shadow: none !important;
}
/* MARKER: Syllable sweep animation CSS */ /* MARKER: Syllable sweep animation CSS */
@keyframes rl-wipe { @keyframes rl-wipe {
from { from {
background-size: 0.75em 100%, 0% 100%, 100% 100%; background-size:
background-position: -0.375em 0%, left, left; 0.75em 100%,
0% 100%,
100% 100%;
background-position:
-0.375em 0%,
left,
left;
} }
to { to {
background-size: 0.75em 100%, 100% 100%, 100% 100%; background-size:
background-position: calc(100% + 0.375em) 0%, left, left; 0.75em 100%,
100% 100%,
100% 100%;
background-position:
calc(100% + 0.375em) 0%,
left,
left;
} }
} }
@@ -268,11 +290,22 @@
background-clip: text !important; background-clip: text !important;
background-repeat: no-repeat; background-repeat: no-repeat;
background-image: background-image:
linear-gradient(90deg, transparent 0%, var(--cl-glow1, #fff) 50%, transparent 100%), linear-gradient(
90deg,
transparent 0%,
var(--cl-glow1, #fff) 50%,
transparent 100%
),
linear-gradient(90deg, var(--cl-glow1, #fff) 100%, transparent 100%), linear-gradient(90deg, var(--cl-glow1, #fff) 100%, transparent 100%),
linear-gradient(90deg, rgba(128, 128, 128, 0.4), rgba(128, 128, 128, 0.4)); linear-gradient(90deg, rgba(128, 128, 128, 0.4), rgba(128, 128, 128, 0.4));
background-size: 0.75em 100%, 0% 100%, 100% 100%; background-size:
background-position: -0.375em 0%, left, left; 0.75em 100%,
0% 100%,
100% 100%;
background-position:
-0.375em 0%,
left,
left;
/* biome-ignore lint: No glow for syllable mode */ /* biome-ignore lint: No glow for syllable mode */
text-shadow: none !important; text-shadow: none !important;
/* biome-ignore lint: No glow for syllable mode */ /* biome-ignore lint: No glow for syllable mode */
@@ -295,18 +328,26 @@
/* syllableStyle: 0 = none, 1 = Pop!, 2 = Jump */ /* syllableStyle: 0 = none, 1 = Pop!, 2 = Jump */
@keyframes rl-pop { @keyframes rl-pop {
0%, 100% { 0%,
100% {
transform: scale(1); transform: scale(1);
} }
25%, 35% { 25%,
35% {
transform: scale(1.03) translateY(-0.5%); transform: scale(1.03) translateY(-0.5%);
} }
} }
@keyframes rl-jump { @keyframes rl-jump {
0% { transform: translateY(8px); } 0% {
50% { transform: translateY(-3px); } transform: translateY(8px);
100% { transform: translateY(0); } }
50% {
transform: translateY(-3px);
}
100% {
transform: translateY(0);
}
} }
/* Pop! for word mode */ /* Pop! for word mode */
@@ -347,14 +388,18 @@
opacity: 0; opacity: 0;
font-size: 0.55em; font-size: 0.55em;
padding-top: 0.15em; padding-top: 0.15em;
transition: max-height 0.3s ease, opacity 0.5s ease; transition:
max-height 0.3s ease,
opacity 0.5s ease;
color: rgba(128, 128, 128, 0.4); color: rgba(128, 128, 128, 0.4);
} }
.rl-wbw-line.rl-wbw-line-active .rl-wbw-bg-container { .rl-wbw-line.rl-wbw-line-active .rl-wbw-bg-container {
max-height: 3em; max-height: 3em;
opacity: 1; opacity: 1;
transition: max-height 0.5s ease, opacity 0.5s ease; transition:
max-height 0.5s ease,
opacity 0.5s ease;
} }
/* Singer duet positioning */ /* Singer duet positioning */
@@ -397,10 +442,19 @@
} }
/* kill glow on hovered word */ /* kill glow on hovered word */
.lyrics-glow-disabled .rl-wbw-line:not(.rl-wbw-line-active) > .rl-wbw-word:hover, .lyrics-glow-disabled .rl-wbw-line:not(.rl-wbw-line-active)
.lyrics-glow-disabled .rl-wbw-line:not(.rl-wbw-line-active) > .rl-wbw-word.rl-wbw-word-hover, > .rl-wbw-word:hover,
.lyrics-glow-disabled .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-main .rl-wbw-word:hover, .lyrics-glow-disabled
.lyrics-glow-disabled .rl-wbw-line:not(.rl-wbw-line-active) .rl-wbw-main .rl-wbw-word.rl-wbw-word-hover { .rl-wbw-line:not(.rl-wbw-line-active)
> .rl-wbw-word.rl-wbw-word-hover,
.lyrics-glow-disabled
.rl-wbw-line:not(.rl-wbw-line-active)
.rl-wbw-main
.rl-wbw-word:hover,
.lyrics-glow-disabled
.rl-wbw-line:not(.rl-wbw-line-active)
.rl-wbw-main
.rl-wbw-word.rl-wbw-word-hover {
/* biome-ignore lint: Kill glow on hovered word */ /* biome-ignore lint: Kill glow on hovered word */
text-shadow: none !important; text-shadow: none !important;
} }