mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 15:15:44 -03:00
update
This commit is contained in:
@@ -6,6 +6,181 @@ import { NSFW_LEVELS } from '../utils/constants.js';
|
|||||||
import { replacePreview, saveModelMetadata } from '../api/loraApi.js'
|
import { replacePreview, saveModelMetadata } from '../api/loraApi.js'
|
||||||
import { showDeleteModal } from '../utils/modalUtils.js';
|
import { showDeleteModal } from '../utils/modalUtils.js';
|
||||||
|
|
||||||
|
// Add a global event delegation handler
|
||||||
|
export function setupLoraCardEventDelegation() {
|
||||||
|
const gridElement = document.getElementById('loraGrid');
|
||||||
|
if (!gridElement) return;
|
||||||
|
|
||||||
|
// Remove any existing event listener to prevent duplication
|
||||||
|
gridElement.removeEventListener('click', handleLoraCardEvent);
|
||||||
|
|
||||||
|
// Add the event delegation handler
|
||||||
|
gridElement.addEventListener('click', handleLoraCardEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event delegation handler for all lora card events
|
||||||
|
function handleLoraCardEvent(event) {
|
||||||
|
// Find the closest card element
|
||||||
|
const card = event.target.closest('.lora-card');
|
||||||
|
if (!card) return;
|
||||||
|
|
||||||
|
// Handle specific elements within the card
|
||||||
|
if (event.target.closest('.toggle-blur-btn')) {
|
||||||
|
event.stopPropagation();
|
||||||
|
toggleBlurContent(card);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.target.closest('.show-content-btn')) {
|
||||||
|
event.stopPropagation();
|
||||||
|
showBlurredContent(card);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.target.closest('.fa-star')) {
|
||||||
|
event.stopPropagation();
|
||||||
|
toggleFavorite(card);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.target.closest('.fa-globe')) {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (card.dataset.from_civitai === 'true') {
|
||||||
|
openCivitai(card.dataset.name);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.target.closest('.fa-copy')) {
|
||||||
|
event.stopPropagation();
|
||||||
|
copyLoraCode(card);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.target.closest('.fa-trash')) {
|
||||||
|
event.stopPropagation();
|
||||||
|
showDeleteModal(card.dataset.filepath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.target.closest('.fa-image')) {
|
||||||
|
event.stopPropagation();
|
||||||
|
replacePreview(card.dataset.filepath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no specific element was clicked, handle the card click (show modal or toggle selection)
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions for event handling
|
||||||
|
function toggleBlurContent(card) {
|
||||||
|
const preview = card.querySelector('.card-preview');
|
||||||
|
const isBlurred = preview.classList.toggle('blurred');
|
||||||
|
const icon = card.querySelector('.toggle-blur-btn 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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showBlurredContent(card) {
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleFavorite(card) {
|
||||||
|
const starIcon = card.querySelector('.fa-star');
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyLoraCode(card) {
|
||||||
|
const usageTips = JSON.parse(card.dataset.usage_tips || '{}');
|
||||||
|
const strength = usageTips.strength || 1;
|
||||||
|
const loraSyntax = `<lora:${card.dataset.file_name}:${strength}>`;
|
||||||
|
|
||||||
|
await copyToClipboard(loraSyntax, 'LoRA syntax copied');
|
||||||
|
}
|
||||||
|
|
||||||
export function createLoraCard(lora) {
|
export function createLoraCard(lora) {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'lora-card';
|
card.className = 'lora-card';
|
||||||
@@ -123,162 +298,12 @@ export function createLoraCard(lora) {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Main card click event - modified to handle bulk mode
|
// Add a special class for virtual scroll positioning if needed
|
||||||
card.addEventListener('click', () => {
|
if (state.virtualScroller) {
|
||||||
// Check if we're in bulk mode
|
card.classList.add('virtual-scroll-item');
|
||||||
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
|
// Add video auto-play on hover functionality if needed
|
||||||
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 = `<lora:${card.dataset.file_name}:${strength}>`;
|
|
||||||
|
|
||||||
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) {
|
|
||||||
const actions = card.querySelectorAll('.card-actions');
|
|
||||||
actions.forEach(actionGroup => {
|
|
||||||
actionGroup.style.display = 'none';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add autoplayOnHover handlers for video elements if needed
|
|
||||||
const videoElement = card.querySelector('video');
|
const videoElement = card.querySelector('video');
|
||||||
if (videoElement && autoplayOnHover) {
|
if (videoElement && autoplayOnHover) {
|
||||||
const cardPreview = card.querySelector('.card-preview');
|
const cardPreview = card.querySelector('.card-preview');
|
||||||
@@ -287,20 +312,10 @@ export function createLoraCard(lora) {
|
|||||||
videoElement.removeAttribute('autoplay');
|
videoElement.removeAttribute('autoplay');
|
||||||
videoElement.pause();
|
videoElement.pause();
|
||||||
|
|
||||||
// Add mouse events to trigger play/pause
|
// Add mouse events to trigger play/pause using event attributes
|
||||||
cardPreview.addEventListener('mouseenter', () => {
|
// This approach reduces the number of event listeners created
|
||||||
videoElement.play();
|
cardPreview.setAttribute('onmouseenter', 'this.querySelector("video")?.play()');
|
||||||
});
|
cardPreview.setAttribute('onmouseleave', 'const v=this.querySelector("video"); if(v){v.pause();v.currentTime=0;}');
|
||||||
|
|
||||||
cardPreview.addEventListener('mouseleave', () => {
|
|
||||||
videoElement.pause();
|
|
||||||
videoElement.currentTime = 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a special class for virtual scroll positioning if needed
|
|
||||||
if (state.virtualScroller) {
|
|
||||||
card.classList.add('virtual-scroll-item');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return card;
|
return card;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { exampleImagesManager } from './managers/ExampleImagesManager.js';
|
|||||||
import { showToast, initTheme, initBackToTop, lazyLoadImages } from './utils/uiHelpers.js';
|
import { showToast, initTheme, initBackToTop, lazyLoadImages } from './utils/uiHelpers.js';
|
||||||
import { initializeInfiniteScroll } from './utils/infiniteScroll.js';
|
import { initializeInfiniteScroll } from './utils/infiniteScroll.js';
|
||||||
import { migrateStorageItems } from './utils/storageHelpers.js';
|
import { migrateStorageItems } from './utils/storageHelpers.js';
|
||||||
|
import { setupLoraCardEventDelegation } from './components/LoraCard.js';
|
||||||
|
|
||||||
// Core application class
|
// Core application class
|
||||||
export class AppCore {
|
export class AppCore {
|
||||||
@@ -63,6 +64,11 @@ export class AppCore {
|
|||||||
// Initialize lazy loading for images on all pages
|
// Initialize lazy loading for images on all pages
|
||||||
lazyLoadImages();
|
lazyLoadImages();
|
||||||
|
|
||||||
|
// Setup event delegation for lora cards if on the loras page
|
||||||
|
if (pageType === 'loras') {
|
||||||
|
setupLoraCardEventDelegation();
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize virtual scroll for pages that need it
|
// Initialize virtual scroll for pages that need it
|
||||||
if (['loras', 'recipes', 'checkpoints'].includes(pageType)) {
|
if (['loras', 'recipes', 'checkpoints'].includes(pageType)) {
|
||||||
initializeInfiniteScroll(pageType);
|
initializeInfiniteScroll(pageType);
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ export class VirtualScroller {
|
|||||||
this.pendingScroll = null;
|
this.pendingScroll = null;
|
||||||
this.resizeObserver = null;
|
this.resizeObserver = null;
|
||||||
|
|
||||||
|
// Data windowing parameters
|
||||||
|
this.windowSize = options.windowSize || 2000; // ±1000 items from current view
|
||||||
|
this.windowPadding = options.windowPadding || 500; // Buffer before loading more
|
||||||
|
this.dataWindow = { start: 0, end: 0 }; // Current data window indices
|
||||||
|
this.absoluteWindowStart = 0; // Start index in absolute terms
|
||||||
|
this.fetchingWindow = false; // Flag to track window fetching state
|
||||||
|
|
||||||
// Responsive layout state
|
// Responsive layout state
|
||||||
this.itemWidth = 0;
|
this.itemWidth = 0;
|
||||||
this.itemHeight = 0;
|
this.itemHeight = 0;
|
||||||
@@ -176,9 +183,13 @@ export class VirtualScroller {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { items, totalItems, hasMore } = await this.fetchItemsFn(1, this.pageSize);
|
const { items, totalItems, hasMore } = await this.fetchItemsFn(1, this.pageSize);
|
||||||
|
|
||||||
|
// Initialize the data window with the first batch of items
|
||||||
this.items = items || [];
|
this.items = items || [];
|
||||||
this.totalItems = totalItems || 0;
|
this.totalItems = totalItems || 0;
|
||||||
this.hasMore = hasMore;
|
this.hasMore = hasMore;
|
||||||
|
this.dataWindow = { start: 0, end: this.items.length };
|
||||||
|
this.absoluteWindowStart = 0;
|
||||||
|
|
||||||
// Update the spacer height based on the total number of items
|
// Update the spacer height based on the total number of items
|
||||||
this.updateSpacerHeight();
|
this.updateSpacerHeight();
|
||||||
@@ -337,20 +348,31 @@ export class VirtualScroller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new visible items
|
// Use DocumentFragment for batch DOM operations
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
|
||||||
|
// Add new visible items to the fragment
|
||||||
for (let i = start; i < end && i < this.items.length; i++) {
|
for (let i = start; i < end && i < this.items.length; i++) {
|
||||||
if (!this.renderedItems.has(i)) {
|
if (!this.renderedItems.has(i)) {
|
||||||
const item = this.items[i];
|
const item = this.items[i];
|
||||||
const element = this.createItemElement(item, i);
|
const element = this.createItemElement(item, i);
|
||||||
this.gridElement.appendChild(element);
|
fragment.appendChild(element);
|
||||||
this.renderedItems.set(i, element);
|
this.renderedItems.set(i, element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the fragment to the grid (single DOM operation)
|
||||||
|
if (fragment.childNodes.length > 0) {
|
||||||
|
this.gridElement.appendChild(fragment);
|
||||||
|
}
|
||||||
|
|
||||||
// If we're close to the end and have more items to load, fetch them
|
// If we're close to the end and have more items to load, fetch them
|
||||||
if (end > this.items.length - (this.columnsCount * 2) && this.hasMore && !this.isLoading) {
|
if (end > this.items.length - (this.columnsCount * 2) && this.hasMore && !this.isLoading) {
|
||||||
this.loadMoreItems();
|
this.loadMoreItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if we need to slide the data window
|
||||||
|
this.slideDataWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
clearRenderedItems() {
|
clearRenderedItems() {
|
||||||
@@ -404,11 +426,30 @@ export class VirtualScroller {
|
|||||||
this.scrollDirection = scrollTop > this.lastScrollTop ? 'down' : 'up';
|
this.scrollDirection = scrollTop > this.lastScrollTop ? 'down' : 'up';
|
||||||
this.lastScrollTop = scrollTop;
|
this.lastScrollTop = scrollTop;
|
||||||
|
|
||||||
|
// Handle large jumps in scroll position - check if we need to fetch a new window
|
||||||
|
const { scrollHeight } = this.scrollContainer;
|
||||||
|
const scrollRatio = scrollTop / scrollHeight;
|
||||||
|
|
||||||
|
// If we've jumped to a position that's significantly outside our current window
|
||||||
|
// and we know there are many items, fetch a new data window
|
||||||
|
if (this.totalItems > this.windowSize) {
|
||||||
|
const estimatedIndex = Math.floor(scrollRatio * this.totalItems);
|
||||||
|
const currentWindowStart = this.absoluteWindowStart;
|
||||||
|
const currentWindowEnd = currentWindowStart + this.items.length;
|
||||||
|
|
||||||
|
// If the estimated position is outside our current window by a significant amount
|
||||||
|
if (estimatedIndex < currentWindowStart || estimatedIndex > currentWindowEnd) {
|
||||||
|
// Fetch a new data window centered on the estimated position
|
||||||
|
this.fetchDataWindow(Math.max(0, estimatedIndex - Math.floor(this.windowSize / 2)));
|
||||||
|
return; // Skip normal rendering until new data is loaded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Render visible items
|
// Render visible items
|
||||||
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.scrollContainer;
|
const { clientHeight } = this.scrollContainer;
|
||||||
const scrollBottom = scrollTop + clientHeight;
|
const scrollBottom = scrollTop + clientHeight;
|
||||||
|
|
||||||
// Fix the threshold calculation - use percentage of remaining height instead
|
// Fix the threshold calculation - use percentage of remaining height instead
|
||||||
@@ -423,24 +464,94 @@ export class VirtualScroller {
|
|||||||
|
|
||||||
const shouldLoadMore = remainingScroll <= scrollThreshold;
|
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) {
|
if (shouldLoadMore && this.hasMore && !this.isLoading) {
|
||||||
this.loadMoreItems();
|
this.loadMoreItems();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method to fetch data for a specific window position
|
||||||
|
async fetchDataWindow(targetIndex) {
|
||||||
|
if (this.fetchingWindow) return;
|
||||||
|
this.fetchingWindow = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Calculate which page we need to fetch based on target index
|
||||||
|
const targetPage = Math.floor(targetIndex / this.pageSize) + 1;
|
||||||
|
console.log(`Fetching data window for index ${targetIndex}, page ${targetPage}`);
|
||||||
|
|
||||||
|
const { items, totalItems, hasMore } = await this.fetchItemsFn(targetPage, this.pageSize);
|
||||||
|
|
||||||
|
if (items && items.length > 0) {
|
||||||
|
// Calculate new absolute window start
|
||||||
|
this.absoluteWindowStart = (targetPage - 1) * this.pageSize;
|
||||||
|
|
||||||
|
// Replace the entire data window with new items
|
||||||
|
this.items = items;
|
||||||
|
this.dataWindow = {
|
||||||
|
start: 0,
|
||||||
|
end: items.length
|
||||||
|
};
|
||||||
|
|
||||||
|
this.totalItems = totalItems || 0;
|
||||||
|
this.hasMore = hasMore;
|
||||||
|
|
||||||
|
// Update the current page for future fetches
|
||||||
|
const pageState = getCurrentPageState();
|
||||||
|
pageState.currentPage = targetPage + 1;
|
||||||
|
pageState.hasMore = hasMore;
|
||||||
|
|
||||||
|
// Update the spacer height and clear current rendered items
|
||||||
|
this.updateSpacerHeight();
|
||||||
|
this.clearRenderedItems();
|
||||||
|
this.scheduleRender();
|
||||||
|
|
||||||
|
console.log(`Loaded ${items.length} items for window at absolute index ${this.absoluteWindowStart}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch data window:', err);
|
||||||
|
showToast('Failed to load items at this position', 'error');
|
||||||
|
} finally {
|
||||||
|
this.fetchingWindow = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to slide the data window if we're approaching its edges
|
||||||
|
async slideDataWindow() {
|
||||||
|
const { start, end } = this.getVisibleRange();
|
||||||
|
const windowStart = this.dataWindow.start;
|
||||||
|
const windowEnd = this.dataWindow.end;
|
||||||
|
const absoluteIndex = this.absoluteWindowStart + windowStart;
|
||||||
|
|
||||||
|
// Calculate the midpoint of the visible range
|
||||||
|
const visibleMidpoint = Math.floor((start + end) / 2);
|
||||||
|
const absoluteMidpoint = this.absoluteWindowStart + visibleMidpoint;
|
||||||
|
|
||||||
|
// Check if we're too close to the window edges
|
||||||
|
const closeToStart = start - windowStart < this.windowPadding;
|
||||||
|
const closeToEnd = windowEnd - end < this.windowPadding;
|
||||||
|
|
||||||
|
// If we're close to either edge and have total items > window size
|
||||||
|
if ((closeToStart || closeToEnd) && this.totalItems > this.windowSize) {
|
||||||
|
// Calculate a new target index centered around the current viewport
|
||||||
|
const halfWindow = Math.floor(this.windowSize / 2);
|
||||||
|
const targetIndex = Math.max(0, absoluteMidpoint - halfWindow);
|
||||||
|
|
||||||
|
// Don't fetch a new window if we're already showing items near the beginning
|
||||||
|
if (targetIndex === 0 && this.absoluteWindowStart === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't fetch if we're showing the end of the list and are near the end
|
||||||
|
if (this.absoluteWindowStart + this.items.length >= this.totalItems &&
|
||||||
|
this.totalItems - end < halfWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the new data window
|
||||||
|
await this.fetchDataWindow(targetIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
// Remove all rendered items
|
// Remove all rendered items
|
||||||
this.clearRenderedItems();
|
this.clearRenderedItems();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { state, getCurrentPageState } from '../state/index.js';
|
import { state, getCurrentPageState } from '../state/index.js';
|
||||||
import { debounce } from './debounce.js';
|
import { debounce } from './debounce.js';
|
||||||
import { VirtualScroller } from './VirtualScroller.js';
|
import { VirtualScroller } from './VirtualScroller.js';
|
||||||
import { createLoraCard } from '../components/LoraCard.js';
|
import { createLoraCard, setupLoraCardEventDelegation } from '../components/LoraCard.js';
|
||||||
import { fetchLorasPage } from '../api/loraApi.js';
|
import { fetchLorasPage } from '../api/loraApi.js';
|
||||||
import { showToast } from './uiHelpers.js';
|
import { showToast } from './uiHelpers.js';
|
||||||
|
|
||||||
@@ -73,6 +73,11 @@ export async function initializeInfiniteScroll(pageType = 'loras') {
|
|||||||
|
|
||||||
// Use virtual scrolling for all page types
|
// Use virtual scrolling for all page types
|
||||||
await initializeVirtualScroll(pageType);
|
await initializeVirtualScroll(pageType);
|
||||||
|
|
||||||
|
// Setup event delegation for lora cards if on the loras page
|
||||||
|
if (pageType === 'loras') {
|
||||||
|
setupLoraCardEventDelegation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initializeVirtualScroll(pageType) {
|
async function initializeVirtualScroll(pageType) {
|
||||||
|
|||||||
Reference in New Issue
Block a user