// Recipe Card Component import { showToast, copyToClipboard, sendLoraToWorkflow } from '../utils/uiHelpers.js'; import { modalManager } from '../managers/ModalManager.js'; import { getCurrentPageState } from '../state/index.js'; class RecipeCard { constructor(recipe, clickHandler) { this.recipe = recipe; this.clickHandler = clickHandler; this.element = this.createCardElement(); // Store reference to this instance on the DOM element for updates this.element._recipeCardInstance = this; } 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 && !lora.isDeleted).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'); // Check if in duplicates mode const pageState = getCurrentPageState(); const isDuplicatesMode = pageState.duplicatesMode; card.innerHTML = ` ${!isDuplicatesMode ? `
R
` : ''}
${this.recipe.title} ${!isDuplicatesMode ? `
${baseModel ? `${baseModel}` : ''}
` : ''}
`; this.attachEventListeners(card, isDuplicatesMode); 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, isDuplicatesMode) { // Recipe card click event - only attach if not in duplicates mode if (!isDuplicatesMode) { 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(); }); // Send button click event - prevent propagation to card card.querySelector('.fa-paper-plane')?.addEventListener('click', (e) => { e.stopPropagation(); this.sendRecipeToWorkflow(e.shiftKey); }); // Delete button click event - prevent propagation to card card.querySelector('.fa-trash')?.addEventListener('click', (e) => { e.stopPropagation(); this.showDeleteConfirmation(); }); } } // Replace copyRecipeSyntax with sendRecipeToWorkflow sendRecipeToWorkflow(replaceMode = false) { try { // Get recipe ID const recipeId = this.recipe.id; if (!recipeId) { showToast('Cannot send recipe: Missing recipe ID', 'error'); return; } fetch(`/api/recipe/${recipeId}/syntax`) .then(response => response.json()) .then(data => { if (data.success && data.syntax) { return sendLoraToWorkflow(data.syntax, replaceMode, 'recipe'); } else { throw new Error(data.error || 'No syntax returned'); } }) .catch(err => { console.error('Failed to send recipe to workflow: ', err); showToast('Failed to send recipe to workflow', 'error'); }); } catch (error) { console.error('Error sending recipe to workflow:', error); showToast('Error sending recipe to workflow', '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 = ` `; // 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 };