From 258118eb13c6b4de50395075418b8c968b7dcda3 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 20:19:27 +0000 Subject: [PATCH] feat(ux): add loading spinner to generate button Replaces the full-screen loading overlay with an inline spinner on the "Generate Poster" button. This provides immediate, contextual feedback to the user, making the interface feel more responsive. The button is disabled during the loading state to prevent multiple clicks. Input validation is now performed before the loading state is activated to prevent the button from getting stuck. --- web_app/static/index.html | 7 ++++- web_app/static/script.js | 60 +++++++++++++++++++-------------------- web_app/static/style.css | 17 +++++++++++ 3 files changed, 52 insertions(+), 32 deletions(-) diff --git a/web_app/static/index.html b/web_app/static/index.html index 0d56371..e3d70dd 100644 --- a/web_app/static/index.html +++ b/web_app/static/index.html @@ -116,7 +116,12 @@

Lyrics

- + diff --git a/web_app/static/script.js b/web_app/static/script.js index 441a4ed..aaac259 100644 --- a/web_app/static/script.js +++ b/web_app/static/script.js @@ -613,42 +613,40 @@ function handleLyricLineClick(lineNumber) { async function generatePoster() { if (!currentMetadata) return; - loadingOverlay.style.display = 'flex'; - - const indexingToggle = document.getElementById('indexingToggle'); - const accentToggle = document.getElementById('accentToggle'); - - let payload = { - track_id: currentMetadata.id, - metadata: currentMetadata, - theme: themeInput.value, - font: fontInput.value, - accent: accentToggle ? accentToggle.checked : false, - type: currentMetadata.type || 'track' - }; - - if (payload.type === 'track') { - // Lyrics Logic (Same as before) + if (currentMetadata.type === 'track') { 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'; + if (isNaN(startVal) || isNaN(endVal) || startVal >= endVal) { showToast("Please enter a valid range (Start must be less than End).", "error"); return; } - - payload.lyrics_start = start; - payload.lyrics_end = end; - - } else { - // Album Logic - payload.indexing = indexingToggle ? indexingToggle.checked : false; } + generateBtn.classList.add('loading'); + generateBtn.disabled = true; + try { + const indexingToggle = document.getElementById('indexingToggle'); + const accentToggle = document.getElementById('accentToggle'); + + let payload = { + track_id: currentMetadata.id, + metadata: currentMetadata, + theme: themeInput.value, + font: fontInput.value, + accent: accentToggle ? accentToggle.checked : false, + type: currentMetadata.type || 'track' + }; + + if (payload.type === 'track') { + const startVal = parseInt(startLineInput.value) || 1; + const endVal = parseInt(endLineInput.value) || 1; + payload.lyrics_start = startVal - 1; + payload.lyrics_end = endVal; + } else { + payload.indexing = indexingToggle ? indexingToggle.checked : false; + } + const response = await fetch('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -669,13 +667,13 @@ async function generatePoster() { posterContainer.innerHTML = ''; posterContainer.appendChild(img); showDownloadButton(imageUrl, data.filename); - loadingOverlay.style.display = 'none'; }; - } catch (error) { console.error("Generation failed", error); showToast(`Error: ${error.message}`, "error"); - loadingOverlay.style.display = 'none'; + } finally { + generateBtn.classList.remove('loading'); + generateBtn.disabled = false; } } diff --git a/web_app/static/style.css b/web_app/static/style.css index 468eb76..b2cc07a 100644 --- a/web_app/static/style.css +++ b/web_app/static/style.css @@ -166,6 +166,23 @@ header p { transform: translateY(-1px); } +.primary-btn.loading { + background: var(--accent-hover); + cursor: not-allowed; +} + +.primary-btn .btn-spinner { + display: none; +} + +.primary-btn.loading .btn-spinner { + display: inline-block; +} + +.primary-btn.loading .btn-text { + display: none; +} + .secondary-btn { background: transparent; border-color: var(--border-color);