diff --git a/internal/web/static/css/style.css b/internal/web/static/css/style.css index 9d6c2fb..bb8021c 100644 --- a/internal/web/static/css/style.css +++ b/internal/web/static/css/style.css @@ -540,6 +540,44 @@ main.container { color: #ff8a8a; } +.global-loader { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.55); + backdrop-filter: blur(2px); + display: flex; + align-items: center; + justify-content: center; + z-index: 2000; +} + +.global-loader .loader-content { + background: var(--color-bg-card); + padding: 1.5rem 2rem; + border-radius: 12px; + border: 1px solid var(--color-border); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35); + display: flex; + gap: 1rem; + align-items: center; + color: var(--color-text-primary); + min-width: 280px; + justify-content: center; +} + +.global-loader .spinner { + width: 24px; + height: 24px; + border: 3px solid rgba(255, 255, 255, 0.2); + border-top-color: var(--color-brand); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + /* Detail views */ .breadcrumb { margin-bottom: 1.5rem; diff --git a/internal/web/static/js/app.js b/internal/web/static/js/app.js index 5501ea1..6870429 100644 --- a/internal/web/static/js/app.js +++ b/internal/web/static/js/app.js @@ -97,7 +97,9 @@ function displayGlobalSearchResults(data) { // Bulk Import Functions async function bulkImportAll() { + showLoader('Importing all data from TPDB...'); if (!confirm('This will import ALL data from TPDB. This may take several hours. Continue?')) { + hideLoader(); return; } @@ -127,11 +129,15 @@ async function bulkImportAll() { } } catch (error) { setImportStatus('import-all', 'Error: ' + error.message, false); + } finally { + hideLoader(); } } async function bulkImportPerformers() { + showLoader('Importing performers from TPDB...'); if (!confirm('This will import ALL performers from TPDB. Continue?')) { + hideLoader(); return; } @@ -165,11 +171,14 @@ async function bulkImportPerformers() { eventSource.onerror = function() { updateProgress('performers', 0, 0, 'Connection error', true); eventSource.close(); + hideLoader(); }; } async function bulkImportStudios() { + showLoader('Importing studios from TPDB...'); if (!confirm('This will import ALL studios from TPDB. Continue?')) { + hideLoader(); return; } @@ -203,11 +212,14 @@ async function bulkImportStudios() { eventSource.onerror = function() { updateProgress('studios', 0, 0, 'Connection error', true); eventSource.close(); + hideLoader(); }; } async function bulkImportScenes() { + showLoader('Importing scenes from TPDB...'); if (!confirm('This will import ALL scenes from TPDB. Continue?')) { + hideLoader(); return; } @@ -241,6 +253,7 @@ async function bulkImportScenes() { eventSource.onerror = function() { updateProgress('scenes', 0, 0, 'Connection error', true); eventSource.close(); + hideLoader(); }; } @@ -291,6 +304,7 @@ async function aeImportPerformerByName() { const name = prompt('Import performer by name (Adult Empire):'); if (!name) return; setAEStatus(`Searching Adult Empire for "${name}"...`); + showLoader(`Importing performer "${name}" from Adult Empire...`); try { const res = await fetch('/api/ae/import/performer', { method: 'POST', @@ -306,6 +320,8 @@ async function aeImportPerformerByName() { } } catch (err) { setAEStatus(`Error: ${err.message}`, true); + } finally { + hideLoader(); } } @@ -313,6 +329,7 @@ async function aeImportPerformerByURL() { const url = prompt('Paste Adult Empire performer URL:'); if (!url) return; setAEStatus('Importing performer from Adult Empire URL...'); + showLoader('Importing performer from Adult Empire URL...'); try { const res = await fetch('/api/ae/import/performer-by-url', { method: 'POST', @@ -328,6 +345,8 @@ async function aeImportPerformerByURL() { } } catch (err) { setAEStatus(`Error: ${err.message}`, true); + } finally { + hideLoader(); } } @@ -335,6 +354,7 @@ async function aeImportSceneByName() { const title = prompt('Import scene by title (Adult Empire):'); if (!title) return; setAEStatus(`Searching Adult Empire for "${title}"...`); + showLoader(`Importing scene "${title}" from Adult Empire...`); try { const res = await fetch('/api/ae/import/scene', { method: 'POST', @@ -350,6 +370,8 @@ async function aeImportSceneByName() { } } catch (err) { setAEStatus(`Error: ${err.message}`, true); + } finally { + hideLoader(); } } @@ -357,6 +379,7 @@ async function aeImportSceneByURL() { const url = prompt('Paste Adult Empire scene URL:'); if (!url) return; setAEStatus('Importing scene from Adult Empire URL...'); + showLoader('Importing scene from Adult Empire URL...'); try { const res = await fetch('/api/ae/import/scene-by-url', { method: 'POST', @@ -372,6 +395,8 @@ async function aeImportSceneByURL() { } } catch (err) { setAEStatus(`Error: ${err.message}`, true); + } finally { + hideLoader(); } } @@ -539,6 +564,23 @@ function setImportStatus(type, message, success) { } // Close modals when clicking outside + +// Global loader helpers +function showLoader(msg) { + const overlay = document.getElementById('global-loader'); + const text = document.getElementById('global-loader-text'); + if (overlay) { + overlay.style.display = 'flex'; + } + if (text && msg) { + text.textContent = msg; + } +} + +function hideLoader() { + const overlay = document.getElementById('global-loader'); + if (overlay) overlay.style.display = 'none'; +} window.onclick = function(event) { if (event.target.classList.contains('modal')) { event.target.classList.remove('active'); diff --git a/internal/web/templates/layout.html b/internal/web/templates/layout.html index fd44c32..1007011 100644 --- a/internal/web/templates/layout.html +++ b/internal/web/templates/layout.html @@ -57,4 +57,10 @@ +
{{end}}