diff --git a/locales/en.json b/locales/en.json index 52bc8580..343b813a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -300,7 +300,11 @@ "downloadError": "Failed to download metadata archive database", "removeSuccess": "Metadata archive database removed successfully", "removeError": "Failed to remove metadata archive database", - "removeConfirm": "Are you sure you want to remove the metadata archive database? This will delete the local database file and you'll need to download it again to use this feature." + "removeConfirm": "Are you sure you want to remove the metadata archive database? This will delete the local database file and you'll need to download it again to use this feature.", + "preparing": "Preparing download...", + "connecting": "Connecting to download server...", + "completed": "Completed", + "downloadComplete": "Download completed successfully" } }, "loras": { diff --git a/py/routes/misc_routes.py b/py/routes/misc_routes.py index 9a29a24d..118afea6 100644 --- a/py/routes/misc_routes.py +++ b/py/routes/misc_routes.py @@ -711,13 +711,23 @@ class MiscRoutes: try: archive_manager = await get_metadata_archive_manager() + # Get the download_id from query parameters if provided + download_id = request.query.get('download_id') + # Progress callback to send updates via WebSocket def progress_callback(stage, message): - asyncio.create_task(ws_manager.broadcast({ + data = { 'stage': stage, 'message': message, 'type': 'metadata_archive_download' - })) + } + + if download_id: + # Send to specific download WebSocket if download_id is provided + asyncio.create_task(ws_manager.broadcast_download_progress(download_id, data)) + else: + # Fallback to general broadcast + asyncio.create_task(ws_manager.broadcast(data)) # Download and extract in background success = await archive_manager.download_and_extract_database(progress_callback) diff --git a/py/services/metadata_archive_manager.py b/py/services/metadata_archive_manager.py index a1ba9b74..49b22c01 100644 --- a/py/services/metadata_archive_manager.py +++ b/py/services/metadata_archive_manager.py @@ -79,7 +79,7 @@ class MetadataArchiveManager: # Custom progress callback to report download progress async def download_progress(progress): if progress_callback: - progress_callback("download", f"Downloaded {progress:.1f}%") + progress_callback("download", f"Downloading archive... {progress:.1f}%") success, result = await downloader.download_file( url=url, diff --git a/static/css/components/modal/_base.css b/static/css/components/modal/_base.css index 2b1d542d..57d01ac9 100644 --- a/static/css/components/modal/_base.css +++ b/static/css/components/modal/_base.css @@ -208,6 +208,14 @@ body.modal-open { pointer-events: none; } +button:disabled, +.primary-btn:disabled, +.danger-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + .restart-required-icon { color: var(--lora-warning); margin-left: 5px; @@ -228,14 +236,76 @@ body.modal-open { background-color: oklch(35% 0.02 256 / 0.98); } -.primary-btn.disabled { - opacity: 0.5; - cursor: not-allowed; +/* Danger button styles */ +.danger-btn { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background-color: var(--lora-error); + color: white; + border: none; + border-radius: var(--border-radius-sm); + cursor: pointer; + transition: background-color 0.2s; + font-size: 0.95em; } -.primary-btn.disabled { - opacity: 0.5; - cursor: not-allowed; +.danger-btn:hover { + background-color: oklch(from var(--lora-error) l c h / 85%); + color: white; +} + +/* Metadata archive status styles */ +.metadata-archive-status { + background: rgba(0, 0, 0, 0.03); + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: var(--border-radius-sm); + padding: var(--space-2); + margin-bottom: var(--space-2); +} + +[data-theme="dark"] .metadata-archive-status { + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--lora-border); +} + +.archive-status-item { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + font-size: 0.95em; +} + +.archive-status-item:last-child { + margin-bottom: 0; +} + +.archive-status-label { + font-weight: 500; + color: var(--text-color); + opacity: 0.8; +} + +.archive-status-value { + color: var(--text-color); +} + +.archive-status-value.status-available { + color: var(--lora-success, #10b981); +} + +.archive-status-value.status-unavailable { + color: var(--lora-warning, #f59e0b); +} + +.archive-status-value.status-enabled { + color: var(--lora-success, #10b981); +} + +.archive-status-value.status-disabled { + color: var(--lora-error, #ef4444); } /* Add styles for delete preview image */ diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index 1a15bdb4..ac795903 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -789,6 +789,8 @@ export class SettingsManager { state.global.settings.compactMode = value; } else if (settingKey === 'include_trigger_words') { state.global.settings.includeTriggerWords = value; + } else if (settingKey === 'enable_metadata_archive_db') { + state.global.settings.enable_metadata_archive_db = value; } else { // For any other settings that might be added in the future state.global.settings[settingKey] = value; @@ -799,7 +801,7 @@ export class SettingsManager { try { // For backend settings, make API call - if (['show_only_sfw'].includes(settingKey)) { + if (['show_only_sfw', 'enable_metadata_archive_db'].includes(settingKey)) { const payload = {}; payload[settingKey] = value; @@ -814,6 +816,11 @@ export class SettingsManager { if (!response.ok) { throw new Error('Failed to save setting'); } + + // Refresh metadata archive status when enable setting changes + if (settingKey === 'enable_metadata_archive_db') { + await this.updateMetadataArchiveStatus(); + } } showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success'); @@ -872,6 +879,8 @@ export class SettingsManager { state.global.settings.compactMode = (value !== 'default'); } else if (settingKey === 'card_info_display') { state.global.settings.cardInfoDisplay = value; + } else if (settingKey === 'metadata_provider_priority') { + state.global.settings.metadata_provider_priority = value; } else { // For any other settings that might be added in the future state.global.settings[settingKey] = value; @@ -882,7 +891,7 @@ export class SettingsManager { try { // For backend settings, make API call - if (settingKey === 'default_lora_root' || settingKey === 'default_checkpoint_root' || settingKey === 'default_embedding_root' || settingKey === 'download_path_templates') { + if (settingKey === 'default_lora_root' || settingKey === 'default_checkpoint_root' || settingKey === 'default_embedding_root' || settingKey === 'download_path_templates' || settingKey === 'metadata_provider_priority') { const payload = {}; if (settingKey === 'download_path_templates') { payload[settingKey] = state.global.settings.download_path_templates; @@ -903,6 +912,11 @@ export class SettingsManager { } showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success'); + + // Refresh metadata archive status when provider priority changes + if (settingKey === 'metadata_provider_priority') { + await this.updateMetadataArchiveStatus(); + } } // Apply frontend settings immediately @@ -960,24 +974,24 @@ export class SettingsManager { const sizeText = status.databaseSize > 0 ? ` (${this.formatFileSize(status.databaseSize)})` : ''; statusContainer.innerHTML = ` -
-
- ${translate('settings.metadataArchive.status')}: - - ${status.isAvailable ? translate('settings.metadataArchive.statusAvailable') : translate('settings.metadataArchive.statusUnavailable')} - +
+ ${translate('settings.metadataArchive.status')}: + + ${status.isAvailable ? translate('settings.metadataArchive.statusAvailable') : translate('settings.metadataArchive.statusUnavailable')} ${sizeText} -
-
- ${translate('settings.metadataArchive.enabled')}: - - ${status.isEnabled ? translate('common.enabled') : translate('common.disabled')} - -
-
- ${translate('settings.metadataArchive.currentPriority')}: + +
+
+ ${translate('settings.metadataArchive.enabled')}: + + ${status.isEnabled ? translate('common.status.enabled') : translate('common.status.disabled')} + +
+
+ ${translate('settings.metadataArchive.currentPriority')}: + ${status.priority === 'archive_db' ? translate('settings.metadataArchive.priorityArchiveDb') : translate('settings.metadataArchive.priorityCivitaiApi')} -
+
`; @@ -1012,12 +1026,81 @@ export class SettingsManager { async downloadMetadataArchive() { try { const downloadBtn = document.getElementById('downloadMetadataArchiveBtn'); + if (downloadBtn) { downloadBtn.disabled = true; downloadBtn.textContent = translate('settings.metadataArchive.downloadingButton'); } + + // Show loading with enhanced progress + const progressUpdater = state.loadingManager.showEnhancedProgress(translate('settings.metadataArchive.preparing')); - const response = await fetch('/api/download-metadata-archive', { + // Set up WebSocket for progress updates + const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; + const downloadId = `metadata_archive_${Date.now()}`; + const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${downloadId}`); + + let wsConnected = false; + let actualDownloadId = downloadId; // Will be updated when WebSocket confirms the ID + + // Promise to wait for WebSocket connection and ID confirmation + const wsReady = new Promise((resolve) => { + ws.onopen = () => { + wsConnected = true; + console.log('Connected to metadata archive download progress WebSocket'); + }; + + ws.onmessage = (event) => { + const data = JSON.parse(event.data); + + // Handle download ID confirmation + if (data.type === 'download_id') { + actualDownloadId = data.download_id; + console.log(`Connected to metadata archive download progress with ID: ${data.download_id}`); + resolve(data.download_id); + return; + } + + // Handle metadata archive download progress + if (data.type === 'metadata_archive_download') { + const message = data.message || ''; + + // Update progress bar based on stage + let progressPercent = 0; + if (data.stage === 'download') { + // Extract percentage from message if available + const percentMatch = data.message.match(/(\d+\.?\d*)%/); + if (percentMatch) { + progressPercent = Math.min(parseFloat(percentMatch[1]), 90); // Cap at 90% for download + } else { + progressPercent = 0; // Default download progress + } + } else if (data.stage === 'extract') { + progressPercent = 95; // Near completion for extraction + } + + // Update loading manager progress + progressUpdater.updateProgress(progressPercent, '', `${message}`); + } + }; + + ws.onerror = (error) => { + console.error('WebSocket error:', error); + resolve(downloadId); // Fallback to original ID + }; + + // Timeout fallback + setTimeout(() => resolve(downloadId), 5000); + }); + + ws.onclose = () => { + console.log('WebSocket connection closed'); + }; + + // Wait for WebSocket to be ready + await wsReady; + + const response = await fetch(`/api/download-metadata-archive?download_id=${encodeURIComponent(actualDownloadId)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -1026,8 +1109,16 @@ export class SettingsManager { const data = await response.json(); + // Close WebSocket + if (ws.readyState === WebSocket.OPEN) { + ws.close(); + } + if (data.success) { - showNotification(translate('settings.metadataArchive.downloadSuccess'), 'success'); + // Complete progress + await progressUpdater.complete(translate('settings.metadataArchive.downloadComplete')); + + showToast('settings.metadataArchive.downloadSuccess', 'success'); // Update settings in state state.global.settings.enable_metadata_archive_db = true; @@ -1041,11 +1132,17 @@ export class SettingsManager { await this.updateMetadataArchiveStatus(); } else { - showNotification(translate('settings.metadataArchive.downloadError') + ': ' + data.error, 'error'); + // Hide loading on error + state.loadingManager.hide(); + showToast('settings.metadataArchive.downloadError' + ': ' + data.error, 'error'); } } catch (error) { console.error('Error downloading metadata archive:', error); - showNotification(translate('settings.metadataArchive.downloadError') + ': ' + error.message, 'error'); + + // Hide loading on error + state.loadingManager.hide(); + + showToast('settings.metadataArchive.downloadError' + ': ' + error.message, 'error'); } finally { const downloadBtn = document.getElementById('downloadMetadataArchiveBtn'); if (downloadBtn) { @@ -1077,8 +1174,8 @@ export class SettingsManager { const data = await response.json(); if (data.success) { - showNotification(translate('settings.metadataArchive.removeSuccess'), 'success'); - + showToast('settings.metadataArchive.removeSuccess', 'success'); + // Update settings in state state.global.settings.enable_metadata_archive_db = false; setStorageItem('settings', state.global.settings); @@ -1091,11 +1188,11 @@ export class SettingsManager { await this.updateMetadataArchiveStatus(); } else { - showNotification(translate('settings.metadataArchive.removeError') + ': ' + data.error, 'error'); + showToast('settings.metadataArchive.removeError' + ': ' + data.error, 'error'); } } catch (error) { console.error('Error removing metadata archive:', error); - showNotification(translate('settings.metadataArchive.removeError') + ': ' + error.message, 'error'); + showToast('settings.metadataArchive.removeError' + ': ' + error.message, 'error'); } finally { const removeBtn = document.getElementById('removeMetadataArchiveBtn'); if (removeBtn) { diff --git a/templates/components/modals/settings_modal.html b/templates/components/modals/settings_modal.html index a2f85716..d150c81d 100644 --- a/templates/components/modals/settings_modal.html +++ b/templates/components/modals/settings_modal.html @@ -398,28 +398,6 @@
- -
-

{{ t('settings.sections.misc') }}

-
-
-
- -
-
- -
-
-
- {{ t('settings.misc.includeTriggerWordsHelp') }} -
-
-
-

{{ t('settings.sections.metadataArchive') }}

@@ -470,10 +448,10 @@
- -
@@ -483,6 +461,28 @@ + + +
+

{{ t('settings.sections.misc') }}

+
+
+
+ +
+
+ +
+
+
+ {{ t('settings.misc.includeTriggerWordsHelp') }} +
+
+
\ No newline at end of file