From 7abfc49e08bd16ff53fbb1bf98251d7c95d2a62a Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Tue, 5 Aug 2025 11:23:20 +0800 Subject: [PATCH] feat: implement bulk operations for model management including delete, move, and refresh functionalities --- static/js/api/baseModelApi.js | 42 ++++ static/js/core.js | 7 + static/js/loras.js | 12 - static/js/managers/BulkManager.js | 389 +++++++++++++++-------------- static/js/managers/MoveManager.js | 3 +- static/js/state/index.js | 2 + templates/components/controls.html | 15 +- 7 files changed, 255 insertions(+), 215 deletions(-) diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js index 2efdd313..decd10cc 100644 --- a/static/js/api/baseModelApi.js +++ b/static/js/api/baseModelApi.js @@ -779,4 +779,46 @@ export class BaseModelApiClient { } return successFilePaths; } + + async bulkDeleteModels(filePaths) { + if (!filePaths || filePaths.length === 0) { + throw new Error('No file paths provided'); + } + + try { + state.loadingManager.showSimpleLoading(`Deleting ${this.apiConfig.config.displayName.toLowerCase()}s...`); + + const response = await fetch(this.apiConfig.endpoints.bulkDelete, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + file_paths: filePaths + }) + }); + + if (!response.ok) { + throw new Error(`Failed to delete ${this.apiConfig.config.displayName.toLowerCase()}s: ${response.statusText}`); + } + + const result = await response.json(); + + if (result.success) { + return { + success: true, + deleted_count: result.deleted_count, + failed_count: result.failed_count || 0, + errors: result.errors || [] + }; + } else { + throw new Error(result.error || `Failed to delete ${this.apiConfig.config.displayName.toLowerCase()}s`); + } + } catch (error) { + console.error(`Error during bulk delete of ${this.apiConfig.config.displayName.toLowerCase()}s:`, error); + throw error; + } finally { + state.loadingManager.hide(); + } + } } \ No newline at end of file diff --git a/static/js/core.js b/static/js/core.js index f2ff2dfa..ad28301d 100644 --- a/static/js/core.js +++ b/static/js/core.js @@ -5,6 +5,8 @@ import { modalManager } from './managers/ModalManager.js'; import { updateService } from './managers/UpdateService.js'; import { HeaderManager } from './components/Header.js'; import { settingsManager } from './managers/SettingsManager.js'; +import { moveManager } from './managers/MoveManager.js'; +import { bulkManager } from './managers/BulkManager.js'; import { exampleImagesManager } from './managers/ExampleImagesManager.js'; import { helpManager } from './managers/HelpManager.js'; import { bannerService } from './managers/BannerService.js'; @@ -33,11 +35,16 @@ export class AppCore { window.settingsManager = settingsManager; window.exampleImagesManager = exampleImagesManager; window.helpManager = helpManager; + window.moveManager = moveManager; + window.bulkManager = bulkManager; // Initialize UI components window.headerManager = new HeaderManager(); initTheme(); initBackToTop(); + + // Initialize the bulk manager + bulkManager.initialize(); // Initialize the example images manager exampleImagesManager.initialize(); diff --git a/static/js/loras.js b/static/js/loras.js index 43608ca6..e1177b93 100644 --- a/static/js/loras.js +++ b/static/js/loras.js @@ -1,7 +1,6 @@ import { appCore } from './core.js'; import { state } from './state/index.js'; import { updateCardsForBulkMode } from './components/shared/ModelCard.js'; -import { bulkManager } from './managers/BulkManager.js'; import { LoraContextMenu } from './components/ContextMenu/index.js'; import { createPageControls } from './components/controls/index.js'; import { confirmDelete, closeDeleteModal, confirmExclude, closeExcludeModal } from './utils/modalUtils.js'; @@ -33,14 +32,6 @@ class LoraPageManager { window.confirmExclude = confirmExclude; window.closeExcludeModal = closeExcludeModal; - // Bulk operations - window.toggleBulkMode = () => bulkManager.toggleBulkMode(); - window.clearSelection = () => bulkManager.clearSelection(); - window.toggleCardSelection = (card) => bulkManager.toggleCardSelection(card); - window.copyAllLorasSyntax = () => bulkManager.copyAllLorasSyntax(); - window.updateSelectedCount = () => bulkManager.updateSelectedCount(); - window.bulkManager = bulkManager; - // Expose duplicates manager window.modelDuplicatesManager = this.duplicatesManager; } @@ -54,9 +45,6 @@ class LoraPageManager { // Initialize cards for current bulk mode state (should be false initially) updateCardsForBulkMode(state.bulkMode); - // Initialize the bulk manager - bulkManager.initialize(); - // Initialize common page features (virtual scroll) appCore.initializePageFeatures(); } diff --git a/static/js/managers/BulkManager.js b/static/js/managers/BulkManager.js index 3207128e..f5c2f796 100644 --- a/static/js/managers/BulkManager.js +++ b/static/js/managers/BulkManager.js @@ -1,147 +1,201 @@ -import { state } from '../state/index.js'; +import { state, getCurrentPageState } from '../state/index.js'; import { showToast, copyToClipboard, sendLoraToWorkflow } from '../utils/uiHelpers.js'; import { updateCardsForBulkMode } from '../components/shared/ModelCard.js'; import { modalManager } from './ModalManager.js'; +import { moveManager } from './MoveManager.js'; import { getModelApiClient } from '../api/modelApiFactory.js'; +import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js'; export class BulkManager { constructor() { this.bulkBtn = document.getElementById('bulkOperationsBtn'); this.bulkPanel = document.getElementById('bulkOperationsPanel'); - this.isStripVisible = false; // Track strip visibility state + this.isStripVisible = false; - // Initialize selected loras set in state if not already there - if (!state.selectedLoras) { - state.selectedLoras = new Set(); - } + this.stripMaxThumbnails = 50; - // Cache for lora metadata to handle non-visible selected loras - if (!state.loraMetadataCache) { - state.loraMetadataCache = new Map(); - } - - this.stripMaxThumbnails = 50; // Maximum thumbnails to show in strip + // Model type specific action configurations + this.actionConfig = { + [MODEL_TYPES.LORA]: { + sendToWorkflow: true, + copyAll: true, + refreshAll: true, + moveAll: true, + deleteAll: true + }, + [MODEL_TYPES.EMBEDDING]: { + sendToWorkflow: false, + copyAll: false, + refreshAll: true, + moveAll: true, + deleteAll: true + }, + [MODEL_TYPES.CHECKPOINT]: { + sendToWorkflow: false, + copyAll: false, + refreshAll: true, + moveAll: false, + deleteAll: true + } + }; } initialize() { - // Add event listeners if needed - // (Already handled via onclick attributes in HTML, but could be moved here) - - // Add event listeners for the selected count to toggle thumbnail strip + this.setupEventListeners(); + this.setupGlobalKeyboardListeners(); + } + + setupEventListeners() { + // Bulk operations button listeners + const sendToWorkflowBtn = this.bulkPanel?.querySelector('[data-action="send-to-workflow"]'); + const copyAllBtn = this.bulkPanel?.querySelector('[data-action="copy-all"]'); + const refreshAllBtn = this.bulkPanel?.querySelector('[data-action="refresh-all"]'); + const moveAllBtn = this.bulkPanel?.querySelector('[data-action="move-all"]'); + const deleteAllBtn = this.bulkPanel?.querySelector('[data-action="delete-all"]'); + const clearBtn = this.bulkPanel?.querySelector('[data-action="clear"]'); + + if (sendToWorkflowBtn) { + sendToWorkflowBtn.addEventListener('click', () => this.sendAllModelsToWorkflow()); + } + if (copyAllBtn) { + copyAllBtn.addEventListener('click', () => this.copyAllModelsSyntax()); + } + if (refreshAllBtn) { + refreshAllBtn.addEventListener('click', () => this.refreshAllMetadata()); + } + if (moveAllBtn) { + moveAllBtn.addEventListener('click', () => { + moveManager.showMoveModal('bulk'); + }); + } + if (deleteAllBtn) { + deleteAllBtn.addEventListener('click', () => this.showBulkDeleteModal()); + } + if (clearBtn) { + clearBtn.addEventListener('click', () => this.clearSelection()); + } + + // Selected count click listener const selectedCount = document.getElementById('selectedCount'); if (selectedCount) { selectedCount.addEventListener('click', () => this.toggleThumbnailStrip()); } + } - // Add global keyboard event listener for Ctrl+A + setupGlobalKeyboardListeners() { document.addEventListener('keydown', (e) => { - // First check if any modal is currently open - if so, don't handle Ctrl+A if (modalManager.isAnyModalOpen()) { - return; // Exit early - let the browser handle Ctrl+A within the modal + return; } - // Check if search input is currently focused - if so, don't handle Ctrl+A const searchInput = document.getElementById('searchInput'); if (searchInput && document.activeElement === searchInput) { - return; // Exit early - let the browser handle Ctrl+A within the search input + return; } - // Check if it's Ctrl+A (or Cmd+A on Mac) if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'a') { - - // Prevent default browser "Select All" behavior e.preventDefault(); - - // If not in bulk mode, enable it first if (!state.bulkMode) { this.toggleBulkMode(); - // Small delay to ensure DOM is updated - setTimeout(() => this.selectAllVisibleLoras(), 50); + setTimeout(() => this.selectAllVisibleModels(), 50); } else { - this.selectAllVisibleLoras(); + this.selectAllVisibleModels(); } } else if (e.key === 'Escape' && state.bulkMode) { - // If in bulk mode, exit it on Escape this.toggleBulkMode(); } else if (e.key.toLowerCase() === 'b') { - // If 'b' is pressed, toggle bulk mode this.toggleBulkMode(); } }); } toggleBulkMode() { - // Toggle the state state.bulkMode = !state.bulkMode; - // Update UI this.bulkBtn.classList.toggle('active', state.bulkMode); - // Important: Remove the hidden class when entering bulk mode if (state.bulkMode) { this.bulkPanel.classList.remove('hidden'); - // Use setTimeout to ensure the DOM updates before adding visible class - // This helps with the transition animation + this.updateActionButtonsVisibility(); setTimeout(() => { this.bulkPanel.classList.add('visible'); }, 10); } else { this.bulkPanel.classList.remove('visible'); - // Add hidden class back after transition completes setTimeout(() => { this.bulkPanel.classList.add('hidden'); - }, 400); // Match this with the transition duration in CSS - - // Hide thumbnail strip if it's visible + }, 400); this.hideThumbnailStrip(); } - // First update all cards' visual state before clearing selection updateCardsForBulkMode(state.bulkMode); - // Clear selection if exiting bulk mode - do this after updating cards if (!state.bulkMode) { this.clearSelection(); - // TODO: fix this, no DOM manipulation should be done here - // Force a lightweight refresh of the cards to ensure proper display - // This is less disruptive than a full resetAndReload() + // TODO: document.querySelectorAll('.model-card').forEach(card => { - // Re-apply normal display mode to all card actions const actions = card.querySelectorAll('.card-actions, .card-button'); actions.forEach(action => action.style.display = 'flex'); }); } } + updateActionButtonsVisibility() { + const currentModelType = state.currentPageType; + const config = this.actionConfig[currentModelType]; + + if (!config) return; + + // Update button visibility based on model type + const sendToWorkflowBtn = this.bulkPanel?.querySelector('[data-action="send-to-workflow"]'); + const copyAllBtn = this.bulkPanel?.querySelector('[data-action="copy-all"]'); + const refreshAllBtn = this.bulkPanel?.querySelector('[data-action="refresh-all"]'); + const moveAllBtn = this.bulkPanel?.querySelector('[data-action="move-all"]'); + const deleteAllBtn = this.bulkPanel?.querySelector('[data-action="delete-all"]'); + + if (sendToWorkflowBtn) { + sendToWorkflowBtn.style.display = config.sendToWorkflow ? 'block' : 'none'; + } + if (copyAllBtn) { + copyAllBtn.style.display = config.copyAll ? 'block' : 'none'; + } + if (refreshAllBtn) { + refreshAllBtn.style.display = config.refreshAll ? 'block' : 'none'; + } + if (moveAllBtn) { + moveAllBtn.style.display = config.moveAll ? 'block' : 'none'; + } + if (deleteAllBtn) { + deleteAllBtn.style.display = config.deleteAll ? 'block' : 'none'; + } + } + clearSelection() { document.querySelectorAll('.model-card.selected').forEach(card => { card.classList.remove('selected'); }); - state.selectedLoras.clear(); + state.selectedModels.clear(); this.updateSelectedCount(); - - // Hide thumbnail strip if it's visible this.hideThumbnailStrip(); } updateSelectedCount() { const countElement = document.getElementById('selectedCount'); + const currentConfig = MODEL_CONFIG[state.currentPageType]; + const displayName = currentConfig?.displayName || 'Models'; if (countElement) { - // Set text content without the icon - countElement.textContent = `${state.selectedLoras.size} selected `; + countElement.textContent = `${state.selectedModels.size} ${displayName.toLowerCase()}(s) selected `; - // Update caret icon if it exists const existingCaret = countElement.querySelector('.dropdown-caret'); if (existingCaret) { existingCaret.className = `fas fa-caret-${this.isStripVisible ? 'down' : 'up'} dropdown-caret`; - existingCaret.style.visibility = state.selectedLoras.size > 0 ? 'visible' : 'hidden'; + existingCaret.style.visibility = state.selectedModels.size > 0 ? 'visible' : 'hidden'; } else { - // Create new caret icon if it doesn't exist const caretIcon = document.createElement('i'); caretIcon.className = `fas fa-caret-${this.isStripVisible ? 'down' : 'up'} dropdown-caret`; - caretIcon.style.visibility = state.selectedLoras.size > 0 ? 'visible' : 'hidden'; + caretIcon.style.visibility = state.selectedModels.size > 0 ? 'visible' : 'hidden'; countElement.appendChild(caretIcon); } } @@ -149,16 +203,18 @@ export class BulkManager { toggleCardSelection(card) { const filepath = card.dataset.filepath; + const pageState = getCurrentPageState(); if (card.classList.contains('selected')) { card.classList.remove('selected'); - state.selectedLoras.delete(filepath); + state.selectedModels.delete(filepath); } else { card.classList.add('selected'); - state.selectedLoras.add(filepath); + state.selectedModels.add(filepath); - // Cache the metadata for this lora - state.loraMetadataCache.set(filepath, { + // Cache the metadata for this model + const metadataCache = this.getMetadataCache(); + metadataCache.set(filepath, { fileName: card.dataset.file_name, usageTips: card.dataset.usage_tips, previewUrl: this.getCardPreviewUrl(card), @@ -169,35 +225,49 @@ export class BulkManager { this.updateSelectedCount(); - // Update thumbnail strip if it's visible if (this.isStripVisible) { this.updateThumbnailStrip(); } } + + getMetadataCache() { + const currentType = state.currentPageType; + const pageState = getCurrentPageState(); + + // Initialize metadata cache if it doesn't exist + if (currentType === MODEL_TYPES.LORA) { + if (!state.loraMetadataCache) { + state.loraMetadataCache = new Map(); + } + return state.loraMetadataCache; + } else { + if (!pageState.metadataCache) { + pageState.metadataCache = new Map(); + } + return pageState.metadataCache; + } + } - // Helper method to get preview URL from a card getCardPreviewUrl(card) { const img = card.querySelector('img'); const video = card.querySelector('video source'); return img ? img.src : (video ? video.src : '/loras_static/images/no-preview.png'); } - // Helper method to check if preview is a video isCardPreviewVideo(card) { return card.querySelector('video') !== null; } - // Apply selection state to cards after they are refreshed applySelectionState() { if (!state.bulkMode) return; document.querySelectorAll('.model-card').forEach(card => { const filepath = card.dataset.filepath; - if (state.selectedLoras.has(filepath)) { + if (state.selectedModels.has(filepath)) { card.classList.add('selected'); - // Update the cache with latest data - state.loraMetadataCache.set(filepath, { + const metadataCache = this.getMetadataCache(); + metadataCache.set(filepath, { fileName: card.dataset.file_name, usageTips: card.dataset.usage_tips, previewUrl: this.getCardPreviewUrl(card), @@ -212,30 +282,33 @@ export class BulkManager { this.updateSelectedCount(); } - async copyAllLorasSyntax() { - if (state.selectedLoras.size === 0) { + async copyAllModelsSyntax() { + if (state.currentPageType !== MODEL_TYPES.LORA) { + showToast('Copy syntax is only available for LoRAs', 'warning'); + return; + } + + if (state.selectedModels.size === 0) { showToast('No LoRAs selected', 'warning'); return; } const loraSyntaxes = []; const missingLoras = []; + const metadataCache = this.getMetadataCache(); - // Process all selected loras using our metadata cache - for (const filepath of state.selectedLoras) { - const metadata = state.loraMetadataCache.get(filepath); + for (const filepath of state.selectedModels) { + const metadata = metadataCache.get(filepath); if (metadata) { const usageTips = JSON.parse(metadata.usageTips || '{}'); const strength = usageTips.strength || 1; loraSyntaxes.push(``); } else { - // If we don't have metadata, this is an error case missingLoras.push(filepath); } } - // Handle any loras with missing metadata if (missingLoras.length > 0) { console.warn('Missing metadata for some selected loras:', missingLoras); showToast(`Missing data for ${missingLoras.length} LoRAs`, 'warning'); @@ -249,31 +322,33 @@ export class BulkManager { await copyToClipboard(loraSyntaxes.join(', '), `Copied ${loraSyntaxes.length} LoRA syntaxes to clipboard`); } - // Add method to send all selected loras to workflow - async sendAllLorasToWorkflow() { - if (state.selectedLoras.size === 0) { + async sendAllModelsToWorkflow() { + if (state.currentPageType !== MODEL_TYPES.LORA) { + showToast('Send to workflow is only available for LoRAs', 'warning'); + return; + } + + if (state.selectedModels.size === 0) { showToast('No LoRAs selected', 'warning'); return; } const loraSyntaxes = []; const missingLoras = []; + const metadataCache = this.getMetadataCache(); - // Process all selected loras using our metadata cache - for (const filepath of state.selectedLoras) { - const metadata = state.loraMetadataCache.get(filepath); + for (const filepath of state.selectedModels) { + const metadata = metadataCache.get(filepath); if (metadata) { const usageTips = JSON.parse(metadata.usageTips || '{}'); const strength = usageTips.strength || 1; loraSyntaxes.push(``); } else { - // If we don't have metadata, this is an error case missingLoras.push(filepath); } } - // Handle any loras with missing metadata if (missingLoras.length > 0) { console.warn('Missing metadata for some selected loras:', missingLoras); showToast(`Missing data for ${missingLoras.length} LoRAs`, 'warning'); @@ -284,82 +359,48 @@ export class BulkManager { return; } - // Send the loras to the workflow await sendLoraToWorkflow(loraSyntaxes.join(', '), false, 'lora'); } - // Show the bulk delete confirmation modal showBulkDeleteModal() { - if (state.selectedLoras.size === 0) { - showToast('No LoRAs selected', 'warning'); + if (state.selectedModels.size === 0) { + showToast('No models selected', 'warning'); return; } - // Update the count in the modal const countElement = document.getElementById('bulkDeleteCount'); if (countElement) { - countElement.textContent = state.selectedLoras.size; + countElement.textContent = state.selectedModels.size; } - // Show the modal modalManager.showModal('bulkDeleteModal'); } - // Confirm bulk delete action async confirmBulkDelete() { - if (state.selectedLoras.size === 0) { - showToast('No LoRAs selected', 'warning'); + if (state.selectedModels.size === 0) { + showToast('No models selected', 'warning'); modalManager.closeModal('bulkDeleteModal'); return; } - // Close the modal first before showing loading indicator modalManager.closeModal('bulkDeleteModal'); try { - // Show loading indicator - state.loadingManager.showSimpleLoading('Deleting models...'); + const apiClient = getModelApiClient(); + const filePaths = Array.from(state.selectedModels); - // Gather all file paths for deletion - const filePaths = Array.from(state.selectedLoras); - - // Call the backend API - const response = await fetch('/api/loras/bulk-delete', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - file_paths: filePaths - }) - }); - - const result = await response.json(); + const result = await apiClient.bulkDeleteModels(filePaths); if (result.success) { - showToast(`Successfully deleted ${result.deleted_count} models`, 'success'); + const currentConfig = MODEL_CONFIG[state.currentPageType]; + showToast(`Successfully deleted ${result.deleted_count} ${currentConfig.displayName.toLowerCase()}(s)`, 'success'); - // If virtual scroller exists, update the UI without page reload - if (state.virtualScroller) { - // Remove each deleted item from the virtual scroller - filePaths.forEach(path => { - state.virtualScroller.removeItemByFilePath(path); - }); - - // Clear the selection - this.clearSelection(); - } else { - // Clear the selection - this.clearSelection(); - - // Fall back to page reload for non-virtual scroll mode - setTimeout(() => { - window.location.reload(); - }, 1500); - } + filePaths.forEach(path => { + state.virtualScroller.removeItemByFilePath(path); + }); + this.clearSelection(); if (window.modelDuplicatesManager) { - // Update duplicates badge after refresh window.modelDuplicatesManager.updateDuplicatesBadgeAfterRefresh(); } } else { @@ -368,16 +409,11 @@ export class BulkManager { } catch (error) { console.error('Error during bulk delete:', error); showToast('Failed to delete models', 'error'); - } finally { - // Hide loading indicator - state.loadingManager.hide(); } } - // Create and show the thumbnail strip of selected LoRAs toggleThumbnailStrip() { - // If no items are selected, do nothing - if (state.selectedLoras.size === 0) return; + if (state.selectedModels.size === 0) return; const existing = document.querySelector('.selected-thumbnails-strip'); if (existing) { @@ -388,38 +424,30 @@ export class BulkManager { } showThumbnailStrip() { - // Create the thumbnail strip container const strip = document.createElement('div'); strip.className = 'selected-thumbnails-strip'; - // Create a container for the thumbnails (for scrolling) const thumbnailContainer = document.createElement('div'); thumbnailContainer.className = 'thumbnails-container'; strip.appendChild(thumbnailContainer); - // Position the strip above the bulk operations panel this.bulkPanel.parentNode.insertBefore(strip, this.bulkPanel); - // Populate the thumbnails this.updateThumbnailStrip(); - // Update strip visibility state and caret direction this.isStripVisible = true; - this.updateSelectedCount(); // Update caret + this.updateSelectedCount(); - // Add animation class after a short delay to trigger transition setTimeout(() => strip.classList.add('visible'), 10); } hideThumbnailStrip() { const strip = document.querySelector('.selected-thumbnails-strip'); - if (strip && this.isStripVisible) { // Only hide if actually visible + if (strip && this.isStripVisible) { strip.classList.remove('visible'); - // Update strip visibility state this.isStripVisible = false; - // Update caret without triggering another hide const countElement = document.getElementById('selectedCount'); if (countElement) { const caret = countElement.querySelector('.dropdown-caret'); @@ -428,7 +456,6 @@ export class BulkManager { } } - // Wait for animation to complete before removing setTimeout(() => { if (strip.parentNode) { strip.parentNode.removeChild(strip); @@ -441,33 +468,28 @@ export class BulkManager { const container = document.querySelector('.thumbnails-container'); if (!container) return; - // Clear existing thumbnails container.innerHTML = ''; - // Get all selected loras - const selectedLoras = Array.from(state.selectedLoras); + const selectedModels = Array.from(state.selectedModels); - // Create counter if we have more thumbnails than we'll show - if (selectedLoras.length > this.stripMaxThumbnails) { + if (selectedModels.length > this.stripMaxThumbnails) { const counter = document.createElement('div'); counter.className = 'strip-counter'; - counter.textContent = `Showing ${this.stripMaxThumbnails} of ${selectedLoras.length} selected`; + counter.textContent = `Showing ${this.stripMaxThumbnails} of ${selectedModels.length} selected`; container.appendChild(counter); } - // Limit the number of thumbnails to display - const thumbnailsToShow = selectedLoras.slice(0, this.stripMaxThumbnails); + const thumbnailsToShow = selectedModels.slice(0, this.stripMaxThumbnails); + const metadataCache = this.getMetadataCache(); - // Add a thumbnail for each selected LoRA (limited to max) thumbnailsToShow.forEach(filepath => { - const metadata = state.loraMetadataCache.get(filepath); + const metadata = metadataCache.get(filepath); if (!metadata) return; const thumbnail = document.createElement('div'); thumbnail.className = 'selected-thumbnail'; thumbnail.dataset.filepath = filepath; - // Create the visual element (image or video) if (metadata.isVideo) { thumbnail.innerHTML = `