diff --git a/py/routes/misc_routes.py b/py/routes/misc_routes.py index a6842643..f86b4fab 100644 --- a/py/routes/misc_routes.py +++ b/py/routes/misc_routes.py @@ -5,6 +5,8 @@ import json import time import aiohttp import re +import subprocess +import sys from server import PromptServer # type: ignore from aiohttp import web from ..services.settings_manager import settings @@ -55,7 +57,10 @@ class MiscRoutes: # Lora code update endpoint app.router.add_post('/api/update-lora-code', MiscRoutes.update_lora_code) - + + # Add new route for opening example images folder + app.router.add_post('/api/open-example-images-folder', MiscRoutes.open_example_images_folder) + @staticmethod async def clear_cache(request): """Clear all cache files from the cache folder""" @@ -864,3 +869,63 @@ class MiscRoutes: 'success': False, 'error': str(e) }, status=500) + + @staticmethod + async def open_example_images_folder(request): + """ + Open the example images folder for a specific model + + Expects a JSON body with: + { + "model_hash": "sha256_hash" # SHA256 hash of the model + } + """ + try: + # Parse the request body + data = await request.json() + model_hash = data.get('model_hash') + + if not model_hash: + return web.json_response({ + 'success': False, + 'error': 'Missing model_hash parameter' + }, status=400) + + # Get the example images path from settings + example_images_path = settings.get('example_images_path') + if not example_images_path: + return web.json_response({ + 'success': False, + 'error': 'No example images path configured. Please set it in the settings panel first.' + }, status=400) + + # Construct the folder path for this model + model_folder = os.path.join(example_images_path, model_hash) + + # Check if the folder exists + if not os.path.exists(model_folder): + return web.json_response({ + 'success': False, + 'error': 'No example images found for this model. Download example images first.' + }, status=404) + + # Open the folder in the file explorer + if os.name == 'nt': # Windows + os.startfile(model_folder) + elif os.name == 'posix': # macOS and Linux + if sys.platform == 'darwin': # macOS + subprocess.Popen(['open', model_folder]) + else: # Linux + subprocess.Popen(['xdg-open', model_folder]) + + return web.json_response({ + 'success': True, + 'message': f'Opened example images folder for model {model_hash}' + }) + + except Exception as e: + logger.error(f"Failed to open example images folder: {e}", exc_info=True) + return web.json_response({ + 'success': False, + 'error': str(e) + }, status=500) diff --git a/static/js/components/CheckpointCard.js b/static/js/components/CheckpointCard.js index 4b9b7d20..f494d6f9 100644 --- a/static/js/components/CheckpointCard.js +++ b/static/js/components/CheckpointCard.js @@ -1,4 +1,4 @@ -import { showToast, copyToClipboard } from '../utils/uiHelpers.js'; +import { showToast, copyToClipboard, openExampleImagesFolder } from '../utils/uiHelpers.js'; import { state } from '../state/index.js'; import { showCheckpointModal } from './checkpointModal/index.js'; import { NSFW_LEVELS } from '../utils/constants.js'; @@ -115,8 +115,8 @@ export function createCheckpointCard(checkpoint) { ${checkpoint.model_name}
- +
@@ -272,6 +272,12 @@ export function createCheckpointCard(checkpoint) { replaceCheckpointPreview(checkpoint.file_path); }); + // Open example images folder button click event + card.querySelector('.fa-folder-open')?.addEventListener('click', e => { + e.stopPropagation(); + openExampleImagesFolder(checkpoint.sha256); + }); + // Add autoplayOnHover handlers for video elements if needed const videoElement = card.querySelector('video'); if (videoElement && autoplayOnHover) { diff --git a/static/js/components/ContextMenu/CheckpointContextMenu.js b/static/js/components/ContextMenu/CheckpointContextMenu.js index e5e5aea0..0945901f 100644 --- a/static/js/components/ContextMenu/CheckpointContextMenu.js +++ b/static/js/components/ContextMenu/CheckpointContextMenu.js @@ -1,6 +1,6 @@ import { BaseContextMenu } from './BaseContextMenu.js'; -import { refreshSingleCheckpointMetadata, saveModelMetadata } from '../../api/checkpointApi.js'; -import { showToast, getNSFWLevelName } from '../../utils/uiHelpers.js'; +import { refreshSingleCheckpointMetadata, saveModelMetadata, replaceCheckpointPreview } from '../../api/checkpointApi.js'; +import { showToast, getNSFWLevelName, openExampleImagesFolder } from '../../utils/uiHelpers.js'; import { NSFW_LEVELS } from '../../utils/constants.js'; import { getStorageItem } from '../../utils/storageHelpers.js'; import { showExcludeModal } from '../../utils/modalUtils.js'; @@ -23,10 +23,12 @@ export class CheckpointContextMenu extends BaseContextMenu { this.currentCard.click(); break; case 'preview': - // Replace checkpoint preview - if (this.currentCard.querySelector('.fa-image')) { - this.currentCard.querySelector('.fa-image').click(); - } + // Open example images folder instead of replacing preview + openExampleImagesFolder(this.currentCard.dataset.sha256); + break; + case 'replace-preview': + // Add new action for replacing preview images + replaceCheckpointPreview(this.currentCard.dataset.filepath); break; case 'civitai': // Open civitai page diff --git a/static/js/components/ContextMenu/LoraContextMenu.js b/static/js/components/ContextMenu/LoraContextMenu.js index ed155ab5..c2cdbc8e 100644 --- a/static/js/components/ContextMenu/LoraContextMenu.js +++ b/static/js/components/ContextMenu/LoraContextMenu.js @@ -1,6 +1,6 @@ import { BaseContextMenu } from './BaseContextMenu.js'; -import { refreshSingleLoraMetadata, saveModelMetadata } from '../../api/loraApi.js'; -import { showToast, getNSFWLevelName, copyToClipboard, sendLoraToWorkflow } from '../../utils/uiHelpers.js'; +import { refreshSingleLoraMetadata, saveModelMetadata, replacePreview } from '../../api/loraApi.js'; +import { showToast, getNSFWLevelName, copyToClipboard, sendLoraToWorkflow, openExampleImagesFolder } from '../../utils/uiHelpers.js'; import { NSFW_LEVELS } from '../../utils/constants.js'; import { getStorageItem } from '../../utils/storageHelpers.js'; import { showExcludeModal, showDeleteModal } from '../../utils/modalUtils.js'; @@ -47,7 +47,12 @@ export class LoraContextMenu extends BaseContextMenu { this.sendLoraToWorkflow(true); break; case 'preview': - this.currentCard.querySelector('.fa-image')?.click(); + // Open example images folder instead of showing preview image dialog + openExampleImagesFolder(this.currentCard.dataset.sha256); + break; + case 'replace-preview': + // Add a new action for replacing preview images + replacePreview(this.currentCard.dataset.filepath); break; case 'delete': // Call showDeleteModal directly instead of clicking the trash button diff --git a/static/js/components/LoraCard.js b/static/js/components/LoraCard.js index bba68a54..e1c53375 100644 --- a/static/js/components/LoraCard.js +++ b/static/js/components/LoraCard.js @@ -1,4 +1,4 @@ -import { showToast, openCivitai, copyToClipboard, sendLoraToWorkflow } from '../utils/uiHelpers.js'; +import { showToast, openCivitai, copyToClipboard, sendLoraToWorkflow, openExampleImagesFolder } from '../utils/uiHelpers.js'; import { state } from '../state/index.js'; import { showLoraModal } from './loraModal/index.js'; import { bulkManager } from '../managers/BulkManager.js'; @@ -69,6 +69,12 @@ function handleLoraCardEvent(event) { return; } + if (event.target.closest('.fa-folder-open')) { + event.stopPropagation(); + openExampleImagesFolder(card.dataset.sha256); + return; + } + // If no specific element was clicked, handle the card click (show modal or toggle selection) if (state.bulkMode) { // Toggle selection using the bulk manager @@ -300,14 +306,14 @@ export function createLoraCard(lora) { ${lora.model_name}
- +
`; - + // Add a special class for virtual scroll positioning if needed if (state.virtualScroller) { card.classList.add('virtual-scroll-item'); diff --git a/static/js/utils/uiHelpers.js b/static/js/utils/uiHelpers.js index 37c4ef65..c86e3072 100644 --- a/static/js/utils/uiHelpers.js +++ b/static/js/utils/uiHelpers.js @@ -419,4 +419,36 @@ export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntax showToast(`Failed to send ${syntaxType === 'recipe' ? 'recipe' : 'LoRA'} to workflow`, 'error'); return false; } +} + +/** + * Opens the example images folder for a specific model + * @param {string} modelHash - The SHA256 hash of the model + */ +export async function openExampleImagesFolder(modelHash) { + try { + const response = await fetch('/api/open-example-images-folder', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model_hash: modelHash + }) + }); + + const result = await response.json(); + + if (result.success) { + showToast('Opening example images folder', 'success'); + return true; + } else { + showToast(result.error || 'Failed to open example images folder', 'error'); + return false; + } + } catch (error) { + console.error('Failed to open example images folder:', error); + showToast('Failed to open example images folder', 'error'); + return false; + } } \ No newline at end of file diff --git a/templates/checkpoints.html b/templates/checkpoints.html index 5eb3371d..a455030f 100644 --- a/templates/checkpoints.html +++ b/templates/checkpoints.html @@ -19,7 +19,8 @@
View on CivitAI
Refresh Civitai Data
Copy Model Filename
-
Replace Preview
+
Open Examples Folder
+
Replace Preview
Set Content Rating
Move to Folder
diff --git a/templates/components/context_menu.html b/templates/components/context_menu.html index c60debd1..8f600a4d 100644 --- a/templates/components/context_menu.html +++ b/templates/components/context_menu.html @@ -18,6 +18,9 @@ Send to Workflow (Replace)
+ Open Examples Folder +
+
Replace Preview