feat(gallery): Add thumbnail check on startup

This commit is contained in:
Vito Sansevero
2025-06-19 16:56:04 -07:00
parent fccac4f93d
commit ff12c03619
+418
View File
@@ -92,6 +92,42 @@
display: flex !important;
flex-direction: column !important;
z-index: 10000 !important;
transition: transform 0.3s ease-in-out !important;
}
.metadata-sidebar.collapsed {
transform: translateX(340px) !important;
}
.metadata-collapse-btn {
position: absolute;
left: -40px;
top: 50%;
transform: translateY(-50%);
width: 32px;
height: 80px;
background: rgb(17, 24, 39);
border: 1px solid rgb(55, 65, 81);
border-right: none;
border-radius: 6px 0 0 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
z-index: 10001;
}
.metadata-collapse-btn:hover {
background: rgb(31, 41, 55);
}
.metadata-collapse-btn svg {
transition: transform 0.3s ease;
}
.metadata-sidebar.collapsed .metadata-collapse-btn svg {
transform: rotate(180deg);
}
</style>
</head>
@@ -260,6 +296,17 @@
💡 Thumbnails speed up gallery loading. Original images always used for viewing and metadata.
</div>
</div>
<!-- Check thumbnails at startup -->
<div class="flex items-center justify-between py-2 border-t border-gray-600">
<div>
<label class="text-sm text-gray-300">Check thumbnails at startup</label>
<p class="text-xs text-gray-500">Prompt to generate thumbnails if missing when opening gallery</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="checkThumbnailsAtStartup" class="sr-only peer" checked>
<div class="w-11 h-6 bg-gray-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-violet-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-violet-600"></div>
</label>
</div>
<!-- Progress bar (hidden by default) -->
<div id="thumbnailProgress" class="hidden">
<div class="flex items-center justify-between text-xs text-gray-400 mb-1">
@@ -518,6 +565,9 @@
this.initializeEventListeners();
this.loadImages();
// Check thumbnails at startup if enabled
this.checkThumbnailsAtStartup();
}
initializeEventListeners() {
@@ -850,6 +900,13 @@
sidebar.id = 'metadata-sidebar';
sidebar.className = 'metadata-sidebar';
sidebar.innerHTML = `
<!-- Collapse Button -->
<div class="metadata-collapse-btn" onclick="this.closest('.metadata-sidebar').classList.toggle('collapsed')">
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</div>
<!-- Header -->
<div class="flex items-center justify-between p-4 border-b border-gray-700">
<div class="flex items-center gap-2">
@@ -981,6 +1038,13 @@
<!-- Metadata sidebar (matching the screenshot) -->
<div class="metadata-sidebar">
<!-- Collapse Button -->
<div class="metadata-collapse-btn" onclick="this.closest('.metadata-sidebar').classList.toggle('collapsed')">
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</div>
<!-- Header -->
<div class="flex items-center justify-between p-4 border-b border-gray-700">
<div class="flex items-center gap-2">
@@ -1076,6 +1140,13 @@
<!-- Metadata sidebar (videos have limited metadata) -->
<div class="metadata-sidebar">
<!-- Collapse Button -->
<div class="metadata-collapse-btn" onclick="this.closest('.metadata-sidebar').classList.toggle('collapsed')">
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
</div>
<!-- Header -->
<div class="flex items-center justify-between p-4 border-b border-gray-700">
<div class="flex items-center gap-2">
@@ -1934,6 +2005,7 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
document.getElementById('showFilePathsToggle').checked = settings.showFilePaths;
document.getElementById('showImageInfoToggle').checked = settings.showImageInfo;
document.getElementById('debugModeToggle').checked = settings.debugMode;
document.getElementById('checkThumbnailsAtStartup').checked = settings.checkThumbnailsAtStartup;
// Load video settings
document.getElementById('videoAutoplayToggle').checked = settings.videoAutoplay;
@@ -1967,6 +2039,7 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
debugMode: false,
apiTimeout: 30,
thumbnailsGenerated: false,
checkThumbnailsAtStartup: true,
// Video settings
videoAutoplay: false,
videoMute: true,
@@ -1999,6 +2072,7 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
debugMode: document.getElementById('debugModeToggle').checked,
apiTimeout: parseInt(document.getElementById('apiTimeoutInput').value),
thumbnailsGenerated: this.getSettings().thumbnailsGenerated, // Preserve this
checkThumbnailsAtStartup: document.getElementById('checkThumbnailsAtStartup').checked,
// Video settings
videoAutoplay: document.getElementById('videoAutoplayToggle').checked,
videoMute: document.getElementById('videoMuteToggle').checked,
@@ -2552,6 +2626,350 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
this.loadImages();
}
}
async checkThumbnailsAtStartup() {
// Wait a moment to ensure page is fully loaded
setTimeout(async () => {
const settings = this.getSettings();
// Only check if the feature is enabled
if (!settings.checkThumbnailsAtStartup) {
console.debug('Thumbnail startup check disabled in settings');
return;
}
try {
// Check a sample of images to see if any are missing thumbnails
const response = await fetch('/prompt_manager/images/output?limit=50&offset=0');
const data = await response.json();
console.debug('Checking images for missing thumbnails:', data);
if (data.success && data.images && data.images.length > 0) {
// Count images missing thumbnails
let missingThumbnails = 0;
for (const image of data.images) {
if (!image.thumbnail_url) {
missingThumbnails++;
}
}
console.debug(`Found ${missingThumbnails} images without thumbnails in sample of ${data.images.length}`);
// If more than 10% of sampled images are missing thumbnails, show prompt
const missingPercentage = (missingThumbnails / data.images.length) * 100;
if (missingThumbnails > 0 && missingPercentage > 10) {
// Estimate total missing based on sample
const estimatedMissing = Math.round((missingThumbnails / data.images.length) * data.total);
console.debug(`Estimated ${estimatedMissing} total images missing thumbnails (${missingPercentage.toFixed(1)}% of sample)`);
this.showThumbnailGenerationPrompt(data.total, estimatedMissing);
} else if (missingThumbnails > 0) {
console.debug(`Only ${missingPercentage.toFixed(1)}% of images missing thumbnails, not showing prompt`);
} else {
console.debug('All sampled images have thumbnails');
}
} else {
console.debug('No images found or failed to fetch images');
}
} catch (error) {
console.debug('Could not check for missing thumbnails at startup:', error);
}
}, 1000); // Wait 1 second for page to settle
}
showThumbnailGenerationPrompt(imageCount, missingCount = imageCount) {
const modal = document.createElement('div');
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
modal.innerHTML = `
<div class="bg-gray-800 rounded-xl max-w-md w-full mx-4 border border-gray-700">
<div class="p-6">
<h3 class="text-xl font-semibold text-gray-100 mb-4 flex items-center">
<span class="mr-2">🚀</span>
Generate Thumbnails?
</h3>
<p class="text-gray-300 mb-4">
Found ${missingCount} images without thumbnails (${imageCount} total images). Would you like to generate thumbnails for faster loading?
</p>
<p class="text-sm text-gray-500 mb-6">
💡 Thumbnails speed up gallery loading significantly. You can change this setting in Gallery Settings.
</p>
<div class="flex space-x-3">
<button id="thumbnailPromptNo" class="flex-1 px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-colors">
Not Now
</button>
<button id="thumbnailPromptYes" class="flex-1 px-4 py-2 bg-violet-600 hover:bg-violet-700 text-white rounded-lg transition-colors">
Generate
</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
// Handle button clicks
document.getElementById('thumbnailPromptNo').addEventListener('click', () => {
document.body.removeChild(modal);
});
document.getElementById('thumbnailPromptYes').addEventListener('click', () => {
// Don't remove modal yet - transform it to show progress
this.startThumbnailGenerationInModal(modal, imageCount, missingCount);
});
// Close on outside click
modal.addEventListener('click', (e) => {
if (e.target === modal) {
document.body.removeChild(modal);
}
});
}
async startThumbnailGenerationInModal(modal, imageCount, missingCount) {
// Transform the modal to show progress
const modalContent = modal.querySelector('.bg-gray-800');
modalContent.innerHTML = `
<div class="p-6">
<h3 class="text-xl font-semibold text-gray-100 mb-4 flex items-center">
<span class="mr-2">🚀</span>
Generating Thumbnails
</h3>
<p class="text-gray-300 mb-4">
Processing ${missingCount} images out of ${imageCount} total...
</p>
<!-- Progress Section -->
<div class="mb-6">
<div class="flex items-center justify-between text-sm text-gray-400 mb-2">
<span id="modalProgressText">Initializing...</span>
<span id="modalProgressPercent">0%</span>
</div>
<div class="w-full bg-gray-600 rounded-full h-3">
<div id="modalProgressBar" class="bg-violet-600 h-3 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
<div class="flex items-center justify-between text-xs text-gray-500 mt-2">
<span id="modalProgressDetails">Starting thumbnail generation...</span>
<span id="modalProgressETA" class="text-gray-600"></span>
</div>
</div>
<!-- Status Messages -->
<div id="modalStatusMessages" class="text-sm text-gray-400 mb-4 max-h-32 overflow-y-auto custom-scrollbar">
<div class="text-violet-400">• Starting thumbnail generation...</div>
</div>
<!-- Buttons -->
<div class="flex space-x-3">
<button id="modalCancelBtn" class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors">
Cancel
</button>
<button id="modalCloseBtn" class="flex-1 px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-colors hidden">
Close
</button>
</div>
</div>
`;
// Setup progress tracking
let isCancelled = false;
const progressText = modal.querySelector('#modalProgressText');
const progressPercent = modal.querySelector('#modalProgressPercent');
const progressBar = modal.querySelector('#modalProgressBar');
const progressDetails = modal.querySelector('#modalProgressDetails');
const progressETA = modal.querySelector('#modalProgressETA');
const statusMessages = modal.querySelector('#modalStatusMessages');
const cancelBtn = modal.querySelector('#modalCancelBtn');
const closeBtn = modal.querySelector('#modalCloseBtn');
// Add status message helper
const addStatusMessage = (message, type = 'info') => {
const colors = {
'info': 'text-gray-400',
'success': 'text-green-400',
'error': 'text-red-400',
'warning': 'text-yellow-400'
};
const msgEl = document.createElement('div');
msgEl.className = colors[type] || colors.info;
msgEl.textContent = `• ${message}`;
statusMessages.appendChild(msgEl);
statusMessages.scrollTop = statusMessages.scrollHeight;
};
// Handle cancel
cancelBtn.addEventListener('click', () => {
isCancelled = true;
addStatusMessage('Cancellation requested...', 'warning');
cancelBtn.disabled = true;
cancelBtn.textContent = 'Cancelling...';
});
// Handle close (only appears when done)
closeBtn.addEventListener('click', () => {
document.body.removeChild(modal);
// Refresh the gallery to show new thumbnails
this.loadImages();
});
// Disable outside click while processing
modal.removeEventListener('click', modal.clickHandler);
try {
addStatusMessage('Connecting to thumbnail generation service...');
// Start thumbnail generation with progress monitoring
await this.generateThumbnailsWithProgressInModal({
progressText,
progressPercent,
progressBar,
progressDetails,
progressETA,
addStatusMessage,
isCancelled: () => isCancelled
});
// Show completion
if (!isCancelled) {
progressText.textContent = 'Completed!';
progressPercent.textContent = '100%';
progressBar.style.width = '100%';
addStatusMessage('Thumbnail generation completed successfully!', 'success');
// Update settings
const settings = this.getSettings();
settings.thumbnailsGenerated = true;
localStorage.setItem('gallerySettings', JSON.stringify(settings));
// Show notification
this.showNotification('Thumbnails generated successfully!', 'success');
}
} catch (error) {
console.error('Thumbnail generation failed:', error);
addStatusMessage(`Generation failed: ${error.message}`, 'error');
progressBar.classList.add('bg-red-600');
progressBar.classList.remove('bg-violet-600');
} finally {
// Show close button and hide cancel
cancelBtn.classList.add('hidden');
closeBtn.classList.remove('hidden');
// Re-enable outside click to close
setTimeout(() => {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
document.body.removeChild(modal);
// Refresh the gallery to show new thumbnails
this.loadImages();
}
});
}, 100);
}
}
async generateThumbnailsWithProgressInModal(uiElements) {
const { progressText, progressPercent, progressBar, progressDetails, progressETA, addStatusMessage, isCancelled } = uiElements;
addStatusMessage('Starting thumbnail generation...');
// Get quality setting
const quality = this.getSettings().imageQuality || 'medium';
try {
// Start the progress stream
const response = await fetch('/prompt_manager/images/generate-thumbnails/progress', {
method: 'GET',
headers: {
'Accept': 'text/event-stream',
'Cache-Control': 'no-cache'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let startTime = Date.now();
while (true) {
if (isCancelled()) {
addStatusMessage('Generation cancelled by user', 'warning');
break;
}
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// Process complete lines
let lines = buffer.split('\n');
buffer = lines.pop(); // Keep incomplete line in buffer
for (let line of lines) {
line = line.trim();
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.type === 'progress') {
// Update progress UI
const progress = Math.round(data.progress || 0);
progressPercent.textContent = `${progress}%`;
progressBar.style.width = `${progress}%`;
if (data.status) {
progressText.textContent = data.status;
}
if (data.processed !== undefined && data.found !== undefined) {
progressDetails.textContent = `Processed: ${data.processed}, Generated: ${data.found}`;
}
// Calculate ETA
if (progress > 5) {
const elapsed = (Date.now() - startTime) / 1000;
const estimated = (elapsed / progress) * 100;
const remaining = Math.max(0, estimated - elapsed);
if (remaining > 60) {
progressETA.textContent = `~${Math.round(remaining / 60)}m remaining`;
} else {
progressETA.textContent = `~${Math.round(remaining)}s remaining`;
}
}
// Add status messages for milestones
if (progress === 25) addStatusMessage('25% complete - processing continues...');
if (progress === 50) addStatusMessage('50% complete - halfway there!');
if (progress === 75) addStatusMessage('75% complete - almost done!');
} else if (data.type === 'complete') {
addStatusMessage(`Completed: ${data.generated} new, ${data.skipped} skipped, ${data.total} total`, 'success');
addStatusMessage(`Processing time: ${data.elapsed_time}s`, 'info');
break;
} else if (data.type === 'error') {
throw new Error(data.error || 'Unknown error during generation');
}
} catch (parseError) {
console.debug('Parse error for line:', line, parseError);
}
}
}
}
} catch (error) {
addStatusMessage(`Error: ${error.message}`, 'error');
throw error;
}
}
}
// Initialize gallery when page loads