From 1121d1ee6cfd3639a1ba6c2e9d5108257667f39c Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Fri, 9 May 2025 16:14:10 +0800 Subject: [PATCH] Revert "update" This reverts commit 4793f096af3e4c77b9bddc5def073381fe77ba5f. --- static/js/api/baseModelApi.js | 245 +-------------------- static/js/api/loraApi.js | 15 +- static/js/components/LoraCard.js | 352 +++++++++++++++---------------- static/js/loras.js | 11 +- 4 files changed, 187 insertions(+), 436 deletions(-) diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js index 22353ba5..46c13a42 100644 --- a/static/js/api/baseModelApi.js +++ b/static/js/api/baseModelApi.js @@ -1,177 +1,13 @@ // filepath: d:\Workspace\ComfyUI\custom_nodes\ComfyUI-Lora-Manager\static\js\api\baseModelApi.js import { state, getCurrentPageState } from '../state/index.js'; import { showToast } from '../utils/uiHelpers.js'; +import { showDeleteModal, confirmDelete } from '../utils/modalUtils.js'; import { getSessionItem, saveMapToStorage } from '../utils/storageHelpers.js'; /** * Shared functionality for handling models (loras and checkpoints) */ -// Virtual scrolling configuration -const VIRTUAL_SCROLL_CONFIG = { - MAX_DOM_CARDS: 300, // Maximum DOM elements to keep - BUFFER_SIZE: 20, // Extra items to render above/below viewport - CLEANUP_INTERVAL: 5000, // How often to check for cards to clean up (ms) -} - -// Track rendered items and all loaded items -const virtualScrollState = { - visibleItems: new Map(), // Track rendered items by filepath - allItems: [], // All data items loaded so far - observer: null, // IntersectionObserver for visibility tracking - cleanupTimer: null, // Timer for periodic cleanup - initialized: false // Whether virtual scrolling is initialized -} - -// Initialize virtual scrolling -function initVirtualScroll(modelType) { - if (virtualScrollState.initialized) return; - - const gridId = modelType === 'checkpoint' ? 'checkpointGrid' : 'loraGrid'; - const gridElement = document.getElementById(gridId); - if (!gridElement) return; - - // Create intersection observer to track visible cards - virtualScrollState.observer = new IntersectionObserver((entries) => { - entries.forEach(entry => { - const cardElement = entry.target; - const filepath = cardElement.dataset.filepath; - - if (entry.isIntersecting) { - // Load media for cards entering viewport - lazyLoadCardMedia(cardElement); - } else { - // Card is no longer visible - if (entry.boundingClientRect.top < -1000 || entry.boundingClientRect.top > window.innerHeight + 1000) { - // If card is far outside viewport, consider removing it - virtualScrollState.visibleItems.delete(filepath); - cleanupCardResources(cardElement); - cardElement.remove(); - } - } - }); - }, { - rootMargin: '500px', // Start loading when within 500px of viewport - threshold: 0 - }); - - // Set up periodic cleanup for DOM elements - virtualScrollState.cleanupTimer = setInterval(() => { - checkCardThreshold(modelType); - }, VIRTUAL_SCROLL_CONFIG.CLEANUP_INTERVAL); - - // Set up scroll event listener for loading more content - window.addEventListener('scroll', throttle(() => { - const scrollPosition = window.scrollY + window.innerHeight; - const documentHeight = document.documentElement.scrollHeight; - - // If we're close to the bottom and not already loading, load more - if (scrollPosition > documentHeight - 1000) { - const pageState = getCurrentPageState(); - if (!pageState.isLoading && pageState.hasMore) { - // This will trigger loading more items using the existing pagination - const loadMoreFunction = modelType === 'checkpoint' ? - window.loadMoreCheckpoints : window.loadMoreLoras; - - if (typeof loadMoreFunction === 'function') { - loadMoreFunction(false, false); - } - } - } - }, 200)); - - virtualScrollState.initialized = true; -} - -// Clean up resources for a card -function cleanupCardResources(cardElement) { - try { - // Stop videos and free resources - const video = cardElement.querySelector('video'); - if (video) { - video.pause(); - video.src = ''; - video.load(); - } - - // Remove from observer - if (virtualScrollState.observer) { - virtualScrollState.observer.unobserve(cardElement); - } - } catch (e) { - console.error('Error cleaning up card resources:', e); - } -} - -// Lazy load media content in a card -function lazyLoadCardMedia(cardElement) { - // Lazy load images - const img = cardElement.querySelector('img[data-src]'); - if (img) { - img.src = img.dataset.src; - img.removeAttribute('data-src'); - } - - // Lazy load videos - const video = cardElement.querySelector('video[data-src]'); - if (video) { - video.src = video.dataset.src; - video.removeAttribute('data-src'); - - // Check if we should autoplay this video - const autoplayOnHover = state?.global?.settings?.autoplayOnHover || false; - - if (!autoplayOnHover) { - // If not in hover-only mode, autoplay videos when they enter viewport - video.muted = true; // Muted videos can autoplay without user interaction - video.play().catch(err => { - console.log("Could not autoplay video, likely due to browser policy:", err); - }); - } - } -} - -// Check if we need to clean up any cards -function checkCardThreshold(modelType) { - const gridId = modelType === 'checkpoint' ? 'checkpointGrid' : 'loraGrid'; - const cards = document.querySelectorAll(`#${gridId} .lora-card`); - - if (cards.length > VIRTUAL_SCROLL_CONFIG.MAX_DOM_CARDS) { - // We have more cards than our threshold, remove those far from viewport - const cardsToRemove = cards.length - VIRTUAL_SCROLL_CONFIG.MAX_DOM_CARDS; - console.log(`Cleaning up ${cardsToRemove} cards to maintain performance`); - - let removedCount = 0; - cards.forEach(card => { - if (removedCount >= cardsToRemove) return; - - const rect = card.getBoundingClientRect(); - // Remove cards that are far outside viewport - if (rect.bottom < -1000 || rect.top > window.innerHeight + 1000) { - const filepath = card.dataset.filepath; - virtualScrollState.visibleItems.delete(filepath); - cleanupCardResources(card); - card.remove(); - removedCount++; - } - }); - } -} - -// Utility function to throttle function calls -function throttle(func, limit) { - let inThrottle; - return function() { - const args = arguments; - const context = this; - if (!inThrottle) { - func.apply(context, args); - inThrottle = true; - setTimeout(() => inThrottle = false, limit); - } - } -} - // Generic function to load more models with pagination export async function loadMoreModels(options = {}) { const { @@ -190,20 +26,13 @@ export async function loadMoreModels(options = {}) { document.body.classList.add('loading'); try { - // Initialize virtual scrolling if not already done - initVirtualScroll(modelType); - - // Reset pagination and state if requested + // Reset to first page if requested if (resetPage) { pageState.currentPage = 1; - - // Clear the grid and virtual scroll state + // Clear grid if resetting const gridId = modelType === 'checkpoint' ? 'checkpointGrid' : 'loraGrid'; const grid = document.getElementById(gridId); if (grid) grid.innerHTML = ''; - - virtualScrollState.visibleItems.clear(); - virtualScrollState.allItems = []; } const params = new URLSearchParams({ @@ -306,23 +135,10 @@ export async function loadMoreModels(options = {}) { } else if (data.items.length > 0) { pageState.hasMore = pageState.currentPage < data.total_pages; - // Add new items to our collection of all items - virtualScrollState.allItems = [...virtualScrollState.allItems, ...data.items]; - - // Create and append cards with optimized rendering + // Append model cards using the provided card creation function data.items.forEach(model => { - // Skip if we already have this card rendered - if (virtualScrollState.visibleItems.has(model.file_path)) return; - - // Create the card with lazy loading for media - const card = createOptimizedCard(model, createCardFunction); + const card = createCardFunction(model); grid.appendChild(card); - - // Track this card and observe it - virtualScrollState.visibleItems.set(model.file_path, card); - if (virtualScrollState.observer) { - virtualScrollState.observer.observe(card); - } }); // Increment the page number AFTER successful loading @@ -344,57 +160,6 @@ export async function loadMoreModels(options = {}) { } } -// Create a card with optimizations for lazy loading media -function createOptimizedCard(model, createCardFunction) { - // Create the card using the original function - const card = createCardFunction(model); - - // Optimize image/video loading - const img = card.querySelector('img'); - if (img) { - // Replace src with data-src to defer loading - img.dataset.src = img.src; - img.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; // Tiny transparent placeholder - } - - const video = card.querySelector('video'); - if (video) { - const source = video.querySelector('source'); - if (source) { - // Store the video source for lazy loading - video.dataset.src = source.src; - source.removeAttribute('src'); - } else if (video.src) { - // Handle direct src attribute - video.dataset.src = video.src; - video.removeAttribute('src'); - } - - // Save autoplay state but prevent autoplay until visible - if (video.hasAttribute('autoplay')) { - video.dataset.autoplay = 'true'; - video.removeAttribute('autoplay'); - } - } - - return card; -} - -// Clean up virtual scroll when page changes -export function cleanupVirtualScroll() { - if (virtualScrollState.observer) { - virtualScrollState.observer.disconnect(); - } - - if (virtualScrollState.cleanupTimer) { - clearInterval(virtualScrollState.cleanupTimer); - } - - virtualScrollState.visibleItems.clear(); - virtualScrollState.allItems = []; - virtualScrollState.initialized = false; -} - // Update folder tags in the UI export function updateFolderTags(folders) { const folderTagsContainer = document.querySelector('.folder-tags'); diff --git a/static/js/api/loraApi.js b/static/js/api/loraApi.js index 01176446..80fa3fb7 100644 --- a/static/js/api/loraApi.js +++ b/static/js/api/loraApi.js @@ -1,4 +1,4 @@ -import { createLoraCard, setupLoraCardEventDelegation } from '../components/LoraCard.js'; +import { createLoraCard } from '../components/LoraCard.js'; import { loadMoreModels, resetAndReload as baseResetAndReload, @@ -45,9 +45,6 @@ export async function excludeLora(filePath) { } export async function loadMoreLoras(resetPage = false, updateFolders = false) { - // Make sure event delegation is set up - setupLoraCardEventDelegation(); - return loadMoreModels({ resetPage, updateFolders, @@ -73,6 +70,16 @@ export async function replacePreview(filePath) { return replaceModelPreview(filePath, 'lora'); } +export function appendLoraCards(loras) { + const grid = document.getElementById('loraGrid'); + const sentinel = document.getElementById('scroll-sentinel'); + + loras.forEach(lora => { + const card = createLoraCard(lora); + grid.appendChild(card); + }); +} + export async function resetAndReload(updateFolders = false) { return baseResetAndReload({ updateFolders, diff --git a/static/js/components/LoraCard.js b/static/js/components/LoraCard.js index 40b1c9e2..26c54458 100644 --- a/static/js/components/LoraCard.js +++ b/static/js/components/LoraCard.js @@ -6,184 +6,6 @@ import { NSFW_LEVELS } from '../utils/constants.js'; import { replacePreview, saveModelMetadata } from '../api/loraApi.js' import { showDeleteModal } from '../utils/modalUtils.js'; -// Set up event delegation for all card interactions -export function setupLoraCardEventDelegation() { - const loraGrid = document.getElementById('loraGrid'); - if (!loraGrid) { - console.warn('Lora grid not found, will try to set up event delegation later'); - // Try again when DOM might be ready - setTimeout(setupLoraCardEventDelegation, 500); - return; - } - - // Remove existing event listener if any - const oldListener = loraGrid._cardClickListener; - if (oldListener) { - loraGrid.removeEventListener('click', oldListener); - } - - // Create and store the event listener - loraGrid._cardClickListener = (e) => { - // Find the card that was clicked - const card = e.target.closest('.lora-card'); - if (!card) return; - - // Handle various click targets - if (e.target.closest('.toggle-blur-btn') || e.target.closest('.show-content-btn')) { - e.stopPropagation(); - toggleCardBlur(card); - } else if (e.target.closest('.fa-star')) { - e.stopPropagation(); - toggleFavoriteStatus(card); - } else if (e.target.closest('.fa-globe') && card.dataset.from_civitai === 'true') { - e.stopPropagation(); - openCivitai(card.dataset.name); - } else if (e.target.closest('.fa-copy')) { - e.stopPropagation(); - copyCardLoraText(card); - } else if (e.target.closest('.fa-trash')) { - e.stopPropagation(); - showDeleteModal(card.dataset.filepath); - } else if (e.target.closest('.fa-image')) { - e.stopPropagation(); - replacePreview(card.dataset.filepath); - } else if (state.bulkMode) { - bulkManager.toggleCardSelection(card); - } else { - // Main card click - show modal - const loraMeta = getLoraDataFromCard(card); - showLoraModal(loraMeta); - } - }; - - console.log('Setting up event delegation for LoRA cards'); - // Add the event listener - loraGrid.addEventListener('click', loraGrid._cardClickListener); - - // Set up hover event delegation for video autoplay if needed - if (state.global?.settings?.autoplayOnHover) { - // Remove any existing handlers - if (loraGrid._mouseEnterListener) { - loraGrid.removeEventListener('mouseenter', loraGrid._mouseEnterListener, true); - } - if (loraGrid._mouseLeaveListener) { - loraGrid.removeEventListener('mouseleave', loraGrid._mouseLeaveListener, true); - } - - // Create and save the handlers - loraGrid._mouseEnterListener = (e) => { - const cardPreview = e.target.closest('.card-preview'); - if (!cardPreview) return; - - const video = cardPreview.querySelector('video'); - if (video) video.play().catch(() => {}); - }; - - loraGrid._mouseLeaveListener = (e) => { - const cardPreview = e.target.closest('.card-preview'); - if (!cardPreview) return; - - const video = cardPreview.querySelector('video'); - if (video) { - video.pause(); - video.currentTime = 0; - } - }; - - // Add the listeners - loraGrid.addEventListener('mouseenter', loraGrid._mouseEnterListener, true); - loraGrid.addEventListener('mouseleave', loraGrid._mouseLeaveListener, true); - } -} - -// Helper function to toggle blur state -function toggleCardBlur(card) { - const preview = card.querySelector('.card-preview'); - const isBlurred = preview.classList.toggle('blurred'); - const icon = card.querySelector('.toggle-blur-btn i'); - - if (icon) { - icon.className = isBlurred ? 'fas fa-eye' : 'fas fa-eye-slash'; - } - - const overlay = card.querySelector('.nsfw-overlay'); - if (overlay) { - overlay.style.display = isBlurred ? 'flex' : 'none'; - } -} - -// Helper function to toggle favorite status -async function toggleFavoriteStatus(card) { - const starIcon = card.querySelector('.fa-star'); - if (!starIcon) return; - - const isFavorite = starIcon.classList.contains('fas'); - const newFavoriteState = !isFavorite; - - try { - // Save the new favorite state to the server - await saveModelMetadata(card.dataset.filepath, { - favorite: newFavoriteState - }); - - // Update the UI - if (newFavoriteState) { - starIcon.classList.remove('far'); - starIcon.classList.add('fas', 'favorite-active'); - starIcon.title = 'Remove from favorites'; - card.dataset.favorite = 'true'; - showToast('Added to favorites', 'success'); - } else { - starIcon.classList.remove('fas', 'favorite-active'); - starIcon.classList.add('far'); - starIcon.title = 'Add to favorites'; - card.dataset.favorite = 'false'; - showToast('Removed from favorites', 'success'); - } - } catch (error) { - console.error('Failed to update favorite status:', error); - showToast('Failed to update favorite status', 'error'); - } -} - -// Helper function to copy LoRA syntax -async function copyCardLoraText(card) { - const usageTips = JSON.parse(card.dataset.usage_tips || '{}'); - const strength = usageTips.strength || 1; - const loraSyntax = ``; - - await copyToClipboard(loraSyntax, 'LoRA syntax copied'); -} - -// Helper function to extract LoRA data from card -function getLoraDataFromCard(card) { - return { - sha256: card.dataset.sha256, - file_path: card.dataset.filepath, - model_name: card.dataset.name, - file_name: card.dataset.file_name, - 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', - // Parse civitai metadata from the card's dataset - civitai: (() => { - try { - return JSON.parse(card.dataset.meta || '{}'); - } catch (e) { - console.error('Failed to parse civitai metadata:', e); - return {}; - } - })(), - tags: JSON.parse(card.dataset.tags || '[]'), - modelDescription: card.dataset.modelDescription || '' - }; -} - export function createLoraCard(lora) { const card = document.createElement('div'); card.className = 'lora-card'; @@ -243,8 +65,7 @@ export function createLoraCard(lora) { // Check if autoplayOnHover is enabled for video previews const autoplayOnHover = state.global.settings.autoplayOnHover || false; const isVideo = previewUrl.endsWith('.mp4'); - // Don't automatically play videos until visible - const videoAttrs = autoplayOnHover ? 'controls muted loop' : 'controls muted loop'; + const videoAttrs = autoplayOnHover ? 'controls muted loop' : 'controls autoplay muted loop'; // Get favorite status from the lora data const isFavorite = lora.favorite === true; @@ -255,8 +76,8 @@ export function createLoraCard(lora) { `` : - `${lora.model_name} - `} + `${lora.model_name}` + }
${shouldBlur ? `
`; + + // Main card click event - modified to handle bulk mode + card.addEventListener('click', () => { + // Check if we're in bulk mode + if (state.bulkMode) { + // Toggle selection using the bulk manager + bulkManager.toggleCardSelection(card); + } else { + // Normal behavior - show modal + const loraMeta = { + sha256: card.dataset.sha256, + file_path: card.dataset.filepath, + model_name: card.dataset.name, + file_name: card.dataset.file_name, + 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', + // Parse civitai metadata from the card's dataset + civitai: (() => { + try { + // Attempt to parse the JSON string + return JSON.parse(card.dataset.meta || '{}'); + } catch (e) { + console.error('Failed to parse civitai metadata:', e); + return {}; // Return empty object on error + } + })(), + tags: JSON.parse(card.dataset.tags || '[]'), + modelDescription: card.dataset.modelDescription || '' + }; + showLoraModal(loraMeta); + } + }); + + // Toggle blur button functionality + const toggleBlurBtn = card.querySelector('.toggle-blur-btn'); + if (toggleBlurBtn) { + toggleBlurBtn.addEventListener('click', (e) => { + e.stopPropagation(); + const preview = card.querySelector('.card-preview'); + const isBlurred = preview.classList.toggle('blurred'); + const icon = toggleBlurBtn.querySelector('i'); + + // Update the icon based on blur state + if (isBlurred) { + icon.className = 'fas fa-eye'; + } else { + icon.className = 'fas fa-eye-slash'; + } + + // Toggle the overlay visibility + const overlay = card.querySelector('.nsfw-overlay'); + if (overlay) { + overlay.style.display = isBlurred ? 'flex' : 'none'; + } + }); + } + + // Show content button functionality + const showContentBtn = card.querySelector('.show-content-btn'); + if (showContentBtn) { + showContentBtn.addEventListener('click', (e) => { + e.stopPropagation(); + const preview = card.querySelector('.card-preview'); + preview.classList.remove('blurred'); + + // Update the toggle button icon + const toggleBtn = card.querySelector('.toggle-blur-btn'); + if (toggleBtn) { + toggleBtn.querySelector('i').className = 'fas fa-eye-slash'; + } + + // Hide the overlay + const overlay = card.querySelector('.nsfw-overlay'); + if (overlay) { + overlay.style.display = 'none'; + } + }); + } + + // Favorite button click event + card.querySelector('.fa-star')?.addEventListener('click', async e => { + e.stopPropagation(); + const starIcon = e.currentTarget; + const isFavorite = starIcon.classList.contains('fas'); + const newFavoriteState = !isFavorite; + + try { + // Save the new favorite state to the server + await saveModelMetadata(card.dataset.filepath, { + favorite: newFavoriteState + }); + + // Update the UI + if (newFavoriteState) { + starIcon.classList.remove('far'); + starIcon.classList.add('fas', 'favorite-active'); + starIcon.title = 'Remove from favorites'; + card.dataset.favorite = 'true'; + showToast('Added to favorites', 'success'); + } else { + starIcon.classList.remove('fas', 'favorite-active'); + starIcon.classList.add('far'); + starIcon.title = 'Add to favorites'; + card.dataset.favorite = 'false'; + showToast('Removed from favorites', 'success'); + } + } catch (error) { + console.error('Failed to update favorite status:', error); + showToast('Failed to update favorite status', 'error'); + } + }); + + // Copy button click event + card.querySelector('.fa-copy')?.addEventListener('click', async e => { + e.stopPropagation(); + const usageTips = JSON.parse(card.dataset.usage_tips || '{}'); + const strength = usageTips.strength || 1; + const loraSyntax = ``; + + await copyToClipboard(loraSyntax, 'LoRA syntax copied'); + }); + + // Civitai button click event + if (lora.from_civitai) { + card.querySelector('.fa-globe')?.addEventListener('click', e => { + e.stopPropagation(); + openCivitai(lora.model_name); + }); + } + + // Delete button click event + card.querySelector('.fa-trash')?.addEventListener('click', e => { + e.stopPropagation(); + showDeleteModal(lora.file_path); + }); + + // Replace preview button click event + card.querySelector('.fa-image')?.addEventListener('click', e => { + e.stopPropagation(); + replacePreview(lora.file_path); + }); // Apply bulk mode styling if currently in bulk mode if (state.bulkMode) { @@ -310,6 +278,26 @@ export function createLoraCard(lora) { }); } + // Add autoplayOnHover handlers for video elements if needed + const videoElement = card.querySelector('video'); + if (videoElement && autoplayOnHover) { + const cardPreview = card.querySelector('.card-preview'); + + // Remove autoplay attribute and pause initially + videoElement.removeAttribute('autoplay'); + videoElement.pause(); + + // Add mouse events to trigger play/pause + cardPreview.addEventListener('mouseenter', () => { + videoElement.play(); + }); + + cardPreview.addEventListener('mouseleave', () => { + videoElement.pause(); + videoElement.currentTime = 0; + }); + } + return card; } diff --git a/static/js/loras.js b/static/js/loras.js index 79a562ba..5dcc9638 100644 --- a/static/js/loras.js +++ b/static/js/loras.js @@ -2,14 +2,13 @@ import { appCore } from './core.js'; import { state } from './state/index.js'; import { showLoraModal, toggleShowcase, scrollToTop } from './components/loraModal/index.js'; import { loadMoreLoras } from './api/loraApi.js'; -import { updateCardsForBulkMode, setupLoraCardEventDelegation } from './components/LoraCard.js'; +import { updateCardsForBulkMode } from './components/LoraCard.js'; import { bulkManager } from './managers/BulkManager.js'; import { DownloadManager } from './managers/DownloadManager.js'; import { moveManager } from './managers/MoveManager.js'; import { LoraContextMenu } from './components/ContextMenu/index.js'; import { createPageControls } from './components/controls/index.js'; import { confirmDelete, closeDeleteModal, confirmExclude, closeExcludeModal } from './utils/modalUtils.js'; -import { cleanupVirtualScroll } from './api/baseModelApi.js'; // Initialize the LoRA page class LoraPageManager { @@ -64,16 +63,8 @@ class LoraPageManager { // Initialize the bulk manager bulkManager.initialize(); - // Set up event delegation for card interactions - setupLoraCardEventDelegation(); - // Initialize common page features (lazy loading, infinite scroll) appCore.initializePageFeatures(); - - // Handle cleanup when page is unloaded - window.addEventListener('beforeunload', () => { - cleanupVirtualScroll(); - }); } }