From a77774c3dedd6a2f73862e9b2655b698fa95fd50 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Tue, 6 Jan 2026 20:09:08 +0000
Subject: [PATCH] feat(ui): Replace full-screen loader with in-button spinner
Replaces the disruptive full-screen loading overlay with a contextual loading spinner inside the "Generate Poster" button.
This improves the user experience by providing immediate feedback at the point of interaction without blocking the entire UI. The button is now disabled during the operation, preventing multiple clicks.
---
web_app/static/index.html | 5 ++++-
web_app/static/script.js | 27 +++++++++++++++++++--------
web_app/static/style.css | 27 +++++++++++++++++++++++++++
3 files changed, 50 insertions(+), 9 deletions(-)
diff --git a/web_app/static/index.html b/web_app/static/index.html
index 0d56371..96ab59c 100644
--- a/web_app/static/index.html
+++ b/web_app/static/index.html
@@ -116,7 +116,10 @@
Lyrics
- Generate Poster
+
+ Generate Poster
+
+
diff --git a/web_app/static/script.js b/web_app/static/script.js
index 441a4ed..72ef634 100644
--- a/web_app/static/script.js
+++ b/web_app/static/script.js
@@ -613,7 +613,14 @@ function handleLyricLineClick(lineNumber) {
async function generatePoster() {
if (!currentMetadata) return;
- loadingOverlay.style.display = 'flex';
+ generateBtn.disabled = true;
+ generateBtn.classList.add('loading');
+
+ // Helper to restore button
+ const restoreButton = () => {
+ generateBtn.disabled = false;
+ generateBtn.classList.remove('loading');
+ };
const indexingToggle = document.getElementById('indexingToggle');
const accentToggle = document.getElementById('accentToggle');
@@ -628,23 +635,20 @@ async function generatePoster() {
};
if (payload.type === 'track') {
- // Lyrics Logic (Same as before)
const startVal = parseInt(startLineInput.value) || 1;
const endVal = parseInt(endLineInput.value) || 1;
const start = startVal - 1;
const end = endVal;
if (isNaN(start) || isNaN(end) || start >= end) {
- loadingOverlay.style.display = 'none';
showToast("Please enter a valid range (Start must be less than End).", "error");
+ restoreButton(); // Restore on early exit
return;
}
payload.lyrics_start = start;
payload.lyrics_end = end;
-
} else {
- // Album Logic
payload.indexing = indexingToggle ? indexingToggle.checked : false;
}
@@ -664,18 +668,25 @@ async function generatePoster() {
const imageUrl = data.is_base64 ? data.image_data : `${data.image_url}?t=${new Date().getTime()}`;
const img = document.createElement('img');
- img.src = imageUrl;
+
img.onload = () => {
posterContainer.innerHTML = '';
posterContainer.appendChild(img);
showDownloadButton(imageUrl, data.filename);
- loadingOverlay.style.display = 'none';
+ restoreButton(); // Restore on success
};
+ img.onerror = () => {
+ showToast("Error: Failed to load the generated poster image.", "error");
+ restoreButton(); // Restore on image load failure
+ };
+
+ img.src = imageUrl;
+
} catch (error) {
console.error("Generation failed", error);
showToast(`Error: ${error.message}`, "error");
- loadingOverlay.style.display = 'none';
+ restoreButton(); // Restore on fetch failure
}
}
diff --git a/web_app/static/style.css b/web_app/static/style.css
index 468eb76..422e0c0 100644
--- a/web_app/static/style.css
+++ b/web_app/static/style.css
@@ -166,6 +166,33 @@ header p {
transform: translateY(-1px);
}
+.primary-btn:disabled {
+ background-color: var(--accent-hover);
+ opacity: 0.7;
+ cursor: not-allowed;
+ transform: none;
+}
+
+/* In-button spinner */
+.primary-btn .spinner {
+ width: 20px;
+ height: 20px;
+ border: 2px solid rgba(0, 0, 0, 0.2);
+ border-top-color: #1a1a1f;
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+ margin: 0;
+}
+
+/* Hide text and show spinner when loading */
+.primary-btn.loading .btn-text {
+ display: none;
+}
+
+.primary-btn.loading .spinner {
+ display: block !important; /* Override inline style */
+}
+
.secondary-btn {
background: transparent;
border-color: var(--border-color);