From d7c643ee9bf8cbe92c0860e93757949faaa654ad Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Sun, 16 Mar 2025 17:59:55 +0800 Subject: [PATCH] Enhance LoRA management by introducing deletion status and UI updates. Implement warning indicators for deleted LoRAs in the import modal, update cache handling for added and removed recipes, and improve styling for deleted items. Adjust logic to exclude deleted LoRAs from download prompts and ensure proper display of their status in the UI. --- py/routes/recipe_routes.py | 110 +++++++++++--------- static/css/base.css | 2 + static/css/components/import-modal.css | 90 ++++++++++++++++ static/js/managers/ImportManager.js | 136 ++++++++++++++++++------- 4 files changed, 256 insertions(+), 82 deletions(-) diff --git a/py/routes/recipe_routes.py b/py/routes/recipe_routes.py index df1bd2fd..8d6b1369 100644 --- a/py/routes/recipe_routes.py +++ b/py/routes/recipe_routes.py @@ -231,42 +231,49 @@ class RecipeRoutes: # Get additional info from Civitai civitai_info = await self.civitai_client.get_model_version_info(model_version_id) - # Check if this LoRA exists locally by SHA256 hash - exists_locally = False - local_path = None - sha256 = '' - - if civitai_info and 'files' in civitai_info: - # Find the model file (type="Model") in the files list - model_file = next((file for file in civitai_info.get('files', []) - if file.get('type') == 'Model'), None) - - if model_file: - sha256 = model_file.get('hashes', {}).get('SHA256', '') - if sha256: - exists_locally = self.recipe_scanner._lora_scanner.has_lora_hash(sha256) - if exists_locally: - local_path = self.recipe_scanner._lora_scanner.get_lora_path_by_hash(sha256) - - # Create LoRA entry for frontend display + # Initialize lora entry with default values lora_entry = { 'id': model_version_id, 'name': resource.get('modelName', ''), 'version': resource.get('modelVersionName', ''), 'type': resource.get('type', 'lora'), 'weight': resource.get('weight', 1.0), - 'existsLocally': exists_locally, - 'localPath': local_path, - 'file_name': os.path.splitext(os.path.basename(local_path))[0] if local_path else '', - 'hash': sha256, + 'existsLocally': False, + 'localPath': None, + 'file_name': '', + 'hash': '', 'thumbnailUrl': '', 'baseModel': '', 'size': 0, - 'downloadUrl': '' + 'downloadUrl': '', + 'isDeleted': False # New flag to indicate if the LoRA is deleted from Civitai } - # Add Civitai info if available - if civitai_info: + # Check if this LoRA exists locally by SHA256 hash + if civitai_info and civitai_info.get("error") != "Model not found": + # LoRA exists on Civitai, process its information + if 'files' in civitai_info: + # Find the model file (type="Model") in the files list + model_file = next((file for file in civitai_info.get('files', []) + if file.get('type') == 'Model'), None) + + if model_file: + sha256 = model_file.get('hashes', {}).get('SHA256', '') + if sha256: + exists_locally = self.recipe_scanner._lora_scanner.has_lora_hash(sha256) + if exists_locally: + local_path = self.recipe_scanner._lora_scanner.get_lora_path_by_hash(sha256) + lora_entry['existsLocally'] = True + lora_entry['localPath'] = local_path + lora_entry['file_name'] = os.path.splitext(os.path.basename(local_path))[0] + else: + # For missing LoRAs, get file_name from model_file.name + file_name = model_file.get('name', '') + lora_entry['file_name'] = os.path.splitext(file_name)[0] if file_name else '' + + lora_entry['hash'] = sha256 + lora_entry['size'] = model_file.get('sizeKB', 0) * 1024 + # Get thumbnail URL from first image if 'images' in civitai_info and civitai_info['images']: lora_entry['thumbnailUrl'] = civitai_info['images'][0].get('url', '') @@ -274,12 +281,12 @@ class RecipeRoutes: # Get base model lora_entry['baseModel'] = civitai_info.get('baseModel', '') - # Get file size from model file - if model_file: - lora_entry['size'] = model_file.get('sizeKB', 0) * 1024 - # Get download URL lora_entry['downloadUrl'] = civitai_info.get('downloadUrl', '') + else: + # LoRA is deleted from Civitai or not found + lora_entry['isDeleted'] = True + lora_entry['thumbnailUrl'] = '/loras_static/images/no-preview.png' loras.append(lora_entry) @@ -385,6 +392,10 @@ class RecipeRoutes: # Format loras data according to the recipe.json format loras_data = [] for lora in metadata.get("loras", []): + # Skip deleted LoRAs if they're marked to be excluded + if lora.get("isDeleted", False) and lora.get("exclude", False): + continue + # Convert frontend lora format to recipe format lora_entry = { "file_name": lora.get("file_name", "") or os.path.splitext(os.path.basename(lora.get("localPath", "")))[0], @@ -392,7 +403,8 @@ class RecipeRoutes: "strength": float(lora.get("weight", 1.0)), "modelVersionId": lora.get("id", ""), "modelName": lora.get("name", ""), - "modelVersionName": lora.get("version", "") + "modelVersionName": lora.get("version", ""), + "isDeleted": lora.get("isDeleted", False) # Preserve deletion status in saved recipe } loras_data.append(lora_entry) @@ -434,15 +446,17 @@ class RecipeRoutes: json_path = os.path.join(recipes_dir, json_filename) with open(json_path, 'w', encoding='utf-8') as f: json.dump(recipe_data, f, indent=4, ensure_ascii=False) - - # Add the new recipe directly to the cache instead of forcing a refresh - try: - # Use a timeout to prevent deadlocks - async with asyncio.timeout(5.0): - cache = await self.recipe_scanner.get_cached_data() - await cache.add_recipe(recipe_data) - except asyncio.TimeoutError: - logger.warning("Timeout adding recipe to cache - will be picked up on next refresh") + + # Simplified cache update approach + # Instead of trying to update the cache directly, just set it to None + # to force a refresh on the next get_cached_data call + if self.recipe_scanner._cache is not None: + # Add the recipe to the raw data if the cache exists + # This is a simple direct update without locks or timeouts + self.recipe_scanner._cache.raw_data.append(recipe_data) + # Schedule a background task to resort the cache + asyncio.create_task(self.recipe_scanner._cache.resort()) + logger.info(f"Added recipe {recipe_id} to cache") return web.json_response({ 'success': True, @@ -486,14 +500,16 @@ class RecipeRoutes: os.remove(image_path) logger.info(f"Deleted recipe image: {image_path}") - # Remove from cache without forcing a full refresh - try: - # Use a timeout to prevent deadlocks - async with asyncio.timeout(5.0): - cache = await self.recipe_scanner.get_cached_data(force_refresh=False) - await cache.remove_recipe(recipe_id) - except asyncio.TimeoutError: - logger.warning("Timeout removing recipe from cache - will be picked up on next refresh") + # Simplified cache update approach + if self.recipe_scanner._cache is not None: + # Remove the recipe from raw_data if it exists + self.recipe_scanner._cache.raw_data = [ + r for r in self.recipe_scanner._cache.raw_data + if str(r.get('id', '')) != recipe_id + ] + # Schedule a background task to resort the cache + asyncio.create_task(self.recipe_scanner._cache.resort()) + logger.info(f"Removed recipe {recipe_id} from cache") return web.json_response({"success": True, "message": "Recipe deleted successfully"}) except Exception as e: diff --git a/static/css/base.css b/static/css/base.css index 985cb62f..971d4d85 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -38,6 +38,7 @@ html, body { --lora-border: oklch(90% 0.02 256 / 0.15); --lora-text: oklch(95% 0.02 256); --lora-error: oklch(75% 0.32 29); + --lora-warning: oklch(75% 0.25 80); /* Add warning color for deleted LoRAs */ /* Spacing Scale */ --space-1: calc(8px * 1); @@ -68,6 +69,7 @@ html, body { --lora-surface: oklch(25% 0.02 256 / 0.98); --lora-border: oklch(90% 0.02 256 / 0.15); --lora-text: oklch(98% 0.02 256); + --lora-warning: oklch(75% 0.25 80); /* Add warning color for dark theme too */ } body { diff --git a/static/css/components/import-modal.css b/static/css/components/import-modal.css index 7fbb3c20..9764d99b 100644 --- a/static/css/components/import-modal.css +++ b/static/css/components/import-modal.css @@ -161,6 +161,11 @@ border-left: 4px solid var(--lora-error); } +.lora-item.is-deleted { + background: oklch(var(--lora-warning) / 0.05); + border-left: 4px solid var(--lora-warning); +} + .lora-thumbnail { width: 80px; height: 80px; @@ -521,4 +526,89 @@ .modal-content { padding-right: calc(var(--space-2) + var(--scrollbar-width)); /* Add extra padding for scrollbar */ } +} + +/* Deleted LoRA styles - Fix layout issues */ +.lora-item.is-deleted { + background: oklch(var(--lora-warning) / 0.05); + border-left: 4px solid var(--lora-warning); +} + +.deleted-badge { + display: inline-flex; + align-items: center; + background: var(--lora-warning); + color: white; + padding: 4px 8px; + border-radius: var(--border-radius-xs); + font-size: 0.8em; + font-weight: 500; + white-space: nowrap; + flex-shrink: 0; +} + +.deleted-badge i { + margin-right: 4px; + font-size: 0.9em; +} + +.exclude-lora-checkbox { + display: none; +} + +/* Deleted LoRAs warning - redesigned to not interfere with modal buttons */ +.deleted-loras-warning { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 12px 16px; + background: oklch(var(--lora-warning) / 0.1); + border: 1px solid var(--lora-warning); + border-radius: var(--border-radius-sm); + color: var(--text-color); + margin-bottom: var(--space-2); +} + +.warning-icon { + color: var(--lora-warning); + font-size: 1.2em; + padding-top: 2px; +} + +.warning-content { + flex: 1; +} + +.warning-title { + font-weight: 600; + margin-bottom: 4px; +} + +.warning-text { + font-size: 0.9em; + line-height: 1.4; +} + +/* Remove the old warning-message styles that were causing layout issues */ +.warning-message { + display: none; /* Hide the old style */ +} + +/* Update deleted badge to be more prominent */ +.deleted-badge { + display: inline-flex; + align-items: center; + background: var(--lora-warning); + color: white; + padding: 4px 8px; + border-radius: var(--border-radius-xs); + font-size: 0.8em; + font-weight: 500; + white-space: nowrap; + flex-shrink: 0; +} + +.deleted-badge i { + margin-right: 4px; + font-size: 0.9em; } \ No newline at end of file diff --git a/static/js/managers/ImportManager.js b/static/js/managers/ImportManager.js index bfcb0911..90d85e4a 100644 --- a/static/js/managers/ImportManager.js +++ b/static/js/managers/ImportManager.js @@ -255,31 +255,39 @@ export class ImportManager { if (lorasList) { lorasList.innerHTML = this.recipeData.loras.map(lora => { const existsLocally = lora.existsLocally; + const isDeleted = lora.isDeleted; const localPath = lora.localPath || ''; - // Create local status badge - const localStatus = existsLocally ? - `
- In Library -
${localPath}
-
` : - `
- Not in Library -
`; + // Create status badge based on LoRA status + let statusBadge; + if (isDeleted) { + statusBadge = `
+ Deleted from Civitai +
`; + } else { + statusBadge = existsLocally ? + `
+ In Library +
${localPath}
+
` : + `
+ Not in Library +
`; + } // Format size if available const sizeDisplay = lora.size ? `
${this.formatFileSize(lora.size)}
` : ''; return ` -
+
LoRA preview

${lora.name}

- ${localStatus} + ${statusBadge}
${lora.version ? `
${lora.version}
` : ''}
@@ -301,12 +309,55 @@ export class ImportManager { const nextButton = document.querySelector('#detailsStep .primary-btn'); if (!nextButton) return; - // If we have missing LoRAs, show "Download Missing LoRAs" - // Otherwise show "Save Recipe" - if (this.missingLoras.length > 0) { - nextButton.textContent = 'Download Missing LoRAs'; + // Count deleted LoRAs + const deletedLoras = this.recipeData.loras.filter(lora => lora.isDeleted).length; + + // If we have deleted LoRAs, show a warning and update button text + if (deletedLoras > 0) { + // Remove any existing warning + const existingWarning = document.getElementById('deletedLorasWarning'); + if (existingWarning) { + existingWarning.remove(); + } + + // Create a new warning container above the buttons + const buttonsContainer = document.querySelector('#detailsStep .modal-actions') || nextButton.parentNode; + const warningContainer = document.createElement('div'); + warningContainer.id = 'deletedLorasWarning'; + warningContainer.className = 'deleted-loras-warning'; + + // Create warning message + warningContainer.innerHTML = ` +
+
+
${deletedLoras} LoRA(s) have been deleted from Civitai
+
These LoRAs cannot be downloaded. If you continue, they will be removed from the recipe.
+
+ `; + + // Insert before the buttons container + buttonsContainer.parentNode.insertBefore(warningContainer, buttonsContainer); + + // Update next button text to be more clear + nextButton.textContent = 'Continue Without Deleted LoRAs'; } else { - nextButton.textContent = 'Save Recipe'; + // Remove warning if no deleted LoRAs + const warningMsg = document.getElementById('deletedLorasWarning'); + if (warningMsg) { + warningMsg.remove(); + } + + // If we have missing LoRAs (not deleted), show "Download Missing LoRAs" + // Otherwise show "Save Recipe" + const missingNotDeleted = this.recipeData.loras.filter( + lora => !lora.existsLocally && !lora.isDeleted + ).length; + + if (missingNotDeleted > 0) { + nextButton.textContent = 'Download Missing LoRAs'; + } else { + nextButton.textContent = 'Save Recipe'; + } } } @@ -356,8 +407,23 @@ export class ImportManager { return; } - // If we have missing LoRAs, go to location step + // Automatically mark all deleted LoRAs as excluded + if (this.recipeData && this.recipeData.loras) { + this.recipeData.loras.forEach(lora => { + if (lora.isDeleted) { + lora.exclude = true; + } + }); + } + + // Update missing LoRAs list to exclude deleted LoRAs + this.missingLoras = this.recipeData.loras.filter(lora => + !lora.existsLocally && !lora.isDeleted); + + // If we have downloadable missing LoRAs, go to location step if (this.missingLoras.length > 0) { + // Store only downloadable LoRAs for the download step + this.downloadableLoRAs = this.missingLoras; this.proceedToLocation(); } else { // Otherwise, save the recipe directly @@ -396,9 +462,9 @@ export class ImportManager { try { // Display missing LoRAs that will be downloaded const missingLorasList = document.getElementById('missingLorasList'); - if (missingLorasList && this.missingLoras.length > 0) { + if (missingLorasList && this.downloadableLoRAs.length > 0) { // Calculate total size - const totalSize = this.missingLoras.reduce((sum, lora) => { + const totalSize = this.downloadableLoRAs.reduce((sum, lora) => { return sum + (lora.size ? parseInt(lora.size) : 0); }, 0); @@ -411,11 +477,11 @@ export class ImportManager { // Update header to include count of missing LoRAs const missingLorasHeader = document.querySelector('.summary-header h3'); if (missingLorasHeader) { - missingLorasHeader.innerHTML = `Missing LoRAs (${this.missingLoras.length}) ${this.formatFileSize(totalSize)}`; + missingLorasHeader.innerHTML = `Missing LoRAs (${this.downloadableLoRAs.length}) ${this.formatFileSize(totalSize)}`; } // Generate missing LoRAs list - missingLorasList.innerHTML = this.missingLoras.map(lora => { + missingLorasList.innerHTML = this.downloadableLoRAs.map(lora => { const sizeDisplay = lora.size ? this.formatFileSize(lora.size) : 'Unknown size'; const baseModel = lora.baseModel ? `${lora.baseModel}` : ''; @@ -538,7 +604,7 @@ export class ImportManager { // Check if we need to download LoRAs - if (this.missingLoras.length > 0) { + if (this.downloadableLoRAs && this.downloadableLoRAs.length > 0) { // For download, we need to validate the target path const loraRoot = document.getElementById('importLoraRoot')?.value; if (!loraRoot) { @@ -561,7 +627,7 @@ export class ImportManager { const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`); // Show enhanced loading with progress details for multiple items - const updateProgress = this.loadingManager.showDownloadProgress(this.missingLoras.length); + const updateProgress = this.loadingManager.showDownloadProgress(this.downloadableLoRAs.length); let completedDownloads = 0; let currentLoraProgress = 0; @@ -574,7 +640,7 @@ export class ImportManager { currentLoraProgress = data.progress; // Get current LoRA name - const currentLora = this.missingLoras[completedDownloads]; + const currentLora = this.downloadableLoRAs[completedDownloads]; const loraName = currentLora ? currentLora.name : ''; // Update progress display @@ -583,32 +649,32 @@ export class ImportManager { // Add more detailed status messages based on progress if (currentLoraProgress < 3) { this.loadingManager.setStatus( - `Preparing download for LoRA ${completedDownloads+1}/${this.missingLoras.length}` + `Preparing download for LoRA ${completedDownloads+1}/${this.downloadableLoRAs.length}` ); } else if (currentLoraProgress === 3) { this.loadingManager.setStatus( - `Downloaded preview for LoRA ${completedDownloads+1}/${this.missingLoras.length}` + `Downloaded preview for LoRA ${completedDownloads+1}/${this.downloadableLoRAs.length}` ); } else if (currentLoraProgress > 3 && currentLoraProgress < 100) { this.loadingManager.setStatus( - `Downloading LoRA ${completedDownloads+1}/${this.missingLoras.length}` + `Downloading LoRA ${completedDownloads+1}/${this.downloadableLoRAs.length}` ); } else { this.loadingManager.setStatus( - `Finalizing LoRA ${completedDownloads+1}/${this.missingLoras.length}` + `Finalizing LoRA ${completedDownloads+1}/${this.downloadableLoRAs.length}` ); } } }; - for (let i = 0; i < this.missingLoras.length; i++) { - const lora = this.missingLoras[i]; + for (let i = 0; i < this.downloadableLoRAs.length; i++) { + const lora = this.downloadableLoRAs[i]; // Reset current LoRA progress for new download currentLoraProgress = 0; // Initial status update for new LoRA - this.loadingManager.setStatus(`Starting download for LoRA ${i+1}/${this.missingLoras.length}`); + this.loadingManager.setStatus(`Starting download for LoRA ${i+1}/${this.downloadableLoRAs.length}`); updateProgress(0, completedDownloads, lora.name); try { @@ -633,9 +699,9 @@ export class ImportManager { // Update progress to show completion of current LoRA updateProgress(100, completedDownloads, ''); - if (completedDownloads < this.missingLoras.length) { + if (completedDownloads < this.downloadableLoRAs.length) { this.loadingManager.setStatus( - `Completed ${completedDownloads}/${this.missingLoras.length} LoRAs. Starting next download...` + `Completed ${completedDownloads}/${this.downloadableLoRAs.length} LoRAs. Starting next download...` ); } } @@ -649,10 +715,10 @@ export class ImportManager { ws.close(); // Show final completion message - if (completedDownloads === this.missingLoras.length) { + if (completedDownloads === this.downloadableLoRAs.length) { showToast(`All ${completedDownloads} LoRAs downloaded successfully`, 'success'); } else { - showToast(`Downloaded ${completedDownloads} of ${this.missingLoras.length} LoRAs`, 'warning'); + showToast(`Downloaded ${completedDownloads} of ${this.downloadableLoRAs.length} LoRAs`, 'warning'); } }