mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-24 14:42:11 -03:00
refactor: streamline LoraCard event handling and implement virtual scrolling for improved performance
This commit is contained in:
312
static/js/utils/virtualScroll.js
Normal file
312
static/js/utils/virtualScroll.js
Normal file
@@ -0,0 +1,312 @@
|
||||
import { state, getCurrentPageState } from '../state/index.js';
|
||||
import { loadMoreLoras } from '../api/loraApi.js';
|
||||
import { loadMoreCheckpoints } from '../api/checkpointApi.js';
|
||||
import { debounce } from './debounce.js';
|
||||
import { createLoraCard } from '../components/LoraCard.js';
|
||||
|
||||
export function initializeVirtualScroll(pageType = 'loras') {
|
||||
// Clean up any existing observer or handler
|
||||
if (state.observer) {
|
||||
state.observer.disconnect();
|
||||
state.observer = null;
|
||||
}
|
||||
if (state.scrollHandler) {
|
||||
window.removeEventListener('scroll', state.scrollHandler);
|
||||
state.scrollHandler = null;
|
||||
}
|
||||
if (state.scrollCheckInterval) {
|
||||
clearInterval(state.scrollCheckInterval);
|
||||
state.scrollCheckInterval = null;
|
||||
}
|
||||
|
||||
// Set the current page type
|
||||
state.currentPageType = pageType;
|
||||
|
||||
// Get the current page state
|
||||
const pageState = getCurrentPageState();
|
||||
|
||||
// Skip initializing if in duplicates mode (for recipes page)
|
||||
if (pageType === 'recipes' && pageState.duplicatesMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the grid element and fetch function based on page type
|
||||
let gridId;
|
||||
let fetchMoreItems;
|
||||
let createCardFunction;
|
||||
|
||||
switch (pageType) {
|
||||
case 'recipes':
|
||||
fetchMoreItems = async () => {
|
||||
if (!pageState.isLoading && pageState.hasMore) {
|
||||
await window.recipeManager.loadRecipes(false);
|
||||
return pageState.items;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
gridId = 'recipeGrid';
|
||||
createCardFunction = window.recipeManager?.createRecipeCard;
|
||||
break;
|
||||
case 'checkpoints':
|
||||
fetchMoreItems = async () => {
|
||||
if (!pageState.isLoading && pageState.hasMore) {
|
||||
await loadMoreCheckpoints(false);
|
||||
return pageState.items;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
gridId = 'checkpointGrid';
|
||||
createCardFunction = window.createCheckpointCard;
|
||||
break;
|
||||
case 'loras':
|
||||
default:
|
||||
fetchMoreItems = async () => {
|
||||
if (!pageState.isLoading && pageState.hasMore) {
|
||||
await loadMoreLoras(false);
|
||||
return pageState.items;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
gridId = 'loraGrid';
|
||||
createCardFunction = createLoraCard;
|
||||
break;
|
||||
}
|
||||
|
||||
// Get the grid container
|
||||
const gridContainer = document.getElementById(gridId);
|
||||
if (!gridContainer) {
|
||||
console.warn(`Grid with ID "${gridId}" not found for virtual scroll`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the scrollable container
|
||||
const scrollContainer = document.querySelector('.page-content');
|
||||
if (!scrollContainer) {
|
||||
console.warn('Scrollable container not found for virtual scroll');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize the virtual scroll state
|
||||
const virtualScroll = {
|
||||
itemHeight: 350, // Starting estimate for card height
|
||||
bufferSize: 10, // Extra items to render above/below viewport
|
||||
visibleItems: new Map(), // Track rendered items by file_path
|
||||
allItems: pageState.items || [], // All data items that have been loaded
|
||||
containerHeight: 0, // Will be updated to show proper scrollbar
|
||||
containerElement: document.createElement('div'), // Virtual container
|
||||
gridElement: gridContainer,
|
||||
itemMeasurements: new Map(), // Map of measured item heights
|
||||
isUpdating: false
|
||||
};
|
||||
|
||||
// Create a container for the virtualized content with proper height
|
||||
virtualScroll.containerElement.className = 'virtual-scroll-container';
|
||||
virtualScroll.containerElement.style.position = 'relative';
|
||||
virtualScroll.containerElement.style.width = '100%';
|
||||
virtualScroll.containerElement.style.height = '0px'; // Will be updated
|
||||
|
||||
gridContainer.innerHTML = ''; // Clear existing content
|
||||
gridContainer.appendChild(virtualScroll.containerElement);
|
||||
|
||||
// Store the virtual scroll state in the global state
|
||||
state.virtualScroll = virtualScroll;
|
||||
|
||||
// Function to measure a rendered card's height
|
||||
function measureCardHeight(card) {
|
||||
if (!card) return virtualScroll.itemHeight;
|
||||
const height = card.offsetHeight;
|
||||
return height > 0 ? height : virtualScroll.itemHeight;
|
||||
}
|
||||
|
||||
// Calculate estimated total height for proper scrollbar
|
||||
function updateContainerHeight() {
|
||||
if (virtualScroll.allItems.length === 0) return;
|
||||
|
||||
// If we've measured some items, use average height
|
||||
let totalMeasuredHeight = 0;
|
||||
let measuredCount = 0;
|
||||
|
||||
virtualScroll.itemMeasurements.forEach(height => {
|
||||
totalMeasuredHeight += height;
|
||||
measuredCount++;
|
||||
});
|
||||
|
||||
const avgHeight = measuredCount > 0
|
||||
? totalMeasuredHeight / measuredCount
|
||||
: virtualScroll.itemHeight;
|
||||
|
||||
virtualScroll.itemHeight = avgHeight;
|
||||
virtualScroll.containerHeight = virtualScroll.allItems.length * avgHeight;
|
||||
virtualScroll.containerElement.style.height = `${virtualScroll.containerHeight}px`;
|
||||
}
|
||||
|
||||
// Function to get visible range of items
|
||||
function getVisibleRange() {
|
||||
const scrollTop = scrollContainer.scrollTop;
|
||||
const viewportHeight = scrollContainer.clientHeight;
|
||||
|
||||
// Calculate visible range with buffer
|
||||
const startIndex = Math.max(0, Math.floor(scrollTop / virtualScroll.itemHeight) - virtualScroll.bufferSize);
|
||||
const endIndex = Math.min(
|
||||
virtualScroll.allItems.length - 1,
|
||||
Math.ceil((scrollTop + viewportHeight) / virtualScroll.itemHeight) + virtualScroll.bufferSize
|
||||
);
|
||||
|
||||
return { startIndex, endIndex };
|
||||
}
|
||||
|
||||
// Update visible items based on scroll position
|
||||
async function updateVisibleItems() {
|
||||
if (virtualScroll.isUpdating) return;
|
||||
virtualScroll.isUpdating = true;
|
||||
|
||||
// Get current visible range
|
||||
const { startIndex, endIndex } = getVisibleRange();
|
||||
|
||||
// Set of items that should be visible
|
||||
const shouldBeVisible = new Set();
|
||||
|
||||
// Track total height for accurate positioning
|
||||
let currentOffset = 0;
|
||||
let needHeightUpdate = false;
|
||||
|
||||
// Create or update visible items
|
||||
for (let i = 0; i < virtualScroll.allItems.length; i++) {
|
||||
const item = virtualScroll.allItems[i];
|
||||
if (!item || !item.file_path) continue;
|
||||
|
||||
const itemId = item.file_path;
|
||||
const knownHeight = virtualScroll.itemMeasurements.get(itemId) || virtualScroll.itemHeight;
|
||||
|
||||
// Update position based on known measurements
|
||||
if (i > 0) {
|
||||
currentOffset += knownHeight;
|
||||
}
|
||||
|
||||
// Only create/position items in the visible range
|
||||
if (i >= startIndex && i <= endIndex) {
|
||||
shouldBeVisible.add(itemId);
|
||||
|
||||
// Create item if it doesn't exist
|
||||
if (!virtualScroll.visibleItems.has(itemId)) {
|
||||
const card = createCardFunction(item);
|
||||
card.style.position = 'absolute';
|
||||
card.style.top = `${currentOffset}px`;
|
||||
card.style.left = '0';
|
||||
card.style.right = '0';
|
||||
card.style.width = '100%';
|
||||
|
||||
virtualScroll.containerElement.appendChild(card);
|
||||
virtualScroll.visibleItems.set(itemId, card);
|
||||
|
||||
// Measure actual height after rendering
|
||||
setTimeout(() => {
|
||||
const actualHeight = measureCardHeight(card);
|
||||
if (actualHeight !== knownHeight) {
|
||||
virtualScroll.itemMeasurements.set(itemId, actualHeight);
|
||||
needHeightUpdate = true;
|
||||
window.requestAnimationFrame(updateVisibleItems);
|
||||
}
|
||||
}, 0);
|
||||
} else {
|
||||
// Update position of existing item
|
||||
const card = virtualScroll.visibleItems.get(itemId);
|
||||
card.style.top = `${currentOffset}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove items that shouldn't be visible anymore
|
||||
for (const [itemId, element] of virtualScroll.visibleItems.entries()) {
|
||||
if (!shouldBeVisible.has(itemId)) {
|
||||
// Clean up resources like videos
|
||||
const video = element.querySelector('video');
|
||||
if (video) {
|
||||
video.pause();
|
||||
video.src = '';
|
||||
video.load();
|
||||
}
|
||||
|
||||
element.remove();
|
||||
virtualScroll.visibleItems.delete(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
// Update container height if needed
|
||||
if (needHeightUpdate) {
|
||||
updateContainerHeight();
|
||||
}
|
||||
|
||||
// Check if we're near the end and need to load more
|
||||
if (endIndex >= virtualScroll.allItems.length - 15 && !pageState.isLoading && pageState.hasMore) {
|
||||
fetchMoreItems().then(newItems => {
|
||||
virtualScroll.allItems = pageState.items || [];
|
||||
updateContainerHeight();
|
||||
updateVisibleItems();
|
||||
});
|
||||
}
|
||||
|
||||
virtualScroll.isUpdating = false;
|
||||
}
|
||||
|
||||
// Debounced scroll handler
|
||||
const handleScroll = debounce(() => {
|
||||
requestAnimationFrame(updateVisibleItems);
|
||||
}, 50);
|
||||
|
||||
// Set up event listeners
|
||||
scrollContainer.addEventListener('scroll', handleScroll);
|
||||
window.addEventListener('resize', debounce(() => {
|
||||
updateVisibleItems();
|
||||
}, 100));
|
||||
|
||||
// Store the handler for cleanup
|
||||
state.scrollHandler = handleScroll;
|
||||
|
||||
// Initial update
|
||||
updateContainerHeight();
|
||||
updateVisibleItems();
|
||||
|
||||
// Run periodic updates to catch any rendering issues
|
||||
state.scrollCheckInterval = setInterval(() => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
updateVisibleItems();
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
return virtualScroll;
|
||||
}
|
||||
|
||||
// Helper to clean up virtual scroll resources
|
||||
export function cleanupVirtualScroll() {
|
||||
if (!state.virtualScroll) return;
|
||||
|
||||
// Clean up visible items
|
||||
state.virtualScroll.visibleItems.forEach((element) => {
|
||||
const video = element.querySelector('video');
|
||||
if (video) {
|
||||
video.pause();
|
||||
video.src = '';
|
||||
video.load();
|
||||
}
|
||||
element.remove();
|
||||
});
|
||||
|
||||
state.virtualScroll.visibleItems.clear();
|
||||
state.virtualScroll.containerElement.innerHTML = '';
|
||||
|
||||
// Remove scroll handler
|
||||
if (state.scrollHandler) {
|
||||
document.querySelector('.page-content').removeEventListener('scroll', state.scrollHandler);
|
||||
state.scrollHandler = null;
|
||||
}
|
||||
|
||||
// Clear interval
|
||||
if (state.scrollCheckInterval) {
|
||||
clearInterval(state.scrollCheckInterval);
|
||||
state.scrollCheckInterval = null;
|
||||
}
|
||||
|
||||
// Clear the state
|
||||
state.virtualScroll = null;
|
||||
}
|
||||
Reference in New Issue
Block a user