mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-24 14:42:11 -03:00
Enhance Lora and recipe integration with improved filtering and UI updates
- Added support for filtering LoRAs by hash in both API and UI components. - Implemented session storage management for custom filter states when navigating between recipes and LoRAs. - Introduced a new button in the recipe modal to view associated LoRAs, enhancing user navigation. - Updated CSS styles for new UI elements, including a custom filter indicator and LoRA view button. - Refactored existing JavaScript components to streamline the handling of filter parameters and improve maintainability.
This commit is contained in:
@@ -400,6 +400,27 @@
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
/* View LoRAs button */
|
||||
.view-loras-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.view-loras-btn:hover {
|
||||
opacity: 1;
|
||||
background: var(--lora-surface);
|
||||
color: var(--lora-accent);
|
||||
}
|
||||
|
||||
#recipeLorasCount {
|
||||
font-size: 0.9em;
|
||||
color: var(--text-color);
|
||||
@@ -433,6 +454,14 @@
|
||||
will-change: transform;
|
||||
/* Create a new containing block for absolutely positioned descendants */
|
||||
transform: translateZ(0);
|
||||
cursor: pointer; /* Make it clear the item is clickable */
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.recipe-lora-item:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
border-color: var(--lora-accent);
|
||||
}
|
||||
|
||||
.recipe-lora-item.exists-locally {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createLoraCard } from '../components/LoraCard.js';
|
||||
import { initializeInfiniteScroll } from '../utils/infiniteScroll.js';
|
||||
import { showDeleteModal } from '../utils/modalUtils.js';
|
||||
import { toggleFolder } from '../utils/uiHelpers.js';
|
||||
import { getSessionItem } from '../utils/storageHelpers.js';
|
||||
|
||||
export async function loadMoreLoras(resetPage = false, updateFolders = false) {
|
||||
const pageState = getCurrentPageState();
|
||||
@@ -57,6 +58,28 @@ export async function loadMoreLoras(resetPage = false, updateFolders = false) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for recipe-based filtering parameters from session storage
|
||||
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);
|
||||
}
|
||||
// Add multiple hashes filter if present
|
||||
else if (filterLoraHashes) {
|
||||
try {
|
||||
if (Array.isArray(filterLoraHashes) && filterLoraHashes.length > 0) {
|
||||
params.append('lora_hashes', filterLoraHashes.join(','));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing lora hashes from session storage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/loras?${params}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch loras: ${response.statusText}`);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Recipe Modal Component
|
||||
import { showToast } from '../utils/uiHelpers.js';
|
||||
import { state } from '../state/index.js';
|
||||
import { setSessionItem, removeSessionItem } from '../utils/storageHelpers.js';
|
||||
|
||||
class RecipeModal {
|
||||
constructor() {
|
||||
@@ -294,7 +295,7 @@ class RecipeModal {
|
||||
} else {
|
||||
// No generation parameters available
|
||||
if (promptElement) promptElement.textContent = 'No prompt information available';
|
||||
if (negativePromptElement) negativePromptElement.textContent = 'No negative prompt information available';
|
||||
if (negativePromptElement) promptElement.textContent = 'No negative prompt information available';
|
||||
if (otherParamsElement) otherParamsElement.innerHTML = '<div class="no-params">No parameters available</div>';
|
||||
}
|
||||
|
||||
@@ -342,8 +343,15 @@ class RecipeModal {
|
||||
|
||||
lorasCountElement.innerHTML = `<i class="fas fa-layer-group"></i> ${totalCount} LoRAs ${statusHTML}`;
|
||||
|
||||
// Add click handler for missing LoRAs status
|
||||
// Add event listeners for buttons and status indicators
|
||||
setTimeout(() => {
|
||||
// Set up click handler for View LoRAs button
|
||||
const viewRecipeLorasBtn = document.getElementById('viewRecipeLorasBtn');
|
||||
if (viewRecipeLorasBtn) {
|
||||
viewRecipeLorasBtn.addEventListener('click', () => this.navigateToLorasPage());
|
||||
}
|
||||
|
||||
// Add click handler for missing LoRAs status
|
||||
const missingStatus = document.querySelector('.recipe-status.missing');
|
||||
if (missingStatus && missingLorasCount > 0) {
|
||||
missingStatus.classList.add('clickable');
|
||||
@@ -433,6 +441,7 @@ class RecipeModal {
|
||||
// Add event listeners for reconnect functionality
|
||||
setTimeout(() => {
|
||||
this.setupReconnectButtons();
|
||||
this.setupLoraItemsClickable();
|
||||
}, 100);
|
||||
|
||||
// Generate recipe syntax for copy button (this is now a placeholder, actual syntax will be fetched from the API)
|
||||
@@ -1007,6 +1016,65 @@ class RecipeModal {
|
||||
state.loadingManager.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// New method to navigate to the LoRAs page
|
||||
navigateToLorasPage(specificLoraIndex = null) {
|
||||
// Close the current modal
|
||||
modalManager.closeModal('recipeModal');
|
||||
|
||||
// Clear any previous filters first
|
||||
removeSessionItem('recipe_to_lora_filterLoraHash');
|
||||
removeSessionItem('recipe_to_lora_filterLoraHashes');
|
||||
removeSessionItem('filterRecipeName');
|
||||
removeSessionItem('viewLoraDetail');
|
||||
|
||||
if (specificLoraIndex !== null) {
|
||||
// If a specific LoRA index is provided, navigate to view just that one LoRA
|
||||
const lora = this.currentRecipe.loras[specificLoraIndex];
|
||||
if (lora && lora.hash) {
|
||||
// Set session storage to open the LoRA modal directly
|
||||
setSessionItem('recipe_to_lora_filterLoraHash', lora.hash.toLowerCase());
|
||||
setSessionItem('viewLoraDetail', 'true');
|
||||
setSessionItem('filterRecipeName', this.currentRecipe.title);
|
||||
}
|
||||
} else {
|
||||
// If no specific LoRA index is provided, show all LoRAs from this recipe
|
||||
// Collect all hashes from the recipe's LoRAs
|
||||
const loraHashes = this.currentRecipe.loras
|
||||
.filter(lora => lora.hash)
|
||||
.map(lora => lora.hash.toLowerCase());
|
||||
|
||||
if (loraHashes.length > 0) {
|
||||
// Store the LoRA hashes and recipe name in sessionStorage
|
||||
setSessionItem('recipe_to_lora_filterLoraHashes', JSON.stringify(loraHashes));
|
||||
setSessionItem('filterRecipeName', this.currentRecipe.title);
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to the LoRAs page
|
||||
window.location.href = '/loras';
|
||||
}
|
||||
|
||||
// New method to make LoRA items clickable
|
||||
setupLoraItemsClickable() {
|
||||
const loraItems = document.querySelectorAll('.recipe-lora-item');
|
||||
loraItems.forEach(item => {
|
||||
// Get the lora index from the data attribute
|
||||
const loraIndex = parseInt(item.dataset.loraIndex);
|
||||
|
||||
item.addEventListener('click', (e) => {
|
||||
// If the click is on the reconnect container or badge, don't navigate
|
||||
if (e.target.closest('.lora-reconnect-container') ||
|
||||
e.target.closest('.deleted-badge') ||
|
||||
e.target.closest('.reconnect-tooltip')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate to the LoRAs page with the specific LoRA index
|
||||
this.navigateToLorasPage(loraIndex);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { RecipeModal };
|
||||
export { RecipeModal };
|
||||
@@ -192,29 +192,20 @@ function copyRecipeSyntax(recipeId) {
|
||||
* @param {string} loraHash - The hash of the Lora to filter by
|
||||
* @param {boolean} createNew - Whether to open the create recipe dialog
|
||||
*/
|
||||
function navigateToRecipesPage(loraName, loraHash, createNew = false) {
|
||||
function navigateToRecipesPage(loraName, loraHash) {
|
||||
// Close the current modal
|
||||
if (window.modalManager) {
|
||||
modalManager.closeModal('loraModal');
|
||||
}
|
||||
|
||||
// Clear any previous filters first
|
||||
removeSessionItem('filterLoraName');
|
||||
removeSessionItem('filterLoraHash');
|
||||
removeSessionItem('bypassExistingFilters');
|
||||
removeSessionItem('lora_to_recipe_filterLoraName');
|
||||
removeSessionItem('lora_to_recipe_filterLoraHash');
|
||||
removeSessionItem('viewRecipeId');
|
||||
|
||||
// Store the LoRA name and hash filter in sessionStorage
|
||||
setSessionItem('filterLoraName', loraName);
|
||||
setSessionItem('filterLoraHash', loraHash);
|
||||
|
||||
// Set a flag to indicate we're navigating with a specific filter
|
||||
setSessionItem('bypassExistingFilters', 'true');
|
||||
|
||||
// Set flag to open create dialog if requested
|
||||
if (createNew) {
|
||||
setSessionItem('openCreateRecipeDialog', 'true');
|
||||
}
|
||||
setSessionItem('lora_to_recipe_filterLoraName', loraName);
|
||||
setSessionItem('lora_to_recipe_filterLoraHash', loraHash);
|
||||
|
||||
// Directly navigate to recipes page
|
||||
window.location.href = '/loras/recipes';
|
||||
@@ -233,7 +224,6 @@ function navigateToRecipeDetails(recipeId) {
|
||||
// Clear any previous filters first
|
||||
removeSessionItem('filterLoraName');
|
||||
removeSessionItem('filterLoraHash');
|
||||
removeSessionItem('bypassExistingFilters');
|
||||
removeSessionItem('viewRecipeId');
|
||||
|
||||
// Store the recipe ID in sessionStorage to load on recipes page
|
||||
|
||||
@@ -17,7 +17,7 @@ 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 } from './utils/storageHelpers.js';
|
||||
import { setStorageItem, getStorageItem, getSessionItem, removeSessionItem } from './utils/storageHelpers.js';
|
||||
|
||||
// Initialize the LoRA page
|
||||
class LoraPageManager {
|
||||
@@ -69,6 +69,9 @@ class LoraPageManager {
|
||||
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);
|
||||
|
||||
@@ -79,6 +82,80 @@ class LoraPageManager {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
@@ -71,18 +71,15 @@ class RecipeManager {
|
||||
}
|
||||
|
||||
_checkCustomFilter() {
|
||||
// Check for bypass filter flag
|
||||
const bypassExistingFilters = getSessionItem('bypassExistingFilters');
|
||||
|
||||
// Check for Lora filter
|
||||
const filterLoraName = getSessionItem('filterLoraName');
|
||||
const filterLoraHash = getSessionItem('filterLoraHash');
|
||||
const filterLoraName = getSessionItem('lora_to_recipe_filterLoraName');
|
||||
const filterLoraHash = getSessionItem('lora_to_recipe_filterLoraHash');
|
||||
|
||||
// Check for specific recipe ID
|
||||
const viewRecipeId = getSessionItem('viewRecipeId');
|
||||
|
||||
// Set custom filter if any parameter is present
|
||||
if (bypassExistingFilters || filterLoraName || filterLoraHash || viewRecipeId) {
|
||||
if (filterLoraName || filterLoraHash || viewRecipeId) {
|
||||
this.customFilter = {
|
||||
active: true,
|
||||
loraName: filterLoraName,
|
||||
@@ -90,26 +87,9 @@ class RecipeManager {
|
||||
recipeId: viewRecipeId
|
||||
};
|
||||
|
||||
// Clean up session storage after reading
|
||||
removeSessionItem('bypassExistingFilters');
|
||||
|
||||
// Show custom filter indicator
|
||||
this._showCustomFilterIndicator();
|
||||
}
|
||||
|
||||
// Check for create recipe dialog flag
|
||||
const openCreateRecipeDialog = getSessionItem('openCreateRecipeDialog');
|
||||
if (openCreateRecipeDialog) {
|
||||
// Clean up session storage
|
||||
removeSessionItem('openCreateRecipeDialog');
|
||||
|
||||
// Schedule showing the create dialog after the page loads
|
||||
setTimeout(() => {
|
||||
if (this.importManager && typeof this.importManager.showImportModal === 'function') {
|
||||
this.importManager.showImportModal();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
_showCustomFilterIndicator() {
|
||||
@@ -176,8 +156,8 @@ class RecipeManager {
|
||||
}
|
||||
|
||||
// Clear any session storage items
|
||||
removeSessionItem('filterLoraName');
|
||||
removeSessionItem('filterLoraHash');
|
||||
removeSessionItem('lora_to_recipe_filterLoraName');
|
||||
removeSessionItem('lora_to_recipe_filterLoraHash');
|
||||
removeSessionItem('viewRecipeId');
|
||||
|
||||
// Reload recipes without custom filter
|
||||
@@ -366,8 +346,4 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
});
|
||||
|
||||
// Export for use in other modules
|
||||
export { RecipeManager };
|
||||
|
||||
// The RecipesManager class from the original file is preserved below (commented out)
|
||||
// If needed, functionality can be migrated to the new RecipeManager class above
|
||||
// ...rest of the existing code...
|
||||
export { RecipeManager };
|
||||
Reference in New Issue
Block a user