Implement lazy loading and infinite scroll features in core application

- Added lazy loading for images and initialized infinite scroll in the AppCore class to enhance performance across various pages.
- Updated LoraPageManager and RecipeManager to utilize the new initializePageFeatures method for common UI features.
- Enhanced infinite scroll functionality to dynamically load more content based on the page type, improving user experience.
- Refactored recipes.html to trigger the import modal through the ModalManager for better modal handling.
This commit is contained in:
Will Miao
2025-03-19 17:04:58 +08:00
parent 32fa81cf93
commit 04545c5706
5 changed files with 114 additions and 29 deletions

View File

@@ -5,7 +5,8 @@ import { modalManager } from './managers/ModalManager.js';
import { updateService } from './managers/UpdateService.js';
import { HeaderManager } from './components/Header.js';
import { SettingsManager } from './managers/SettingsManager.js';
import { showToast, initTheme, initBackToTop, updatePanelPositions } from './utils/uiHelpers.js';
import { showToast, initTheme, initBackToTop, updatePanelPositions, lazyLoadImages } from './utils/uiHelpers.js';
import { initializeInfiniteScroll } from './utils/infiniteScroll.js';
// Core application class
export class AppCore {
@@ -55,10 +56,28 @@ export class AppCore {
showToast(message, type = 'info') {
showToast(message, type);
}
// Initialize common UI features based on page type
initializePageFeatures() {
const pageType = this.getPageType();
// Initialize lazy loading for images on all pages
lazyLoadImages();
// Initialize infinite scroll for pages that need it
if (['loras', 'recipes', 'checkpoints'].includes(pageType)) {
initializeInfiniteScroll(pageType);
}
// Update panel positions
updatePanelPositions();
return this;
}
}
// Create and export a singleton instance
export const appCore = new AppCore();
// Export common utilities for global use
export { showToast, modalManager, state };
export { showToast, modalManager, state, lazyLoadImages, initializeInfiniteScroll };

View File

@@ -65,9 +65,7 @@ class LoraPageManager {
async initialize() {
// Initialize page-specific components
initializeInfiniteScroll();
initializeEventListeners();
lazyLoadImages();
restoreFolderFilter();
initFolderTagsVisibility();
new LoraContextMenu();
@@ -77,6 +75,9 @@ class LoraPageManager {
// Initialize the bulk manager
bulkManager.initialize();
// Initialize common page features (lazy loading, infinite scroll)
appCore.initializePageFeatures();
}
}

View File

@@ -16,6 +16,10 @@ class RecipeManager {
// Initialize RecipeModal
this.recipeModal = new RecipeModal();
// Add state tracking for infinite scroll
this.isLoading = false;
this.hasMore = true;
}
async initialize() {
@@ -27,6 +31,9 @@ class RecipeManager {
// Expose necessary functions to the page
this._exposeGlobalFunctions();
// Initialize common page features (lazy loading, infinite scroll)
appCore.initializePageFeatures();
}
_exposeGlobalFunctions() {
@@ -34,6 +41,7 @@ class RecipeManager {
window.recipeManager = this;
window.importRecipes = () => this.importRecipes();
window.importManager = this.importManager;
window.loadMoreRecipes = () => this.loadMoreRecipes();
}
initEventListeners() {
@@ -69,10 +77,19 @@ class RecipeManager {
}
}
async loadRecipes() {
async loadRecipes(resetPage = true) {
try {
// Show loading indicator
document.body.classList.add('loading');
this.isLoading = true;
// Reset to first page if requested
if (resetPage) {
this.currentPage = 1;
// Clear grid if resetting
const grid = document.getElementById('recipeGrid');
if (grid) grid.innerHTML = '';
}
// Build query parameters
const params = new URLSearchParams({
@@ -101,7 +118,10 @@ class RecipeManager {
const data = await response.json();
// Update recipes grid
this.updateRecipesGrid(data);
this.updateRecipesGrid(data, resetPage);
// Update pagination state
this.hasMore = data.has_more || false;
} catch (error) {
console.error('Error loading recipes:', error);
@@ -109,32 +129,61 @@ class RecipeManager {
} finally {
// Hide loading indicator
document.body.classList.remove('loading');
this.isLoading = false;
}
}
updateRecipesGrid(data) {
// Load more recipes for infinite scroll
async loadMoreRecipes() {
if (this.isLoading || !this.hasMore) return;
this.currentPage++;
await this.loadRecipes(false);
}
updateRecipesGrid(data, resetGrid = true) {
const grid = document.getElementById('recipeGrid');
if (!grid) return;
// Check if data exists and has items
if (!data.items || data.items.length === 0) {
grid.innerHTML = `
<div class="placeholder-message">
<p>No recipes found</p>
<p>Add recipe images to your recipes folder to see them here.</p>
</div>
`;
if (resetGrid) {
grid.innerHTML = `
<div class="placeholder-message">
<p>No recipes found</p>
<p>Add recipe images to your recipes folder to see them here.</p>
</div>
`;
}
return;
}
// Clear grid
grid.innerHTML = '';
// Clear grid if resetting
if (resetGrid) {
grid.innerHTML = '';
}
// Create recipe cards
data.items.forEach(recipe => {
const recipeCard = new RecipeCard(recipe, (recipe) => this.showRecipeDetails(recipe));
grid.appendChild(recipeCard.element);
});
// Add sentinel for infinite scroll if needed
if (this.hasMore) {
let sentinel = document.getElementById('scroll-sentinel');
if (!sentinel) {
sentinel = document.createElement('div');
sentinel.id = 'scroll-sentinel';
sentinel.style.height = '10px';
grid.appendChild(sentinel);
// Re-observe the sentinel if we have an observer
if (window.state && window.state.observer) {
window.state.observer.observe(sentinel);
}
}
}
}
showRecipeDetails(recipe) {

View File

@@ -2,12 +2,32 @@ import { state } from '../state/index.js';
import { loadMoreLoras } from '../api/loraApi.js';
import { debounce } from './debounce.js';
export function initializeInfiniteScroll() {
export function initializeInfiniteScroll(pageType = 'loras') {
if (state.observer) {
state.observer.disconnect();
}
const debouncedLoadMore = debounce(loadMoreLoras, 200);
// Determine the load more function and grid ID based on page type
let loadMoreFunction;
let gridId;
switch (pageType) {
case 'recipes':
loadMoreFunction = window.recipeManager?.loadMoreRecipes || (() => console.warn('loadMoreRecipes not found'));
gridId = 'recipeGrid';
break;
case 'checkpoints':
loadMoreFunction = window.checkpointManager?.loadMoreCheckpoints || (() => console.warn('loadMoreCheckpoints not found'));
gridId = 'checkpointGrid';
break;
case 'loras':
default:
loadMoreFunction = loadMoreLoras;
gridId = 'loraGrid';
break;
}
const debouncedLoadMore = debounce(loadMoreFunction, 200);
state.observer = new IntersectionObserver(
(entries) => {
@@ -19,6 +39,12 @@ export function initializeInfiniteScroll() {
{ threshold: 0.1 }
);
const grid = document.getElementById(gridId);
if (!grid) {
console.warn(`Grid with ID "${gridId}" not found for infinite scroll`);
return;
}
const existingSentinel = document.getElementById('scroll-sentinel');
if (existingSentinel) {
state.observer.observe(existingSentinel);
@@ -26,7 +52,7 @@ export function initializeInfiniteScroll() {
const sentinel = document.createElement('div');
sentinel.id = 'scroll-sentinel';
sentinel.style.height = '10px';
document.getElementById('loraGrid').appendChild(sentinel);
grid.appendChild(sentinel);
state.observer.observe(sentinel);
}
}

View File

@@ -27,7 +27,7 @@
<div class="controls">
<div class="action-buttons">
<div title="Import recipes" class="control-group">
<button onclick="importRecipes()"><i class="fas fa-file-import"></i> Import</button>
<button onclick="modalManager.showModal('importModal')"><i class="fas fa-file-import"></i> Import</button>
</div>
</div>
</div>
@@ -85,15 +85,5 @@
// Will be implemented in recipes.js
window.location.reload();
}
// Import recipes
function importRecipes() {
// Show import modal
const importModal = document.getElementById('importModal');
if (importModal) {
importModal.classList.remove('hidden');
importModal.style.display = 'block';
}
}
</script>
{% endblock %}