fix(recipes): preserve scroll on in-place reloads

This commit is contained in:
Will Miao
2026-04-13 10:30:50 +08:00
parent 39c083db79
commit ba1800095e
9 changed files with 173 additions and 18 deletions

View File

@@ -1,6 +1,7 @@
import { RecipeCard } from '../components/RecipeCard.js';
import { state, getCurrentPageState } from '../state/index.js';
import { showToast } from '../utils/uiHelpers.js';
import { captureScrollPosition, restoreScrollPosition } from '../utils/infiniteScroll.js';
const RECIPE_ENDPOINTS = {
list: '/api/lm/recipes',
@@ -182,10 +183,12 @@ export async function resetAndReloadWithVirtualScroll(options = {}) {
const {
modelType = 'lora',
updateFolders = false,
fetchPageFunction
fetchPageFunction,
preserveScroll = false
} = options;
const pageState = getCurrentPageState();
const scrollSnapshot = preserveScroll ? captureScrollPosition() : null;
try {
pageState.isLoading = true;
@@ -207,6 +210,10 @@ export async function resetAndReloadWithVirtualScroll(options = {}) {
pageState.hasMore = result.hasMore;
pageState.currentPage = 2; // Next page will be 2
if (scrollSnapshot) {
await restoreScrollPosition(scrollSnapshot);
}
return result;
} catch (error) {
console.error(`Error reloading ${modelType}s:`, error);
@@ -227,10 +234,12 @@ export async function loadMoreWithVirtualScroll(options = {}) {
modelType = 'lora',
resetPage = false,
updateFolders = false,
fetchPageFunction
fetchPageFunction,
preserveScroll = false
} = options;
const pageState = getCurrentPageState();
const scrollSnapshot = preserveScroll ? captureScrollPosition() : null;
try {
// Start loading state
@@ -255,6 +264,10 @@ export async function loadMoreWithVirtualScroll(options = {}) {
pageState.hasMore = result.hasMore;
pageState.currentPage = 2; // Next page to load would be 2
if (scrollSnapshot) {
await restoreScrollPosition(scrollSnapshot);
}
return result;
} catch (error) {
console.error(`Error loading ${modelType}s:`, error);
@@ -270,11 +283,12 @@ export async function loadMoreWithVirtualScroll(options = {}) {
* @param {boolean} updateFolders - Whether to update folder tags
* @returns {Promise<Object>} The fetch result
*/
export async function resetAndReload(updateFolders = false) {
export async function resetAndReload(updateFolders = false, options = {}) {
return resetAndReloadWithVirtualScroll({
modelType: 'recipe',
updateFolders,
fetchPageFunction: fetchRecipesPage
fetchPageFunction: fetchRecipesPage,
preserveScroll: options.preserveScroll === true
});
}
@@ -286,7 +300,7 @@ export async function syncChanges() {
state.loadingManager.showSimpleLoading('Syncing changes...');
// Simply reload the recipes without rebuilding cache
await resetAndReload();
await resetAndReload(false, { preserveScroll: true });
showToast('toast.recipes.syncComplete', {}, 'success');
} catch (error) {
@@ -314,7 +328,7 @@ export async function refreshRecipes() {
}
// After successful cache rebuild, reload the recipes
await resetAndReload();
await resetAndReload(false, { preserveScroll: true });
showToast('toast.recipes.refreshComplete', {}, 'success');
} catch (error) {

View File

@@ -23,7 +23,7 @@ export class RecipeContextMenu extends BaseContextMenu {
// Override resetAndReload for recipe context
async resetAndReload() {
const { resetAndReload } = await import('../../api/recipeApi.js');
return resetAndReload();
return resetAndReload(false, { preserveScroll: true });
}
showMenu(x, y, card) {

View File

@@ -432,7 +432,7 @@ export class BatchImportManager {
// Refresh recipes list to show newly imported recipes
if (window.recipeManager && typeof window.recipeManager.loadRecipes === 'function') {
window.recipeManager.loadRecipes();
window.recipeManager.loadRecipes({ preserveScroll: true });
}
// Show results step

View File

@@ -311,7 +311,7 @@ export class BulkMissingLoraDownloadManager {
// Refresh the recipes list to update LoRA status
if (window.recipeManager) {
window.recipeManager.loadRecipes();
window.recipeManager.loadRecipes({ preserveScroll: true });
}
}

View File

@@ -122,7 +122,7 @@ export class DownloadManager {
modalManager.closeModal('importModal');
// Refresh the recipe
window.recipeManager.loadRecipes();
window.recipeManager.loadRecipes({ preserveScroll: true });
} catch (error) {
console.error('Error:', error);

View File

@@ -328,16 +328,32 @@ class RecipeManager {
});
}
normalizeLoadRecipesOptions(options = true) {
if (typeof options === 'boolean') {
return {
resetPage: options,
preserveScroll: false
};
}
return {
resetPage: options?.resetPage !== false,
preserveScroll: options?.preserveScroll === true
};
}
// This method is kept for compatibility but now uses virtual scrolling
async loadRecipes(resetPage = true) {
async loadRecipes(options = true) {
// Skip loading if in duplicates mode
const pageState = getCurrentPageState();
if (pageState.duplicatesMode) {
return;
}
const { resetPage, preserveScroll } = this.normalizeLoadRecipesOptions(options);
if (resetPage) {
refreshVirtualScroll();
await refreshVirtualScroll({ preserveScroll });
}
}

View File

@@ -4,6 +4,43 @@ import { createModelCard, setupModelCardEventDelegation } from '../components/sh
import { getModelApiClient } from '../api/modelApiFactory.js';
import { showToast } from './uiHelpers.js';
function getScrollContainer() {
return document.querySelector('.page-content');
}
function getClampedScrollTop(scrollContainer, scrollTop) {
const maxScrollTop = Math.max(0, scrollContainer.scrollHeight - scrollContainer.clientHeight);
return Math.min(Math.max(scrollTop, 0), maxScrollTop);
}
function waitForAnimationFrame() {
return new Promise(resolve => requestAnimationFrame(resolve));
}
export function captureScrollPosition() {
const scrollContainer = getScrollContainer();
if (!scrollContainer) {
return null;
}
return {
scrollContainer,
scrollTop: scrollContainer.scrollTop
};
}
export async function restoreScrollPosition(snapshot) {
if (!snapshot?.scrollContainer) {
return;
}
// Wait for layout and the scheduled virtual-scroll render to settle.
await waitForAnimationFrame();
await waitForAnimationFrame();
snapshot.scrollContainer.scrollTop = getClampedScrollTop(snapshot.scrollContainer, snapshot.scrollTop);
}
// Function to dynamically import the appropriate card creator based on page type
async function getCardCreator(pageType) {
if (pageType === 'recipes') {
@@ -87,7 +124,7 @@ async function initializeVirtualScroll(pageType) {
}
// Change this line to get the actual scrolling container
const scrollContainer = document.querySelector('.page-content');
const scrollContainer = getScrollContainer();
const gridContainer = scrollContainer.querySelector('.container');
if (!gridContainer) {
@@ -200,9 +237,16 @@ export function cleanupKeyboardNavigation() {
}
// Export a method to refresh the virtual scroller when filters change
export function refreshVirtualScroll() {
export async function refreshVirtualScroll(options = {}) {
const { preserveScroll = false } = options;
if (state.virtualScroller) {
const scrollSnapshot = preserveScroll ? captureScrollPosition() : null;
state.virtualScroller.reset();
state.virtualScroller.initialize();
await state.virtualScroller.initialize();
if (scrollSnapshot) {
await restoreScrollPosition(scrollSnapshot);
}
}
}
}