mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 07:05:43 -03:00
checkpoint
This commit is contained in:
@@ -361,6 +361,7 @@
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||||
transform-origin: top right;
|
transform-origin: top right;
|
||||||
|
display: block; /* Ensure it's block by default */
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-options-panel.hidden {
|
.search-options-panel.hidden {
|
||||||
|
|||||||
155
static/js/components/Header.js
Normal file
155
static/js/components/Header.js
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
/**
|
||||||
|
* Header.js - Manages the application header behavior across different pages
|
||||||
|
* Handles initialization of appropriate search and filter managers based on current page
|
||||||
|
*/
|
||||||
|
export class HeaderManager {
|
||||||
|
constructor() {
|
||||||
|
this.currentPage = this.detectCurrentPage();
|
||||||
|
this.searchManager = null;
|
||||||
|
this.filterManager = null;
|
||||||
|
|
||||||
|
// Initialize appropriate managers based on current page
|
||||||
|
this.initializeManagers();
|
||||||
|
|
||||||
|
// Set up common header functionality
|
||||||
|
this.initializeCommonElements();
|
||||||
|
}
|
||||||
|
|
||||||
|
detectCurrentPage() {
|
||||||
|
const path = window.location.pathname;
|
||||||
|
if (path.includes('/loras/recipes')) return 'recipes';
|
||||||
|
if (path.includes('/checkpoints')) return 'checkpoints';
|
||||||
|
if (path.includes('/loras')) return 'loras';
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeManagers() {
|
||||||
|
// Import and initialize appropriate search manager based on page
|
||||||
|
if (this.currentPage === 'loras') {
|
||||||
|
import('../managers/LoraSearchManager.js').then(module => {
|
||||||
|
const { LoraSearchManager } = module;
|
||||||
|
this.searchManager = new LoraSearchManager();
|
||||||
|
window.searchManager = this.searchManager;
|
||||||
|
});
|
||||||
|
|
||||||
|
import('../managers/FilterManager.js').then(module => {
|
||||||
|
const { FilterManager } = module;
|
||||||
|
this.filterManager = new FilterManager();
|
||||||
|
window.filterManager = this.filterManager;
|
||||||
|
});
|
||||||
|
} else if (this.currentPage === 'recipes') {
|
||||||
|
import('../managers/RecipeSearchManager.js').then(module => {
|
||||||
|
const { RecipeSearchManager } = module;
|
||||||
|
this.searchManager = new RecipeSearchManager();
|
||||||
|
window.searchManager = this.searchManager;
|
||||||
|
});
|
||||||
|
|
||||||
|
import('../managers/RecipeFilterManager.js').then(module => {
|
||||||
|
const { RecipeFilterManager } = module;
|
||||||
|
this.filterManager = new RecipeFilterManager();
|
||||||
|
window.filterManager = this.filterManager;
|
||||||
|
});
|
||||||
|
} else if (this.currentPage === 'checkpoints') {
|
||||||
|
import('../managers/CheckpointSearchManager.js').then(module => {
|
||||||
|
const { CheckpointSearchManager } = module;
|
||||||
|
this.searchManager = new CheckpointSearchManager();
|
||||||
|
window.searchManager = this.searchManager;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note: Checkpoints page might get its own filter manager in the future
|
||||||
|
// For now, we can use a basic filter manager or none at all
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeCommonElements() {
|
||||||
|
// Handle theme toggle
|
||||||
|
const themeToggle = document.querySelector('.theme-toggle');
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.addEventListener('click', () => {
|
||||||
|
if (typeof toggleTheme === 'function') {
|
||||||
|
toggleTheme();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle settings toggle
|
||||||
|
const settingsToggle = document.querySelector('.settings-toggle');
|
||||||
|
if (settingsToggle) {
|
||||||
|
settingsToggle.addEventListener('click', () => {
|
||||||
|
if (window.settingsManager && typeof window.settingsManager.toggleSettings === 'function') {
|
||||||
|
window.settingsManager.toggleSettings();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle update toggle
|
||||||
|
const updateToggle = document.getElementById('updateToggleBtn');
|
||||||
|
if (updateToggle) {
|
||||||
|
updateToggle.addEventListener('click', () => {
|
||||||
|
// Handle update check logic
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle support toggle
|
||||||
|
const supportToggle = document.getElementById('supportToggleBtn');
|
||||||
|
if (supportToggle) {
|
||||||
|
supportToggle.addEventListener('click', () => {
|
||||||
|
// Handle support panel logic
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to update panel positions (can be called from window resize handlers)
|
||||||
|
updatePanelPositions() {
|
||||||
|
if (this.searchManager && typeof this.searchManager.updatePanelPositions === 'function') {
|
||||||
|
this.searchManager.updatePanelPositions();
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
if (searchOptionsPanel) {
|
||||||
|
searchOptionsPanel.style.top = `${topPosition}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterPanel) {
|
||||||
|
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
|
||||||
|
if (searchOptionsPanel) {
|
||||||
|
searchOptionsPanel.style.right = `${window.innerWidth - searchRect.right}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position the filter panel aligned with the filter button
|
||||||
|
if (filterPanel) {
|
||||||
|
const filterButton = document.getElementById('filterButton');
|
||||||
|
if (filterButton) {
|
||||||
|
const filterRect = filterButton.getBoundingClientRect();
|
||||||
|
filterPanel.style.right = `${window.innerWidth - filterRect.right}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the header manager when the DOM is loaded
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
window.headerManager = new HeaderManager();
|
||||||
|
});
|
||||||
@@ -30,6 +30,7 @@ import { moveManager } from './managers/MoveManager.js';
|
|||||||
import { FilterManager } from './managers/FilterManager.js';
|
import { FilterManager } from './managers/FilterManager.js';
|
||||||
import { createLoraCard, updateCardsForBulkMode } from './components/LoraCard.js';
|
import { createLoraCard, updateCardsForBulkMode } from './components/LoraCard.js';
|
||||||
import { bulkManager } from './managers/BulkManager.js';
|
import { bulkManager } from './managers/BulkManager.js';
|
||||||
|
import { HeaderManager } from './components/Header.js';
|
||||||
|
|
||||||
// Add bulk mode to state
|
// Add bulk mode to state
|
||||||
state.bulkMode = false;
|
state.bulkMode = false;
|
||||||
@@ -76,7 +77,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
modalManager.initialize(); // Initialize modalManager after DOM is loaded
|
modalManager.initialize(); // Initialize modalManager after DOM is loaded
|
||||||
updateService.initialize(); // Initialize updateService after modalManager
|
updateService.initialize(); // Initialize updateService after modalManager
|
||||||
window.downloadManager = new DownloadManager(); // Move this after modalManager initialization
|
window.downloadManager = new DownloadManager(); // Move this after modalManager initialization
|
||||||
window.filterManager = new FilterManager(); // Initialize filter manager
|
|
||||||
|
|
||||||
// Initialize state filters from filterManager if available
|
// Initialize state filters from filterManager if available
|
||||||
if (window.filterManager && window.filterManager.filters) {
|
if (window.filterManager && window.filterManager.filters) {
|
||||||
@@ -90,7 +90,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
initTheme();
|
initTheme();
|
||||||
initFolderTagsVisibility();
|
initFolderTagsVisibility();
|
||||||
initBackToTop();
|
initBackToTop();
|
||||||
window.searchManager = new SearchManager();
|
|
||||||
new LoraContextMenu();
|
new LoraContextMenu();
|
||||||
|
|
||||||
// Initialize cards for current bulk mode state (should be false initially)
|
// Initialize cards for current bulk mode state (should be false initially)
|
||||||
|
|||||||
150
static/js/managers/CheckpointSearchManager.js
Normal file
150
static/js/managers/CheckpointSearchManager.js
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
/**
|
||||||
|
* CheckpointSearchManager - Specialized search manager for the Checkpoints page
|
||||||
|
* Extends the base SearchManager with checkpoint-specific functionality
|
||||||
|
*/
|
||||||
|
import { SearchManager } from './SearchManager.js';
|
||||||
|
import { state } from '../state/index.js';
|
||||||
|
import { showToast } from '../utils/uiHelpers.js';
|
||||||
|
|
||||||
|
export class CheckpointSearchManager extends SearchManager {
|
||||||
|
constructor(options = {}) {
|
||||||
|
super({
|
||||||
|
page: 'checkpoints',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
|
||||||
|
this.currentSearchTerm = '';
|
||||||
|
|
||||||
|
// Store this instance in the state
|
||||||
|
if (state) {
|
||||||
|
state.searchManager = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async performSearch() {
|
||||||
|
const searchTerm = this.searchInput.value.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (searchTerm === this.currentSearchTerm && !this.isSearching) {
|
||||||
|
return; // Avoid duplicate searches
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentSearchTerm = searchTerm;
|
||||||
|
|
||||||
|
const grid = document.getElementById('checkpointGrid');
|
||||||
|
|
||||||
|
if (!searchTerm) {
|
||||||
|
if (state) {
|
||||||
|
state.currentPage = 1;
|
||||||
|
}
|
||||||
|
this.resetAndReloadCheckpoints();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.isSearching = true;
|
||||||
|
if (state && state.loadingManager) {
|
||||||
|
state.loadingManager.showSimpleLoading('Searching checkpoints...');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store current scroll position
|
||||||
|
const scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
state.currentPage = 1;
|
||||||
|
state.hasMore = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL('/api/checkpoints', window.location.origin);
|
||||||
|
url.searchParams.set('page', '1');
|
||||||
|
url.searchParams.set('page_size', '20');
|
||||||
|
url.searchParams.set('sort_by', state ? state.sortBy : 'name');
|
||||||
|
url.searchParams.set('search', searchTerm);
|
||||||
|
url.searchParams.set('fuzzy', 'true');
|
||||||
|
|
||||||
|
// Add search options
|
||||||
|
const searchOptions = this.getActiveSearchOptions();
|
||||||
|
url.searchParams.set('search_filename', searchOptions.filename.toString());
|
||||||
|
url.searchParams.set('search_modelname', searchOptions.modelname.toString());
|
||||||
|
|
||||||
|
// Always send folder parameter if there is an active folder
|
||||||
|
if (state && state.activeFolder) {
|
||||||
|
url.searchParams.set('folder', state.activeFolder);
|
||||||
|
// Add recursive parameter when recursive search is enabled
|
||||||
|
const recursive = this.recursiveSearchToggle ? this.recursiveSearchToggle.checked : false;
|
||||||
|
url.searchParams.set('recursive', 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) {
|
||||||
|
grid.innerHTML = '';
|
||||||
|
|
||||||
|
if (data.items.length === 0) {
|
||||||
|
grid.innerHTML = '<div class="no-results">No matching checkpoints found</div>';
|
||||||
|
if (state) {
|
||||||
|
state.hasMore = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.appendCheckpointCards(data.items);
|
||||||
|
if (state) {
|
||||||
|
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('Checkpoint search error:', error);
|
||||||
|
showToast('Checkpoint search failed', 'error');
|
||||||
|
} finally {
|
||||||
|
this.isSearching = false;
|
||||||
|
if (state && state.loadingManager) {
|
||||||
|
state.loadingManager.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetAndReloadCheckpoints() {
|
||||||
|
// This function would be implemented in the checkpoints page
|
||||||
|
if (typeof window.loadCheckpoints === 'function') {
|
||||||
|
window.loadCheckpoints();
|
||||||
|
} else {
|
||||||
|
// Fallback to reloading the page
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendCheckpointCards(checkpoints) {
|
||||||
|
// This function would be implemented in the checkpoints page
|
||||||
|
const grid = document.getElementById('checkpointGrid');
|
||||||
|
if (!grid) return;
|
||||||
|
|
||||||
|
if (typeof window.appendCheckpointCards === 'function') {
|
||||||
|
window.appendCheckpointCards(checkpoints);
|
||||||
|
} else {
|
||||||
|
// Fallback implementation
|
||||||
|
checkpoints.forEach(checkpoint => {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'checkpoint-card';
|
||||||
|
card.innerHTML = `
|
||||||
|
<h3>${checkpoint.name}</h3>
|
||||||
|
<p>${checkpoint.filename || 'No filename'}</p>
|
||||||
|
`;
|
||||||
|
grid.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
135
static/js/managers/LoraSearchManager.js
Normal file
135
static/js/managers/LoraSearchManager.js
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* LoraSearchManager - Specialized search manager for the LoRAs page
|
||||||
|
* Extends the base SearchManager with LoRA-specific functionality
|
||||||
|
*/
|
||||||
|
import { SearchManager } from './SearchManager.js';
|
||||||
|
import { appendLoraCards } from '../api/loraApi.js';
|
||||||
|
import { resetAndReload } from '../api/loraApi.js';
|
||||||
|
import { state } from '../state/index.js';
|
||||||
|
import { showToast } from '../utils/uiHelpers.js';
|
||||||
|
|
||||||
|
export class LoraSearchManager extends SearchManager {
|
||||||
|
constructor(options = {}) {
|
||||||
|
console.log("initializing lora search manager");
|
||||||
|
super({
|
||||||
|
page: 'loras',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
|
||||||
|
this.currentSearchTerm = '';
|
||||||
|
|
||||||
|
// Store this instance in the state
|
||||||
|
if (state) {
|
||||||
|
state.searchManager = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async performSearch() {
|
||||||
|
const searchTerm = this.searchInput.value.trim().toLowerCase();
|
||||||
|
|
||||||
|
// Log the search attempt for debugging
|
||||||
|
console.log('LoraSearchManager performSearch called with:', searchTerm);
|
||||||
|
|
||||||
|
if (searchTerm === this.currentSearchTerm && !this.isSearching) {
|
||||||
|
return; // Avoid duplicate searches
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentSearchTerm = searchTerm;
|
||||||
|
|
||||||
|
const grid = document.getElementById('loraGrid');
|
||||||
|
if (!grid) {
|
||||||
|
console.error('Error: Could not find loraGrid element');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!searchTerm) {
|
||||||
|
if (state) {
|
||||||
|
state.currentPage = 1;
|
||||||
|
}
|
||||||
|
await resetAndReload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.isSearching = true;
|
||||||
|
if (state && state.loadingManager) {
|
||||||
|
state.loadingManager.showSimpleLoading('Searching...');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store current scroll position
|
||||||
|
const scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
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 ? state.sortBy : 'name');
|
||||||
|
url.searchParams.set('search', searchTerm);
|
||||||
|
url.searchParams.set('fuzzy', 'true');
|
||||||
|
|
||||||
|
// Add search options
|
||||||
|
const searchOptions = this.getActiveSearchOptions();
|
||||||
|
console.log('Active search options:', searchOptions);
|
||||||
|
|
||||||
|
// Make sure we're sending boolean values as strings
|
||||||
|
url.searchParams.set('search_filename', searchOptions.filename ? 'true' : 'false');
|
||||||
|
url.searchParams.set('search_modelname', searchOptions.modelname ? 'true' : 'false');
|
||||||
|
url.searchParams.set('search_tags', searchOptions.tags ? 'true' : 'false');
|
||||||
|
|
||||||
|
// Always send folder parameter if there is an active folder
|
||||||
|
if (state && state.activeFolder) {
|
||||||
|
url.searchParams.set('folder', state.activeFolder);
|
||||||
|
// Add recursive parameter when recursive search is enabled
|
||||||
|
const recursive = this.recursiveSearchToggle ? this.recursiveSearchToggle.checked : false;
|
||||||
|
url.searchParams.set('recursive', recursive.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Search URL:', url.toString());
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Search failed with status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('Search results:', data);
|
||||||
|
|
||||||
|
if (searchTerm === this.currentSearchTerm) {
|
||||||
|
grid.innerHTML = '';
|
||||||
|
|
||||||
|
if (data.items.length === 0) {
|
||||||
|
grid.innerHTML = '<div class="no-results">No matching loras found</div>';
|
||||||
|
if (state) {
|
||||||
|
state.hasMore = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
appendLoraCards(data.items);
|
||||||
|
if (state) {
|
||||||
|
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;
|
||||||
|
if (state && state.loadingManager) {
|
||||||
|
state.loadingManager.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
145
static/js/managers/RecipeSearchManager.js
Normal file
145
static/js/managers/RecipeSearchManager.js
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* RecipeSearchManager - Specialized search manager for the Recipes page
|
||||||
|
* Extends the base SearchManager with recipe-specific functionality
|
||||||
|
*/
|
||||||
|
import { SearchManager } from './SearchManager.js';
|
||||||
|
import { state } from '../state/index.js';
|
||||||
|
import { showToast } from '../utils/uiHelpers.js';
|
||||||
|
|
||||||
|
export class RecipeSearchManager extends SearchManager {
|
||||||
|
constructor(options = {}) {
|
||||||
|
super({
|
||||||
|
page: 'recipes',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
|
||||||
|
this.currentSearchTerm = '';
|
||||||
|
|
||||||
|
// Store this instance in the state
|
||||||
|
if (state) {
|
||||||
|
state.searchManager = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async performSearch() {
|
||||||
|
const searchTerm = this.searchInput.value.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (searchTerm === this.currentSearchTerm && !this.isSearching) {
|
||||||
|
return; // Avoid duplicate searches
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentSearchTerm = searchTerm;
|
||||||
|
|
||||||
|
const grid = document.getElementById('recipeGrid');
|
||||||
|
|
||||||
|
if (!searchTerm) {
|
||||||
|
if (state) {
|
||||||
|
state.currentPage = 1;
|
||||||
|
}
|
||||||
|
this.resetAndReloadRecipes();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.isSearching = true;
|
||||||
|
if (state && state.loadingManager) {
|
||||||
|
state.loadingManager.showSimpleLoading('Searching recipes...');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store current scroll position
|
||||||
|
const scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
state.currentPage = 1;
|
||||||
|
state.hasMore = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL('/api/recipes', window.location.origin);
|
||||||
|
url.searchParams.set('page', '1');
|
||||||
|
url.searchParams.set('page_size', '20');
|
||||||
|
url.searchParams.set('sort_by', state ? state.sortBy : 'name');
|
||||||
|
url.searchParams.set('search', searchTerm);
|
||||||
|
url.searchParams.set('fuzzy', 'true');
|
||||||
|
|
||||||
|
// Add search options
|
||||||
|
const searchOptions = this.getActiveSearchOptions();
|
||||||
|
url.searchParams.set('search_name', searchOptions.modelname.toString());
|
||||||
|
url.searchParams.set('search_tags', searchOptions.tags.toString());
|
||||||
|
url.searchParams.set('search_loras', searchOptions.loras.toString());
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Search failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (searchTerm === this.currentSearchTerm && grid) {
|
||||||
|
grid.innerHTML = '';
|
||||||
|
|
||||||
|
if (data.items.length === 0) {
|
||||||
|
grid.innerHTML = '<div class="no-results">No matching recipes found</div>';
|
||||||
|
if (state) {
|
||||||
|
state.hasMore = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.appendRecipeCards(data.items);
|
||||||
|
if (state) {
|
||||||
|
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('Recipe search error:', error);
|
||||||
|
showToast('Recipe search failed', 'error');
|
||||||
|
} finally {
|
||||||
|
this.isSearching = false;
|
||||||
|
if (state && state.loadingManager) {
|
||||||
|
state.loadingManager.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetAndReloadRecipes() {
|
||||||
|
// This function would be implemented in the recipes page
|
||||||
|
// Similar to resetAndReload for loras
|
||||||
|
if (typeof window.loadRecipes === 'function') {
|
||||||
|
window.loadRecipes();
|
||||||
|
} else {
|
||||||
|
// Fallback to reloading the page
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendRecipeCards(recipes) {
|
||||||
|
// This function would be implemented in the recipes page
|
||||||
|
// Similar to appendLoraCards for loras
|
||||||
|
const grid = document.getElementById('recipeGrid');
|
||||||
|
if (!grid) return;
|
||||||
|
|
||||||
|
if (typeof window.appendRecipeCards === 'function') {
|
||||||
|
window.appendRecipeCards(recipes);
|
||||||
|
} else {
|
||||||
|
// Fallback implementation
|
||||||
|
recipes.forEach(recipe => {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'recipe-card';
|
||||||
|
card.innerHTML = `
|
||||||
|
<h3>${recipe.name}</h3>
|
||||||
|
<p>${recipe.description || 'No description'}</p>
|
||||||
|
`;
|
||||||
|
grid.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,10 +18,20 @@ export class SearchManager {
|
|||||||
this.recursiveSearchToggle = document.getElementById('recursiveSearchToggle');
|
this.recursiveSearchToggle = document.getElementById('recursiveSearchToggle');
|
||||||
|
|
||||||
this.searchTimeout = null;
|
this.searchTimeout = null;
|
||||||
this.currentPage = document.body.dataset.page || 'loras';
|
this.currentPage = options.page || document.body.dataset.page || 'loras';
|
||||||
|
this.isSearching = false;
|
||||||
|
|
||||||
|
// Create clear button for search input
|
||||||
|
this.createClearButton();
|
||||||
|
|
||||||
this.initEventListeners();
|
this.initEventListeners();
|
||||||
this.loadSearchPreferences();
|
this.loadSearchPreferences();
|
||||||
|
|
||||||
|
// Initialize panel positions
|
||||||
|
this.updatePanelPositions();
|
||||||
|
|
||||||
|
// Add resize listener
|
||||||
|
window.addEventListener('resize', this.updatePanelPositions.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
initEventListeners() {
|
initEventListeners() {
|
||||||
@@ -30,12 +40,14 @@ export class SearchManager {
|
|||||||
this.searchInput.addEventListener('input', () => {
|
this.searchInput.addEventListener('input', () => {
|
||||||
clearTimeout(this.searchTimeout);
|
clearTimeout(this.searchTimeout);
|
||||||
this.searchTimeout = setTimeout(() => this.performSearch(), this.options.searchDelay);
|
this.searchTimeout = setTimeout(() => this.performSearch(), this.options.searchDelay);
|
||||||
|
this.updateClearButtonVisibility();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear search with Escape key
|
// Clear search with Escape key
|
||||||
this.searchInput.addEventListener('keydown', (e) => {
|
this.searchInput.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
this.searchInput.value = '';
|
this.searchInput.value = '';
|
||||||
|
this.updateClearButtonVisibility();
|
||||||
this.performSearch();
|
this.performSearch();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -44,14 +56,14 @@ export class SearchManager {
|
|||||||
// Search options toggle
|
// Search options toggle
|
||||||
if (this.searchOptionsToggle) {
|
if (this.searchOptionsToggle) {
|
||||||
this.searchOptionsToggle.addEventListener('click', () => {
|
this.searchOptionsToggle.addEventListener('click', () => {
|
||||||
this.searchOptionsPanel.classList.toggle('hidden');
|
this.toggleSearchOptionsPanel();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close search options
|
// Close search options
|
||||||
if (this.closeSearchOptions) {
|
if (this.closeSearchOptions) {
|
||||||
this.closeSearchOptions.addEventListener('click', () => {
|
this.closeSearchOptions.addEventListener('click', () => {
|
||||||
this.searchOptionsPanel.classList.add('hidden');
|
this.closeSearchOptionsPanel();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +71,16 @@ export class SearchManager {
|
|||||||
if (this.searchOptionTags) {
|
if (this.searchOptionTags) {
|
||||||
this.searchOptionTags.forEach(tag => {
|
this.searchOptionTags.forEach(tag => {
|
||||||
tag.addEventListener('click', () => {
|
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
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast('At least one search option must be selected', 'info');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
tag.classList.toggle('active');
|
tag.classList.toggle('active');
|
||||||
this.saveSearchPreferences();
|
this.saveSearchPreferences();
|
||||||
this.performSearch();
|
this.performSearch();
|
||||||
@@ -73,6 +95,89 @@ export class SearchManager {
|
|||||||
this.performSearch();
|
this.performSearch();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createClearButton() {
|
||||||
|
// Create clear button if it doesn't exist
|
||||||
|
if (!this.searchInput) return;
|
||||||
|
|
||||||
|
// Check if clear button already exists
|
||||||
|
let clearButton = this.searchInput.parentNode.querySelector('.search-clear');
|
||||||
|
|
||||||
|
if (!clearButton) {
|
||||||
|
// Create clear button
|
||||||
|
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.updateClearButtonVisibility();
|
||||||
|
this.performSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// Ensure the panel is visible
|
||||||
|
this.searchOptionsPanel.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
this.closeSearchOptionsPanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeSearchOptionsPanel() {
|
||||||
|
if (this.searchOptionsPanel) {
|
||||||
|
this.searchOptionsPanel.classList.add('hidden');
|
||||||
|
this.searchOptionsToggle.classList.remove('active');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSearchPreferences() {
|
loadSearchPreferences() {
|
||||||
@@ -93,11 +198,45 @@ export class SearchManager {
|
|||||||
if (this.recursiveSearchToggle && preferences.recursive !== undefined) {
|
if (this.recursiveSearchToggle && preferences.recursive !== undefined) {
|
||||||
this.recursiveSearchToggle.checked = preferences.recursive;
|
this.recursiveSearchToggle.checked = preferences.recursive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure at least one search option is selected
|
||||||
|
this.validateSearchOptions();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading search preferences:', error);
|
console.error('Error loading search preferences:', error);
|
||||||
|
// Set default options if loading fails
|
||||||
|
this.setDefaultSearchOptions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateSearchOptions() {
|
||||||
|
// Check if at least one search option is active
|
||||||
|
const hasActiveOption = Array.from(this.searchOptionTags).some(tag =>
|
||||||
|
tag.classList.contains('active')
|
||||||
|
);
|
||||||
|
|
||||||
|
// If no search options are active, activate default options
|
||||||
|
if (!hasActiveOption) {
|
||||||
|
this.setDefaultSearchOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setDefaultSearchOptions() {
|
||||||
|
// Default to filename search option if available
|
||||||
|
const filenameOption = Array.from(this.searchOptionTags).find(tag =>
|
||||||
|
tag.dataset.option === 'filename'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filenameOption) {
|
||||||
|
filenameOption.classList.add('active');
|
||||||
|
} else if (this.searchOptionTags.length > 0) {
|
||||||
|
// Otherwise, select the first option
|
||||||
|
this.searchOptionTags[0].classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the default preferences
|
||||||
|
this.saveSearchPreferences();
|
||||||
|
}
|
||||||
|
|
||||||
saveSearchPreferences() {
|
saveSearchPreferences() {
|
||||||
try {
|
try {
|
||||||
const options = {};
|
const options = {};
|
||||||
@@ -117,15 +256,63 @@ export class SearchManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getActiveSearchOptions() {
|
getActiveSearchOptions() {
|
||||||
const options = [];
|
const options = {};
|
||||||
this.searchOptionTags.forEach(tag => {
|
this.searchOptionTags.forEach(tag => {
|
||||||
if (tag.classList.contains('active')) {
|
options[tag.dataset.option] = tag.classList.contains('active');
|
||||||
options.push(tag.dataset.option);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
if (searchOptionsPanel) {
|
||||||
|
searchOptionsPanel.style.top = `${topPosition}px`;
|
||||||
|
|
||||||
|
// Make sure the panel is visible when positioned
|
||||||
|
if (!searchOptionsPanel.classList.contains('hidden') &&
|
||||||
|
window.getComputedStyle(searchOptionsPanel).display === 'none') {
|
||||||
|
searchOptionsPanel.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterPanel) {
|
||||||
|
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
|
||||||
|
if (searchOptionsPanel) {
|
||||||
|
searchOptionsPanel.style.right = `${window.innerWidth - searchRect.right}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position the filter panel aligned with the filter button
|
||||||
|
if (filterPanel) {
|
||||||
|
const filterButton = document.getElementById('filterButton');
|
||||||
|
if (filterButton) {
|
||||||
|
const filterRect = filterButton.getBoundingClientRect();
|
||||||
|
filterPanel.style.right = `${window.innerWidth - filterRect.right}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
performSearch() {
|
performSearch() {
|
||||||
const query = this.searchInput.value.trim();
|
const query = this.searchInput.value.trim();
|
||||||
const options = this.getActiveSearchOptions();
|
const options = this.getActiveSearchOptions();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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 { SearchManager } from './managers/SearchManager.js';
|
||||||
|
import { HeaderManager } from './components/Header.js';
|
||||||
|
|
||||||
class RecipeManager {
|
class RecipeManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
@@ -142,19 +142,38 @@
|
|||||||
const recipesNavItem = document.getElementById('recipesNavItem');
|
const recipesNavItem = document.getElementById('recipesNavItem');
|
||||||
const checkpointsNavItem = document.getElementById('checkpointsNavItem');
|
const checkpointsNavItem = document.getElementById('checkpointsNavItem');
|
||||||
|
|
||||||
const lorasIndicator = document.getElementById('lorasIndicator');
|
|
||||||
const recipesIndicator = document.getElementById('recipesIndicator');
|
|
||||||
const checkpointsIndicator = document.getElementById('checkpointsIndicator');
|
|
||||||
|
|
||||||
if (currentPath === '/loras') {
|
if (currentPath === '/loras') {
|
||||||
lorasNavItem.classList.add('active');
|
lorasNavItem.classList.add('active');
|
||||||
lorasIndicator.style.display = 'block';
|
|
||||||
} else if (currentPath === '/loras/recipes') {
|
} else if (currentPath === '/loras/recipes') {
|
||||||
recipesNavItem.classList.add('active');
|
recipesNavItem.classList.add('active');
|
||||||
recipesIndicator.style.display = 'block';
|
|
||||||
} else if (currentPath === '/checkpoints') {
|
} else if (currentPath === '/checkpoints') {
|
||||||
checkpointsNavItem.classList.add('active');
|
checkpointsNavItem.classList.add('active');
|
||||||
checkpointsIndicator.style.display = 'block';
|
}
|
||||||
|
|
||||||
|
// Initialize search options panel toggle
|
||||||
|
const searchOptionsToggle = document.getElementById('searchOptionsToggle');
|
||||||
|
const searchOptionsPanel = document.getElementById('searchOptionsPanel');
|
||||||
|
|
||||||
|
if (searchOptionsToggle && searchOptionsPanel) {
|
||||||
|
searchOptionsToggle.addEventListener('click', function() {
|
||||||
|
const isHidden = searchOptionsPanel.classList.contains('hidden');
|
||||||
|
if (isHidden) {
|
||||||
|
searchOptionsPanel.classList.remove('hidden');
|
||||||
|
searchOptionsToggle.classList.add('active');
|
||||||
|
} else {
|
||||||
|
searchOptionsPanel.classList.add('hidden');
|
||||||
|
searchOptionsToggle.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close search options panel when clicking the close button
|
||||||
|
const closeSearchOptions = document.getElementById('closeSearchOptions');
|
||||||
|
if (closeSearchOptions) {
|
||||||
|
closeSearchOptions.addEventListener('click', function() {
|
||||||
|
searchOptionsPanel.classList.add('hidden');
|
||||||
|
searchOptionsToggle.classList.remove('active');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
Reference in New Issue
Block a user