diff --git a/web_app/static/index.html b/web_app/static/index.html index 0d56371..15c8365 100644 --- a/web_app/static/index.html +++ b/web_app/static/index.html @@ -116,7 +116,10 @@

Lyrics

- + diff --git a/web_app/static/script.js b/web_app/static/script.js index 441a4ed..d1413cc 100644 --- a/web_app/static/script.js +++ b/web_app/static/script.js @@ -613,7 +613,15 @@ function handleLyricLineClick(lineNumber) { async function generatePoster() { if (!currentMetadata) return; - loadingOverlay.style.display = 'flex'; + const btn = document.getElementById('generateBtn'); + const btnText = btn.querySelector('.btn-text'); + const spinner = btn.querySelector('.spinner-inline'); + + // UX: Disable button and show spinner + btn.classList.add('loading'); + btn.disabled = true; + btn.setAttribute('aria-label', 'Generating poster, please wait'); + const indexingToggle = document.getElementById('indexingToggle'); const accentToggle = document.getElementById('accentToggle'); @@ -635,8 +643,11 @@ async function generatePoster() { 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"); + // UX: Reset button state on failure + btn.classList.remove('loading'); + btn.disabled = false; + btn.removeAttribute('aria-label'); return; } @@ -669,13 +680,26 @@ async function generatePoster() { posterContainer.innerHTML = ''; posterContainer.appendChild(img); showDownloadButton(imageUrl, data.filename); - loadingOverlay.style.display = 'none'; + // UX: Reset button state on success + btn.classList.remove('loading'); + btn.disabled = false; + btn.removeAttribute('aria-label'); + }; + img.onerror = () => { + showToast("Error loading generated image.", "error"); + // UX: Reset button state on failure + btn.classList.remove('loading'); + btn.disabled = false; + btn.removeAttribute('aria-label'); }; } catch (error) { console.error("Generation failed", error); showToast(`Error: ${error.message}`, "error"); - loadingOverlay.style.display = 'none'; + // UX: Reset button state on failure + btn.classList.remove('loading'); + btn.disabled = false; + btn.removeAttribute('aria-label'); } } diff --git a/web_app/static/style.css b/web_app/static/style.css index 468eb76..196b796 100644 --- a/web_app/static/style.css +++ b/web_app/static/style.css @@ -908,4 +908,34 @@ input:focus { .result-action svg { pointer-events: none; +} + +/* --- Inline Spinner for Buttons --- */ +.spinner-inline { + display: none; /* Hidden by default */ + width: 20px; + height: 20px; + border: 2px solid rgba(0, 0, 0, 0.2); + border-top-color: #1a1a1f; /* Match button text color */ + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +.primary-btn.loading { + cursor: not-allowed; + background: var(--accent-hover); +} + +.primary-btn.loading .btn-text { + display: none; +} + +.primary-btn.loading .spinner-inline { + display: block; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } } \ No newline at end of file