Add example images access modal and API integration for checking image availability. Fixes #183 and #209

This commit is contained in:
Will Miao
2025-06-16 21:33:49 +08:00
parent 1ccaf33aac
commit 2cb4f3aac8
5 changed files with 313 additions and 2 deletions

View File

@@ -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 []
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)
})

View File

@@ -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);
}

View File

@@ -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';

View File

@@ -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;
}

View File

@@ -572,4 +572,32 @@
<button class="confirm-btn" id="confirmRelinkBtn">Confirm Re-link</button>
</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')">&times;</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>