From 4d6ea0236b41305c960f2d697e476739f9d2a7e3 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Sun, 8 Jun 2025 17:38:46 +0800 Subject: [PATCH] Add centralized example images setting and update related UI components --- py/routes/misc_routes.py | 93 +++++++++++++++++++ static/css/components/modal.css | 6 ++ static/js/components/checkpointModal/index.js | 30 ++++-- static/js/components/loraModal/index.js | 31 +++++-- static/js/managers/SettingsManager.js | 55 ++++++++++- templates/components/modals.html | 24 ++++- 6 files changed, 220 insertions(+), 19 deletions(-) diff --git a/py/routes/misc_routes.py b/py/routes/misc_routes.py index b1524453..73c5dd7e 100644 --- a/py/routes/misc_routes.py +++ b/py/routes/misc_routes.py @@ -5,6 +5,9 @@ from aiohttp import web from ..services.settings_manager import settings from ..utils.usage_stats import UsageStats from ..utils.lora_metadata import extract_trained_words +from ..config import config +from ..utils.constants import SUPPORTED_MEDIA_EXTENSIONS +import re logger = logging.getLogger(__name__) @@ -44,6 +47,9 @@ class MiscRoutes: # Add new route for getting trained words app.router.add_get('/api/trained-words', MiscRoutes.get_trained_words) + + # Add new route for getting model example files + app.router.add_get('/api/model-example-files', MiscRoutes.get_model_example_files) @staticmethod async def clear_cache(request): @@ -310,3 +316,90 @@ class MiscRoutes: 'success': False, 'error': str(e) }, status=500) + + @staticmethod + async def get_model_example_files(request): + """ + Get list of example image files for a specific model based on file path + + Expects: + - file_path in query parameters + + Returns: + - List of image files with their paths as static URLs + """ + try: + # Get the model file path from query parameters + file_path = request.query.get('file_path') + + if not file_path: + return web.json_response({ + 'success': False, + 'error': 'Missing file_path parameter' + }, status=400) + + # Extract directory and base filename + model_dir = os.path.dirname(file_path) + model_filename = os.path.basename(file_path) + model_name = os.path.splitext(model_filename)[0] + + # Check if the directory exists + if not os.path.exists(model_dir): + return web.json_response({ + 'success': False, + 'error': 'Model directory not found', + 'files': [] + }, status=404) + + # Look for files matching the pattern modelname.example.. + files = [] + pattern = f"{model_name}.example." + + for file in os.listdir(model_dir): + file_lower = file.lower() + if file_lower.startswith(pattern.lower()): + file_full_path = os.path.join(model_dir, file) + if os.path.isfile(file_full_path): + # Check if the file is a supported media file + file_ext = os.path.splitext(file)[1].lower() + if (file_ext in SUPPORTED_MEDIA_EXTENSIONS['images'] or + file_ext in SUPPORTED_MEDIA_EXTENSIONS['videos']): + + # Extract the index from the filename + try: + # Extract the part after '.example.' and before file extension + index_part = file[len(pattern):].split('.')[0] + # Try to parse it as an integer + index = int(index_part) + except (ValueError, IndexError): + # If we can't parse the index, use infinity to sort at the end + index = float('inf') + + # Convert file path to static URL + static_url = config.get_preview_static_url(file_full_path) + + files.append({ + 'name': file, + 'path': static_url, + 'extension': file_ext, + 'is_video': file_ext in SUPPORTED_MEDIA_EXTENSIONS['videos'], + 'index': index + }) + + # Sort files by their index for consistent ordering + files.sort(key=lambda x: x['index']) + # Remove the index field as it's only used for sorting + for file in files: + file.pop('index', None) + + return web.json_response({ + 'success': True, + 'files': files + }) + + except Exception as e: + logger.error(f"Failed to get model example files: {e}", exc_info=True) + return web.json_response({ + 'success': False, + 'error': str(e) + }, status=500) diff --git a/static/css/components/modal.css b/static/css/components/modal.css index ad725409..973d2179 100644 --- a/static/css/components/modal.css +++ b/static/css/components/modal.css @@ -375,6 +375,12 @@ body.modal-open { background: rgba(255, 255, 255, 0.05); } +/* Add disabled style for setting items */ +.setting-item[data-requires-centralized="true"].disabled { + opacity: 0.6; + pointer-events: none; +} + /* Control row with label and input together */ .setting-row { display: flex; diff --git a/static/js/components/checkpointModal/index.js b/static/js/components/checkpointModal/index.js index d7bd688a..c4c2d076 100644 --- a/static/js/components/checkpointModal/index.js +++ b/static/js/components/checkpointModal/index.js @@ -15,6 +15,7 @@ import { import { saveModelMetadata } from '../../api/checkpointApi.js'; import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js'; import { updateCheckpointCard } from '../../utils/cardUpdater.js'; +import { state } from '../../state/index.js'; /** * Display the checkpoint modal with the given checkpoint data @@ -149,28 +150,41 @@ export function showCheckpointModal(checkpoint) { } // Load example images asynchronously - loadExampleImages(checkpoint.civitai?.images, checkpoint.sha256); + loadExampleImages(checkpoint.civitai?.images, checkpoint.sha256, checkpoint.file_path); } /** * Load example images asynchronously * @param {Array} images - Array of image objects * @param {string} modelHash - Model hash for fetching local files + * @param {string} filePath - File path for fetching local files */ -async function loadExampleImages(images, modelHash) { +async function loadExampleImages(images, modelHash, filePath) { try { const showcaseTab = document.getElementById('showcase-tab'); if (!showcaseTab) return; // First fetch local example files let localFiles = []; - if (modelHash) { - try { - localFiles = await getExampleImageFiles(modelHash); - } catch (error) { - console.error("Failed to get example files:", error); + try { + // Choose endpoint based on centralized examples setting + const useCentralized = state.global.settings.useCentralizedExamples !== false; + const endpoint = useCentralized ? '/api/example-image-files' : '/api/model-example-files'; + + // Use different params based on endpoint + const params = useCentralized ? + `model_hash=${modelHash}` : + `file_path=${encodeURIComponent(filePath)}`; + + const response = await fetch(`${endpoint}?${params}`); + const result = await response.json(); + + if (result.success) { + localFiles = result.files; } - } + } catch (error) { + console.error("Failed to get example files:", error); + } // Then render with both remote images and local files showcaseTab.innerHTML = renderShowcaseContent(images, localFiles); diff --git a/static/js/components/loraModal/index.js b/static/js/components/loraModal/index.js index bf6124fb..730a2787 100644 --- a/static/js/components/loraModal/index.js +++ b/static/js/components/loraModal/index.js @@ -18,6 +18,7 @@ import { import { saveModelMetadata } from '../../api/loraApi.js'; import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js'; import { updateLoraCard } from '../../utils/cardUpdater.js'; +import { state } from '../../state/index.js'; /** * 显示LoRA模型弹窗 @@ -186,28 +187,42 @@ export function showLoraModal(lora) { loadRecipesForLora(lora.model_name, lora.sha256); // Load example images asynchronously - loadExampleImages(lora.civitai?.images, lora.sha256); + loadExampleImages(lora.civitai?.images, lora.sha256, lora.file_path); } /** * Load example images asynchronously * @param {Array} images - Array of image objects * @param {string} modelHash - Model hash for fetching local files + * @param {string} filePath - File path for fetching local files */ -async function loadExampleImages(images, modelHash) { +async function loadExampleImages(images, modelHash, filePath) { try { const showcaseTab = document.getElementById('showcase-tab'); if (!showcaseTab) return; // First fetch local example files let localFiles = []; - if (modelHash) { - try { - localFiles = await getExampleImageFiles(modelHash); - } catch (error) { - console.error("Failed to get example files:", error); + + try { + // Choose endpoint based on centralized examples setting + const useCentralized = state.global.settings.useCentralizedExamples !== false; + const endpoint = useCentralized ? '/api/example-image-files' : '/api/model-example-files'; + + // Use different params based on endpoint + const params = useCentralized ? + `model_hash=${modelHash}` : + `file_path=${encodeURIComponent(filePath)}`; + + const response = await fetch(`${endpoint}?${params}`); + const result = await response.json(); + + if (result.success) { + localFiles = result.files; } - } + } catch (error) { + console.error("Failed to get example files:", error); + } // Then render with both remote images and local files showcaseTab.innerHTML = renderShowcaseContent(images, localFiles); diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index 0fbaef06..766a540f 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -37,6 +37,11 @@ export class SettingsManager { state.global.settings.optimizeExampleImages = true; } + // Set default for useCentralizedExamples if undefined + if (state.global.settings.useCentralizedExamples === undefined) { + state.global.settings.useCentralizedExamples = true; + } + // Convert old boolean compactMode to new displayDensity string if (typeof state.global.settings.displayDensity === 'undefined') { if (state.global.settings.compactMode === true) { @@ -109,6 +114,14 @@ export class SettingsManager { optimizeExampleImagesCheckbox.checked = state.global.settings.optimizeExampleImages || false; } + // Set centralized examples setting + const useCentralizedExamplesCheckbox = document.getElementById('useCentralizedExamples'); + if (useCentralizedExamplesCheckbox) { + useCentralizedExamplesCheckbox.checked = state.global.settings.useCentralizedExamples !== false; + // Update dependent controls + this.updateExamplesControlsState(); + } + // Load default lora root await this.loadLoraRoots(); @@ -183,6 +196,10 @@ export class SettingsManager { state.global.settings.optimizeExampleImages = value; } else if (settingKey === 'compact_mode') { state.global.settings.compactMode = value; + } else if (settingKey === 'use_centralized_examples') { + state.global.settings.useCentralizedExamples = value; + // Update dependent controls state + this.updateExamplesControlsState(); } else { // For any other settings that might be added in the future state.global.settings[settingKey] = value; @@ -193,7 +210,7 @@ export class SettingsManager { try { // For backend settings, make API call - if (['show_only_sfw', 'blur_mature_content', 'autoplay_on_hover', 'optimize_example_images'].includes(settingKey)) { + if (['show_only_sfw', 'blur_mature_content', 'autoplay_on_hover', 'optimize_example_images', 'use_centralized_examples'].includes(settingKey)) { const payload = {}; payload[settingKey] = value; @@ -506,6 +523,42 @@ export class SettingsManager { // Add the appropriate density class grid.classList.add(`${density}-density`); } + + // Apply centralized examples toggle state + this.updateExamplesControlsState(); + } + + // Add new method to update example control states + updateExamplesControlsState() { + const useCentralized = state.global.settings.useCentralizedExamples !== false; + + // Find all controls that require centralized mode + const exampleSections = document.querySelectorAll('[data-requires-centralized="true"]'); + exampleSections.forEach(section => { + // Enable/disable all inputs and buttons in the section + const controls = section.querySelectorAll('input, button, select'); + controls.forEach(control => { + control.disabled = !useCentralized; + + // Add/remove disabled class for styling + if (control.classList.contains('primary-btn') || control.classList.contains('secondary-btn')) { + if (!useCentralized) { + control.classList.add('disabled'); + } else { + control.classList.remove('disabled'); + } + } + }); + + // Visually show the section as disabled + if (!useCentralized) { + section.style.opacity = '0.6'; + section.style.pointerEvents = 'none'; + } else { + section.style.opacity = ''; + section.style.pointerEvents = ''; + } + }); } } diff --git a/templates/components/modals.html b/templates/components/modals.html index fd22cce0..71bb2e44 100644 --- a/templates/components/modals.html +++ b/templates/components/modals.html @@ -256,6 +256,26 @@

Example Images

+
+
+ +
+
+ +
+
+
+ When enabled (recommended), example images are stored in a central folder for better organization and performance. + When disabled, only example images stored alongside models (e.g., model-name.example.0.jpg) will be shown, but download + and management features will be unavailable. +
+
+ +
@@ -273,7 +293,7 @@
-
+
@@ -293,7 +313,7 @@
-
+