mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 15:15:44 -03:00
Add example images access modal and API integration for checking image availability. Fixes #183 and #209
This commit is contained in:
@@ -45,6 +45,7 @@ class ExampleImagesRoutes:
|
|||||||
app.router.add_post('/api/resume-example-images', ExampleImagesRoutes.resume_example_images)
|
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_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/example-image-files', ExampleImagesRoutes.get_example_image_files)
|
||||||
|
app.router.add_get('/api/has-example-images', ExampleImagesRoutes.has_example_images)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def download_example_images(request):
|
async def download_example_images(request):
|
||||||
@@ -1245,3 +1246,63 @@ class ExampleImagesRoutes:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to update metadata after import: {e}", exc_info=True)
|
logger.error(f"Failed to update metadata after import: {e}", exc_info=True)
|
||||||
return []
|
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)
|
||||||
|
})
|
||||||
@@ -1009,3 +1009,76 @@ input:checked + .toggle-slider:before {
|
|||||||
[data-theme="dark"] .video-container {
|
[data-theme="dark"] .video-container {
|
||||||
background-color: rgba(255, 255, 255, 0.03);
|
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);
|
||||||
|
}
|
||||||
@@ -70,7 +70,7 @@ function handleLoraCardEvent(event) {
|
|||||||
|
|
||||||
if (event.target.closest('.fa-folder-open')) {
|
if (event.target.closest('.fa-folder-open')) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
openExampleImagesFolder(card.dataset.sha256);
|
handleExampleImagesAccess(card);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +200,142 @@ function copyLoraSyntax(card) {
|
|||||||
copyToClipboard(loraSyntax, 'LoRA syntax copied to clipboard');
|
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) {
|
export function createLoraCard(lora) {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'lora-card';
|
card.className = 'lora-card';
|
||||||
|
|||||||
@@ -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);
|
document.addEventListener('keydown', this.boundHandleEscape);
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -573,3 +573,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Example Images Access Modal -->
|
||||||
|
<div id="exampleAccessModal" class="modal">
|
||||||
|
<div class="modal-content example-access-modal">
|
||||||
|
<button class="close" onclick="modalManager.closeModal('exampleAccessModal')">×</button>
|
||||||
|
<h2>Local Example Images</h2>
|
||||||
|
<p>No local example images found for this model. View options:</p>
|
||||||
|
|
||||||
|
<div class="example-access-options">
|
||||||
|
<button id="downloadExamplesBtn" class="example-option-btn">
|
||||||
|
<i class="fas fa-cloud-download-alt"></i>
|
||||||
|
<span class="option-title">Download from Civitai</span>
|
||||||
|
<span class="option-desc">Save remote examples locally for offline use and faster loading</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="importExamplesBtn" class="example-option-btn">
|
||||||
|
<i class="fas fa-file-import"></i>
|
||||||
|
<span class="option-title">Import Your Own</span>
|
||||||
|
<span class="option-desc">Add your own custom examples for this model</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer-note">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<span>Remote examples are still viewable in the model details even without local copies</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user