diff --git a/web_app/static/index.html b/web_app/static/index.html index 0d56371..6ad94e8 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..be4fcb0 100644 --- a/web_app/static/script.js +++ b/web_app/static/script.js @@ -20,7 +20,6 @@ const startLineInput = document.getElementById('startLine'); const endLineInput = document.getElementById('endLine'); const themeInput = document.getElementById('themeInput'); const fontInput = document.getElementById('fontInput'); -const loadingOverlay = document.getElementById('loadingOverlay'); let currentMetadata = null; let searchDebounceTimer = null; @@ -613,7 +612,8 @@ function handleLyricLineClick(lineNumber) { async function generatePoster() { if (!currentMetadata) return; - loadingOverlay.style.display = 'flex'; + generateBtn.classList.add('loading'); + generateBtn.disabled = true; const indexingToggle = document.getElementById('indexingToggle'); const accentToggle = document.getElementById('accentToggle'); @@ -635,7 +635,8 @@ async function generatePoster() { const end = endVal; if (isNaN(start) || isNaN(end) || start >= end) { - loadingOverlay.style.display = 'none'; + generateBtn.classList.remove('loading'); + generateBtn.disabled = false; showToast("Please enter a valid range (Start must be less than End).", "error"); return; } @@ -669,13 +670,20 @@ async function generatePoster() { posterContainer.innerHTML = ''; posterContainer.appendChild(img); showDownloadButton(imageUrl, data.filename); - loadingOverlay.style.display = 'none'; + generateBtn.classList.remove('loading'); + generateBtn.disabled = false; + }; + img.onerror = () => { + showToast("Error loading generated image.", "error"); + generateBtn.classList.remove('loading'); + generateBtn.disabled = false; }; } catch (error) { console.error("Generation failed", error); showToast(`Error: ${error.message}`, "error"); - loadingOverlay.style.display = 'none'; + generateBtn.classList.remove('loading'); + generateBtn.disabled = false; } } diff --git a/web_app/static/style.css b/web_app/static/style.css index 468eb76..906cfb0 100644 --- a/web_app/static/style.css +++ b/web_app/static/style.css @@ -908,4 +908,53 @@ input:focus { .result-action svg { pointer-events: none; -} \ No newline at end of file +} + +/* --- Button Loading State --- */ +.primary-btn { + position: relative; /* Needed for spinner positioning */ +} + +.btn-spinner { + display: none; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 20px; + height: 20px; + border: 2px solid rgba(0, 0, 0, 0.2); + border-top-color: #1a1a1f; /* Spinner color matching button text */ + border-radius: 50%; + animation: btn-spin 0.8s linear infinite; +} + +.btn-text { + transition: opacity 0.2s ease, visibility 0.2s ease; +} + +/* Loading state */ +.primary-btn.loading .btn-text { + visibility: hidden; + opacity: 0; +} + +.primary-btn.loading .btn-spinner { + display: block; +} + +.primary-btn.loading { + cursor: wait; + background: var(--accent-hover); + pointer-events: none; /* Disables clicks while loading */ +} + + +@keyframes btn-spin { + from { + transform: translate(-50%, -50%) rotate(0deg); + } + to { + transform: translate(-50%, -50%) rotate(360deg); + } +}