mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-26 15:38:52 -03:00
checkpoint
This commit is contained in:
@@ -2,7 +2,8 @@
|
|||||||
.card-grid {
|
.card-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); /* Base size */
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); /* Base size */
|
||||||
gap: 12px; /* Reduced from var(--space-2) for tighter horizontal spacing */
|
gap: 12px; /* Consistent gap for both row and column spacing */
|
||||||
|
row-gap: 20px; /* Increase vertical spacing between rows */
|
||||||
margin-top: var(--space-2);
|
margin-top: var(--space-2);
|
||||||
padding-top: 4px; /* 添加顶部内边距,为悬停动画提供空间 */
|
padding-top: 4px; /* 添加顶部内边距,为悬停动画提供空间 */
|
||||||
padding-bottom: 4px; /* 添加底部内边距,为悬停动画提供空间 */
|
padding-bottom: 4px; /* 添加底部内边距,为悬停动画提供空间 */
|
||||||
@@ -395,7 +396,6 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
transition: transform 160ms ease-out;
|
transition: transform 160ms ease-out;
|
||||||
margin: 0; /* Remove margins, positioning is handled by VirtualScroller */
|
margin: 0; /* Remove margins, positioning is handled by VirtualScroller */
|
||||||
padding: 6px; /* Add consistent padding on all sides */
|
|
||||||
width: 100%; /* Allow width to be set by the VirtualScroller */
|
width: 100%; /* Allow width to be set by the VirtualScroller */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ export async function loadMoreLoras(resetPage = false, updateFolders = false) {
|
|||||||
* @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 = 50) {
|
||||||
console.log('Fetching loras page:', page, pageSize);
|
|
||||||
return fetchModelsPage({
|
return fetchModelsPage({
|
||||||
modelType: 'lora',
|
modelType: 'lora',
|
||||||
page,
|
page,
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ export class VirtualScroller {
|
|||||||
this.fetchItemsFn = options.fetchItemsFn;
|
this.fetchItemsFn = options.fetchItemsFn;
|
||||||
this.overscan = options.overscan || 5; // Extra items to render above/below viewport
|
this.overscan = options.overscan || 5; // Extra items to render above/below viewport
|
||||||
this.containerElement = options.containerElement || this.gridElement.parentElement;
|
this.containerElement = options.containerElement || this.gridElement.parentElement;
|
||||||
|
this.scrollContainer = options.scrollContainer || this.containerElement;
|
||||||
this.batchSize = options.batchSize || 50;
|
this.batchSize = options.batchSize || 50;
|
||||||
this.pageSize = options.pageSize || 100;
|
this.pageSize = options.pageSize || 100;
|
||||||
this.itemAspectRatio = 896/1152; // Aspect ratio of cards
|
this.itemAspectRatio = 896/1152; // Aspect ratio of cards
|
||||||
|
this.rowGap = options.rowGap || 20; // Add vertical gap between rows (default 20px)
|
||||||
|
|
||||||
// State
|
// State
|
||||||
this.items = []; // All items metadata
|
this.items = []; // All items metadata
|
||||||
@@ -32,6 +34,10 @@ export class VirtualScroller {
|
|||||||
this.gridPadding = 12; // Gap between cards
|
this.gridPadding = 12; // Gap between cards
|
||||||
this.columnGap = 12; // Horizontal gap
|
this.columnGap = 12; // Horizontal gap
|
||||||
|
|
||||||
|
// Add loading timeout state
|
||||||
|
this.loadingTimeout = null;
|
||||||
|
this.loadingTimeoutDuration = options.loadingTimeoutDuration || 15000; // 15 seconds default
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
this.initializeContainer();
|
this.initializeContainer();
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
@@ -63,8 +69,14 @@ export class VirtualScroller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
calculateLayout() {
|
calculateLayout() {
|
||||||
// Get container width
|
// Get container width and style information
|
||||||
const containerWidth = this.containerElement.clientWidth;
|
const containerWidth = this.containerElement.clientWidth;
|
||||||
|
const containerStyle = getComputedStyle(this.containerElement);
|
||||||
|
const paddingLeft = parseInt(containerStyle.paddingLeft, 10) || 0;
|
||||||
|
const paddingRight = parseInt(containerStyle.paddingRight, 10) || 0;
|
||||||
|
|
||||||
|
// Calculate available content width (excluding padding)
|
||||||
|
const availableContentWidth = containerWidth - paddingLeft - paddingRight;
|
||||||
|
|
||||||
// Calculate ideal card width based on breakpoints
|
// Calculate ideal card width based on breakpoints
|
||||||
let baseCardWidth = 260; // Default for 1080p
|
let baseCardWidth = 260; // Default for 1080p
|
||||||
@@ -77,37 +89,41 @@ export class VirtualScroller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate how many columns can fit
|
// Calculate how many columns can fit
|
||||||
const availableWidth = Math.min(
|
const maxGridWidth = window.innerWidth >= 3000 ? 2400 : // 4K
|
||||||
containerWidth,
|
window.innerWidth >= 2000 ? 1800 : // 2K
|
||||||
window.innerWidth >= 3000 ? 2400 : // 4K
|
1400; // 1080p
|
||||||
window.innerWidth >= 2000 ? 1800 : // 2K
|
|
||||||
1400 // 1080p
|
// Use the smaller of available content width or max grid width
|
||||||
);
|
const actualGridWidth = Math.min(availableContentWidth, maxGridWidth);
|
||||||
|
|
||||||
// Calculate column count based on available width and card width
|
// Calculate column count based on available width and card width
|
||||||
this.columnsCount = Math.max(1, Math.floor((availableWidth + this.columnGap) / (baseCardWidth + this.columnGap)));
|
this.columnsCount = Math.max(1, Math.floor((actualGridWidth + this.columnGap) / (baseCardWidth + this.columnGap)));
|
||||||
|
|
||||||
// Calculate actual item width based on container and column count
|
// Calculate actual item width
|
||||||
this.itemWidth = (availableWidth - (this.columnsCount - 1) * this.columnGap) / this.columnsCount;
|
this.itemWidth = (actualGridWidth - (this.columnsCount - 1) * this.columnGap) / this.columnsCount;
|
||||||
|
|
||||||
// Calculate height based on aspect ratio
|
// Calculate height based on aspect ratio
|
||||||
this.itemHeight = this.itemWidth / this.itemAspectRatio;
|
this.itemHeight = this.itemWidth / this.itemAspectRatio;
|
||||||
|
|
||||||
// Calculate the left offset to center the grid
|
// Calculate the left offset to center the grid within the content area
|
||||||
this.leftOffset = Math.max(0, (containerWidth - availableWidth) / 2);
|
this.leftOffset = Math.max(0, (availableContentWidth - actualGridWidth) / 2);
|
||||||
|
|
||||||
// Log layout info
|
// Log layout info
|
||||||
console.log('Virtual Scroll Layout:', {
|
console.log('Virtual Scroll Layout:', {
|
||||||
containerWidth,
|
containerWidth,
|
||||||
availableWidth,
|
availableContentWidth,
|
||||||
|
actualGridWidth,
|
||||||
columnsCount: this.columnsCount,
|
columnsCount: this.columnsCount,
|
||||||
itemWidth: this.itemWidth,
|
itemWidth: this.itemWidth,
|
||||||
itemHeight: this.itemHeight,
|
itemHeight: this.itemHeight,
|
||||||
leftOffset: this.leftOffset
|
leftOffset: this.leftOffset,
|
||||||
|
paddingLeft,
|
||||||
|
paddingRight,
|
||||||
|
rowGap: this.rowGap // Log row gap for debugging
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update grid element max-width to match available width
|
// Update grid element max-width to match available width
|
||||||
this.gridElement.style.maxWidth = `${availableWidth}px`;
|
this.gridElement.style.maxWidth = `${actualGridWidth}px`;
|
||||||
|
|
||||||
// Update spacer height
|
// Update spacer height
|
||||||
this.updateSpacerHeight();
|
this.updateSpacerHeight();
|
||||||
@@ -122,7 +138,7 @@ export class VirtualScroller {
|
|||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
// Debounced scroll handler
|
// Debounced scroll handler
|
||||||
this.scrollHandler = this.debounce(() => this.handleScroll(), 10);
|
this.scrollHandler = this.debounce(() => this.handleScroll(), 10);
|
||||||
this.containerElement.addEventListener('scroll', this.scrollHandler);
|
this.scrollContainer.addEventListener('scroll', this.scrollHandler);
|
||||||
|
|
||||||
// Window resize handler for layout recalculation
|
// Window resize handler for layout recalculation
|
||||||
this.resizeHandler = this.debounce(() => {
|
this.resizeHandler = this.debounce(() => {
|
||||||
@@ -156,6 +172,8 @@ export class VirtualScroller {
|
|||||||
if (this.isLoading) return;
|
if (this.isLoading) return;
|
||||||
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
this.setLoadingTimeout(); // Add loading timeout safety
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { items, totalItems, hasMore } = await this.fetchItemsFn(1, this.pageSize);
|
const { items, totalItems, hasMore } = await this.fetchItemsFn(1, this.pageSize);
|
||||||
this.items = items || [];
|
this.items = items || [];
|
||||||
@@ -173,8 +191,10 @@ export class VirtualScroller {
|
|||||||
return { items, totalItems, hasMore };
|
return { items, totalItems, hasMore };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load initial batch:', err);
|
console.error('Failed to load initial batch:', err);
|
||||||
this.isLoading = false;
|
|
||||||
throw err;
|
throw err;
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
this.clearLoadingTimeout(); // Clear the timeout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,8 +204,10 @@ export class VirtualScroller {
|
|||||||
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
pageState.isLoading = true;
|
pageState.isLoading = true;
|
||||||
|
this.setLoadingTimeout(); // Add loading timeout safety
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('Loading more items, page:', pageState.currentPage);
|
||||||
const { items, hasMore } = await this.fetchItemsFn(pageState.currentPage, this.pageSize);
|
const { items, hasMore } = await this.fetchItemsFn(pageState.currentPage, this.pageSize);
|
||||||
|
|
||||||
if (items && items.length > 0) {
|
if (items && items.length > 0) {
|
||||||
@@ -201,9 +223,12 @@ export class VirtualScroller {
|
|||||||
|
|
||||||
// Render the newly loaded items if they're in view
|
// Render the newly loaded items if they're in view
|
||||||
this.scheduleRender();
|
this.scheduleRender();
|
||||||
|
|
||||||
|
console.log(`Loaded ${items.length} more items, total now: ${this.items.length}`);
|
||||||
} else {
|
} else {
|
||||||
this.hasMore = false;
|
this.hasMore = false;
|
||||||
pageState.hasMore = false;
|
pageState.hasMore = false;
|
||||||
|
console.log('No more items to load');
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
@@ -213,6 +238,30 @@ export class VirtualScroller {
|
|||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
pageState.isLoading = false;
|
pageState.isLoading = false;
|
||||||
|
this.clearLoadingTimeout(); // Clear the timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new methods for loading timeout
|
||||||
|
setLoadingTimeout() {
|
||||||
|
// Clear any existing timeout first
|
||||||
|
this.clearLoadingTimeout();
|
||||||
|
|
||||||
|
// Set a new timeout to prevent loading state from getting stuck
|
||||||
|
this.loadingTimeout = setTimeout(() => {
|
||||||
|
if (this.isLoading) {
|
||||||
|
console.warn('Loading timeout occurred. Resetting loading state.');
|
||||||
|
this.isLoading = false;
|
||||||
|
const pageState = getCurrentPageState();
|
||||||
|
pageState.isLoading = false;
|
||||||
|
}
|
||||||
|
}, this.loadingTimeoutDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearLoadingTimeout() {
|
||||||
|
if (this.loadingTimeout) {
|
||||||
|
clearTimeout(this.loadingTimeout);
|
||||||
|
this.loadingTimeout = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,19 +270,21 @@ export class VirtualScroller {
|
|||||||
|
|
||||||
// Calculate total rows needed based on total items and columns
|
// Calculate total rows needed based on total items and columns
|
||||||
const totalRows = Math.ceil(this.totalItems / this.columnsCount);
|
const totalRows = Math.ceil(this.totalItems / this.columnsCount);
|
||||||
const totalHeight = totalRows * this.itemHeight;
|
// Add row gaps to the total height calculation
|
||||||
|
const totalHeight = totalRows * this.itemHeight + (totalRows - 1) * this.rowGap;
|
||||||
|
|
||||||
// Update spacer height to represent all items
|
// Update spacer height to represent all items
|
||||||
this.spacerElement.style.height = `${totalHeight}px`;
|
this.spacerElement.style.height = `${totalHeight}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getVisibleRange() {
|
getVisibleRange() {
|
||||||
const scrollTop = this.containerElement.scrollTop;
|
const scrollTop = this.scrollContainer.scrollTop;
|
||||||
const viewportHeight = this.containerElement.clientHeight;
|
const viewportHeight = this.scrollContainer.clientHeight;
|
||||||
|
|
||||||
// Calculate the visible row range
|
// Calculate the visible row range, accounting for row gaps
|
||||||
const startRow = Math.floor(scrollTop / this.itemHeight);
|
const rowHeight = this.itemHeight + this.rowGap;
|
||||||
const endRow = Math.ceil((scrollTop + viewportHeight) / this.itemHeight);
|
const startRow = Math.floor(scrollTop / rowHeight);
|
||||||
|
const endRow = Math.ceil((scrollTop + viewportHeight) / rowHeight);
|
||||||
|
|
||||||
// Add overscan for smoother scrolling
|
// Add overscan for smoother scrolling
|
||||||
const overscanRows = this.overscan;
|
const overscanRows = this.overscan;
|
||||||
@@ -330,8 +381,11 @@ export class VirtualScroller {
|
|||||||
const row = Math.floor(index / this.columnsCount);
|
const row = Math.floor(index / this.columnsCount);
|
||||||
const col = index % this.columnsCount;
|
const col = index % this.columnsCount;
|
||||||
|
|
||||||
// Calculate precise positions
|
// Calculate precise positions with row gap included
|
||||||
const topPos = row * this.itemHeight;
|
const topPos = row * (this.itemHeight + this.rowGap);
|
||||||
|
|
||||||
|
// Position correctly with leftOffset (no need to add padding as absolute
|
||||||
|
// positioning is already relative to the padding edge of the container)
|
||||||
const leftPos = this.leftOffset + (col * (this.itemWidth + this.columnGap));
|
const leftPos = this.leftOffset + (col * (this.itemWidth + this.columnGap));
|
||||||
|
|
||||||
// Position the element with absolute positioning
|
// Position the element with absolute positioning
|
||||||
@@ -346,7 +400,7 @@ export class VirtualScroller {
|
|||||||
|
|
||||||
handleScroll() {
|
handleScroll() {
|
||||||
// Determine scroll direction
|
// Determine scroll direction
|
||||||
const scrollTop = this.containerElement.scrollTop;
|
const scrollTop = this.scrollContainer.scrollTop;
|
||||||
this.scrollDirection = scrollTop > this.lastScrollTop ? 'down' : 'up';
|
this.scrollDirection = scrollTop > this.lastScrollTop ? 'down' : 'up';
|
||||||
this.lastScrollTop = scrollTop;
|
this.lastScrollTop = scrollTop;
|
||||||
|
|
||||||
@@ -354,11 +408,35 @@ export class VirtualScroller {
|
|||||||
this.scheduleRender();
|
this.scheduleRender();
|
||||||
|
|
||||||
// If we're near the bottom and have more items, load them
|
// If we're near the bottom and have more items, load them
|
||||||
const { clientHeight, scrollHeight } = this.containerElement;
|
const { clientHeight, scrollHeight } = this.scrollContainer;
|
||||||
const scrollBottom = scrollTop + clientHeight;
|
const scrollBottom = scrollTop + clientHeight;
|
||||||
const scrollThreshold = scrollHeight - (this.itemHeight * this.overscan);
|
|
||||||
|
|
||||||
if (scrollBottom >= scrollThreshold && this.hasMore && !this.isLoading) {
|
// Fix the threshold calculation - use percentage of remaining height instead
|
||||||
|
// We'll trigger loading when within 20% of the bottom of rendered content
|
||||||
|
const remainingScroll = scrollHeight - scrollBottom;
|
||||||
|
const scrollThreshold = Math.min(
|
||||||
|
// Either trigger when within 20% of the total height from bottom
|
||||||
|
scrollHeight * 0.2,
|
||||||
|
// Or when within 2 rows of content from the bottom, whichever is larger
|
||||||
|
(this.itemHeight + this.rowGap) * 2
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldLoadMore = remainingScroll <= scrollThreshold;
|
||||||
|
|
||||||
|
// Enhanced debugging
|
||||||
|
// console.log('Scroll metrics:', {
|
||||||
|
// scrollBottom,
|
||||||
|
// scrollHeight,
|
||||||
|
// remainingScroll,
|
||||||
|
// scrollThreshold,
|
||||||
|
// shouldLoad: shouldLoadMore,
|
||||||
|
// hasMore: this.hasMore,
|
||||||
|
// isLoading: this.isLoading,
|
||||||
|
// itemsLoaded: this.items.length,
|
||||||
|
// totalItems: this.totalItems
|
||||||
|
// });
|
||||||
|
|
||||||
|
if (shouldLoadMore && this.hasMore && !this.isLoading) {
|
||||||
this.loadMoreItems();
|
this.loadMoreItems();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -381,7 +459,7 @@ export class VirtualScroller {
|
|||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
// Remove event listeners
|
// Remove event listeners
|
||||||
this.containerElement.removeEventListener('scroll', this.scrollHandler);
|
this.scrollContainer.removeEventListener('scroll', this.scrollHandler);
|
||||||
window.removeEventListener('resize', this.resizeHandler);
|
window.removeEventListener('resize', this.resizeHandler);
|
||||||
|
|
||||||
// Clean up the resize observer if present
|
// Clean up the resize observer if present
|
||||||
@@ -397,6 +475,9 @@ export class VirtualScroller {
|
|||||||
|
|
||||||
// Remove virtual scroll class
|
// Remove virtual scroll class
|
||||||
this.gridElement.classList.remove('virtual-scroll');
|
this.gridElement.classList.remove('virtual-scroll');
|
||||||
|
|
||||||
|
// Clear any pending timeout
|
||||||
|
this.clearLoadingTimeout();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility method for debouncing
|
// Utility method for debouncing
|
||||||
|
|||||||
@@ -99,7 +99,9 @@ async function initializeVirtualScroll(pageType) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageContent = document.querySelector('.page-content');
|
// Change this line to get the actual scrolling container
|
||||||
|
const pageContainer = document.querySelector('.page-content');
|
||||||
|
const pageContent = pageContainer.querySelector('.container');
|
||||||
|
|
||||||
if (!pageContent) {
|
if (!pageContent) {
|
||||||
console.warn('Page content element not found for virtual scroll');
|
console.warn('Page content element not found for virtual scroll');
|
||||||
@@ -115,13 +117,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`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the virtual scroller
|
// Pass the correct scrolling container
|
||||||
state.virtualScroller = new VirtualScroller({
|
state.virtualScroller = new VirtualScroller({
|
||||||
gridElement: grid,
|
gridElement: grid,
|
||||||
containerElement: pageContent,
|
containerElement: pageContent,
|
||||||
|
scrollContainer: pageContainer, // Add this new parameter
|
||||||
createItemFn: createCardFn,
|
createItemFn: createCardFn,
|
||||||
fetchItemsFn: fetchDataFn,
|
fetchItemsFn: fetchDataFn,
|
||||||
pageSize: 100
|
pageSize: 100,
|
||||||
|
rowGap: 20 // Add consistent vertical spacing between rows
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize the virtual scroller
|
// Initialize the virtual scroller
|
||||||
|
|||||||
Reference in New Issue
Block a user