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 `