diff --git a/js/shared/galleryGrid.js b/js/shared/galleryGrid.js index 75e313d..a07c0cf 100644 --- a/js/shared/galleryGrid.js +++ b/js/shared/galleryGrid.js @@ -7,7 +7,8 @@ import { getThumbnailSize } from "./config.js"; import { formatFileSize } from "../reports/reportGenerator.js"; import { actions, selectors } from "./stateManager.js"; -import { copyImageToClipboard, generateThumbnail } from "./imageUtils.js"; +import { copyImageToClipboard } from "./imageUtils.js"; +import { getLazyLoader } from "./lazyImageLoader.js"; import { showFullImage } from "./imageViewer.js"; /** @@ -52,58 +53,35 @@ export function createImageItem(image, showImageContextMenu, showMetadata) { background: #333; `; - // Set thumbnail source with error handling + // Set thumbnail source with lazy loader and error handling const thumbnailSizeConfig = getThumbnailSize(selectors.thumbnailSize()); - - // Load thumbnail using centralized loader - const loadThumbnailUrl = async () => { - try { - const sizeName = thumbnailSizeConfig.width <= 120 ? 'small' : - thumbnailSizeConfig.width <= 200 ? 'medium' : 'large'; - return await generateThumbnail(image, sizeName); - } catch (error) { - console.error('Error generating thumbnail:', error); - return null; - } - }; + const sizeName = thumbnailSizeConfig.width <= 120 ? 'small' : + thumbnailSizeConfig.width <= 200 ? 'medium' : 'large'; + + const errorDiv = document.createElement('div'); + errorDiv.style.cssText = ` + width: 100%; + height: 150px; + display: flex; + align-items: center; + justify-content: center; + background: #333; + color: #999; + font-size: 12px; + position: absolute; + inset: 0; + `; + errorDiv.textContent = 'Thumbnail failed'; + errorDiv.style.display = 'none'; + item.appendChild(errorDiv); - loadThumbnailUrl().then(thumbnailUrl => { - if (thumbnailUrl) { - thumbnail.src = thumbnailUrl; - } else { - thumbnail.style.display = 'none'; - const errorDiv = document.createElement('div'); - errorDiv.style.cssText = ` - width: 100%; - height: 150px; - display: flex; - align-items: center; - justify-content: center; - background: #333; - color: #999; - font-size: 12px; - `; - errorDiv.textContent = 'Thumbnail failed'; - item.appendChild(errorDiv); - } - }); - thumbnail.addEventListener('error', () => { thumbnail.style.display = 'none'; - const errorDiv = document.createElement('div'); - errorDiv.style.cssText = ` - width: 100%; - height: 150px; - display: flex; - align-items: center; - justify-content: center; - background: #333; - color: #888; - font-size: 12px; - `; - errorDiv.textContent = '❌ Thumbnail failed'; - item.insertBefore(errorDiv, thumbnail.nextSibling); + errorDiv.style.display = 'flex'; }); + + const lazyLoader = getLazyLoader(); + lazyLoader.observe(thumbnail, image, sizeName); // Create overlay with filename and info const overlay = document.createElement('div'); diff --git a/js/shared/lazyImageLoader.js b/js/shared/lazyImageLoader.js index b77c457..2993616 100644 --- a/js/shared/lazyImageLoader.js +++ b/js/shared/lazyImageLoader.js @@ -9,6 +9,12 @@ import { loadThumbnail } from './imageLoader.js'; +function getImagePath(imageInput) { + if (!imageInput) return null; + if (typeof imageInput === 'string') return imageInput; + return imageInput.path || imageInput.relative_path || imageInput.name || null; +} + /** * Configuration for lazy loading and retry behavior */ @@ -79,11 +85,17 @@ export class LazyImageLoader { * @param {string} imagePath - Path to the image * @param {string} thumbnailSize - Size of thumbnail to load */ - observe(img, imagePath, thumbnailSize = 'large') { + observe(img, imageInput, thumbnailSize = 'large') { if (!this.observer) { this.init(); } + const imagePath = getImagePath(imageInput); + if (!imagePath) { + console.warn('[LazyImageLoader] No image path available for lazy load', imageInput); + return; + } + // Store metadata on element img.dataset.imagePath = imagePath; img.dataset.thumbnailSize = thumbnailSize; @@ -149,7 +161,7 @@ export class LazyImageLoader { try { // Attempt to load thumbnail - const thumbnailUrl = await loadThumbnail({ path: imagePath }, thumbnailSize); + const thumbnailUrl = await loadThumbnail(imagePath, thumbnailSize); // Set the image source img.src = thumbnailUrl; diff --git a/js/sidebar/imageGalleryTab.js b/js/sidebar/imageGalleryTab.js index 2f655f3..478ca53 100644 --- a/js/sidebar/imageGalleryTab.js +++ b/js/sidebar/imageGalleryTab.js @@ -311,7 +311,27 @@ function setupGalleryEventHandlers(folderAndControls, unused, grid, metadata, he // Show loading indicator in grid with immediate count update grid.imageCountSpan.textContent = '(Loading...)'; + let progressStarted = false; + const ensureProgressContainer = () => { + if (!progressContainer || !grid.gridContainer.contains(progressContainer)) { + if (progressContainer && progressContainer.parentNode) { + progressContainer.remove(); + } + if (!progressStarted) { + grid.gridContainer.innerHTML = ''; + } + progressContainer = document.createElement('div'); + progressContainer.className = 'gallery-loading-progress'; + if (grid.gridContainer.firstChild) { + grid.gridContainer.insertBefore(progressContainer, grid.gridContainer.firstChild); + } else { + grid.gridContainer.appendChild(progressContainer); + } + progressStarted = true; + } + }; + // Create a progress update function const updateProgress = async (current, total, message = '') => { // Don't update if aborted @@ -319,12 +339,7 @@ function setupGalleryEventHandlers(folderAndControls, unused, grid, metadata, he return; } - if (!progressContainer) { - grid.gridContainer.innerHTML = ''; - progressContainer = document.createElement('div'); - progressContainer.className = 'gallery-loading-progress'; - grid.gridContainer.appendChild(progressContainer); - } + ensureProgressContainer(); const percentage = total > 0 ? Math.round((current / total) * 100) : 0; @@ -337,6 +352,7 @@ function setupGalleryEventHandlers(folderAndControls, unused, grid, metadata, he const fill = progressContainer.querySelector('.gallery-progress-bar__fill'); if (fill) { fill.style.setProperty('--fill-width', `${percentage}%`); + fill.style.width = `${percentage}%`; } }; @@ -396,6 +412,15 @@ function setupGalleryEventHandlers(folderAndControls, unused, grid, metadata, he renderImageGrid(currentImages, currentFolders, loadImagesWrapper) .catch(err => console.warn('Gallery: Final re-render after metadata completion failed:', err)); } + + // Remove any remaining loading banner after metadata completes + if (progressContainer && progressContainer.parentNode) { + setTimeout(() => { + if (progressContainer && progressContainer.parentNode) { + progressContainer.remove(); + } + }, 300); + } } }; @@ -422,14 +447,14 @@ function setupGalleryEventHandlers(folderAndControls, unused, grid, metadata, he // Update UI immediately with actual counts grid.imageCountSpan.textContent = `${images.length} images / ${folders.length} folders`; - // Clear the progress container if it exists + // Render image grid with both images and folders immediately + await renderImageGrid(images, folders, loadImagesWrapper); + + // Remove the gallery load progress banner after initial grid render if (progressContainer && progressContainer.parentNode) { progressContainer.remove(); } - // Render image grid with both images and folders immediately - await renderImageGrid(images, folders, loadImagesWrapper); - // Auto-show metadata for the first image if available if (images && images.length > 0) { showImageMetadata(images[0]); @@ -485,7 +510,11 @@ function setupGalleryEventHandlers(folderAndControls, unused, grid, metadata, he // Only clear if not doing incremental update if (!incremental) { + const loadingProgress = grid.gridContainer.querySelector('.gallery-loading-progress'); grid.gridContainer.innerHTML = ''; + if (loadingProgress) { + grid.gridContainer.appendChild(loadingProgress); + } } if (!images || (images.length === 0 && folders.length === 0)) { @@ -611,6 +640,7 @@ function setupGalleryEventHandlers(folderAndControls, unused, grid, metadata, he const batchSize = 20; // Process 20 images at a time const totalImages = images.length; let processedCount = 0; + let renderedCount = incremental ? grid.gridContainer.querySelectorAll('.gallery-image-item').length : 0; // Find or create progress container let progressContainer = grid.gridContainer.querySelector('.gallery-progress-container'); @@ -640,13 +670,9 @@ function setupGalleryEventHandlers(folderAndControls, unused, grid, metadata, he const fill = progressContainer.querySelector('.gallery-thumbnail-progress__fill'); if (fill) { fill.style.setProperty('--fill-width', `${percentage}%`); + fill.style.width = `${percentage}%`; } - }; - - await updateProgress(); - - // Track which images have been rendered (by index) - let renderedCount = incremental ? grid.gridContainer.querySelectorAll('.gallery-image-item').length : 0; + } // For large folders, show skeleton loaders first for better perceived performance const skeletonThreshold = 100; // Show skeletons for folders with 100+ images