mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-22 13:42:12 -03:00
- Updated CSS for recipe modal to improve layout and responsiveness, including adjustments to header and badge styles. - Added tooltip positioning logic to ensure correct display of local-badge tooltips on hover. - Refactored HTML structure for local status badges to enhance stability and positioning. - Removed unnecessary console logs from recipe fetching process in JavaScript for cleaner output.
280 lines
11 KiB
JavaScript
280 lines
11 KiB
JavaScript
// Recipe Card Component
|
|
import { showToast } from '../utils/uiHelpers.js';
|
|
import { modalManager } from '../managers/ModalManager.js';
|
|
|
|
class RecipeCard {
|
|
constructor(recipe, clickHandler) {
|
|
this.recipe = recipe;
|
|
this.clickHandler = clickHandler;
|
|
this.element = this.createCardElement();
|
|
}
|
|
|
|
createCardElement() {
|
|
const card = document.createElement('div');
|
|
card.className = 'lora-card';
|
|
card.dataset.filePath = this.recipe.file_path;
|
|
card.dataset.title = this.recipe.title;
|
|
card.dataset.created = this.recipe.created_date;
|
|
card.dataset.id = this.recipe.id || '';
|
|
|
|
// Get base model
|
|
const baseModel = this.recipe.base_model || '';
|
|
|
|
// Ensure loras array exists
|
|
const loras = this.recipe.loras || [];
|
|
const lorasCount = loras.length;
|
|
|
|
// Check if all LoRAs are available in the library
|
|
const missingLorasCount = loras.filter(lora => !lora.inLibrary).length;
|
|
const allLorasAvailable = missingLorasCount === 0 && lorasCount > 0;
|
|
|
|
// Ensure file_url exists, fallback to file_path if needed
|
|
const imageUrl = this.recipe.file_url ||
|
|
(this.recipe.file_path ? `/loras_static/root1/preview/${this.recipe.file_path.split('/').pop()}` :
|
|
'/loras_static/images/no-preview.png');
|
|
|
|
card.innerHTML = `
|
|
<div class="recipe-indicator" title="Recipe">R</div>
|
|
<div class="card-preview">
|
|
<img src="${imageUrl}" alt="${this.recipe.title}">
|
|
<div class="card-header">
|
|
<div class="base-model-wrapper">
|
|
${baseModel ? `<span class="base-model-label" title="${baseModel}">${baseModel}</span>` : ''}
|
|
</div>
|
|
<div class="card-actions">
|
|
<i class="fas fa-share-alt" title="Share Recipe"></i>
|
|
<i class="fas fa-copy" title="Copy Recipe Syntax"></i>
|
|
<i class="fas fa-trash" title="Delete Recipe"></i>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer">
|
|
<div class="model-info">
|
|
<span class="model-name">${this.recipe.title}</span>
|
|
</div>
|
|
<div class="lora-count ${allLorasAvailable ? 'ready' : (lorasCount > 0 ? 'missing' : '')}"
|
|
title="${this.getLoraStatusTitle(lorasCount, missingLorasCount)}">
|
|
<i class="fas fa-layer-group"></i> ${lorasCount}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
this.attachEventListeners(card);
|
|
return card;
|
|
}
|
|
|
|
getLoraStatusTitle(totalCount, missingCount) {
|
|
if (totalCount === 0) return "No LoRAs in this recipe";
|
|
if (missingCount === 0) return "All LoRAs available - Ready to use";
|
|
return `${missingCount} of ${totalCount} LoRAs missing`;
|
|
}
|
|
|
|
attachEventListeners(card) {
|
|
// Recipe card click event
|
|
card.addEventListener('click', () => {
|
|
this.clickHandler(this.recipe);
|
|
});
|
|
|
|
// Share button click event - prevent propagation to card
|
|
card.querySelector('.fa-share-alt')?.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
this.shareRecipe();
|
|
});
|
|
|
|
// Copy button click event - prevent propagation to card
|
|
card.querySelector('.fa-copy')?.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
this.copyRecipeSyntax();
|
|
});
|
|
|
|
// Delete button click event - prevent propagation to card
|
|
card.querySelector('.fa-trash')?.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
this.showDeleteConfirmation();
|
|
});
|
|
}
|
|
|
|
copyRecipeSyntax() {
|
|
try {
|
|
// Generate recipe syntax in the format <lora:file_name:strength> separated by spaces
|
|
const loras = this.recipe.loras || [];
|
|
if (loras.length === 0) {
|
|
showToast('No LoRAs in this recipe to copy', 'warning');
|
|
return;
|
|
}
|
|
|
|
const syntax = loras.map(lora => {
|
|
// Use file_name if available, otherwise use empty placeholder
|
|
const fileName = lora.file_name || '[missing-lora]';
|
|
const strength = lora.strength || 1.0;
|
|
return `<lora:${fileName}:${strength}>`;
|
|
}).join(' ');
|
|
|
|
// Copy to clipboard
|
|
navigator.clipboard.writeText(syntax)
|
|
.then(() => {
|
|
showToast('Recipe syntax copied to clipboard', 'success');
|
|
})
|
|
.catch(err => {
|
|
console.error('Failed to copy: ', err);
|
|
showToast('Failed to copy recipe syntax', 'error');
|
|
});
|
|
} catch (error) {
|
|
console.error('Error copying recipe syntax:', error);
|
|
showToast('Error copying recipe syntax', 'error');
|
|
}
|
|
}
|
|
|
|
showDeleteConfirmation() {
|
|
try {
|
|
// Get recipe ID
|
|
const recipeId = this.recipe.id;
|
|
if (!recipeId) {
|
|
showToast('Cannot delete recipe: Missing recipe ID', 'error');
|
|
return;
|
|
}
|
|
|
|
// Create delete modal content
|
|
const deleteModalContent = `
|
|
<div class="modal-content delete-modal-content">
|
|
<h2>Delete Recipe</h2>
|
|
<p class="delete-message">Are you sure you want to delete this recipe?</p>
|
|
<div class="delete-model-info">
|
|
<div class="delete-preview">
|
|
<img src="${this.recipe.file_url || '/loras_static/images/no-preview.png'}" alt="${this.recipe.title}">
|
|
</div>
|
|
<div class="delete-info">
|
|
<h3>${this.recipe.title}</h3>
|
|
<p>This action cannot be undone.</p>
|
|
</div>
|
|
</div>
|
|
<p class="delete-note">Note: Deleting this recipe will not affect the LoRA files used in it.</p>
|
|
<div class="modal-actions">
|
|
<button class="cancel-btn" onclick="closeDeleteModal()">Cancel</button>
|
|
<button class="delete-btn" onclick="confirmDelete()">Delete</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Show the modal with custom content and setup callbacks
|
|
modalManager.showModal('deleteModal', deleteModalContent, () => {
|
|
// This is the onClose callback
|
|
const deleteModal = document.getElementById('deleteModal');
|
|
const deleteBtn = deleteModal.querySelector('.delete-btn');
|
|
deleteBtn.textContent = 'Delete';
|
|
deleteBtn.disabled = false;
|
|
});
|
|
|
|
// Set up the delete and cancel buttons with proper event handlers
|
|
const deleteModal = document.getElementById('deleteModal');
|
|
const cancelBtn = deleteModal.querySelector('.cancel-btn');
|
|
const deleteBtn = deleteModal.querySelector('.delete-btn');
|
|
|
|
// Store recipe ID in the modal for the delete confirmation handler
|
|
deleteModal.dataset.recipeId = recipeId;
|
|
|
|
// Update button event handlers
|
|
cancelBtn.onclick = () => modalManager.closeModal('deleteModal');
|
|
deleteBtn.onclick = () => this.confirmDeleteRecipe();
|
|
|
|
} catch (error) {
|
|
console.error('Error showing delete confirmation:', error);
|
|
showToast('Error showing delete confirmation', 'error');
|
|
}
|
|
}
|
|
|
|
confirmDeleteRecipe() {
|
|
const deleteModal = document.getElementById('deleteModal');
|
|
const recipeId = deleteModal.dataset.recipeId;
|
|
|
|
if (!recipeId) {
|
|
showToast('Cannot delete recipe: Missing recipe ID', 'error');
|
|
modalManager.closeModal('deleteModal');
|
|
return;
|
|
}
|
|
|
|
// Show loading state
|
|
const deleteBtn = deleteModal.querySelector('.delete-btn');
|
|
const originalText = deleteBtn.textContent;
|
|
deleteBtn.textContent = 'Deleting...';
|
|
deleteBtn.disabled = true;
|
|
|
|
// Call API to delete the recipe
|
|
fetch(`/api/recipe/${recipeId}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to delete recipe');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
showToast('Recipe deleted successfully', 'success');
|
|
|
|
window.recipeManager.loadRecipes();
|
|
|
|
modalManager.closeModal('deleteModal');
|
|
})
|
|
.catch(error => {
|
|
console.error('Error deleting recipe:', error);
|
|
showToast('Error deleting recipe: ' + error.message, 'error');
|
|
|
|
// Reset button state
|
|
deleteBtn.textContent = originalText;
|
|
deleteBtn.disabled = false;
|
|
});
|
|
}
|
|
|
|
shareRecipe() {
|
|
try {
|
|
// Get recipe ID
|
|
const recipeId = this.recipe.id;
|
|
if (!recipeId) {
|
|
showToast('Cannot share recipe: Missing recipe ID', 'error');
|
|
return;
|
|
}
|
|
|
|
// Show loading toast
|
|
showToast('Preparing recipe for sharing...', 'info');
|
|
|
|
// Call the API to process the image with metadata
|
|
fetch(`/api/recipe/${recipeId}/share`)
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to prepare recipe for sharing');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
if (!data.success) {
|
|
throw new Error(data.error || 'Unknown error');
|
|
}
|
|
|
|
// Create a temporary anchor element for download
|
|
const downloadLink = document.createElement('a');
|
|
downloadLink.href = data.download_url;
|
|
downloadLink.download = data.filename;
|
|
|
|
// Append to body, click and remove
|
|
document.body.appendChild(downloadLink);
|
|
downloadLink.click();
|
|
document.body.removeChild(downloadLink);
|
|
|
|
showToast('Recipe download started', 'success');
|
|
})
|
|
.catch(error => {
|
|
console.error('Error sharing recipe:', error);
|
|
showToast('Error sharing recipe: ' + error.message, 'error');
|
|
});
|
|
} catch (error) {
|
|
console.error('Error sharing recipe:', error);
|
|
showToast('Error preparing recipe for sharing', 'error');
|
|
}
|
|
}
|
|
}
|
|
|
|
export { RecipeCard };
|