From 90f74018aef366a098e465c30b89e1a197ee0322 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Wed, 19 Mar 2025 21:12:04 +0800 Subject: [PATCH] Refactor state management to support hierarchical structure and page-specific states - Introduced a new hierarchical state structure to manage global and page-specific states, enhancing organization and maintainability. - Updated various managers and components to utilize the new state structure, ensuring consistent access to page-specific data. - Removed the initSettings function and replaced it with initPageState for better initialization of page-specific states. - Adjusted imports across multiple files to accommodate the new state management approach, improving code clarity. --- static/js/api/loraApi.js | 54 +++---- static/js/core.js | 7 +- static/js/loras.js | 13 +- static/js/managers/BulkManager.js | 37 +++-- static/js/managers/FilterManager.js | 11 +- static/js/managers/LoraSearchManager.js | 32 +++-- static/js/managers/SettingsManager.js | 16 ++- static/js/state/index.js | 181 ++++++++++++++++++------ static/js/utils/infiniteScroll.js | 10 +- 9 files changed, 240 insertions(+), 121 deletions(-) diff --git a/static/js/api/loraApi.js b/static/js/api/loraApi.js index 86278c21..781739ea 100644 --- a/static/js/api/loraApi.js +++ b/static/js/api/loraApi.js @@ -1,4 +1,4 @@ -import { state } from '../state/index.js'; +import { state, getCurrentPageState } from '../state/index.js'; import { showToast } from '../utils/uiHelpers.js'; import { createLoraCard } from '../components/LoraCard.js'; import { initializeInfiniteScroll } from '../utils/infiniteScroll.js'; @@ -6,21 +6,23 @@ import { showDeleteModal } from '../utils/modalUtils.js'; import { toggleFolder } from '../utils/uiHelpers.js'; export async function loadMoreLoras(boolUpdateFolders = false) { - if (state.isLoading || !state.hasMore) return; + const pageState = getCurrentPageState(); - state.isLoading = true; + if (pageState.isLoading || !pageState.hasMore) return; + + pageState.isLoading = true; try { const params = new URLSearchParams({ - page: state.currentPage, + page: pageState.currentPage, page_size: 20, - sort_by: state.sortBy + sort_by: pageState.sortBy }); - // 使用 state 中的 searchManager 获取递归搜索状态 - const isRecursiveSearch = state.searchManager?.isRecursiveSearch ?? false; + // Use pageState instead of state + const isRecursiveSearch = pageState.searchManager?.isRecursiveSearch ?? false; - if (state.activeFolder !== null) { - params.append('folder', state.activeFolder); + if (pageState.activeFolder !== null) { + params.append('folder', pageState.activeFolder); params.append('recursive', isRecursiveSearch.toString()); } @@ -32,14 +34,14 @@ export async function loadMoreLoras(boolUpdateFolders = false) { } // Add filter parameters if active - if (state.filters) { - if (state.filters.tags && state.filters.tags.length > 0) { + if (pageState.filters) { + if (pageState.filters.tags && pageState.filters.tags.length > 0) { // Convert the array of tags to a comma-separated string - params.append('tags', state.filters.tags.join(',')); + params.append('tags', pageState.filters.tags.join(',')); } - if (state.filters.baseModel && state.filters.baseModel.length > 0) { + if (pageState.filters.baseModel && pageState.filters.baseModel.length > 0) { // Convert the array of base models to a comma-separated string - params.append('base_models', state.filters.baseModel.join(',')); + params.append('base_models', pageState.filters.baseModel.join(',')); } } @@ -53,13 +55,13 @@ export async function loadMoreLoras(boolUpdateFolders = false) { const data = await response.json(); console.log('Received data:', data); - if (data.items.length === 0 && state.currentPage === 1) { + if (data.items.length === 0 && pageState.currentPage === 1) { const grid = document.getElementById('loraGrid'); grid.innerHTML = '
No loras found in this folder
'; - state.hasMore = false; + pageState.hasMore = false; } else if (data.items.length > 0) { - state.hasMore = state.currentPage < data.total_pages; - state.currentPage++; + pageState.hasMore = pageState.currentPage < data.total_pages; + pageState.currentPage++; appendLoraCards(data.items); const sentinel = document.getElementById('scroll-sentinel'); @@ -67,7 +69,7 @@ export async function loadMoreLoras(boolUpdateFolders = false) { state.observer.observe(sentinel); } } else { - state.hasMore = false; + pageState.hasMore = false; } if (boolUpdateFolders && data.folders) { @@ -78,7 +80,7 @@ export async function loadMoreLoras(boolUpdateFolders = false) { console.error('Error loading loras:', error); showToast('Failed to load loras: ' + error.message, 'error'); } finally { - state.isLoading = false; + pageState.isLoading = false; } } @@ -87,7 +89,8 @@ function updateFolderTags(folders) { if (!folderTagsContainer) return; // Keep track of currently selected folder - const currentFolder = state.activeFolder; + const pageState = getCurrentPageState(); + const currentFolder = pageState.activeFolder; // Create HTML for folder tags const tagsHTML = folders.map(folder => { @@ -269,11 +272,12 @@ export function appendLoraCards(loras) { } export async function resetAndReload(boolUpdateFolders = false) { - console.log('Resetting with state:', { ...state }); + const pageState = getCurrentPageState(); + console.log('Resetting with state:', { ...pageState }); - state.currentPage = 1; - state.hasMore = true; - state.isLoading = false; + pageState.currentPage = 1; + pageState.hasMore = true; + pageState.isLoading = false; const grid = document.getElementById('loraGrid'); grid.innerHTML = ''; diff --git a/static/js/core.js b/static/js/core.js index a81ef9de..78abffb9 100644 --- a/static/js/core.js +++ b/static/js/core.js @@ -1,5 +1,5 @@ // Core application functionality -import { state, initSettings } from './state/index.js'; +import { state } from './state/index.js'; import { LoadingManager } from './managers/LoadingManager.js'; import { modalManager } from './managers/ModalManager.js'; import { updateService } from './managers/UpdateService.js'; @@ -18,9 +18,6 @@ export class AppCore { async initialize() { if (this.initialized) return; - // Initialize settings - initSettings(); - // Initialize managers state.loadingManager = new LoadingManager(); modalManager.initialize(); @@ -80,4 +77,4 @@ export class AppCore { export const appCore = new AppCore(); // Export common utilities for global use -export { showToast, modalManager, state, lazyLoadImages, initializeInfiniteScroll }; \ No newline at end of file +export { showToast, lazyLoadImages, initializeInfiniteScroll }; \ No newline at end of file diff --git a/static/js/loras.js b/static/js/loras.js index 1a7380db..b4c0bc74 100644 --- a/static/js/loras.js +++ b/static/js/loras.js @@ -1,23 +1,21 @@ -import { appCore, state } from './core.js'; +import { appCore } from './core.js'; +import { state, initPageState } from './state/index.js'; import { showLoraModal, toggleShowcase, scrollToTop } from './components/LoraModal.js'; import { loadMoreLoras, fetchCivitai, deleteModel, replacePreview, resetAndReload, refreshLoras } from './api/loraApi.js'; import { - lazyLoadImages, restoreFolderFilter, toggleFolder, copyTriggerWord, openCivitai, toggleFolderTags, initFolderTagsVisibility, - updatePanelPositions } from './utils/uiHelpers.js'; -import { initializeInfiniteScroll } from './utils/infiniteScroll.js'; -import { showDeleteModal, confirmDelete, closeDeleteModal } from './utils/modalUtils.js'; +import { confirmDelete, closeDeleteModal } from './utils/modalUtils.js'; import { DownloadManager } from './managers/DownloadManager.js'; import { toggleApiKeyVisibility } from './managers/SettingsManager.js'; import { LoraContextMenu } from './components/ContextMenu.js'; import { moveManager } from './managers/MoveManager.js'; -import { createLoraCard, updateCardsForBulkMode } from './components/LoraCard.js'; +import { updateCardsForBulkMode } from './components/LoraCard.js'; import { bulkManager } from './managers/BulkManager.js'; // Initialize the LoRA page @@ -99,6 +97,9 @@ function initializeEventListeners() { // Initialize everything when DOM is ready document.addEventListener('DOMContentLoaded', async () => { + // Initialize page state + initPageState('loras'); + // Initialize core application await appCore.initialize(); diff --git a/static/js/managers/BulkManager.js b/static/js/managers/BulkManager.js index b510efe2..76fa5043 100644 --- a/static/js/managers/BulkManager.js +++ b/static/js/managers/BulkManager.js @@ -91,16 +91,17 @@ export class BulkManager { // Set text content without the icon countElement.textContent = `${state.selectedLoras.size} selected `; - // Re-add the caret icon with proper direction - const caretIcon = document.createElement('i'); - // Use down arrow if strip is visible, up arrow if not - caretIcon.className = `fas fa-caret-${this.isStripVisible ? 'down' : 'up'} dropdown-caret`; - caretIcon.style.visibility = state.selectedLoras.size > 0 ? 'visible' : 'hidden'; - countElement.appendChild(caretIcon); - - // If there are no selections, hide the thumbnail strip - if (state.selectedLoras.size === 0) { - this.hideThumbnailStrip(); + // Update caret icon if it exists + const existingCaret = countElement.querySelector('.dropdown-caret'); + if (existingCaret) { + existingCaret.className = `fas fa-caret-${this.isStripVisible ? 'down' : 'up'} dropdown-caret`; + existingCaret.style.visibility = state.selectedLoras.size > 0 ? 'visible' : 'hidden'; + } else { + // Create new caret icon if it doesn't exist + const caretIcon = document.createElement('i'); + caretIcon.className = `fas fa-caret-${this.isStripVisible ? 'down' : 'up'} dropdown-caret`; + caretIcon.style.visibility = state.selectedLoras.size > 0 ? 'visible' : 'hidden'; + countElement.appendChild(caretIcon); } } } @@ -252,12 +253,20 @@ export class BulkManager { hideThumbnailStrip() { const strip = document.querySelector('.selected-thumbnails-strip'); - if (strip) { + if (strip && this.isStripVisible) { // Only hide if actually visible strip.classList.remove('visible'); - // Update strip visibility state and caret direction + // Update strip visibility state this.isStripVisible = false; - this.updateSelectedCount(); // Update caret + + // Update caret without triggering another hide + const countElement = document.getElementById('selectedCount'); + if (countElement) { + const caret = countElement.querySelector('.dropdown-caret'); + if (caret) { + caret.className = 'fas fa-caret-up dropdown-caret'; + } + } // Wait for animation to complete before removing setTimeout(() => { @@ -340,4 +349,4 @@ export class BulkManager { } // Create a singleton instance -export const bulkManager = new BulkManager(); \ No newline at end of file +export const bulkManager = new BulkManager(); diff --git a/static/js/managers/FilterManager.js b/static/js/managers/FilterManager.js index 36239927..e691da24 100644 --- a/static/js/managers/FilterManager.js +++ b/static/js/managers/FilterManager.js @@ -1,11 +1,12 @@ import { BASE_MODELS, BASE_MODEL_CLASSES } from '../utils/constants.js'; -import { state } from '../state/index.js'; +import { state, getCurrentPageState } from '../state/index.js'; import { showToast } from '../utils/uiHelpers.js'; import { resetAndReload } from '../api/loraApi.js'; export class FilterManager { constructor() { - this.filters = { + const pageState = getCurrentPageState(); + this.filters = pageState.filters || { baseModel: [], tags: [] }; @@ -219,7 +220,8 @@ export class FilterManager { localStorage.setItem('loraFilters', JSON.stringify(this.filters)); // Update state with current filters - state.filters = { ...this.filters }; + const pageState = getCurrentPageState(); + pageState.filters = { ...this.filters }; // Reload loras with filters applied await resetAndReload(); @@ -258,7 +260,8 @@ export class FilterManager { }; // Update state - state.filters = { ...this.filters }; + const pageState = getCurrentPageState(); + pageState.filters = { ...this.filters }; // Update UI this.updateTagSelections(); diff --git a/static/js/managers/LoraSearchManager.js b/static/js/managers/LoraSearchManager.js index c350416f..bae212b3 100644 --- a/static/js/managers/LoraSearchManager.js +++ b/static/js/managers/LoraSearchManager.js @@ -5,7 +5,7 @@ import { SearchManager } from './SearchManager.js'; import { appendLoraCards } from '../api/loraApi.js'; import { resetAndReload } from '../api/loraApi.js'; -import { state } from '../state/index.js'; +import { state, getCurrentPageState } from '../state/index.js'; import { showToast } from '../utils/uiHelpers.js'; export class LoraSearchManager extends SearchManager { @@ -19,12 +19,14 @@ export class LoraSearchManager extends SearchManager { // Store this instance in the state if (state) { - state.searchManager = this; + const pageState = getCurrentPageState(); + pageState.searchManager = this; } } async performSearch() { const searchTerm = this.searchInput.value.trim().toLowerCase(); + const pageState = getCurrentPageState(); // Log the search attempt for debugging console.log('LoraSearchManager performSearch called with:', searchTerm); @@ -42,8 +44,8 @@ export class LoraSearchManager extends SearchManager { } if (!searchTerm) { - if (state) { - state.currentPage = 1; + if (pageState) { + pageState.currentPage = 1; } await resetAndReload(); return; @@ -58,15 +60,15 @@ export class LoraSearchManager extends SearchManager { // Store current scroll position const scrollPosition = window.pageYOffset || document.documentElement.scrollTop; - if (state) { - state.currentPage = 1; - state.hasMore = true; + if (pageState) { + pageState.currentPage = 1; + pageState.hasMore = true; } const url = new URL('/api/loras', window.location.origin); url.searchParams.set('page', '1'); url.searchParams.set('page_size', '20'); - url.searchParams.set('sort_by', state ? state.sortBy : 'name'); + url.searchParams.set('sort_by', pageState ? pageState.sortBy : 'name'); url.searchParams.set('search', searchTerm); url.searchParams.set('fuzzy', 'true'); @@ -80,8 +82,8 @@ export class LoraSearchManager extends SearchManager { url.searchParams.set('search_tags', searchOptions.tags ? 'true' : 'false'); // Always send folder parameter if there is an active folder - if (state && state.activeFolder) { - url.searchParams.set('folder', state.activeFolder); + if (pageState && pageState.activeFolder) { + url.searchParams.set('folder', pageState.activeFolder); // Add recursive parameter when recursive search is enabled const recursive = this.recursiveSearchToggle ? this.recursiveSearchToggle.checked : false; url.searchParams.set('recursive', recursive.toString()); @@ -102,14 +104,14 @@ export class LoraSearchManager extends SearchManager { if (data.items.length === 0) { grid.innerHTML = '
No matching loras found
'; - if (state) { - state.hasMore = false; + if (pageState) { + pageState.hasMore = false; } } else { appendLoraCards(data.items); - if (state) { - state.hasMore = state.currentPage < data.total_pages; - state.currentPage++; + if (pageState) { + pageState.hasMore = pageState.currentPage < data.total_pages; + pageState.currentPage++; } } diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index 2afea5a0..da485760 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -1,6 +1,6 @@ import { modalManager } from './ModalManager.js'; import { showToast } from '../utils/uiHelpers.js'; -import { state, saveSettings } from '../state/index.js'; +import { state } from '../state/index.js'; import { resetAndReload } from '../api/loraApi.js'; export class SettingsManager { @@ -41,13 +41,13 @@ export class SettingsManager { // Set frontend settings from state const blurMatureContentCheckbox = document.getElementById('blurMatureContent'); if (blurMatureContentCheckbox) { - blurMatureContentCheckbox.checked = state.settings.blurMatureContent; + blurMatureContentCheckbox.checked = state.global.settings.blurMatureContent; } const showOnlySFWCheckbox = document.getElementById('showOnlySFW'); if (showOnlySFWCheckbox) { // Sync with state (backend will set this via template) - state.settings.show_only_sfw = showOnlySFWCheckbox.checked; + state.global.settings.show_only_sfw = showOnlySFWCheckbox.checked; } // Backend settings are loaded from the template directly @@ -71,9 +71,11 @@ export class SettingsManager { const showOnlySFW = document.getElementById('showOnlySFW').checked; // Update frontend state and save to localStorage - state.settings.blurMatureContent = blurMatureContent; - state.settings.show_only_sfw = showOnlySFW; - saveSettings(); + state.global.settings.blurMatureContent = blurMatureContent; + state.global.settings.show_only_sfw = showOnlySFW; + + // Save settings to localStorage + localStorage.setItem('settings', JSON.stringify(state.global.settings)); try { // Save backend settings via API @@ -107,7 +109,7 @@ export class SettingsManager { applyFrontendSettings() { // Apply blur setting to existing content - const blurSetting = state.settings.blurMatureContent; + const blurSetting = state.global.settings.blurMatureContent; document.querySelectorAll('.lora-card[data-nsfw="true"] .card-image').forEach(img => { if (blurSetting) { img.classList.add('nsfw-blur'); diff --git a/static/js/state/index.js b/static/js/state/index.js index cde287b1..1a54a7d0 100644 --- a/static/js/state/index.js +++ b/static/js/state/index.js @@ -1,53 +1,148 @@ +// Create the new hierarchical state structure export const state = { - currentPage: 1, - isLoading: false, - hasMore: true, - sortBy: 'name', - activeFolder: null, - loadingManager: null, - observer: null, - previewVersions: new Map(), - searchManager: null, - searchOptions: { - filename: true, - modelname: true, - tags: false, - recursive: false + // Global state + global: { + settings: { + blurMatureContent: true, + show_only_sfw: false + }, + loadingManager: null, + observer: null, }, - filters: { - baseModel: [], - tags: [] + + // Page-specific states + pages: { + loras: { + currentPage: 1, + isLoading: false, + hasMore: true, + sortBy: 'name', + activeFolder: null, + previewVersions: new Map(), + searchManager: null, + searchOptions: { + filename: true, + modelname: true, + tags: false, + recursive: false + }, + filters: { + baseModel: [], + tags: [] + }, + bulkMode: false, + selectedLoras: new Set(), + loraMetadataCache: new Map(), + }, + + recipes: { + currentPage: 1, + isLoading: false, + hasMore: true, + sortBy: 'date', + searchManager: null, + searchOptions: { + filename: true, + modelname: true, + tags: true, + loras: true, + recursive: false + }, + filters: { + baseModel: [], + tags: [] + } + }, + + checkpoints: { + currentPage: 1, + isLoading: false, + hasMore: true, + sortBy: 'name', + activeFolder: null, + searchManager: null, + searchOptions: { + filename: true, + modelname: true, + recursive: false + }, + filters: { + baseModel: [], + tags: [] + } + } }, - bulkMode: false, - selectedLoras: new Set(), - loraMetadataCache: new Map(), - settings: { - blurMatureContent: true, - show_only_sfw: false - } + + // Current active page + currentPageType: 'loras', + + // Backward compatibility - proxy properties + get currentPage() { return this.pages[this.currentPageType].currentPage; }, + set currentPage(value) { this.pages[this.currentPageType].currentPage = value; }, + + get isLoading() { return this.pages[this.currentPageType].isLoading; }, + set isLoading(value) { this.pages[this.currentPageType].isLoading = value; }, + + get hasMore() { return this.pages[this.currentPageType].hasMore; }, + set hasMore(value) { this.pages[this.currentPageType].hasMore = value; }, + + get sortBy() { return this.pages[this.currentPageType].sortBy; }, + set sortBy(value) { this.pages[this.currentPageType].sortBy = value; }, + + get activeFolder() { return this.pages[this.currentPageType].activeFolder; }, + set activeFolder(value) { this.pages[this.currentPageType].activeFolder = value; }, + + get loadingManager() { return this.global.loadingManager; }, + set loadingManager(value) { this.global.loadingManager = value; }, + + get observer() { return this.global.observer; }, + set observer(value) { this.global.observer = value; }, + + get previewVersions() { return this.pages.loras.previewVersions; }, + set previewVersions(value) { this.pages.loras.previewVersions = value; }, + + get searchManager() { return this.pages[this.currentPageType].searchManager; }, + set searchManager(value) { this.pages[this.currentPageType].searchManager = value; }, + + get searchOptions() { return this.pages[this.currentPageType].searchOptions; }, + set searchOptions(value) { this.pages[this.currentPageType].searchOptions = value; }, + + get filters() { return this.pages[this.currentPageType].filters; }, + set filters(value) { this.pages[this.currentPageType].filters = value; }, + + get bulkMode() { return this.pages.loras.bulkMode; }, + set bulkMode(value) { this.pages.loras.bulkMode = value; }, + + get selectedLoras() { return this.pages.loras.selectedLoras; }, + set selectedLoras(value) { this.pages.loras.selectedLoras = value; }, + + get loraMetadataCache() { return this.pages.loras.loraMetadataCache; }, + set loraMetadataCache(value) { this.pages.loras.loraMetadataCache = value; }, + + get settings() { return this.global.settings; }, + set settings(value) { this.global.settings = value; } }; -// Initialize settings from localStorage if available -export function initSettings() { - try { - const savedSettings = localStorage.getItem('loraManagerSettings'); - if (savedSettings) { - const parsedSettings = JSON.parse(savedSettings); - state.settings = { ...state.settings, ...parsedSettings }; - } - } catch (error) { - console.error('Error loading settings from localStorage:', error); - } +// Get the current page state +export function getCurrentPageState() { + return state.pages[state.currentPageType]; } -// Save settings to localStorage -export function saveSettings() { - try { - localStorage.setItem('loraManagerSettings', JSON.stringify(state.settings)); - } catch (error) { - console.error('Error saving settings to localStorage:', error); +// Set the current page type +export function setCurrentPageType(pageType) { + if (state.pages[pageType]) { + state.currentPageType = pageType; + return true; } + console.warn(`Unknown page type: ${pageType}`); + return false; } -// Initialize settings on load -initSettings(); \ No newline at end of file +// Initialize page state when a page loads +export function initPageState(pageType) { + if (setCurrentPageType(pageType)) { + console.log(`Initialized state for page: ${pageType}`); + return getCurrentPageState(); + } + return null; +} \ No newline at end of file diff --git a/static/js/utils/infiniteScroll.js b/static/js/utils/infiniteScroll.js index a9360b96..5e95e5a8 100644 --- a/static/js/utils/infiniteScroll.js +++ b/static/js/utils/infiniteScroll.js @@ -1,4 +1,4 @@ -import { state } from '../state/index.js'; +import { state, getCurrentPageState } from '../state/index.js'; import { loadMoreLoras } from '../api/loraApi.js'; import { debounce } from './debounce.js'; @@ -7,6 +7,12 @@ export function initializeInfiniteScroll(pageType = 'loras') { state.observer.disconnect(); } + // Set the current page type + state.currentPageType = pageType; + + // Get the current page state + const pageState = getCurrentPageState(); + // Determine the load more function and grid ID based on page type let loadMoreFunction; let gridId; @@ -32,7 +38,7 @@ export function initializeInfiniteScroll(pageType = 'loras') { state.observer = new IntersectionObserver( (entries) => { const target = entries[0]; - if (target.isIntersecting && !state.isLoading && state.hasMore) { + if (target.isIntersecting && !pageState.isLoading && pageState.hasMore) { debouncedLoadMore(); } },