Implement virtual scrolling for model loading and checkpoint management

This commit is contained in:
Will Miao
2025-05-12 17:47:57 +08:00
parent d13b1a83ad
commit 01ba3c14f8
4 changed files with 194 additions and 113 deletions

View File

@@ -1,7 +1,6 @@
// filepath: d:\Workspace\ComfyUI\custom_nodes\ComfyUI-Lora-Manager\static\js\api\baseModelApi.js // filepath: d:\Workspace\ComfyUI\custom_nodes\ComfyUI-Lora-Manager\static\js\api\baseModelApi.js
import { state, getCurrentPageState } from '../state/index.js'; import { state, getCurrentPageState } from '../state/index.js';
import { showToast } from '../utils/uiHelpers.js'; import { showToast } from '../utils/uiHelpers.js';
import { showDeleteModal, confirmDelete } from '../utils/modalUtils.js';
import { getSessionItem, saveMapToStorage } from '../utils/storageHelpers.js'; import { getSessionItem, saveMapToStorage } from '../utils/storageHelpers.js';
/** /**
@@ -279,6 +278,112 @@ export async function fetchModelsPage(options = {}) {
} }
} }
/**
* Reset and reload models using virtual scrolling
* @param {Object} options - Operation options
* @returns {Promise<Object>} The fetch result
*/
export async function resetAndReloadWithVirtualScroll(options = {}) {
const {
modelType = 'lora',
updateFolders = false,
fetchPageFunction
} = options;
const pageState = getCurrentPageState();
try {
pageState.isLoading = true;
document.body.classList.add('loading');
// Reset page counter
pageState.currentPage = 1;
// Fetch the first page
const result = await fetchPageFunction(1, pageState.pageSize || 50);
// Update the virtual scroller
state.virtualScroller.refreshWithData(
result.items,
result.totalItems,
result.hasMore
);
// Update state
pageState.hasMore = result.hasMore;
pageState.currentPage = 2; // Next page will be 2
// Update folders if needed
if (updateFolders && result.folders) {
updateFolderTags(result.folders);
}
return result;
} catch (error) {
console.error(`Error reloading ${modelType}s:`, error);
showToast(`Failed to reload ${modelType}s: ${error.message}`, 'error');
throw error;
} finally {
pageState.isLoading = false;
document.body.classList.remove('loading');
}
}
/**
* Load more models using virtual scrolling
* @param {Object} options - Operation options
* @returns {Promise<Object>} The fetch result
*/
export async function loadMoreWithVirtualScroll(options = {}) {
const {
modelType = 'lora',
resetPage = false,
updateFolders = false,
fetchPageFunction
} = options;
const pageState = getCurrentPageState();
try {
// Start loading state
pageState.isLoading = true;
document.body.classList.add('loading');
// Reset to first page if requested
if (resetPage) {
pageState.currentPage = 1;
}
// Fetch the first page of data
const result = await fetchPageFunction(pageState.currentPage, pageState.pageSize || 50);
// Update virtual scroller with the new data
state.virtualScroller.refreshWithData(
result.items,
result.totalItems,
result.hasMore
);
// Update state
pageState.hasMore = result.hasMore;
pageState.currentPage = 2; // Next page to load would be 2
// Update folders if needed
if (updateFolders && result.folders) {
updateFolderTags(result.folders);
}
return result;
} catch (error) {
console.error(`Error loading ${modelType}s:`, error);
showToast(`Failed to load ${modelType}s: ${error.message}`, 'error');
throw error;
} finally {
pageState.isLoading = false;
document.body.classList.remove('loading');
}
}
// Update folder tags in the UI // Update folder tags in the UI
export function updateFolderTags(folders) { export function updateFolderTags(folders) {
const folderTagsContainer = document.querySelector('.folder-tags'); const folderTagsContainer = document.querySelector('.folder-tags');

View File

@@ -1,7 +1,10 @@
import { createCheckpointCard } from '../components/CheckpointCard.js'; import { createCheckpointCard } from '../components/CheckpointCard.js';
import { import {
loadMoreModels, loadMoreModels,
fetchModelsPage,
resetAndReload as baseResetAndReload, resetAndReload as baseResetAndReload,
resetAndReloadWithVirtualScroll,
loadMoreWithVirtualScroll,
refreshModels as baseRefreshModels, refreshModels as baseRefreshModels,
deleteModel as baseDeleteModel, deleteModel as baseDeleteModel,
replaceModelPreview, replaceModelPreview,
@@ -9,25 +12,67 @@ import {
refreshSingleModelMetadata, refreshSingleModelMetadata,
excludeModel as baseExcludeModel excludeModel as baseExcludeModel
} from './baseModelApi.js'; } from './baseModelApi.js';
import { state } from '../state/index.js';
// Load more checkpoints with pagination /**
export async function loadMoreCheckpoints(resetPagination = true) { * Fetch checkpoints with pagination for virtual scrolling
return loadMoreModels({ * @param {number} page - Page number to fetch
resetPage: resetPagination, * @param {number} pageSize - Number of items per page
updateFolders: true, * @returns {Promise<Object>} Object containing items, total count, and pagination info
*/
export async function fetchCheckpointsPage(page = 1, pageSize = 100) {
return fetchModelsPage({
modelType: 'checkpoint', modelType: 'checkpoint',
createCardFunction: createCheckpointCard, page,
pageSize,
endpoint: '/api/checkpoints' endpoint: '/api/checkpoints'
}); });
} }
/**
* Load more checkpoints with pagination - updated to work with VirtualScroller
* @param {boolean} resetPage - Whether to reset to the first page
* @param {boolean} updateFolders - Whether to update folder tags
* @returns {Promise<void>}
*/
export async function loadMoreCheckpoints(resetPage = false, updateFolders = false) {
// Check if virtual scroller is available
if (state.virtualScroller) {
return loadMoreWithVirtualScroll({
modelType: 'checkpoint',
resetPage,
updateFolders,
fetchPageFunction: fetchCheckpointsPage
});
} else {
// Fall back to the original implementation if virtual scroller isn't available
return loadMoreModels({
resetPage,
updateFolders,
modelType: 'checkpoint',
createCardFunction: createCheckpointCard,
endpoint: '/api/checkpoints'
});
}
}
// Reset and reload checkpoints // Reset and reload checkpoints
export async function resetAndReload() { export async function resetAndReload(updateFolders = false) {
return baseResetAndReload({ // Check if virtual scroller is available
updateFolders: true, if (state.virtualScroller) {
modelType: 'checkpoint', return resetAndReloadWithVirtualScroll({
loadMoreFunction: loadMoreCheckpoints modelType: 'checkpoint',
}); updateFolders,
fetchPageFunction: fetchCheckpointsPage
});
} else {
// Fall back to original implementation
return baseResetAndReload({
updateFolders,
modelType: 'checkpoint',
loadMoreFunction: loadMoreCheckpoints
});
}
} }
// Refresh checkpoints // Refresh checkpoints
@@ -60,7 +105,11 @@ export async function fetchCivitai() {
// Refresh single checkpoint metadata // Refresh single checkpoint metadata
export async function refreshSingleCheckpointMetadata(filePath) { export async function refreshSingleCheckpointMetadata(filePath) {
return refreshSingleModelMetadata(filePath, 'checkpoint'); const success = await refreshSingleModelMetadata(filePath, 'checkpoint');
if (success) {
// Reload the current view to show updated data
await resetAndReload();
}
} }
/** /**

View File

@@ -3,6 +3,8 @@ import {
loadMoreModels, loadMoreModels,
fetchModelsPage, fetchModelsPage,
resetAndReload as baseResetAndReload, resetAndReload as baseResetAndReload,
resetAndReloadWithVirtualScroll,
loadMoreWithVirtualScroll,
refreshModels as baseRefreshModels, refreshModels as baseRefreshModels,
deleteModel as baseDeleteModel, deleteModel as baseDeleteModel,
replaceModelPreview, replaceModelPreview,
@@ -58,45 +60,12 @@ export async function loadMoreLoras(resetPage = false, updateFolders = false) {
// Check if virtual scroller is available // Check if virtual scroller is available
if (state.virtualScroller) { if (state.virtualScroller) {
try { return loadMoreWithVirtualScroll({
// Start loading state modelType: 'lora',
pageState.isLoading = true; resetPage,
document.body.classList.add('loading'); updateFolders,
fetchPageFunction: fetchLorasPage
// Reset to first page if requested });
if (resetPage) {
pageState.currentPage = 1;
}
// Fetch the first page of data
const result = await fetchLorasPage(pageState.currentPage, pageState.pageSize || 50);
// Update virtual scroller with the new data
state.virtualScroller.refreshWithData(
result.items,
result.totalItems,
result.hasMore
);
// Update state
pageState.hasMore = result.hasMore;
pageState.currentPage = 2; // Next page to load would be 2
// Update folders if needed
if (updateFolders && result.folders) {
// Import function dynamically to avoid circular dependencies
const { updateFolderTags } = await import('./baseModelApi.js');
updateFolderTags(result.folders);
}
return result;
} catch (error) {
console.error('Error loading loras:', error);
showToast(`Failed to load loras: ${error.message}`, 'error');
} finally {
pageState.isLoading = false;
document.body.classList.remove('loading');
}
} else { } else {
// Fall back to the original implementation if virtual scroller isn't available // Fall back to the original implementation if virtual scroller isn't available
return loadMoreModels({ return loadMoreModels({
@@ -115,7 +84,7 @@ export async function loadMoreLoras(resetPage = false, updateFolders = false) {
* @param {number} pageSize - Number of items per page * @param {number} pageSize - Number of items per page
* @returns {Promise<Object>} Object containing items, total count, and pagination info * @returns {Promise<Object>} Object containing items, total count, and pagination info
*/ */
export async function fetchLorasPage(page = 1, pageSize = 50) { export async function fetchLorasPage(page = 1, pageSize = 100) {
return fetchModelsPage({ return fetchModelsPage({
modelType: 'lora', modelType: 'lora',
page, page,
@@ -160,42 +129,11 @@ export async function resetAndReload(updateFolders = false) {
// Check if virtual scroller is available // Check if virtual scroller is available
if (state.virtualScroller) { if (state.virtualScroller) {
try { return resetAndReloadWithVirtualScroll({
pageState.isLoading = true; modelType: 'lora',
document.body.classList.add('loading'); updateFolders,
fetchPageFunction: fetchLorasPage
// Reset page counter });
pageState.currentPage = 1;
// Fetch the first page
const result = await fetchLorasPage(1, pageState.pageSize || 50);
// Update the virtual scroller
state.virtualScroller.refreshWithData(
result.items,
result.totalItems,
result.hasMore
);
// Update state
pageState.hasMore = result.hasMore;
pageState.currentPage = 2; // Next page will be 2
// Update folders if needed
if (updateFolders && result.folders) {
// Import function dynamically to avoid circular dependencies
const { updateFolderTags } = await import('./baseModelApi.js');
updateFolderTags(result.folders);
}
return result;
} catch (error) {
console.error('Error reloading loras:', error);
showToast(`Failed to reload loras: ${error.message}`, 'error');
} finally {
pageState.isLoading = false;
document.body.classList.remove('loading');
}
} else { } else {
// Fall back to original implementation // Fall back to original implementation
return baseResetAndReload({ return baseResetAndReload({

View File

@@ -1,8 +1,9 @@
import { state, getCurrentPageState } from '../state/index.js'; import { state, getCurrentPageState } from '../state/index.js';
import { debounce } from './debounce.js';
import { VirtualScroller } from './VirtualScroller.js'; import { VirtualScroller } from './VirtualScroller.js';
import { createLoraCard, setupLoraCardEventDelegation } from '../components/LoraCard.js'; import { createLoraCard, setupLoraCardEventDelegation } from '../components/LoraCard.js';
import { createCheckpointCard } from '../components/CheckpointCard.js';
import { fetchLorasPage } from '../api/loraApi.js'; import { fetchLorasPage } from '../api/loraApi.js';
import { fetchCheckpointsPage } from '../api/checkpointApi.js';
import { showToast } from './uiHelpers.js'; import { showToast } from './uiHelpers.js';
// Function to dynamically import the appropriate card creator based on page type // Function to dynamically import the appropriate card creator based on page type
@@ -18,13 +19,7 @@ async function getCardCreator(pageType) {
return null; return null;
} }
} else if (pageType === 'checkpoints') { } else if (pageType === 'checkpoints') {
try { return createCheckpointCard;
const { createCheckpointCard } = await import('../components/CheckpointCard.js');
return createCheckpointCard;
} catch (err) {
console.error('Failed to load checkpoint card creator:', err);
return null;
}
} }
return null; return null;
} }
@@ -42,13 +37,7 @@ async function getDataFetcher(pageType) {
return null; return null;
} }
} else if (pageType === 'checkpoints') { } else if (pageType === 'checkpoints') {
try { return fetchCheckpointsPage;
const { fetchCheckpointsPage } = await import('../api/checkpointApi.js');
return fetchCheckpointsPage;
} catch (err) {
console.error('Failed to load checkpoint data fetcher:', err);
return null;
}
} }
return null; return null;
} }
@@ -105,11 +94,11 @@ async function initializeVirtualScroll(pageType) {
} }
// Change this line to get the actual scrolling container // Change this line to get the actual scrolling container
const pageContainer = document.querySelector('.page-content'); const scrollContainer = document.querySelector('.page-content');
const pageContent = pageContainer.querySelector('.container'); const gridContainer = scrollContainer.querySelector('.container');
if (!pageContent) { if (!gridContainer) {
console.warn('Page content element not found for virtual scroll'); console.warn('Grid container element not found for virtual scroll');
return; return;
} }
@@ -122,15 +111,15 @@ async function initializeVirtualScroll(pageType) {
throw new Error(`Required components not available for ${pageType} page`); throw new Error(`Required components not available for ${pageType} page`);
} }
// Pass the correct scrolling container // Initialize virtual scroller with renamed container elements
state.virtualScroller = new VirtualScroller({ state.virtualScroller = new VirtualScroller({
gridElement: grid, gridElement: grid,
containerElement: pageContent, containerElement: gridContainer,
scrollContainer: pageContainer, // Add this new parameter scrollContainer: scrollContainer,
createItemFn: createCardFn, createItemFn: createCardFn,
fetchItemsFn: fetchDataFn, fetchItemsFn: fetchDataFn,
pageSize: 100, pageSize: 100,
rowGap: 20 // Add consistent vertical spacing between rows rowGap: 20
}); });
// Initialize the virtual scroller // Initialize the virtual scroller