mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22: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);
|
||||
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');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
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 { 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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,21 +16,25 @@
|
||||
</select>
|
||||
</div>
|
||||
<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 class="control-group">
|
||||
<button onclick="fetchCivitai()" title="Fetch from Civitai"><i class="fas fa-download"></i> Fetch</button>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<button onclick="downloadManager.showDownloadModal()" title="Download from URL">
|
||||
<button data-action="download" title="Download from URL">
|
||||
<i class="fas fa-cloud-download-alt"></i> Download
|
||||
</button>
|
||||
</div>
|
||||
<!-- Conditional buttons based on page -->
|
||||
{% if request.path == '/loras' %}
|
||||
<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
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="customFilterIndicator" class="control-group hidden">
|
||||
<div class="filter-active">
|
||||
<i class="fas fa-filter"></i> <span class="customFilterText" title=""></span>
|
||||
@@ -39,7 +43,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user