mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 15:15:44 -03:00
update
This commit is contained in:
@@ -5,7 +5,6 @@ import { initializeCommonComponents } from './common.js';
|
|||||||
import { ImportManager } from './managers/ImportManager.js';
|
import { ImportManager } from './managers/ImportManager.js';
|
||||||
import { RecipeCard } from './components/RecipeCard.js';
|
import { RecipeCard } from './components/RecipeCard.js';
|
||||||
import { RecipeModal } from './components/RecipeModal.js';
|
import { RecipeModal } from './components/RecipeModal.js';
|
||||||
import { SearchManager } from './managers/SearchManager.js';
|
|
||||||
import { HeaderManager } from './components/Header.js';
|
import { HeaderManager } from './components/Header.js';
|
||||||
|
|
||||||
class RecipeManager {
|
class RecipeManager {
|
||||||
@@ -31,18 +30,6 @@ class RecipeManager {
|
|||||||
// Load initial set of recipes
|
// Load initial set of recipes
|
||||||
this.loadRecipes();
|
this.loadRecipes();
|
||||||
|
|
||||||
// Initialize search manager with Recipe-specific options
|
|
||||||
const recipeSearchManager = new SearchManager({
|
|
||||||
searchCallback: (query, options, recursive) => {
|
|
||||||
// Recipe-specific search implementation
|
|
||||||
fetchRecipes({
|
|
||||||
search: query,
|
|
||||||
search_options: options,
|
|
||||||
recursive: recursive
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set the current page for proper context
|
// Set the current page for proper context
|
||||||
document.body.dataset.page = 'recipes';
|
document.body.dataset.page = 'recipes';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,328 +0,0 @@
|
|||||||
import { appendLoraCards } from '../api/loraApi.js';
|
|
||||||
import { state } from '../state/index.js';
|
|
||||||
import { resetAndReload } from '../api/loraApi.js';
|
|
||||||
import { showToast } from './uiHelpers.js';
|
|
||||||
|
|
||||||
export class SearchManager {
|
|
||||||
constructor() {
|
|
||||||
// Initialize search manager
|
|
||||||
this.searchInput = document.getElementById('searchInput');
|
|
||||||
this.searchOptionsToggle = document.getElementById('searchOptionsToggle');
|
|
||||||
this.searchOptionsPanel = document.getElementById('searchOptionsPanel');
|
|
||||||
this.recursiveSearchToggle = document.getElementById('recursiveSearchToggle');
|
|
||||||
this.searchDebounceTimeout = null;
|
|
||||||
this.currentSearchTerm = '';
|
|
||||||
this.isSearching = false;
|
|
||||||
|
|
||||||
// Add clear button
|
|
||||||
this.createClearButton();
|
|
||||||
|
|
||||||
// Add this instance to state
|
|
||||||
state.searchManager = this;
|
|
||||||
|
|
||||||
if (this.searchInput) {
|
|
||||||
this.searchInput.addEventListener('input', this.handleSearch.bind(this));
|
|
||||||
// Update clear button visibility on input
|
|
||||||
this.searchInput.addEventListener('input', () => {
|
|
||||||
this.updateClearButtonVisibility();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize search options
|
|
||||||
this.initSearchOptions();
|
|
||||||
|
|
||||||
// Add global click handler to close panels when clicking outside
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
// Close search options panel when clicking outside
|
|
||||||
if (this.searchOptionsPanel &&
|
|
||||||
!this.searchOptionsPanel.contains(e.target) &&
|
|
||||||
e.target !== this.searchOptionsToggle &&
|
|
||||||
!this.searchOptionsToggle.contains(e.target)) {
|
|
||||||
this.closeSearchOptionsPanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close filter panel when clicking outside (if filterManager exists)
|
|
||||||
const filterPanel = document.getElementById('filterPanel');
|
|
||||||
const filterButton = document.getElementById('filterButton');
|
|
||||||
if (filterPanel &&
|
|
||||||
!filterPanel.contains(e.target) &&
|
|
||||||
e.target !== filterButton &&
|
|
||||||
!filterButton.contains(e.target) &&
|
|
||||||
window.filterManager) {
|
|
||||||
window.filterManager.closeFilterPanel();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize panel positions
|
|
||||||
this.updatePanelPositions();
|
|
||||||
|
|
||||||
// Add resize listener
|
|
||||||
window.addEventListener('resize', this.updatePanelPositions.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
initSearchOptions() {
|
|
||||||
// Load recursive search state from localStorage
|
|
||||||
state.searchOptions.recursive = localStorage.getItem('recursiveSearch') === 'true';
|
|
||||||
|
|
||||||
if (this.recursiveSearchToggle) {
|
|
||||||
this.recursiveSearchToggle.checked = state.searchOptions.recursive;
|
|
||||||
this.recursiveSearchToggle.addEventListener('change', (e) => {
|
|
||||||
state.searchOptions.recursive = e.target.checked;
|
|
||||||
localStorage.setItem('recursiveSearch', state.searchOptions.recursive);
|
|
||||||
|
|
||||||
// Rerun search if there's an active search term
|
|
||||||
if (this.currentSearchTerm) {
|
|
||||||
this.performSearch(this.currentSearchTerm);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup search options toggle
|
|
||||||
if (this.searchOptionsToggle) {
|
|
||||||
this.searchOptionsToggle.addEventListener('click', () => {
|
|
||||||
this.toggleSearchOptionsPanel();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close button for search options panel
|
|
||||||
const closeButton = document.getElementById('closeSearchOptions');
|
|
||||||
if (closeButton) {
|
|
||||||
closeButton.addEventListener('click', () => {
|
|
||||||
this.closeSearchOptionsPanel();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup search option tags
|
|
||||||
const optionTags = document.querySelectorAll('.search-option-tag');
|
|
||||||
optionTags.forEach(tag => {
|
|
||||||
const option = tag.dataset.option;
|
|
||||||
|
|
||||||
// Initialize tag state from state
|
|
||||||
tag.classList.toggle('active', state.searchOptions[option]);
|
|
||||||
|
|
||||||
tag.addEventListener('click', () => {
|
|
||||||
// Check if clicking would deselect the last active option
|
|
||||||
const activeOptions = document.querySelectorAll('.search-option-tag.active');
|
|
||||||
if (activeOptions.length === 1 && activeOptions[0] === tag) {
|
|
||||||
// Don't allow deselecting the last option and show toast
|
|
||||||
showToast('At least one search option must be selected', 'info');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tag.classList.toggle('active');
|
|
||||||
state.searchOptions[option] = tag.classList.contains('active');
|
|
||||||
|
|
||||||
// Save to localStorage
|
|
||||||
localStorage.setItem(`searchOption_${option}`, state.searchOptions[option]);
|
|
||||||
|
|
||||||
// Rerun search if there's an active search term
|
|
||||||
if (this.currentSearchTerm) {
|
|
||||||
this.performSearch(this.currentSearchTerm);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load option state from localStorage or use default
|
|
||||||
const savedState = localStorage.getItem(`searchOption_${option}`);
|
|
||||||
if (savedState !== null) {
|
|
||||||
state.searchOptions[option] = savedState === 'true';
|
|
||||||
tag.classList.toggle('active', state.searchOptions[option]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ensure at least one search option is selected
|
|
||||||
this.validateSearchOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add method to validate search options
|
|
||||||
validateSearchOptions() {
|
|
||||||
const hasActiveOption = Object.values(state.searchOptions)
|
|
||||||
.some(value => value === true && value !== state.searchOptions.recursive);
|
|
||||||
|
|
||||||
// If no search options are active, activate at least one default option
|
|
||||||
if (!hasActiveOption) {
|
|
||||||
state.searchOptions.filename = true;
|
|
||||||
localStorage.setItem('searchOption_filename', 'true');
|
|
||||||
|
|
||||||
// Update UI to match
|
|
||||||
const fileNameTag = document.querySelector('.search-option-tag[data-option="filename"]');
|
|
||||||
if (fileNameTag) {
|
|
||||||
fileNameTag.classList.add('active');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleSearchOptionsPanel() {
|
|
||||||
if (this.searchOptionsPanel) {
|
|
||||||
const isHidden = this.searchOptionsPanel.classList.contains('hidden');
|
|
||||||
if (isHidden) {
|
|
||||||
// Update position before showing
|
|
||||||
this.updatePanelPositions();
|
|
||||||
this.searchOptionsPanel.classList.remove('hidden');
|
|
||||||
this.searchOptionsToggle.classList.add('active');
|
|
||||||
} else {
|
|
||||||
this.closeSearchOptionsPanel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
closeSearchOptionsPanel() {
|
|
||||||
if (this.searchOptionsPanel) {
|
|
||||||
this.searchOptionsPanel.classList.add('hidden');
|
|
||||||
this.searchOptionsToggle.classList.remove('active');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createClearButton() {
|
|
||||||
// Create clear button
|
|
||||||
const clearButton = document.createElement('button');
|
|
||||||
clearButton.className = 'search-clear';
|
|
||||||
clearButton.innerHTML = '<i class="fas fa-times"></i>';
|
|
||||||
clearButton.title = 'Clear search';
|
|
||||||
|
|
||||||
// Add click handler
|
|
||||||
clearButton.addEventListener('click', () => {
|
|
||||||
this.searchInput.value = '';
|
|
||||||
this.currentSearchTerm = '';
|
|
||||||
this.updateClearButtonVisibility();
|
|
||||||
resetAndReload();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Insert after search input
|
|
||||||
this.searchInput.parentNode.appendChild(clearButton);
|
|
||||||
this.clearButton = clearButton;
|
|
||||||
|
|
||||||
// Set initial visibility
|
|
||||||
this.updateClearButtonVisibility();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateClearButtonVisibility() {
|
|
||||||
if (this.clearButton) {
|
|
||||||
this.clearButton.classList.toggle('visible', this.searchInput.value.length > 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSearch(event) {
|
|
||||||
if (this.searchDebounceTimeout) {
|
|
||||||
clearTimeout(this.searchDebounceTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.searchDebounceTimeout = setTimeout(async () => {
|
|
||||||
const searchTerm = event.target.value.trim().toLowerCase();
|
|
||||||
|
|
||||||
if (searchTerm !== this.currentSearchTerm && !this.isSearching) {
|
|
||||||
this.currentSearchTerm = searchTerm;
|
|
||||||
await this.performSearch(searchTerm);
|
|
||||||
}
|
|
||||||
}, 250);
|
|
||||||
}
|
|
||||||
|
|
||||||
async performSearch(searchTerm) {
|
|
||||||
const grid = document.getElementById('loraGrid');
|
|
||||||
|
|
||||||
if (!searchTerm) {
|
|
||||||
state.currentPage = 1;
|
|
||||||
await resetAndReload();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.isSearching = true;
|
|
||||||
state.loadingManager.showSimpleLoading('Searching...');
|
|
||||||
|
|
||||||
// Store current scroll position
|
|
||||||
const scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
|
|
||||||
|
|
||||||
state.currentPage = 1;
|
|
||||||
state.hasMore = true;
|
|
||||||
|
|
||||||
const url = new URL('/api/loras', window.location.origin);
|
|
||||||
url.searchParams.set('page', '1');
|
|
||||||
url.searchParams.set('page_size', '20');
|
|
||||||
url.searchParams.set('sort_by', state.sortBy);
|
|
||||||
url.searchParams.set('search', searchTerm);
|
|
||||||
url.searchParams.set('fuzzy', 'true');
|
|
||||||
|
|
||||||
// Add search options
|
|
||||||
url.searchParams.set('search_filename', state.searchOptions.filename.toString());
|
|
||||||
url.searchParams.set('search_modelname', state.searchOptions.modelname.toString());
|
|
||||||
url.searchParams.set('search_tags', state.searchOptions.tags.toString());
|
|
||||||
|
|
||||||
// Always send folder parameter if there is an active folder
|
|
||||||
if (state.activeFolder) {
|
|
||||||
url.searchParams.set('folder', state.activeFolder);
|
|
||||||
// Add recursive parameter when recursive search is enabled
|
|
||||||
url.searchParams.set('recursive', state.searchOptions.recursive.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Search failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (searchTerm === this.currentSearchTerm) {
|
|
||||||
grid.innerHTML = '';
|
|
||||||
|
|
||||||
if (data.items.length === 0) {
|
|
||||||
grid.innerHTML = '<div class="no-results">No matching loras found</div>';
|
|
||||||
state.hasMore = false;
|
|
||||||
} else {
|
|
||||||
appendLoraCards(data.items);
|
|
||||||
state.hasMore = state.currentPage < data.total_pages;
|
|
||||||
state.currentPage++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore scroll position after content is loaded
|
|
||||||
setTimeout(() => {
|
|
||||||
window.scrollTo({
|
|
||||||
top: scrollPosition,
|
|
||||||
behavior: 'instant' // Use 'instant' to prevent animation
|
|
||||||
});
|
|
||||||
}, 10);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Search error:', error);
|
|
||||||
showToast('Search failed', 'error');
|
|
||||||
} finally {
|
|
||||||
this.isSearching = false;
|
|
||||||
state.loadingManager.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePanelPositions() {
|
|
||||||
const searchOptionsPanel = document.getElementById('searchOptionsPanel');
|
|
||||||
const filterPanel = document.getElementById('filterPanel');
|
|
||||||
|
|
||||||
if (!searchOptionsPanel || !filterPanel) return;
|
|
||||||
|
|
||||||
// Get the header element
|
|
||||||
const header = document.querySelector('.app-header');
|
|
||||||
if (!header) return;
|
|
||||||
|
|
||||||
// Calculate the position based on the bottom of the header
|
|
||||||
const headerRect = header.getBoundingClientRect();
|
|
||||||
const topPosition = headerRect.bottom + 5; // Add 5px padding
|
|
||||||
|
|
||||||
// Set the positions
|
|
||||||
searchOptionsPanel.style.top = `${topPosition}px`;
|
|
||||||
filterPanel.style.top = `${topPosition}px`;
|
|
||||||
|
|
||||||
// Adjust panel horizontal position based on the search container
|
|
||||||
const searchContainer = document.querySelector('.header-search');
|
|
||||||
if (searchContainer) {
|
|
||||||
const searchRect = searchContainer.getBoundingClientRect();
|
|
||||||
|
|
||||||
// Position the search options panel aligned with the search container
|
|
||||||
searchOptionsPanel.style.right = `${window.innerWidth - searchRect.right}px`;
|
|
||||||
|
|
||||||
// Position the filter panel aligned with the filter button
|
|
||||||
const filterButton = document.getElementById('filterButton');
|
|
||||||
if (filterButton) {
|
|
||||||
const filterRect = filterButton.getBoundingClientRect();
|
|
||||||
filterPanel.style.right = `${window.innerWidth - filterRect.right}px`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user