Merge pull request #156 from frostplexx/loading-spinner

feat: add loading lyrics indicator
This commit is contained in:
meoware.exe authored and GitHub committed 2026-10-06 23:41:16 +11:00
commit a50bdea428
2 files changed
+104 -3

No files matched your search

+62 -3
View File
@@ -536,14 +536,59 @@ const toggleRadiantLyrics = function (): void {
// Create buttons
let flushLocked = false;
let flushSpinning = false;
// Swap the flush button icon between the lyrics-sync glyph and a loading spinner
const setFlushSpinner = (on: boolean): void => {
flushSpinning = on;
const btn = document.querySelector(".flush-lyrics-button") as HTMLButtonElement | null;
if (!btn) return;
const svg = btn.querySelector("svg");
if (!svg) return;
const svgClass = (svg.getAttribute("class") ?? "")
.split(/\s+/)
.filter((c) => c && c !== "rl-spinner");
if (on) {
if (svg.classList.contains("rl-spinner")) return;
btn.setAttribute("data-tip", "Loading lyrics…");
btn.setAttribute("aria-label", "Loading lyrics…");
const spinner = document.createElementNS("http://www.w3.org/2000/svg", "svg");
if (svgClass.length) spinner.setAttribute("class", svgClass.join(" "));
spinner.classList.add("rl-spinner");
spinner.setAttribute("viewBox", "0 0 20 20");
spinner.setAttribute("aria-hidden", "true");
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
circle.setAttribute("cx", "10");
circle.setAttribute("cy", "10");
circle.setAttribute("r", "8");
circle.setAttribute(
"style",
"fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-dasharray:42 8",
);
spinner.appendChild(circle);
svg.replaceWith(spinner);
} else {
const spinner = btn.querySelector("svg.rl-spinner");
if (!spinner) return;
const syncSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
if (svgClass.length) syncSvg.setAttribute("class", svgClass.join(" "));
syncSvg.setAttribute("viewBox", "0 0 20 20");
const use = document.createElementNS("http://www.w3.org/2000/svg", "use");
use.setAttribute("href", "#general__lyrics-sync");
syncSvg.appendChild(use);
spinner.replaceWith(syncSvg);
}
};
const unlockFlush = (): void => {
flushLocked = false;
setFlushSpinner(false);
const btn = document.querySelector(".flush-lyrics-button") as HTMLButtonElement;
if (btn) {
btn.disabled = false;
btn.style.opacity = "";
btn.style.cursor = "";
btn.setAttribute("title", "Flush Lyrics");
btn.setAttribute("data-tip", "Flush Lyrics");
btn.setAttribute("aria-label", "Flush Lyrics");
}
};
@@ -555,15 +600,17 @@ const lockFlush = (): void => {
btn.style.opacity = "0.3";
btn.style.cursor = "not-allowed";
}
setFlushSpinner(true);
};
const disableFlushNoLyrics = (): void => {
flushLocked = true;
setFlushSpinner(false);
const btn = document.querySelector(".flush-lyrics-button") as HTMLButtonElement;
if (btn) {
btn.disabled = true;
btn.style.opacity = "0.3";
btn.style.cursor = "not-allowed";
btn.setAttribute("title", "Track has no lyrics");
btn.setAttribute("data-tip", "Track has no lyrics");
btn.setAttribute("aria-label", "Track has no lyrics");
}
};
@@ -584,9 +631,11 @@ const flushLyrics = async (): Promise<void> => {
if (!result.ok) {
if (result.notFound) {
toast("No lyrics found for this track");
unlockFlush();
return;
}
toastErr(`Flush failed (${result.status})`);
unlockFlush();
return;
}
const data = result.data;
@@ -602,9 +651,12 @@ const flushLyrics = async (): Promise<void> => {
cachedLyricsKey = null;
cachedLyricsData = null;
onTrackChange();
} else {
unlockFlush();
}
} catch (err) {
toastErr(`Flush error: ${err instanceof Error ? err.message : String(err)}`);
unlockFlush();
}
};
@@ -622,7 +674,7 @@ const flushBtn = function (): void {
flushButton.removeAttribute("data-test");
flushButton.setAttribute("type", "button");
flushButton.setAttribute("aria-label", "Flush Lyrics");
flushButton.setAttribute("title", "Flush Lyrics");
flushButton.setAttribute("data-tip", "Flush Lyrics");
flushButton.disabled = true;
flushButton.style.opacity = "0.3";
flushButton.style.cursor = "not-allowed";
@@ -640,6 +692,12 @@ const flushBtn = function (): void {
svgEl.appendChild(useEl);
spanWrapper.appendChild(svgEl);
flushButton.appendChild(spanWrapper);
// Button recreated while lyrics are still loading — show the spinner again
if (flushSpinning) {
flushButton.setAttribute("data-tip", "Loading lyrics…");
flushButton.setAttribute("aria-label", "Loading lyrics…");
setFlushSpinner(true);
}
flushButton.onclick = () => flushLyrics();
@@ -4009,6 +4067,7 @@ const onTrackChange = async (): Promise<void> => {
if (token !== trackChangeToken) return;
if (!trackInfo) {
trace.log("could not get track info from playback state");
setFlushSpinner(false);
return;
}
const nativeHasLyrics = trackHasNativeLyrics(trackInfo.trackId);
@@ -49,6 +49,48 @@
/* MARKER: Lyrics Menu CSS */
/* Lyrics toggle button */
/* Flush lyrics button — loading spinner while waiting for lyrics */
.flush-lyrics-button svg.rl-spinner {
animation: rl-spinner-rotate 0.9s linear infinite;
}
@keyframes rl-spinner-rotate {
to {
transform: rotate(360deg);
}
}
/* Flush lyrics button — hover tooltip (native title doesn't show reliably) */
.flush-lyrics-button {
position: relative !important;
}
.flush-lyrics-button::after {
content: attr(data-tip);
position: absolute;
top: calc(100% + 6px);
right: 0;
padding: 4px 8px;
border-radius: 4px;
background: rgba(0, 0, 0, 0.85);
color: #fff;
font-size: 12px;
font-weight: 500;
line-height: 1.3;
white-space: nowrap;
pointer-events: none;
z-index: 10000;
opacity: 0;
transform: translateY(-2px);
transition: opacity 0.12s ease, transform 0.12s ease;
}
.flush-lyrics-button:hover::after {
opacity: 1;
transform: translateY(0);
}
[data-test="toggle-lyrics"]:has(.rl-lyrics-menu-trigger) {
position: relative !important;
padding-right: 38px !important;