From ee04df40c34d38dc74d73fa71e2a370dc83e232d Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Thu, 10 Apr 2025 19:41:02 +0800 Subject: [PATCH] Refactor controls and pagination for Checkpoints and LoRAs: Implement unified PageControls, enhance API integration, and improve event handling for better user experience. --- static/js/api/checkpointApi.js | 5 + static/js/api/loraApi.js | 16 +- static/js/checkpoints.js | 103 +---- .../controls/CheckpointsControls.js | 46 +++ .../js/components/controls/LorasControls.js | 147 +++++++ static/js/components/controls/PageControls.js | 391 ++++++++++++++++++ static/js/components/controls/index.js | 23 ++ static/js/loras.js | 164 +------- static/js/recipes.js | 5 + static/js/utils/infiniteScroll.js | 13 +- templates/components/controls.html | 18 +- 11 files changed, 667 insertions(+), 264 deletions(-) create mode 100644 static/js/components/controls/CheckpointsControls.js create mode 100644 static/js/components/controls/LorasControls.js create mode 100644 static/js/components/controls/PageControls.js create mode 100644 static/js/components/controls/index.js diff --git a/static/js/api/checkpointApi.js b/static/js/api/checkpointApi.js index cbf1ca0b..1dd0c8fe 100644 --- a/static/js/api/checkpointApi.js +++ b/static/js/api/checkpointApi.js @@ -101,6 +101,11 @@ export async function loadMoreCheckpoints(resetPagination = true) { const card = createCheckpointCard(checkpoint); grid.appendChild(card); }); + + // Increment the page number AFTER successful loading + if (data.items.length > 0) { + pageState.currentPage++; + } } catch (error) { console.error('Error loading checkpoints:', error); showToast('Failed to load checkpoints', 'error'); diff --git a/static/js/api/loraApi.js b/static/js/api/loraApi.js index 8ebee93c..c344e930 100644 --- a/static/js/api/loraApi.js +++ b/static/js/api/loraApi.js @@ -19,7 +19,6 @@ export async function loadMoreLoras(resetPage = false, updateFolders = false) { // Clear grid if resetting const grid = document.getElementById('loraGrid'); if (grid) grid.innerHTML = ''; - initializeInfiniteScroll(); } const params = new URLSearchParams({ @@ -62,9 +61,6 @@ export async function loadMoreLoras(resetPage = false, updateFolders = false) { const filterLoraHash = getSessionItem('recipe_to_lora_filterLoraHash'); const filterLoraHashes = getSessionItem('recipe_to_lora_filterLoraHashes'); - console.log('Filter Lora Hash:', filterLoraHash); - console.log('Filter Lora Hashes:', filterLoraHashes); - // Add hash filter parameter if present if (filterLoraHash) { params.append('lora_hash', filterLoraHash); @@ -93,13 +89,10 @@ export async function loadMoreLoras(resetPage = false, updateFolders = false) { pageState.hasMore = false; } else if (data.items.length > 0) { pageState.hasMore = pageState.currentPage < data.total_pages; - pageState.currentPage++; appendLoraCards(data.items); - const sentinel = document.getElementById('scroll-sentinel'); - if (sentinel && state.observer) { - state.observer.observe(sentinel); - } + // Increment the page number AFTER successful loading + pageState.currentPage++; } else { pageState.hasMore = false; } @@ -303,10 +296,7 @@ export async function resetAndReload(updateFolders = false) { const pageState = getCurrentPageState(); console.log('Resetting with state:', { ...pageState }); - // Initialize infinite scroll - will reset the observer - initializeInfiniteScroll(); - - // Load more loras with reset flag + // Reset pagination and load more loras await loadMoreLoras(true, updateFolders); } diff --git a/static/js/checkpoints.js b/static/js/checkpoints.js index 8b563f8c..fc09463d 100644 --- a/static/js/checkpoints.js +++ b/static/js/checkpoints.js @@ -1,68 +1,30 @@ import { appCore } from './core.js'; -import { state, getCurrentPageState } from './state/index.js'; -import { - loadMoreCheckpoints, - resetAndReload, - refreshCheckpoints, - deleteCheckpoint, - replaceCheckpointPreview -} from './api/checkpointApi.js'; -import { - restoreFolderFilter, - toggleFolder, - openCivitai, - showToast -} from './utils/uiHelpers.js'; -import { confirmDelete, closeDeleteModal } from './utils/modalUtils.js'; import { toggleApiKeyVisibility } from './managers/SettingsManager.js'; import { initializeInfiniteScroll } from './utils/infiniteScroll.js'; -import { setStorageItem, getStorageItem } from './utils/storageHelpers.js'; +import { confirmDelete, closeDeleteModal } from './utils/modalUtils.js'; +import { createPageControls } from './components/controls/index.js'; // Initialize the Checkpoints page class CheckpointsPageManager { constructor() { - // Get page state - this.pageState = getCurrentPageState(); + // Initialize page controls + this.pageControls = createPageControls('checkpoints'); - // Set default values - this.pageState.pageSize = 20; - this.pageState.isLoading = false; - this.pageState.hasMore = true; - - // Expose functions to window object - this._exposeGlobalFunctions(); + // Expose only necessary functions to global scope + this._exposeRequiredGlobalFunctions(); } - _exposeGlobalFunctions() { - // API functions - window.loadCheckpoints = (reset = true) => this.loadCheckpoints(reset); - window.refreshCheckpoints = refreshCheckpoints; - window.deleteCheckpoint = deleteCheckpoint; - window.replaceCheckpointPreview = replaceCheckpointPreview; - - // UI helper functions - window.toggleFolder = toggleFolder; - window.openCivitai = openCivitai; + _exposeRequiredGlobalFunctions() { + // Minimal set of functions that need to remain global window.confirmDelete = confirmDelete; window.closeDeleteModal = closeDeleteModal; window.toggleApiKeyVisibility = toggleApiKeyVisibility; - - // Add reference to this manager - window.checkpointManager = this; } async initialize() { - // Initialize event listeners - this._initEventListeners(); - - // Restore folder filters if available - restoreFolderFilter('checkpoints'); - - // Load sort preference - this._loadSortPreference(); - - // Load initial checkpoints - await this.loadCheckpoints(); + // Initialize page-specific components + this.pageControls.restoreFolderFilter(); + this.pageControls.initFolderTagsVisibility(); // Initialize infinite scroll initializeInfiniteScroll('checkpoints'); @@ -72,49 +34,6 @@ class CheckpointsPageManager { console.log('Checkpoints Manager initialized'); } - - _initEventListeners() { - // Sort select handler - const sortSelect = document.getElementById('sortSelect'); - if (sortSelect) { - sortSelect.addEventListener('change', async (e) => { - this.pageState.sortBy = e.target.value; - this._saveSortPreference(e.target.value); - await resetAndReload(); - }); - } - - // Folder tags handler - document.querySelectorAll('.folder-tags .tag').forEach(tag => { - tag.addEventListener('click', toggleFolder); - }); - - // Refresh button handler - const refreshBtn = document.getElementById('refreshBtn'); - if (refreshBtn) { - refreshBtn.addEventListener('click', () => refreshCheckpoints()); - } - } - - _loadSortPreference() { - const savedSort = getStorageItem('checkpoints_sort'); - if (savedSort) { - this.pageState.sortBy = savedSort; - const sortSelect = document.getElementById('sortSelect'); - if (sortSelect) { - sortSelect.value = savedSort; - } - } - } - - _saveSortPreference(sortValue) { - setStorageItem('checkpoints_sort', sortValue); - } - - // Load checkpoints with optional pagination reset - async loadCheckpoints(resetPage = true) { - await loadMoreCheckpoints(resetPage); - } } // Initialize everything when DOM is ready diff --git a/static/js/components/controls/CheckpointsControls.js b/static/js/components/controls/CheckpointsControls.js new file mode 100644 index 00000000..2dd968e2 --- /dev/null +++ b/static/js/components/controls/CheckpointsControls.js @@ -0,0 +1,46 @@ +// CheckpointsControls.js - Specific implementation for the Checkpoints page +import { PageControls } from './PageControls.js'; +import { loadMoreCheckpoints, resetAndReload, refreshCheckpoints } from '../../api/checkpointApi.js'; +import { showToast } from '../../utils/uiHelpers.js'; + +/** + * CheckpointsControls class - Extends PageControls for Checkpoint-specific functionality + */ +export class CheckpointsControls extends PageControls { + constructor() { + // Initialize with 'checkpoints' page type + super('checkpoints'); + + // Register API methods specific to the Checkpoints page + this.registerCheckpointsAPI(); + } + + /** + * Register Checkpoint-specific API methods + */ + registerCheckpointsAPI() { + const checkpointsAPI = { + // Core API functions + loadMoreModels: async (resetPage = false, updateFolders = false) => { + return await loadMoreCheckpoints(resetPage, updateFolders); + }, + + resetAndReload: async (updateFolders = false) => { + return await resetAndReload(updateFolders); + }, + + refreshModels: async () => { + return await refreshCheckpoints(); + }, + + // No clearCustomFilter implementation is needed for checkpoints + // as custom filters are currently only used for LoRAs + clearCustomFilter: async () => { + showToast('No custom filter to clear', 'info'); + } + }; + + // Register the API + this.registerAPI(checkpointsAPI); + } +} \ No newline at end of file diff --git a/static/js/components/controls/LorasControls.js b/static/js/components/controls/LorasControls.js new file mode 100644 index 00000000..4d0cc9eb --- /dev/null +++ b/static/js/components/controls/LorasControls.js @@ -0,0 +1,147 @@ +// LorasControls.js - Specific implementation for the LoRAs page +import { PageControls } from './PageControls.js'; +import { loadMoreLoras, fetchCivitai, resetAndReload, refreshLoras } from '../../api/loraApi.js'; +import { getSessionItem, removeSessionItem } from '../../utils/storageHelpers.js'; +import { showToast } from '../../utils/uiHelpers.js'; + +/** + * LorasControls class - Extends PageControls for LoRA-specific functionality + */ +export class LorasControls extends PageControls { + constructor() { + // Initialize with 'loras' page type + super('loras'); + + // Register API methods specific to the LoRAs page + this.registerLorasAPI(); + + // Check for custom filters (e.g., from recipe navigation) + this.checkCustomFilters(); + } + + /** + * Register LoRA-specific API methods + */ + registerLorasAPI() { + const lorasAPI = { + // Core API functions + loadMoreModels: async (resetPage = false, updateFolders = false) => { + return await loadMoreLoras(resetPage, updateFolders); + }, + + resetAndReload: async (updateFolders = false) => { + return await resetAndReload(updateFolders); + }, + + refreshModels: async () => { + return await refreshLoras(); + }, + + // LoRA-specific API functions + fetchFromCivitai: async () => { + return await fetchCivitai(); + }, + + showDownloadModal: () => { + if (window.downloadManager) { + window.downloadManager.showDownloadModal(); + } else { + console.error('Download manager not available'); + } + }, + + toggleBulkMode: () => { + if (window.bulkManager) { + window.bulkManager.toggleBulkMode(); + } else { + console.error('Bulk manager not available'); + } + }, + + clearCustomFilter: async () => { + await this.clearCustomFilter(); + } + }; + + // Register the API + this.registerAPI(lorasAPI); + } + + /** + * Check for custom filter parameters in session storage (e.g., from recipe page navigation) + */ + checkCustomFilters() { + const filterLoraHash = getSessionItem('recipe_to_lora_filterLoraHash'); + const filterLoraHashes = getSessionItem('recipe_to_lora_filterLoraHashes'); + const filterRecipeName = getSessionItem('filterRecipeName'); + const viewLoraDetail = getSessionItem('viewLoraDetail'); + + if ((filterLoraHash || filterLoraHashes) && filterRecipeName) { + // Found custom filter parameters, set up the custom filter + + // Show the filter indicator + const indicator = document.getElementById('customFilterIndicator'); + const filterText = indicator?.querySelector('.customFilterText'); + + if (indicator && filterText) { + indicator.classList.remove('hidden'); + + // Set text content with recipe name + const filterType = filterLoraHash && viewLoraDetail ? "Viewing LoRA from" : "Viewing LoRAs from"; + const displayText = `${filterType}: ${filterRecipeName}`; + + filterText.textContent = this._truncateText(displayText, 30); + filterText.setAttribute('title', displayText); + + // Add pulse animation + const filterElement = indicator.querySelector('.filter-active'); + if (filterElement) { + filterElement.classList.add('animate'); + setTimeout(() => filterElement.classList.remove('animate'), 600); + } + } + + // If we're viewing a specific LoRA detail, set up to open the modal + if (filterLoraHash && viewLoraDetail) { + this.pageState.pendingLoraHash = filterLoraHash; + } + } + } + + /** + * Clear the custom filter and reload the page + */ + async clearCustomFilter() { + console.log("Clearing custom filter..."); + // Remove filter parameters from session storage + removeSessionItem('recipe_to_lora_filterLoraHash'); + removeSessionItem('recipe_to_lora_filterLoraHashes'); + removeSessionItem('filterRecipeName'); + removeSessionItem('viewLoraDetail'); + + // Hide the filter indicator + const indicator = document.getElementById('customFilterIndicator'); + if (indicator) { + indicator.classList.add('hidden'); + } + + // Reset state + if (this.pageState.pendingLoraHash) { + delete this.pageState.pendingLoraHash; + } + + // Reload the loras + await resetAndReload(); + showToast('Filter cleared', 'info'); + } + + /** + * Helper to truncate text with ellipsis + * @param {string} text - Text to truncate + * @param {number} maxLength - Maximum length before truncating + * @returns {string} - Truncated text + */ + _truncateText(text, maxLength) { + return text.length > maxLength ? text.substring(0, maxLength - 3) + '...' : text; + } +} \ No newline at end of file diff --git a/static/js/components/controls/PageControls.js b/static/js/components/controls/PageControls.js new file mode 100644 index 00000000..43498f12 --- /dev/null +++ b/static/js/components/controls/PageControls.js @@ -0,0 +1,391 @@ +// PageControls.js - Manages controls for both LoRAs and Checkpoints pages +import { state, getCurrentPageState, setCurrentPageType } from '../../state/index.js'; +import { getStorageItem, setStorageItem } from '../../utils/storageHelpers.js'; +import { showToast } from '../../utils/uiHelpers.js'; + +/** + * PageControls class - Unified control management for model pages + */ +export class PageControls { + constructor(pageType) { + // Set the current page type in state + setCurrentPageType(pageType); + + // Store the page type + this.pageType = pageType; + + // Get the current page state + this.pageState = getCurrentPageState(); + + // Initialize state based on page type + this.initializeState(); + + // Store API methods + this.api = null; + + // Initialize event listeners + this.initEventListeners(); + + console.log(`PageControls initialized for ${pageType} page`); + } + + /** + * Initialize state based on page type + */ + initializeState() { + // Set default values + this.pageState.pageSize = 20; + this.pageState.isLoading = false; + this.pageState.hasMore = true; + + // Load sort preference + this.loadSortPreference(); + } + + /** + * Register API methods for the page + * @param {Object} api - API methods for the page + */ + registerAPI(api) { + this.api = api; + console.log(`API methods registered for ${this.pageType} page`); + } + + /** + * Initialize event listeners for controls + */ + initEventListeners() { + // Sort select handler + const sortSelect = document.getElementById('sortSelect'); + if (sortSelect) { + sortSelect.value = this.pageState.sortBy; + sortSelect.addEventListener('change', async (e) => { + this.pageState.sortBy = e.target.value; + this.saveSortPreference(e.target.value); + await this.resetAndReload(); + }); + } + + // Folder tags handler + document.querySelectorAll('.folder-tags .tag').forEach(tag => { + tag.addEventListener('click', (e) => this.handleFolderClick(e.currentTarget)); + }); + + // Refresh button handler + const refreshBtn = document.querySelector('[data-action="refresh"]'); + if (refreshBtn) { + refreshBtn.addEventListener('click', () => this.refreshModels()); + } + + // Toggle folders button + const toggleFoldersBtn = document.querySelector('.toggle-folders-btn'); + if (toggleFoldersBtn) { + toggleFoldersBtn.addEventListener('click', () => this.toggleFolderTags()); + } + + // Clear custom filter handler + const clearFilterBtn = document.querySelector('.clear-filter'); + if (clearFilterBtn) { + clearFilterBtn.addEventListener('click', () => this.clearCustomFilter()); + } + + // Page-specific event listeners + this.initPageSpecificListeners(); + } + + /** + * Initialize page-specific event listeners + */ + initPageSpecificListeners() { + if (this.pageType === 'loras') { + // Fetch from Civitai button + const fetchButton = document.querySelector('[data-action="fetch"]'); + if (fetchButton) { + fetchButton.addEventListener('click', () => this.fetchFromCivitai()); + } + + // Download button + const downloadButton = document.querySelector('[data-action="download"]'); + if (downloadButton) { + downloadButton.addEventListener('click', () => this.showDownloadModal()); + } + + // Bulk operations button + const bulkButton = document.querySelector('[data-action="bulk"]'); + if (bulkButton) { + bulkButton.addEventListener('click', () => this.toggleBulkMode()); + } + } + } + + /** + * Toggle folder selection + * @param {HTMLElement} tagElement - The folder tag element that was clicked + */ + handleFolderClick(tagElement) { + const folder = tagElement.dataset.folder; + const wasActive = tagElement.classList.contains('active'); + + document.querySelectorAll('.folder-tags .tag').forEach(t => { + t.classList.remove('active'); + }); + + if (!wasActive) { + tagElement.classList.add('active'); + this.pageState.activeFolder = folder; + setStorageItem(`${this.pageType}_activeFolder`, folder); + } else { + this.pageState.activeFolder = null; + setStorageItem(`${this.pageType}_activeFolder`, null); + } + + this.resetAndReload(); + } + + /** + * Restore folder filter from storage + */ + restoreFolderFilter() { + const activeFolder = getStorageItem(`${this.pageType}_activeFolder`); + const folderTag = activeFolder && document.querySelector(`.tag[data-folder="${activeFolder}"]`); + + if (folderTag) { + folderTag.classList.add('active'); + this.pageState.activeFolder = activeFolder; + this.filterByFolder(activeFolder); + } + } + + /** + * Filter displayed cards by folder + * @param {string} folderPath - Folder path to filter by + */ + filterByFolder(folderPath) { + const cardSelector = this.pageType === 'loras' ? '.lora-card' : '.checkpoint-card'; + document.querySelectorAll(cardSelector).forEach(card => { + card.style.display = card.dataset.folder === folderPath ? '' : 'none'; + }); + } + + /** + * Update the folder tags display with new folder list + * @param {Array} folders - List of folder names + */ + updateFolderTags(folders) { + const folderTagsContainer = document.querySelector('.folder-tags'); + if (!folderTagsContainer) return; + + // Keep track of currently selected folder + const currentFolder = this.pageState.activeFolder; + + // Create HTML for folder tags + const tagsHTML = folders.map(folder => { + const isActive = folder === currentFolder; + return `
${folder}
`; + }).join(''); + + // Update the container + folderTagsContainer.innerHTML = tagsHTML; + + // Reattach click handlers + const tags = folderTagsContainer.querySelectorAll('.tag'); + tags.forEach(tag => { + tag.addEventListener('click', (e) => this.handleFolderClick(e.currentTarget)); + if (tag.dataset.folder === currentFolder) { + tag.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + }); + } + + /** + * Toggle visibility of folder tags + */ + toggleFolderTags() { + const folderTags = document.querySelector('.folder-tags'); + const toggleBtn = document.querySelector('.toggle-folders-btn i'); + + if (folderTags) { + folderTags.classList.toggle('collapsed'); + + if (folderTags.classList.contains('collapsed')) { + // Change icon to indicate folders are hidden + toggleBtn.className = 'fas fa-folder-plus'; + toggleBtn.parentElement.title = 'Show folder tags'; + setStorageItem('folderTagsCollapsed', 'true'); + } else { + // Change icon to indicate folders are visible + toggleBtn.className = 'fas fa-folder-minus'; + toggleBtn.parentElement.title = 'Hide folder tags'; + setStorageItem('folderTagsCollapsed', 'false'); + } + } + } + + /** + * Initialize folder tags visibility based on stored preference + */ + initFolderTagsVisibility() { + const isCollapsed = getStorageItem('folderTagsCollapsed'); + if (isCollapsed) { + const folderTags = document.querySelector('.folder-tags'); + const toggleBtn = document.querySelector('.toggle-folders-btn i'); + if (folderTags) { + folderTags.classList.add('collapsed'); + } + if (toggleBtn) { + toggleBtn.className = 'fas fa-folder-plus'; + toggleBtn.parentElement.title = 'Show folder tags'; + } + } else { + const toggleBtn = document.querySelector('.toggle-folders-btn i'); + if (toggleBtn) { + toggleBtn.className = 'fas fa-folder-minus'; + toggleBtn.parentElement.title = 'Hide folder tags'; + } + } + } + + /** + * Load sort preference from storage + */ + loadSortPreference() { + const savedSort = getStorageItem(`${this.pageType}_sort`); + if (savedSort) { + this.pageState.sortBy = savedSort; + const sortSelect = document.getElementById('sortSelect'); + if (sortSelect) { + sortSelect.value = savedSort; + } + } + } + + /** + * Save sort preference to storage + * @param {string} sortValue - The sort value to save + */ + saveSortPreference(sortValue) { + setStorageItem(`${this.pageType}_sort`, sortValue); + } + + /** + * Open model page on Civitai + * @param {string} modelName - Name of the model + */ + openCivitai(modelName) { + // Get card selector based on page type + const cardSelector = this.pageType === 'loras' + ? `.lora-card[data-name="${modelName}"]` + : `.checkpoint-card[data-name="${modelName}"]`; + + const card = document.querySelector(cardSelector); + if (!card) return; + + const metaData = JSON.parse(card.dataset.meta); + const civitaiId = metaData.modelId; + const versionId = metaData.id; + + // Build URL + if (civitaiId) { + let url = `https://civitai.com/models/${civitaiId}`; + if (versionId) { + url += `?modelVersionId=${versionId}`; + } + window.open(url, '_blank'); + } else { + // If no ID, try searching by name + window.open(`https://civitai.com/models?query=${encodeURIComponent(modelName)}`, '_blank'); + } + } + + /** + * Reset and reload the models list + */ + async resetAndReload(updateFolders = false) { + if (!this.api) { + console.error('API methods not registered'); + return; + } + + try { + await this.api.resetAndReload(updateFolders); + } catch (error) { + console.error(`Error reloading ${this.pageType}:`, error); + showToast(`Failed to reload ${this.pageType}: ${error.message}`, 'error'); + } + } + + /** + * Refresh models list + */ + async refreshModels() { + if (!this.api) { + console.error('API methods not registered'); + return; + } + + try { + await this.api.refreshModels(); + } catch (error) { + console.error(`Error refreshing ${this.pageType}:`, error); + showToast(`Failed to refresh ${this.pageType}: ${error.message}`, 'error'); + } + } + + /** + * Fetch metadata from Civitai (LoRAs only) + */ + async fetchFromCivitai() { + if (this.pageType !== 'loras' || !this.api) { + console.error('Fetch from Civitai is only available for LoRAs'); + return; + } + + try { + await this.api.fetchFromCivitai(); + } catch (error) { + console.error('Error fetching metadata:', error); + showToast('Failed to fetch metadata: ' + error.message, 'error'); + } + } + + /** + * Show download modal (LoRAs only) + */ + showDownloadModal() { + if (this.pageType !== 'loras' || !this.api) { + console.error('Download modal is only available for LoRAs'); + return; + } + + this.api.showDownloadModal(); + } + + /** + * Toggle bulk mode (LoRAs only) + */ + toggleBulkMode() { + if (this.pageType !== 'loras' || !this.api) { + console.error('Bulk mode is only available for LoRAs'); + return; + } + + this.api.toggleBulkMode(); + } + + /** + * Clear custom filter + */ + async clearCustomFilter() { + if (!this.api) { + console.error('API methods not registered'); + return; + } + + try { + await this.api.clearCustomFilter(); + } catch (error) { + console.error('Error clearing custom filter:', error); + showToast('Failed to clear custom filter: ' + error.message, 'error'); + } + } +} \ No newline at end of file diff --git a/static/js/components/controls/index.js b/static/js/components/controls/index.js new file mode 100644 index 00000000..c767c62f --- /dev/null +++ b/static/js/components/controls/index.js @@ -0,0 +1,23 @@ +// Controls components index file +import { PageControls } from './PageControls.js'; +import { LorasControls } from './LorasControls.js'; +import { CheckpointsControls } from './CheckpointsControls.js'; + +// Export the classes +export { PageControls, LorasControls, CheckpointsControls }; + +/** + * Factory function to create the appropriate controls based on page type + * @param {string} pageType - The type of page ('loras' or 'checkpoints') + * @returns {PageControls} - The appropriate controls instance + */ +export function createPageControls(pageType) { + if (pageType === 'loras') { + return new LorasControls(); + } else if (pageType === 'checkpoints') { + return new CheckpointsControls(); + } else { + console.error(`Unknown page type: ${pageType}`); + return null; + } +} \ No newline at end of file diff --git a/static/js/loras.js b/static/js/loras.js index d9750786..03c4dfa3 100644 --- a/static/js/loras.js +++ b/static/js/loras.js @@ -1,23 +1,14 @@ import { appCore } from './core.js'; import { state } from './state/index.js'; import { showLoraModal, toggleShowcase, scrollToTop } from './components/loraModal/index.js'; -import { loadMoreLoras, fetchCivitai, deleteModel, replacePreview, resetAndReload, refreshLoras } from './api/loraApi.js'; -import { - restoreFolderFilter, - toggleFolder, - copyTriggerWord, - openCivitai, - toggleFolderTags, - initFolderTagsVisibility, -} from './utils/uiHelpers.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 { updateCardsForBulkMode } from './components/LoraCard.js'; import { bulkManager } from './managers/BulkManager.js'; -import { setStorageItem, getStorageItem, getSessionItem, removeSessionItem } from './utils/storageHelpers.js'; +import { DownloadManager } from './managers/DownloadManager.js'; +import { toggleApiKeyVisibility } from './managers/SettingsManager.js'; +import { moveManager } from './managers/MoveManager.js'; +import { LoraContextMenu } from './components/ContextMenu.js'; +import { createPageControls } from './components/controls/index.js'; +import { confirmDelete, closeDeleteModal } from './utils/modalUtils.js'; // Initialize the LoRA page class LoraPageManager { @@ -29,24 +20,20 @@ class LoraPageManager { // Initialize managers this.downloadManager = new DownloadManager(); - // Expose necessary functions to the page - this._exposeGlobalFunctions(); + // Initialize page controls + this.pageControls = createPageControls('loras'); + + // Expose necessary functions to the page that still need global access + // These will be refactored in future updates + this._exposeRequiredGlobalFunctions(); } - _exposeGlobalFunctions() { - // Only expose what's needed for the page - window.loadMoreLoras = loadMoreLoras; - window.fetchCivitai = fetchCivitai; - window.deleteModel = deleteModel; - window.replacePreview = replacePreview; - window.toggleFolder = toggleFolder; - window.copyTriggerWord = copyTriggerWord; + _exposeRequiredGlobalFunctions() { + // Only expose what's still needed globally + // Most functionality is now handled by the PageControls component window.showLoraModal = showLoraModal; window.confirmDelete = confirmDelete; window.closeDeleteModal = closeDeleteModal; - window.refreshLoras = refreshLoras; - window.openCivitai = openCivitai; - window.toggleFolderTags = toggleFolderTags; window.toggleApiKeyVisibility = toggleApiKeyVisibility; window.downloadManager = this.downloadManager; window.moveManager = moveManager; @@ -64,14 +51,10 @@ class LoraPageManager { async initialize() { // Initialize page-specific components - this.initEventListeners(); - restoreFolderFilter(); - initFolderTagsVisibility(); + this.pageControls.restoreFolderFilter(); + this.pageControls.initFolderTagsVisibility(); new LoraContextMenu(); - // Check for custom filters from recipe page navigation - this.checkCustomFilters(); - // Initialize cards for current bulk mode state (should be false initially) updateCardsForBulkMode(state.bulkMode); @@ -81,119 +64,6 @@ class LoraPageManager { // Initialize common page features (lazy loading, infinite scroll) appCore.initializePageFeatures(); } - - // Check for custom filter parameters in session storage - checkCustomFilters() { - const filterLoraHash = getSessionItem('recipe_to_lora_filterLoraHash'); - const filterLoraHashes = getSessionItem('recipe_to_lora_filterLoraHashes'); - const filterRecipeName = getSessionItem('filterRecipeName'); - const viewLoraDetail = getSessionItem('viewLoraDetail'); - - console.log("Checking custom filters..."); - console.log("filterLoraHash:", filterLoraHash); - console.log("filterLoraHashes:", filterLoraHashes); - console.log("filterRecipeName:", filterRecipeName); - console.log("viewLoraDetail:", viewLoraDetail); - - if ((filterLoraHash || filterLoraHashes) && filterRecipeName) { - // Found custom filter parameters, set up the custom filter - - // Show the filter indicator - const indicator = document.getElementById('customFilterIndicator'); - const filterText = indicator.querySelector('.customFilterText'); - - if (indicator && filterText) { - indicator.classList.remove('hidden'); - - // Set text content with recipe name - const filterType = filterLoraHash && viewLoraDetail ? "Viewing LoRA from" : "Viewing LoRAs from"; - const displayText = `${filterType}: ${filterRecipeName}`; - - filterText.textContent = this._truncateText(displayText, 30); - filterText.setAttribute('title', displayText); - - // Add click handler for the clear button - const clearBtn = indicator.querySelector('.clear-filter'); - if (clearBtn) { - clearBtn.addEventListener('click', this.clearCustomFilter); - } - - // Add pulse animation - const filterElement = indicator.querySelector('.filter-active'); - if (filterElement) { - filterElement.classList.add('animate'); - setTimeout(() => filterElement.classList.remove('animate'), 600); - } - } - - // If we're viewing a specific LoRA detail, set up to open the modal - if (filterLoraHash && viewLoraDetail) { - // Store this to fetch after initial load completes - state.pendingLoraHash = filterLoraHash; - } - } - } - - // Helper to truncate text with ellipsis - _truncateText(text, maxLength) { - return text.length > maxLength ? text.substring(0, maxLength - 3) + '...' : text; - } - - // Clear the custom filter and reload the page - clearCustomFilter = async () => { - console.log("Clearing custom filter..."); - // Remove filter parameters from session storage - removeSessionItem('recipe_to_lora_filterLoraHash'); - removeSessionItem('recipe_to_lora_filterLoraHashes'); - removeSessionItem('filterRecipeName'); - removeSessionItem('viewLoraDetail'); - - // Hide the filter indicator - const indicator = document.getElementById('customFilterIndicator'); - if (indicator) { - indicator.classList.add('hidden'); - } - - // Reset state - if (state.pendingLoraHash) { - delete state.pendingLoraHash; - } - - // Reload the loras - await resetAndReload(); - } - - loadSortPreference() { - const savedSort = getStorageItem('loras_sort'); - if (savedSort) { - state.sortBy = savedSort; - const sortSelect = document.getElementById('sortSelect'); - if (sortSelect) { - sortSelect.value = savedSort; - } - } - } - - saveSortPreference(sortValue) { - setStorageItem('loras_sort', sortValue); - } - - initEventListeners() { - const sortSelect = document.getElementById('sortSelect'); - if (sortSelect) { - sortSelect.value = state.sortBy; - this.loadSortPreference(); - sortSelect.addEventListener('change', async (e) => { - state.sortBy = e.target.value; - this.saveSortPreference(e.target.value); - await resetAndReload(); - }); - } - - document.querySelectorAll('.folder-tags .tag').forEach(tag => { - tag.addEventListener('click', toggleFolder); - }); - } } // Initialize everything when DOM is ready diff --git a/static/js/recipes.js b/static/js/recipes.js index ba55e62c..3c190e56 100644 --- a/static/js/recipes.js +++ b/static/js/recipes.js @@ -251,6 +251,11 @@ class RecipeManager { // Update pagination state based on current page and total pages this.pageState.hasMore = data.page < data.total_pages; + // Increment the page number AFTER successful loading + if (data.items.length > 0) { + this.pageState.currentPage++; + } + } catch (error) { console.error('Error loading recipes:', error); appCore.showToast('Failed to load recipes', 'error'); diff --git a/static/js/utils/infiniteScroll.js b/static/js/utils/infiniteScroll.js index ddb0e341..60795e14 100644 --- a/static/js/utils/infiniteScroll.js +++ b/static/js/utils/infiniteScroll.js @@ -1,5 +1,6 @@ import { state, getCurrentPageState } from '../state/index.js'; import { loadMoreLoras } from '../api/loraApi.js'; +import { loadMoreCheckpoints } from '../api/checkpointApi.js'; import { debounce } from './debounce.js'; export function initializeInfiniteScroll(pageType = 'loras') { @@ -21,7 +22,6 @@ export function initializeInfiniteScroll(pageType = 'loras') { case 'recipes': loadMoreFunction = () => { if (!pageState.isLoading && pageState.hasMore) { - pageState.currentPage++; window.recipeManager.loadRecipes(false); // false to not reset pagination } }; @@ -30,15 +30,18 @@ export function initializeInfiniteScroll(pageType = 'loras') { case 'checkpoints': loadMoreFunction = () => { if (!pageState.isLoading && pageState.hasMore) { - pageState.currentPage++; - window.checkpointManager.loadCheckpoints(false); // false to not reset pagination + loadMoreCheckpoints(false); // false to not reset } }; gridId = 'checkpointGrid'; break; case 'loras': default: - loadMoreFunction = () => loadMoreLoras(false); // false to not reset + loadMoreFunction = () => { + if (!pageState.isLoading && pageState.hasMore) { + loadMoreLoras(false); // false to not reset + } + }; gridId = 'loraGrid'; break; } @@ -85,4 +88,4 @@ export function initializeInfiniteScroll(pageType = 'loras') { state.observer.observe(sentinel); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/templates/components/controls.html b/templates/components/controls.html index 45acc7a4..56921cd5 100644 --- a/templates/components/controls.html +++ b/templates/components/controls.html @@ -16,21 +16,25 @@
- + +
+ +
+
- -
-
-
+ + {% if request.path == '/loras' %}
-
+ {% endif %}
-