mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-19 00:42:05 -03:00
fix(virtual-scroll): avoid full reload on move-to-folder, scroll to top on filter/page reset
- MoveManager/SidebarManager: replace resetAndReload with in-place VirtualScroller update after move operations (remove non-visible, update visible items' file_path). Preserves scroll position and avoids empty grid. - VirtualScroller: add removeMultipleItemsByFilePath for efficient batch removal with Array.isArray guard. - baseModelApi: scroll to top on loadMoreWithVirtualScroll(true), covering filter/sort/search/folder/views changes. - SidebarManager selectFolder: scroll now handled centrally.
This commit is contained in:
@@ -133,6 +133,16 @@ export class BaseModelApiClient {
|
|||||||
pageState.hasMore = result.hasMore;
|
pageState.hasMore = result.hasMore;
|
||||||
pageState.currentPage = pageState.currentPage + 1;
|
pageState.currentPage = pageState.currentPage + 1;
|
||||||
|
|
||||||
|
// When resetting to page 1, scroll back to the top
|
||||||
|
// This covers: folder selection, filter/sort/search changes,
|
||||||
|
// favorites/update/excluded view toggles, alphabet filter, etc.
|
||||||
|
if (resetPage) {
|
||||||
|
const scrollContainer = document.querySelector('.page-content');
|
||||||
|
if (scrollContainer) {
|
||||||
|
scrollContainer.scrollTop = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (updateFolders) {
|
if (updateFolders) {
|
||||||
sidebarManager.refresh();
|
sidebarManager.refresh();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
|
import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
|
||||||
import { getModelApiClient } from '../api/modelApiFactory.js';
|
import { getModelApiClient } from '../api/modelApiFactory.js';
|
||||||
import { translate } from '../utils/i18nHelpers.js';
|
import { translate } from '../utils/i18nHelpers.js';
|
||||||
import { state } from '../state/index.js';
|
import { state, getCurrentPageState } from '../state/index.js';
|
||||||
import { bulkManager } from '../managers/BulkManager.js';
|
import { bulkManager } from '../managers/BulkManager.js';
|
||||||
import { showToast } from '../utils/uiHelpers.js';
|
import { showToast } from '../utils/uiHelpers.js';
|
||||||
import { performFolderUpdateCheck } from '../utils/updateCheckHelpers.js';
|
import { performFolderUpdateCheck } from '../utils/updateCheckHelpers.js';
|
||||||
@@ -457,21 +457,69 @@ export class SidebarManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[SidebarManager] calling apiClient.move, useBulkMove:', useBulkMove);
|
console.log('[SidebarManager] calling apiClient.move, useBulkMove:', useBulkMove);
|
||||||
|
let movedFiles = []; // Array of { original_file_path, new_file_path }
|
||||||
|
|
||||||
if (useBulkMove) {
|
if (useBulkMove) {
|
||||||
await this.apiClient.moveBulkModels(this.draggedFilePaths, destination);
|
const results = await this.apiClient.moveBulkModels(this.draggedFilePaths, destination);
|
||||||
|
movedFiles = (results || [])
|
||||||
|
.filter(r => r.success)
|
||||||
|
.map(r => ({ original_file_path: r.original_file_path, new_file_path: r.new_file_path }));
|
||||||
} else {
|
} else {
|
||||||
await this.apiClient.moveSingleModel(this.draggedFilePaths[0], destination);
|
const result = await this.apiClient.moveSingleModel(this.draggedFilePaths[0], destination);
|
||||||
|
if (result) {
|
||||||
|
movedFiles.push({
|
||||||
|
original_file_path: result.original_file_path || this.draggedFilePaths[0],
|
||||||
|
new_file_path: result.new_file_path
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
console.log('[SidebarManager] apiClient.move successful');
|
console.log('[SidebarManager] apiClient.move successful');
|
||||||
|
|
||||||
if (this.pageControls && typeof this.pageControls.resetAndReload === 'function') {
|
// Update VirtualScroller in-place instead of full reload
|
||||||
console.log('[SidebarManager] calling resetAndReload');
|
if (movedFiles.length > 0 && state.virtualScroller) {
|
||||||
await this.pageControls.resetAndReload(true);
|
const pageState = getCurrentPageState();
|
||||||
} else {
|
const normalizedActive = (pageState.activeFolder || '').replace(/\\/g, '/').replace(/\/$/, '');
|
||||||
console.log('[SidebarManager] calling refresh');
|
const isRecursive = pageState.searchOptions?.recursive ?? true;
|
||||||
await this.refresh();
|
const isFolderFiltered = pageState.activeFolder !== null;
|
||||||
|
|
||||||
|
const normalizedTarget = targetRelativePath.replace(/\\/g, '/').replace(/\/$/, '');
|
||||||
|
|
||||||
|
// Determine if items in the target folder are visible in the current view
|
||||||
|
let itemsRemainVisible = true;
|
||||||
|
if (isFolderFiltered) {
|
||||||
|
if (isRecursive) {
|
||||||
|
itemsRemainVisible = normalizedActive === '' ||
|
||||||
|
normalizedTarget === normalizedActive ||
|
||||||
|
normalizedTarget.startsWith(normalizedActive + '/');
|
||||||
|
} else {
|
||||||
|
itemsRemainVisible = normalizedTarget === normalizedActive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemsRemainVisible) {
|
||||||
|
// Items stay visible — update each item's file_path to reflect new location
|
||||||
|
for (const moved of movedFiles) {
|
||||||
|
if (moved.original_file_path && moved.new_file_path) {
|
||||||
|
state.virtualScroller.updateSingleItem(moved.original_file_path, {
|
||||||
|
file_path: moved.new_file_path,
|
||||||
|
folder: normalizedTarget
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Items no longer visible in current folder — remove from VirtualScroller
|
||||||
|
const pathsToRemove = movedFiles
|
||||||
|
.map(m => m.original_file_path)
|
||||||
|
.filter(Boolean);
|
||||||
|
if (pathsToRemove.length > 0) {
|
||||||
|
state.virtualScroller.removeMultipleItemsByFilePath(pathsToRemove);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh sidebar folder tree only (no model data reload)
|
||||||
|
await this.refresh();
|
||||||
|
|
||||||
if (this.draggedFromBulk && state.bulkMode && typeof bulkManager?.toggleBulkMode === 'function') {
|
if (this.draggedFromBulk && state.bulkMode && typeof bulkManager?.toggleBulkMode === 'function') {
|
||||||
bulkManager.toggleBulkMode();
|
bulkManager.toggleBulkMode();
|
||||||
}
|
}
|
||||||
@@ -530,21 +578,69 @@ export class SidebarManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[SidebarManager] calling apiClient.move, useBulkMove:', useBulkMove);
|
console.log('[SidebarManager] calling apiClient.move, useBulkMove:', useBulkMove);
|
||||||
|
let movedFiles = []; // Array of { original_file_path, new_file_path }
|
||||||
|
|
||||||
if (useBulkMove) {
|
if (useBulkMove) {
|
||||||
await this.apiClient.moveBulkModels(draggedFilePaths, destination);
|
const results = await this.apiClient.moveBulkModels(draggedFilePaths, destination);
|
||||||
|
movedFiles = (results || [])
|
||||||
|
.filter(r => r.success)
|
||||||
|
.map(r => ({ original_file_path: r.original_file_path, new_file_path: r.new_file_path }));
|
||||||
} else {
|
} else {
|
||||||
await this.apiClient.moveSingleModel(draggedFilePaths[0], destination);
|
const result = await this.apiClient.moveSingleModel(draggedFilePaths[0], destination);
|
||||||
|
if (result) {
|
||||||
|
movedFiles.push({
|
||||||
|
original_file_path: result.original_file_path || draggedFilePaths[0],
|
||||||
|
new_file_path: result.new_file_path
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
console.log('[SidebarManager] apiClient.move successful');
|
console.log('[SidebarManager] apiClient.move successful');
|
||||||
|
|
||||||
if (this.pageControls && typeof this.pageControls.resetAndReload === 'function') {
|
// Update VirtualScroller in-place instead of full reload
|
||||||
console.log('[SidebarManager] calling resetAndReload');
|
if (movedFiles.length > 0 && state.virtualScroller) {
|
||||||
await this.pageControls.resetAndReload(true);
|
const pageState = getCurrentPageState();
|
||||||
} else {
|
const normalizedActive = (pageState.activeFolder || '').replace(/\\/g, '/').replace(/\/$/, '');
|
||||||
console.log('[SidebarManager] calling refresh');
|
const isRecursive = pageState.searchOptions?.recursive ?? true;
|
||||||
await this.refresh();
|
const isFolderFiltered = pageState.activeFolder !== null;
|
||||||
|
|
||||||
|
const normalizedTarget = targetRelativePath.replace(/\\/g, '/').replace(/\/$/, '');
|
||||||
|
|
||||||
|
// Determine if items in the target folder are visible in the current view
|
||||||
|
let itemsRemainVisible = true;
|
||||||
|
if (isFolderFiltered) {
|
||||||
|
if (isRecursive) {
|
||||||
|
itemsRemainVisible = normalizedActive === '' ||
|
||||||
|
normalizedTarget === normalizedActive ||
|
||||||
|
normalizedTarget.startsWith(normalizedActive + '/');
|
||||||
|
} else {
|
||||||
|
itemsRemainVisible = normalizedTarget === normalizedActive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemsRemainVisible) {
|
||||||
|
// Items stay visible — update each item's file_path to reflect new location
|
||||||
|
for (const moved of movedFiles) {
|
||||||
|
if (moved.original_file_path && moved.new_file_path) {
|
||||||
|
state.virtualScroller.updateSingleItem(moved.original_file_path, {
|
||||||
|
file_path: moved.new_file_path,
|
||||||
|
folder: normalizedTarget
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Items no longer visible in current folder — remove from VirtualScroller
|
||||||
|
const pathsToRemove = movedFiles
|
||||||
|
.map(m => m.original_file_path)
|
||||||
|
.filter(Boolean);
|
||||||
|
if (pathsToRemove.length > 0) {
|
||||||
|
state.virtualScroller.removeMultipleItemsByFilePath(pathsToRemove);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh sidebar folder tree only (no model data reload)
|
||||||
|
await this.refresh();
|
||||||
|
|
||||||
if (draggedFromBulk && state.bulkMode && typeof bulkManager?.toggleBulkMode === 'function') {
|
if (draggedFromBulk && state.bulkMode && typeof bulkManager?.toggleBulkMode === 'function') {
|
||||||
bulkManager.toggleBulkMode();
|
bulkManager.toggleBulkMode();
|
||||||
}
|
}
|
||||||
@@ -1346,7 +1442,7 @@ export class SidebarManager {
|
|||||||
this.pageControls.pageState.activeFolder = normalizedPath;
|
this.pageControls.pageState.activeFolder = normalizedPath;
|
||||||
setStorageItem(`${this.pageType}_activeFolder`, normalizedPath);
|
setStorageItem(`${this.pageType}_activeFolder`, normalizedPath);
|
||||||
|
|
||||||
// Reload models with new filter
|
// Reload models with new filter (loadMoreWithVirtualScroll will scroll to top)
|
||||||
await this.pageControls.resetAndReload();
|
await this.pageControls.resetAndReload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -321,29 +321,94 @@ class MoveManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let movedFiles = []; // Array of { original_file_path, new_file_path }
|
||||||
|
|
||||||
if (this.bulkFilePaths) {
|
if (this.bulkFilePaths) {
|
||||||
// Bulk move mode
|
// Bulk move mode
|
||||||
await apiClient.moveBulkModels(this.bulkFilePaths, targetPath, this.useDefaultPath);
|
const results = await apiClient.moveBulkModels(this.bulkFilePaths, targetPath, this.useDefaultPath);
|
||||||
|
movedFiles = (results || [])
|
||||||
|
.filter(r => r.success)
|
||||||
|
.map(r => ({ original_file_path: r.original_file_path, new_file_path: r.new_file_path }));
|
||||||
|
|
||||||
// Deselect moving items
|
// Deselect moving items
|
||||||
this.bulkFilePaths.forEach(path => bulkManager.deselectItem(path));
|
this.bulkFilePaths.forEach(path => bulkManager.deselectItem(path));
|
||||||
} else {
|
} else {
|
||||||
// Single move mode
|
// Single move mode
|
||||||
await apiClient.moveSingleModel(this.currentFilePath, targetPath, this.useDefaultPath);
|
const result = await apiClient.moveSingleModel(this.currentFilePath, targetPath, this.useDefaultPath);
|
||||||
|
if (result) {
|
||||||
|
movedFiles.push({
|
||||||
|
original_file_path: result.original_file_path || this.currentFilePath,
|
||||||
|
new_file_path: result.new_file_path
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Deselect moving item
|
// Deselect moving item
|
||||||
bulkManager.deselectItem(this.currentFilePath);
|
bulkManager.deselectItem(this.currentFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh UI by reloading the current page, same as drag-and-drop behavior
|
// Update VirtualScroller in-place instead of full reload
|
||||||
// This ensures all metadata (like preview URLs) are correctly formatted by the backend
|
if (movedFiles.length > 0 && state.virtualScroller) {
|
||||||
if (sidebarManager.pageControls && typeof sidebarManager.pageControls.resetAndReload === 'function') {
|
// Get current page state for folder filter check
|
||||||
await sidebarManager.pageControls.resetAndReload(true);
|
const pageState = getCurrentPageState();
|
||||||
} else if (sidebarManager.lastPageControls && typeof sidebarManager.lastPageControls.resetAndReload === 'function') {
|
const normalizedActive = (pageState.activeFolder || '').replace(/\\/g, '/').replace(/\/$/, '');
|
||||||
await sidebarManager.lastPageControls.resetAndReload(true);
|
const isRecursive = pageState.searchOptions?.recursive ?? true;
|
||||||
|
const isFolderFiltered = pageState.activeFolder !== null;
|
||||||
|
|
||||||
|
// Determine which items are still visible after the move
|
||||||
|
const pathsToRemove = [];
|
||||||
|
const pathsToUpdate = []; // { originalPath, newData }
|
||||||
|
|
||||||
|
for (const moved of movedFiles) {
|
||||||
|
if (!moved.original_file_path) continue;
|
||||||
|
|
||||||
|
if (isFolderFiltered) {
|
||||||
|
// Compute relative folder of the new path
|
||||||
|
const newRelativeFolder = this._getRelativeFolder(moved.new_file_path);
|
||||||
|
const normalizedNewFolder = newRelativeFolder.replace(/\\/g, '/').replace(/\/$/, '');
|
||||||
|
|
||||||
|
// Check if the new location is still within the active folder
|
||||||
|
let stillVisible;
|
||||||
|
if (isRecursive) {
|
||||||
|
stillVisible = normalizedActive === '' ||
|
||||||
|
normalizedNewFolder === normalizedActive ||
|
||||||
|
normalizedNewFolder.startsWith(normalizedActive + '/');
|
||||||
|
} else {
|
||||||
|
stillVisible = normalizedNewFolder === normalizedActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stillVisible) {
|
||||||
|
pathsToUpdate.push({
|
||||||
|
originalPath: moved.original_file_path,
|
||||||
|
newData: {
|
||||||
|
file_path: moved.new_file_path,
|
||||||
|
folder: newRelativeFolder
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pathsToRemove.push(moved.original_file_path);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No folder filter active — items remain visible, just update path
|
||||||
|
pathsToUpdate.push({
|
||||||
|
originalPath: moved.original_file_path,
|
||||||
|
newData: {
|
||||||
|
file_path: moved.new_file_path,
|
||||||
|
folder: this._getRelativeFolder(moved.new_file_path)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply updates to the VirtualScroller
|
||||||
|
if (pathsToRemove.length > 0) {
|
||||||
|
state.virtualScroller.removeMultipleItemsByFilePath(pathsToRemove);
|
||||||
|
}
|
||||||
|
for (const update of pathsToUpdate) {
|
||||||
|
state.virtualScroller.updateSingleItem(update.originalPath, update.newData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh folder tree in sidebar
|
// Refresh folder tree in sidebar (no model data reload)
|
||||||
await sidebarManager.refresh();
|
await sidebarManager.refresh();
|
||||||
|
|
||||||
modalManager.closeModal('moveModal');
|
modalManager.closeModal('moveModal');
|
||||||
|
|||||||
@@ -931,6 +931,38 @@ export class VirtualScroller {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove multiple items by their file paths.
|
||||||
|
* More efficient than calling removeItemByFilePath individually.
|
||||||
|
* @param {string[]} filePaths - Array of file paths to remove
|
||||||
|
* @returns {boolean} - True if any items were removed
|
||||||
|
*/
|
||||||
|
removeMultipleItemsByFilePath(filePaths) {
|
||||||
|
if (!Array.isArray(filePaths) || filePaths.length === 0 || this.disabled || this.items.length === 0) return false;
|
||||||
|
|
||||||
|
// Build a set for fast lookup
|
||||||
|
const pathsToRemove = new Set(filePaths);
|
||||||
|
const originalLength = this.items.length;
|
||||||
|
|
||||||
|
// Filter out removed items; keep those not in the set
|
||||||
|
this.items = this.items.filter(item => !pathsToRemove.has(item.file_path));
|
||||||
|
|
||||||
|
const removedCount = originalLength - this.items.length;
|
||||||
|
if (removedCount === 0) return false;
|
||||||
|
|
||||||
|
this.totalItems = Math.max(0, this.totalItems - removedCount);
|
||||||
|
|
||||||
|
// Update the spacer height
|
||||||
|
this.updateSpacerHeight();
|
||||||
|
|
||||||
|
// Re-render to fill gaps left by removed items
|
||||||
|
this.clearRenderedItems();
|
||||||
|
this.scheduleRender();
|
||||||
|
|
||||||
|
console.log(`Removed ${removedCount} items from virtual scroller data`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Add keyboard navigation methods
|
// Add keyboard navigation methods
|
||||||
handlePageUpDown(direction) {
|
handlePageUpDown(direction) {
|
||||||
// Prevent duplicate animations by checking last trigger time
|
// Prevent duplicate animations by checking last trigger time
|
||||||
|
|||||||
Reference in New Issue
Block a user