mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-24 14:42:11 -03:00
Refactor controls and pagination for Checkpoints and LoRAs: Implement unified PageControls, enhance API integration, and improve event handling for better user experience.
This commit is contained in:
@@ -101,6 +101,11 @@ export async function loadMoreCheckpoints(resetPagination = true) {
|
|||||||
const card = createCheckpointCard(checkpoint);
|
const card = createCheckpointCard(checkpoint);
|
||||||
grid.appendChild(card);
|
grid.appendChild(card);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Increment the page number AFTER successful loading
|
||||||
|
if (data.items.length > 0) {
|
||||||
|
pageState.currentPage++;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading checkpoints:', error);
|
console.error('Error loading checkpoints:', error);
|
||||||
showToast('Failed to load checkpoints', 'error');
|
showToast('Failed to load checkpoints', 'error');
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ export async function loadMoreLoras(resetPage = false, updateFolders = false) {
|
|||||||
// Clear grid if resetting
|
// Clear grid if resetting
|
||||||
const grid = document.getElementById('loraGrid');
|
const grid = document.getElementById('loraGrid');
|
||||||
if (grid) grid.innerHTML = '';
|
if (grid) grid.innerHTML = '';
|
||||||
initializeInfiniteScroll();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
@@ -62,9 +61,6 @@ export async function loadMoreLoras(resetPage = false, updateFolders = false) {
|
|||||||
const filterLoraHash = getSessionItem('recipe_to_lora_filterLoraHash');
|
const filterLoraHash = getSessionItem('recipe_to_lora_filterLoraHash');
|
||||||
const filterLoraHashes = getSessionItem('recipe_to_lora_filterLoraHashes');
|
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
|
// Add hash filter parameter if present
|
||||||
if (filterLoraHash) {
|
if (filterLoraHash) {
|
||||||
params.append('lora_hash', filterLoraHash);
|
params.append('lora_hash', filterLoraHash);
|
||||||
@@ -93,13 +89,10 @@ export async function loadMoreLoras(resetPage = false, updateFolders = false) {
|
|||||||
pageState.hasMore = false;
|
pageState.hasMore = false;
|
||||||
} else if (data.items.length > 0) {
|
} else if (data.items.length > 0) {
|
||||||
pageState.hasMore = pageState.currentPage < data.total_pages;
|
pageState.hasMore = pageState.currentPage < data.total_pages;
|
||||||
pageState.currentPage++;
|
|
||||||
appendLoraCards(data.items);
|
appendLoraCards(data.items);
|
||||||
|
|
||||||
const sentinel = document.getElementById('scroll-sentinel');
|
// Increment the page number AFTER successful loading
|
||||||
if (sentinel && state.observer) {
|
pageState.currentPage++;
|
||||||
state.observer.observe(sentinel);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
pageState.hasMore = false;
|
pageState.hasMore = false;
|
||||||
}
|
}
|
||||||
@@ -303,10 +296,7 @@ export async function resetAndReload(updateFolders = false) {
|
|||||||
const pageState = getCurrentPageState();
|
const pageState = getCurrentPageState();
|
||||||
console.log('Resetting with state:', { ...pageState });
|
console.log('Resetting with state:', { ...pageState });
|
||||||
|
|
||||||
// Initialize infinite scroll - will reset the observer
|
// Reset pagination and load more loras
|
||||||
initializeInfiniteScroll();
|
|
||||||
|
|
||||||
// Load more loras with reset flag
|
|
||||||
await loadMoreLoras(true, updateFolders);
|
await loadMoreLoras(true, updateFolders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,68 +1,30 @@
|
|||||||
import { appCore } from './core.js';
|
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 { toggleApiKeyVisibility } from './managers/SettingsManager.js';
|
||||||
import { initializeInfiniteScroll } from './utils/infiniteScroll.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
|
// Initialize the Checkpoints page
|
||||||
class CheckpointsPageManager {
|
class CheckpointsPageManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
// Get page state
|
// Initialize page controls
|
||||||
this.pageState = getCurrentPageState();
|
this.pageControls = createPageControls('checkpoints');
|
||||||
|
|
||||||
// Set default values
|
// Expose only necessary functions to global scope
|
||||||
this.pageState.pageSize = 20;
|
this._exposeRequiredGlobalFunctions();
|
||||||
this.pageState.isLoading = false;
|
|
||||||
this.pageState.hasMore = true;
|
|
||||||
|
|
||||||
// Expose functions to window object
|
|
||||||
this._exposeGlobalFunctions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_exposeGlobalFunctions() {
|
_exposeRequiredGlobalFunctions() {
|
||||||
// API functions
|
// Minimal set of functions that need to remain global
|
||||||
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;
|
|
||||||
window.confirmDelete = confirmDelete;
|
window.confirmDelete = confirmDelete;
|
||||||
window.closeDeleteModal = closeDeleteModal;
|
window.closeDeleteModal = closeDeleteModal;
|
||||||
window.toggleApiKeyVisibility = toggleApiKeyVisibility;
|
window.toggleApiKeyVisibility = toggleApiKeyVisibility;
|
||||||
|
|
||||||
// Add reference to this manager
|
|
||||||
window.checkpointManager = this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
// Initialize event listeners
|
// Initialize page-specific components
|
||||||
this._initEventListeners();
|
this.pageControls.restoreFolderFilter();
|
||||||
|
this.pageControls.initFolderTagsVisibility();
|
||||||
// Restore folder filters if available
|
|
||||||
restoreFolderFilter('checkpoints');
|
|
||||||
|
|
||||||
// Load sort preference
|
|
||||||
this._loadSortPreference();
|
|
||||||
|
|
||||||
// Load initial checkpoints
|
|
||||||
await this.loadCheckpoints();
|
|
||||||
|
|
||||||
// Initialize infinite scroll
|
// Initialize infinite scroll
|
||||||
initializeInfiniteScroll('checkpoints');
|
initializeInfiniteScroll('checkpoints');
|
||||||
@@ -72,49 +34,6 @@ class CheckpointsPageManager {
|
|||||||
|
|
||||||
console.log('Checkpoints Manager initialized');
|
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
|
// Initialize everything when DOM is ready
|
||||||
|
|||||||
46
static/js/components/controls/CheckpointsControls.js
Normal file
46
static/js/components/controls/CheckpointsControls.js
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
147
static/js/components/controls/LorasControls.js
Normal file
147
static/js/components/controls/LorasControls.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
391
static/js/components/controls/PageControls.js
Normal file
391
static/js/components/controls/PageControls.js
Normal file
@@ -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 `<div class="tag ${isActive ? 'active' : ''}" data-folder="${folder}">${folder}</div>`;
|
||||||
|
}).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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
static/js/components/controls/index.js
Normal file
23
static/js/components/controls/index.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,14 @@
|
|||||||
import { appCore } from './core.js';
|
import { appCore } from './core.js';
|
||||||
import { state } from './state/index.js';
|
import { state } from './state/index.js';
|
||||||
import { showLoraModal, toggleShowcase, scrollToTop } from './components/loraModal/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 { updateCardsForBulkMode } from './components/LoraCard.js';
|
||||||
import { bulkManager } from './managers/BulkManager.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
|
// Initialize the LoRA page
|
||||||
class LoraPageManager {
|
class LoraPageManager {
|
||||||
@@ -29,24 +20,20 @@ class LoraPageManager {
|
|||||||
// Initialize managers
|
// Initialize managers
|
||||||
this.downloadManager = new DownloadManager();
|
this.downloadManager = new DownloadManager();
|
||||||
|
|
||||||
// Expose necessary functions to the page
|
// Initialize page controls
|
||||||
this._exposeGlobalFunctions();
|
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() {
|
_exposeRequiredGlobalFunctions() {
|
||||||
// Only expose what's needed for the page
|
// Only expose what's still needed globally
|
||||||
window.loadMoreLoras = loadMoreLoras;
|
// Most functionality is now handled by the PageControls component
|
||||||
window.fetchCivitai = fetchCivitai;
|
|
||||||
window.deleteModel = deleteModel;
|
|
||||||
window.replacePreview = replacePreview;
|
|
||||||
window.toggleFolder = toggleFolder;
|
|
||||||
window.copyTriggerWord = copyTriggerWord;
|
|
||||||
window.showLoraModal = showLoraModal;
|
window.showLoraModal = showLoraModal;
|
||||||
window.confirmDelete = confirmDelete;
|
window.confirmDelete = confirmDelete;
|
||||||
window.closeDeleteModal = closeDeleteModal;
|
window.closeDeleteModal = closeDeleteModal;
|
||||||
window.refreshLoras = refreshLoras;
|
|
||||||
window.openCivitai = openCivitai;
|
|
||||||
window.toggleFolderTags = toggleFolderTags;
|
|
||||||
window.toggleApiKeyVisibility = toggleApiKeyVisibility;
|
window.toggleApiKeyVisibility = toggleApiKeyVisibility;
|
||||||
window.downloadManager = this.downloadManager;
|
window.downloadManager = this.downloadManager;
|
||||||
window.moveManager = moveManager;
|
window.moveManager = moveManager;
|
||||||
@@ -64,14 +51,10 @@ class LoraPageManager {
|
|||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
// Initialize page-specific components
|
// Initialize page-specific components
|
||||||
this.initEventListeners();
|
this.pageControls.restoreFolderFilter();
|
||||||
restoreFolderFilter();
|
this.pageControls.initFolderTagsVisibility();
|
||||||
initFolderTagsVisibility();
|
|
||||||
new LoraContextMenu();
|
new LoraContextMenu();
|
||||||
|
|
||||||
// Check for custom filters from recipe page navigation
|
|
||||||
this.checkCustomFilters();
|
|
||||||
|
|
||||||
// Initialize cards for current bulk mode state (should be false initially)
|
// Initialize cards for current bulk mode state (should be false initially)
|
||||||
updateCardsForBulkMode(state.bulkMode);
|
updateCardsForBulkMode(state.bulkMode);
|
||||||
|
|
||||||
@@ -81,119 +64,6 @@ class LoraPageManager {
|
|||||||
// Initialize common page features (lazy loading, infinite scroll)
|
// Initialize common page features (lazy loading, infinite scroll)
|
||||||
appCore.initializePageFeatures();
|
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
|
// Initialize everything when DOM is ready
|
||||||
|
|||||||
@@ -251,6 +251,11 @@ class RecipeManager {
|
|||||||
// Update pagination state based on current page and total pages
|
// Update pagination state based on current page and total pages
|
||||||
this.pageState.hasMore = data.page < data.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) {
|
} catch (error) {
|
||||||
console.error('Error loading recipes:', error);
|
console.error('Error loading recipes:', error);
|
||||||
appCore.showToast('Failed to load recipes', 'error');
|
appCore.showToast('Failed to load recipes', 'error');
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { state, getCurrentPageState } from '../state/index.js';
|
import { state, getCurrentPageState } from '../state/index.js';
|
||||||
import { loadMoreLoras } from '../api/loraApi.js';
|
import { loadMoreLoras } from '../api/loraApi.js';
|
||||||
|
import { loadMoreCheckpoints } from '../api/checkpointApi.js';
|
||||||
import { debounce } from './debounce.js';
|
import { debounce } from './debounce.js';
|
||||||
|
|
||||||
export function initializeInfiniteScroll(pageType = 'loras') {
|
export function initializeInfiniteScroll(pageType = 'loras') {
|
||||||
@@ -21,7 +22,6 @@ export function initializeInfiniteScroll(pageType = 'loras') {
|
|||||||
case 'recipes':
|
case 'recipes':
|
||||||
loadMoreFunction = () => {
|
loadMoreFunction = () => {
|
||||||
if (!pageState.isLoading && pageState.hasMore) {
|
if (!pageState.isLoading && pageState.hasMore) {
|
||||||
pageState.currentPage++;
|
|
||||||
window.recipeManager.loadRecipes(false); // false to not reset pagination
|
window.recipeManager.loadRecipes(false); // false to not reset pagination
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -30,15 +30,18 @@ export function initializeInfiniteScroll(pageType = 'loras') {
|
|||||||
case 'checkpoints':
|
case 'checkpoints':
|
||||||
loadMoreFunction = () => {
|
loadMoreFunction = () => {
|
||||||
if (!pageState.isLoading && pageState.hasMore) {
|
if (!pageState.isLoading && pageState.hasMore) {
|
||||||
pageState.currentPage++;
|
loadMoreCheckpoints(false); // false to not reset
|
||||||
window.checkpointManager.loadCheckpoints(false); // false to not reset pagination
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
gridId = 'checkpointGrid';
|
gridId = 'checkpointGrid';
|
||||||
break;
|
break;
|
||||||
case 'loras':
|
case 'loras':
|
||||||
default:
|
default:
|
||||||
loadMoreFunction = () => loadMoreLoras(false); // false to not reset
|
loadMoreFunction = () => {
|
||||||
|
if (!pageState.isLoading && pageState.hasMore) {
|
||||||
|
loadMoreLoras(false); // false to not reset
|
||||||
|
}
|
||||||
|
};
|
||||||
gridId = 'loraGrid';
|
gridId = 'loraGrid';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,21 +16,25 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div title="Refresh model list" class="control-group">
|
<div title="Refresh model list" class="control-group">
|
||||||
<button onclick="refreshLoras()"><i class="fas fa-sync"></i> Refresh</button>
|
<button data-action="refresh"><i class="fas fa-sync"></i> Refresh</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<button data-action="fetch" title="Fetch from Civitai"><i class="fas fa-download"></i> Fetch</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<button onclick="fetchCivitai()" title="Fetch from Civitai"><i class="fas fa-download"></i> Fetch</button>
|
<button data-action="download" title="Download from URL">
|
||||||
</div>
|
|
||||||
<div class="control-group">
|
|
||||||
<button onclick="downloadManager.showDownloadModal()" title="Download from URL">
|
|
||||||
<i class="fas fa-cloud-download-alt"></i> Download
|
<i class="fas fa-cloud-download-alt"></i> Download
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Conditional buttons based on page -->
|
||||||
|
{% if request.path == '/loras' %}
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<button id="bulkOperationsBtn" onclick="bulkManager.toggleBulkMode()" title="Bulk Operations">
|
<button id="bulkOperationsBtn" data-action="bulk" title="Bulk Operations">
|
||||||
<i class="fas fa-th-large"></i> Bulk
|
<i class="fas fa-th-large"></i> Bulk
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div id="customFilterIndicator" class="control-group hidden">
|
<div id="customFilterIndicator" class="control-group hidden">
|
||||||
<div class="filter-active">
|
<div class="filter-active">
|
||||||
<i class="fas fa-filter"></i> <span class="customFilterText" title=""></span>
|
<i class="fas fa-filter"></i> <span class="customFilterText" title=""></span>
|
||||||
@@ -39,7 +43,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="toggle-folders-container">
|
<div class="toggle-folders-container">
|
||||||
<button class="toggle-folders-btn icon-only" onclick="toggleFolderTags()" title="Toggle folder tags">
|
<button class="toggle-folders-btn icon-only" title="Toggle folder tags">
|
||||||
<i class="fas fa-tags"></i>
|
<i class="fas fa-tags"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user