From 2cb4f3aac86aac88af3a244298b08a9033199541 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Mon, 16 Jun 2025 21:33:49 +0800 Subject: [PATCH] Add example images access modal and API integration for checking image availability. Fixes #183 and #209 --- py/routes/example_images_routes.py | 63 ++++++++++++- static/css/components/modal.css | 73 +++++++++++++++ static/js/components/LoraCard.js | 138 ++++++++++++++++++++++++++++- static/js/managers/ModalManager.js | 13 +++ templates/components/modals.html | 28 ++++++ 5 files changed, 313 insertions(+), 2 deletions(-) diff --git a/py/routes/example_images_routes.py b/py/routes/example_images_routes.py index bb8c4cba..93a3329a 100644 --- a/py/routes/example_images_routes.py +++ b/py/routes/example_images_routes.py @@ -45,6 +45,7 @@ class ExampleImagesRoutes: app.router.add_post('/api/resume-example-images', ExampleImagesRoutes.resume_example_images) app.router.add_post('/api/open-example-images-folder', ExampleImagesRoutes.open_example_images_folder) app.router.add_get('/api/example-image-files', ExampleImagesRoutes.get_example_image_files) + app.router.add_get('/api/has-example-images', ExampleImagesRoutes.has_example_images) @staticmethod async def download_example_images(request): @@ -1244,4 +1245,64 @@ class ExampleImagesRoutes: except Exception as e: logger.error(f"Failed to update metadata after import: {e}", exc_info=True) - return [] \ No newline at end of file + return [] + + @staticmethod + async def has_example_images(request): + """ + Check if example images folder exists and is not empty for a model + + Expects: + - model_hash in query parameters + + Returns: + - Boolean value indicating if folder exists and has images/videos + """ + try: + # Get the model hash from query parameters + model_hash = request.query.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({ + 'has_images': False + }) + + # 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) or not os.path.isdir(model_folder): + return web.json_response({ + 'has_images': False + }) + + # Check if the folder has any supported media files + for file in os.listdir(model_folder): + file_path = os.path.join(model_folder, file) + if os.path.isfile(file_path): + file_ext = os.path.splitext(file)[1].lower() + if (file_ext in SUPPORTED_MEDIA_EXTENSIONS['images'] or + file_ext in SUPPORTED_MEDIA_EXTENSIONS['videos']): + return web.json_response({ + 'has_images': True + }) + + # If we reach here, the folder exists but has no supported media files + return web.json_response({ + 'has_images': False + }) + + except Exception as e: + logger.error(f"Failed to check example images folder: {e}", exc_info=True) + return web.json_response({ + 'has_images': False, + 'error': str(e) + }) \ No newline at end of file diff --git a/static/css/components/modal.css b/static/css/components/modal.css index dc6a9b0d..5e3bf739 100644 --- a/static/css/components/modal.css +++ b/static/css/components/modal.css @@ -1008,4 +1008,77 @@ input:checked + .toggle-slider:before { /* Dark theme adjustments */ [data-theme="dark"] .video-container { background-color: rgba(255, 255, 255, 0.03); +} + +/* Example Access Modal */ +.example-access-modal { + max-width: 550px; + text-align: center; +} + +.example-access-options { + display: flex; + flex-direction: column; + gap: var(--space-2); + margin: var(--space-3) 0; +} + +.example-option-btn { + display: flex; + flex-direction: column; + align-items: center; + padding: var(--space-2); + border-radius: var(--border-radius-sm); + border: 1px solid var(--lora-border); + background-color: var(--lora-surface); + cursor: pointer; + transition: all 0.2s; +} + +.example-option-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + border-color: var(--lora-accent); +} + +.example-option-btn i { + font-size: 2em; + margin-bottom: var(--space-1); + color: var(--lora-accent); +} + +.option-title { + font-weight: 500; + margin-bottom: 4px; + font-size: 1.1em; +} + +.option-desc { + font-size: 0.9em; + opacity: 0.8; +} + +.example-option-btn.disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.example-option-btn.disabled i { + color: var(--text-color); + opacity: 0.5; +} + +.modal-footer-note { + font-size: 0.9em; + opacity: 0.7; + margin-top: var(--space-2); + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +/* Dark theme adjustments */ +[data-theme="dark"] .example-option-btn:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); } \ No newline at end of file diff --git a/static/js/components/LoraCard.js b/static/js/components/LoraCard.js index 3b2b8475..292eb5bd 100644 --- a/static/js/components/LoraCard.js +++ b/static/js/components/LoraCard.js @@ -70,7 +70,7 @@ function handleLoraCardEvent(event) { if (event.target.closest('.fa-folder-open')) { event.stopPropagation(); - openExampleImagesFolder(card.dataset.sha256); + handleExampleImagesAccess(card); return; } @@ -200,6 +200,142 @@ function copyLoraSyntax(card) { copyToClipboard(loraSyntax, 'LoRA syntax copied to clipboard'); } +// New function to handle example images access +async function handleExampleImagesAccess(card) { + const modelHash = card.dataset.sha256; + + try { + // Check if example images exist + const response = await fetch(`/api/has-example-images?model_hash=${modelHash}`); + const data = await response.json(); + + if (data.has_images) { + // If images exist, open the folder directly (existing behavior) + openExampleImagesFolder(modelHash); + } else { + // If no images exist, show the new modal + showExampleAccessModal(card); + } + } catch (error) { + console.error('Error checking for example images:', error); + showToast('Error checking for example images', 'error'); + } +} + +// Function to show the example access modal +function showExampleAccessModal(card) { + const modal = document.getElementById('exampleAccessModal'); + if (!modal) return; + + // Get download button and determine if download should be enabled + const downloadBtn = modal.querySelector('#downloadExamplesBtn'); + let hasRemoteExamples = false; + + try { + const metaData = JSON.parse(card.dataset.meta || '{}'); + hasRemoteExamples = metaData.images && + Array.isArray(metaData.images) && + metaData.images.length > 0 && + metaData.images[0].url; + } catch (e) { + console.error('Error parsing meta data:', e); + } + + // Enable or disable download button + if (downloadBtn) { + if (hasRemoteExamples) { + downloadBtn.classList.remove('disabled'); + downloadBtn.removeAttribute('title'); // Remove any previous tooltip + downloadBtn.onclick = () => { + modalManager.closeModal('exampleAccessModal'); + // Open settings modal and scroll to example images section + const settingsModal = document.getElementById('settingsModal'); + if (settingsModal) { + modalManager.showModal('settingsModal'); + // Scroll to example images section after modal is visible + setTimeout(() => { + const exampleSection = settingsModal.querySelector('.settings-section:nth-child(5)'); // Example Images section + if (exampleSection) { + exampleSection.scrollIntoView({ behavior: 'smooth' }); + } + }, 300); + } + }; + } else { + downloadBtn.classList.add('disabled'); + downloadBtn.setAttribute('title', 'No remote example images available for this model on Civitai'); + downloadBtn.onclick = null; + } + } + + // Set up import button + const importBtn = modal.querySelector('#importExamplesBtn'); + if (importBtn) { + importBtn.onclick = () => { + modalManager.closeModal('exampleAccessModal'); + + // Get the lora data from card dataset + const loraMeta = { + sha256: card.dataset.sha256, + file_path: card.dataset.filepath, + model_name: card.dataset.name, + file_name: card.dataset.file_name, + // Other properties needed for showLoraModal + folder: card.dataset.folder, + modified: card.dataset.modified, + file_size: card.dataset.file_size, + from_civitai: card.dataset.from_civitai === 'true', + base_model: card.dataset.base_model, + usage_tips: card.dataset.usage_tips, + notes: card.dataset.notes, + favorite: card.dataset.favorite === 'true', + civitai: (() => { + try { + return JSON.parse(card.dataset.meta || '{}'); + } catch (e) { + return {}; + } + })(), + tags: JSON.parse(card.dataset.tags || '[]'), + modelDescription: card.dataset.modelDescription || '' + }; + + // Show the lora modal + showLoraModal(loraMeta); + + // Scroll to import area after modal is visible + setTimeout(() => { + const importArea = document.querySelector('.example-import-area'); + if (importArea) { + const showcaseTab = document.getElementById('showcase-tab'); + if (showcaseTab) { + // First make sure showcase tab is visible + const tabBtn = document.querySelector('.tab-btn[data-tab="showcase"]'); + if (tabBtn && !tabBtn.classList.contains('active')) { + tabBtn.click(); + } + + // Then toggle showcase if collapsed + const carousel = showcaseTab.querySelector('.carousel'); + if (carousel && carousel.classList.contains('collapsed')) { + const scrollIndicator = showcaseTab.querySelector('.scroll-indicator'); + if (scrollIndicator) { + scrollIndicator.click(); + } + } + + // Finally scroll to the import area + importArea.scrollIntoView({ behavior: 'smooth' }); + } + } + }, 500); + }; + } + + // Show the modal + modalManager.showModal('exampleAccessModal'); +} + export function createLoraCard(lora) { const card = document.createElement('div'); card.className = 'lora-card'; diff --git a/static/js/managers/ModalManager.js b/static/js/managers/ModalManager.js index 2785d558..636c36a4 100644 --- a/static/js/managers/ModalManager.js +++ b/static/js/managers/ModalManager.js @@ -234,6 +234,19 @@ export class ModalManager { }); } + // Add exampleAccessModal registration + const exampleAccessModal = document.getElementById('exampleAccessModal'); + if (exampleAccessModal) { + this.registerModal('exampleAccessModal', { + element: exampleAccessModal, + onClose: () => { + this.getModal('exampleAccessModal').element.style.display = 'none'; + document.body.classList.remove('modal-open'); + }, + closeOnOutsideClick: true + }); + } + document.addEventListener('keydown', this.boundHandleEscape); this.initialized = true; } diff --git a/templates/components/modals.html b/templates/components/modals.html index 1c5643ce..659c4dc8 100644 --- a/templates/components/modals.html +++ b/templates/components/modals.html @@ -572,4 +572,32 @@ + + + + \ No newline at end of file