mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-24 14:31:26 -03:00
feat(ui): add delete button and Del shortcut to model and recipe modals
This commit is contained in:
+4
-2
@@ -855,7 +855,8 @@
|
|||||||
"title": "LoRA-Rezepte",
|
"title": "LoRA-Rezepte",
|
||||||
"actions": {
|
"actions": {
|
||||||
"sendCheckpoint": "Send to ComfyUI",
|
"sendCheckpoint": "Send to ComfyUI",
|
||||||
"sendRecipe": "Send to ComfyUI"
|
"sendRecipe": "Send to ComfyUI",
|
||||||
|
"deleteRecipeWithShortcut": "Rezept löschen (Del)"
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"label": "Rezeptnavigation",
|
"label": "Rezeptnavigation",
|
||||||
@@ -1440,7 +1441,8 @@
|
|||||||
"openFileLocation": "Dateispeicherort öffnen",
|
"openFileLocation": "Dateispeicherort öffnen",
|
||||||
"sendToWorkflow": "An ComfyUI senden",
|
"sendToWorkflow": "An ComfyUI senden",
|
||||||
"sendToWorkflowText": "An ComfyUI senden",
|
"sendToWorkflowText": "An ComfyUI senden",
|
||||||
"copyHash": "Hash kopieren"
|
"copyHash": "Hash kopieren",
|
||||||
|
"deleteModelWithShortcut": "Modell löschen (Del)"
|
||||||
},
|
},
|
||||||
"openFileLocation": {
|
"openFileLocation": {
|
||||||
"success": "Dateispeicherort erfolgreich geöffnet",
|
"success": "Dateispeicherort erfolgreich geöffnet",
|
||||||
|
|||||||
+4
-2
@@ -855,7 +855,8 @@
|
|||||||
"title": "LoRA Recipes",
|
"title": "LoRA Recipes",
|
||||||
"actions": {
|
"actions": {
|
||||||
"sendCheckpoint": "Send to ComfyUI",
|
"sendCheckpoint": "Send to ComfyUI",
|
||||||
"sendRecipe": "Send to ComfyUI"
|
"sendRecipe": "Send to ComfyUI",
|
||||||
|
"deleteRecipeWithShortcut": "Delete recipe (Del)"
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"label": "Recipe navigation",
|
"label": "Recipe navigation",
|
||||||
@@ -1440,7 +1441,8 @@
|
|||||||
"openFileLocation": "Open File Location",
|
"openFileLocation": "Open File Location",
|
||||||
"sendToWorkflow": "Send to ComfyUI",
|
"sendToWorkflow": "Send to ComfyUI",
|
||||||
"sendToWorkflowText": "Send to ComfyUI",
|
"sendToWorkflowText": "Send to ComfyUI",
|
||||||
"copyHash": "Copy hash"
|
"copyHash": "Copy hash",
|
||||||
|
"deleteModelWithShortcut": "Delete model (Del)"
|
||||||
},
|
},
|
||||||
"openFileLocation": {
|
"openFileLocation": {
|
||||||
"success": "File location opened successfully",
|
"success": "File location opened successfully",
|
||||||
|
|||||||
+4
-2
@@ -855,7 +855,8 @@
|
|||||||
"title": "Recetas de LoRA",
|
"title": "Recetas de LoRA",
|
||||||
"actions": {
|
"actions": {
|
||||||
"sendCheckpoint": "Enviar a ComfyUI",
|
"sendCheckpoint": "Enviar a ComfyUI",
|
||||||
"sendRecipe": "Enviar a ComfyUI"
|
"sendRecipe": "Enviar a ComfyUI",
|
||||||
|
"deleteRecipeWithShortcut": "Eliminar receta (Del)"
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"label": "Navegación de recetas",
|
"label": "Navegación de recetas",
|
||||||
@@ -1440,7 +1441,8 @@
|
|||||||
"openFileLocation": "Abrir ubicación del archivo",
|
"openFileLocation": "Abrir ubicación del archivo",
|
||||||
"sendToWorkflow": "Enviar a ComfyUI",
|
"sendToWorkflow": "Enviar a ComfyUI",
|
||||||
"sendToWorkflowText": "Enviar a ComfyUI",
|
"sendToWorkflowText": "Enviar a ComfyUI",
|
||||||
"copyHash": "Copiar hash"
|
"copyHash": "Copiar hash",
|
||||||
|
"deleteModelWithShortcut": "Eliminar modelo (Del)"
|
||||||
},
|
},
|
||||||
"openFileLocation": {
|
"openFileLocation": {
|
||||||
"success": "Ubicación del archivo abierta exitosamente",
|
"success": "Ubicación del archivo abierta exitosamente",
|
||||||
|
|||||||
+4
-2
@@ -855,7 +855,8 @@
|
|||||||
"title": "LoRA Recipes",
|
"title": "LoRA Recipes",
|
||||||
"actions": {
|
"actions": {
|
||||||
"sendCheckpoint": "Envoyer vers ComfyUI",
|
"sendCheckpoint": "Envoyer vers ComfyUI",
|
||||||
"sendRecipe": "Envoyer vers ComfyUI"
|
"sendRecipe": "Envoyer vers ComfyUI",
|
||||||
|
"deleteRecipeWithShortcut": "Supprimer la recette (Del)"
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"label": "Navigation des recettes",
|
"label": "Navigation des recettes",
|
||||||
@@ -1440,7 +1441,8 @@
|
|||||||
"openFileLocation": "Ouvrir l'emplacement du fichier",
|
"openFileLocation": "Ouvrir l'emplacement du fichier",
|
||||||
"sendToWorkflow": "Envoyer vers ComfyUI",
|
"sendToWorkflow": "Envoyer vers ComfyUI",
|
||||||
"sendToWorkflowText": "Envoyer vers ComfyUI",
|
"sendToWorkflowText": "Envoyer vers ComfyUI",
|
||||||
"copyHash": "Copier le hash"
|
"copyHash": "Copier le hash",
|
||||||
|
"deleteModelWithShortcut": "Supprimer le modèle (Del)"
|
||||||
},
|
},
|
||||||
"openFileLocation": {
|
"openFileLocation": {
|
||||||
"success": "Emplacement du fichier ouvert avec succès",
|
"success": "Emplacement du fichier ouvert avec succès",
|
||||||
|
|||||||
+4
-2
@@ -855,7 +855,8 @@
|
|||||||
"title": "מתכוני LoRA",
|
"title": "מתכוני LoRA",
|
||||||
"actions": {
|
"actions": {
|
||||||
"sendCheckpoint": "שלח ל-ComfyUI",
|
"sendCheckpoint": "שלח ל-ComfyUI",
|
||||||
"sendRecipe": "שלח ל-ComfyUI"
|
"sendRecipe": "שלח ל-ComfyUI",
|
||||||
|
"deleteRecipeWithShortcut": "מחק מתכון (Del)"
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"label": "ניווט מתכונים",
|
"label": "ניווט מתכונים",
|
||||||
@@ -1440,7 +1441,8 @@
|
|||||||
"openFileLocation": "פתח מיקום קובץ",
|
"openFileLocation": "פתח מיקום קובץ",
|
||||||
"sendToWorkflow": "שלח ל-ComfyUI",
|
"sendToWorkflow": "שלח ל-ComfyUI",
|
||||||
"sendToWorkflowText": "שלח ל-ComfyUI",
|
"sendToWorkflowText": "שלח ל-ComfyUI",
|
||||||
"copyHash": "העתק האש"
|
"copyHash": "העתק האש",
|
||||||
|
"deleteModelWithShortcut": "מחק מודל (Del)"
|
||||||
},
|
},
|
||||||
"openFileLocation": {
|
"openFileLocation": {
|
||||||
"success": "מיקום הקובץ נפתח בהצלחה",
|
"success": "מיקום הקובץ נפתח בהצלחה",
|
||||||
|
|||||||
+4
-2
@@ -855,7 +855,8 @@
|
|||||||
"title": "LoRAレシピ",
|
"title": "LoRAレシピ",
|
||||||
"actions": {
|
"actions": {
|
||||||
"sendCheckpoint": "ComfyUIへ送信",
|
"sendCheckpoint": "ComfyUIへ送信",
|
||||||
"sendRecipe": "ComfyUIへ送信"
|
"sendRecipe": "ComfyUIへ送信",
|
||||||
|
"deleteRecipeWithShortcut": "レシピを削除(Del)"
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"label": "レシピナビゲーション",
|
"label": "レシピナビゲーション",
|
||||||
@@ -1440,7 +1441,8 @@
|
|||||||
"openFileLocation": "ファイルの場所を開く",
|
"openFileLocation": "ファイルの場所を開く",
|
||||||
"sendToWorkflow": "ComfyUI に送信",
|
"sendToWorkflow": "ComfyUI に送信",
|
||||||
"sendToWorkflowText": "ComfyUI に送信",
|
"sendToWorkflowText": "ComfyUI に送信",
|
||||||
"copyHash": "ハッシュをコピー"
|
"copyHash": "ハッシュをコピー",
|
||||||
|
"deleteModelWithShortcut": "モデルを削除(Del)"
|
||||||
},
|
},
|
||||||
"openFileLocation": {
|
"openFileLocation": {
|
||||||
"success": "ファイルの場所を正常に開きました",
|
"success": "ファイルの場所を正常に開きました",
|
||||||
|
|||||||
+4
-2
@@ -855,7 +855,8 @@
|
|||||||
"title": "LoRA 레시피",
|
"title": "LoRA 레시피",
|
||||||
"actions": {
|
"actions": {
|
||||||
"sendCheckpoint": "ComfyUI로 보내기",
|
"sendCheckpoint": "ComfyUI로 보내기",
|
||||||
"sendRecipe": "ComfyUI로 보내기"
|
"sendRecipe": "ComfyUI로 보내기",
|
||||||
|
"deleteRecipeWithShortcut": "레시피 삭제(Del)"
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"label": "레시피 탐색",
|
"label": "레시피 탐색",
|
||||||
@@ -1440,7 +1441,8 @@
|
|||||||
"openFileLocation": "파일 위치 열기",
|
"openFileLocation": "파일 위치 열기",
|
||||||
"sendToWorkflow": "ComfyUI로 보내기",
|
"sendToWorkflow": "ComfyUI로 보내기",
|
||||||
"sendToWorkflowText": "ComfyUI로 보내기",
|
"sendToWorkflowText": "ComfyUI로 보내기",
|
||||||
"copyHash": "해시 복사"
|
"copyHash": "해시 복사",
|
||||||
|
"deleteModelWithShortcut": "모델 삭제(Del)"
|
||||||
},
|
},
|
||||||
"openFileLocation": {
|
"openFileLocation": {
|
||||||
"success": "파일 위치가 성공적으로 열렸습니다",
|
"success": "파일 위치가 성공적으로 열렸습니다",
|
||||||
|
|||||||
+4
-2
@@ -855,7 +855,8 @@
|
|||||||
"title": "Рецепты LoRA",
|
"title": "Рецепты LoRA",
|
||||||
"actions": {
|
"actions": {
|
||||||
"sendCheckpoint": "Отправить в ComfyUI",
|
"sendCheckpoint": "Отправить в ComfyUI",
|
||||||
"sendRecipe": "Отправить в ComfyUI"
|
"sendRecipe": "Отправить в ComfyUI",
|
||||||
|
"deleteRecipeWithShortcut": "Удалить рецепт (Del)"
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"label": "Навигация по рецептам",
|
"label": "Навигация по рецептам",
|
||||||
@@ -1440,7 +1441,8 @@
|
|||||||
"openFileLocation": "Открыть расположение файла",
|
"openFileLocation": "Открыть расположение файла",
|
||||||
"sendToWorkflow": "Отправить в ComfyUI",
|
"sendToWorkflow": "Отправить в ComfyUI",
|
||||||
"sendToWorkflowText": "Отправить в ComfyUI",
|
"sendToWorkflowText": "Отправить в ComfyUI",
|
||||||
"copyHash": "Копировать хэш"
|
"copyHash": "Копировать хэш",
|
||||||
|
"deleteModelWithShortcut": "Удалить модель (Del)"
|
||||||
},
|
},
|
||||||
"openFileLocation": {
|
"openFileLocation": {
|
||||||
"success": "Расположение файла успешно открыто",
|
"success": "Расположение файла успешно открыто",
|
||||||
|
|||||||
+4
-2
@@ -855,7 +855,8 @@
|
|||||||
"title": "LoRA 配方",
|
"title": "LoRA 配方",
|
||||||
"actions": {
|
"actions": {
|
||||||
"sendCheckpoint": "发送到 ComfyUI",
|
"sendCheckpoint": "发送到 ComfyUI",
|
||||||
"sendRecipe": "发送到 ComfyUI"
|
"sendRecipe": "发送到 ComfyUI",
|
||||||
|
"deleteRecipeWithShortcut": "删除配方(Del)"
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"label": "配方导航",
|
"label": "配方导航",
|
||||||
@@ -1440,7 +1441,8 @@
|
|||||||
"openFileLocation": "打开文件位置",
|
"openFileLocation": "打开文件位置",
|
||||||
"sendToWorkflow": "发送到 ComfyUI",
|
"sendToWorkflow": "发送到 ComfyUI",
|
||||||
"sendToWorkflowText": "发送到 ComfyUI",
|
"sendToWorkflowText": "发送到 ComfyUI",
|
||||||
"copyHash": "复制哈希值"
|
"copyHash": "复制哈希值",
|
||||||
|
"deleteModelWithShortcut": "删除模型(Del)"
|
||||||
},
|
},
|
||||||
"openFileLocation": {
|
"openFileLocation": {
|
||||||
"success": "文件位置已成功打开",
|
"success": "文件位置已成功打开",
|
||||||
|
|||||||
+4
-2
@@ -855,7 +855,8 @@
|
|||||||
"title": "LoRA 配方",
|
"title": "LoRA 配方",
|
||||||
"actions": {
|
"actions": {
|
||||||
"sendCheckpoint": "傳送到 ComfyUI",
|
"sendCheckpoint": "傳送到 ComfyUI",
|
||||||
"sendRecipe": "傳送到 ComfyUI"
|
"sendRecipe": "傳送到 ComfyUI",
|
||||||
|
"deleteRecipeWithShortcut": "刪除配方(Del)"
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"label": "配方導覽",
|
"label": "配方導覽",
|
||||||
@@ -1440,7 +1441,8 @@
|
|||||||
"openFileLocation": "開啟檔案位置",
|
"openFileLocation": "開啟檔案位置",
|
||||||
"sendToWorkflow": "傳送到 ComfyUI",
|
"sendToWorkflow": "傳送到 ComfyUI",
|
||||||
"sendToWorkflowText": "傳送到 ComfyUI",
|
"sendToWorkflowText": "傳送到 ComfyUI",
|
||||||
"copyHash": "複製雜湊值"
|
"copyHash": "複製雜湊值",
|
||||||
|
"deleteModelWithShortcut": "刪除模型(Del)"
|
||||||
},
|
},
|
||||||
"openFileLocation": {
|
"openFileLocation": {
|
||||||
"success": "檔案位置已成功開啟",
|
"success": "檔案位置已成功開啟",
|
||||||
|
|||||||
@@ -68,14 +68,40 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header-actions .license-restrictions {
|
/* Destructive modal action: ghost icon button pushed to the far right of the
|
||||||
|
header actions row, revealing the danger color only on hover/focus. Shared
|
||||||
|
by the model modal and the recipe modal. */
|
||||||
|
.modal-delete-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s ease, border-color 0.2s ease, background-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header-actions .license-permissions {
|
.modal-delete-btn:hover,
|
||||||
margin-left: auto;
|
.modal-delete-btn:focus-visible {
|
||||||
|
color: var(--lora-error);
|
||||||
|
border-color: var(--lora-error);
|
||||||
|
background: oklch(from var(--lora-error) l c h / 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-delete-btn i {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* License icons no longer carry their own auto margin: the delete button's
|
||||||
|
margin-left: auto right-anchors the [delete][license] cluster, keeping the
|
||||||
|
icons flush against the right edge as before the delete button existed. */
|
||||||
|
|
||||||
.license-restrictions {
|
.license-restrictions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
+132
-115
@@ -339,124 +339,11 @@ class RecipeCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showDeleteConfirmation() {
|
showDeleteConfirmation() {
|
||||||
try {
|
showRecipeDeleteConfirmation(this.recipe);
|
||||||
// Get recipe ID
|
|
||||||
const recipeId = this.recipe.id;
|
|
||||||
const filePath = this.recipe.file_path;
|
|
||||||
if (!recipeId) {
|
|
||||||
showToast('toast.recipes.cannotDelete', {}, 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create delete modal content
|
|
||||||
const previewUrl = this.recipe.file_url || '/loras_static/images/no-preview.png';
|
|
||||||
const isVideo = previewUrl.endsWith('.mp4') || previewUrl.endsWith('.webm');
|
|
||||||
|
|
||||||
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">
|
|
||||||
${isVideo ?
|
|
||||||
`<video src="${previewUrl}" controls muted loop playsinline style="max-width: 100%;"></video>` :
|
|
||||||
`<img src="${previewUrl}" alt="${this.recipe.title}" onerror="this.onerror=null; this.src='/loras_static/images/no-preview.png'">`
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="delete-info">
|
|
||||||
<h3>${this.recipe.title}</h3>
|
|
||||||
<p>${translate('modals.deleteRecipe.recoverableWarning')}</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;
|
|
||||||
deleteModal.dataset.filePath = filePath;
|
|
||||||
|
|
||||||
// Update button event handlers
|
|
||||||
cancelBtn.onclick = () => modalManager.closeModal('deleteModal');
|
|
||||||
deleteBtn.onclick = () => this.confirmDeleteRecipe();
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error showing delete confirmation:', error);
|
|
||||||
showToast('toast.recipes.deleteConfirmationError', {}, 'error');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmDeleteRecipe() {
|
confirmDeleteRecipe() {
|
||||||
const deleteModal = document.getElementById('deleteModal');
|
confirmRecipeDelete(this.recipe);
|
||||||
const recipeId = deleteModal.dataset.recipeId;
|
|
||||||
|
|
||||||
if (!recipeId) {
|
|
||||||
showToast('toast.recipes.cannotDelete', {}, '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/lm/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 => {
|
|
||||||
if (data.batch_id) {
|
|
||||||
// Staged delete: offer undo instead of the plain success toast
|
|
||||||
const batchId = data.batch_id;
|
|
||||||
showActionToast('toast.undo.deleted', { name: this.recipe.title }, 'success', {
|
|
||||||
actionText: translate('toast.undo.action'),
|
|
||||||
onAction: () => handleUndoDelete(batchId, () => window.recipeManager.loadRecipes(true)),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
showToast('toast.recipes.deletedSuccessfully', {}, 'success');
|
|
||||||
}
|
|
||||||
|
|
||||||
state.virtualScroller.removeItemByFilePath(deleteModal.dataset.filePath);
|
|
||||||
|
|
||||||
modalManager.closeModal('deleteModal');
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error deleting recipe:', error);
|
|
||||||
showToast('toast.recipes.deleteFailed', { message: error.message }, 'error');
|
|
||||||
|
|
||||||
// Reset button state
|
|
||||||
deleteBtn.textContent = originalText;
|
|
||||||
deleteBtn.disabled = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
shareRecipe() {
|
shareRecipe() {
|
||||||
@@ -507,4 +394,134 @@ class RecipeCard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the delete confirmation modal for a recipe. Shared by RecipeCard and
|
||||||
|
* RecipeModal so the flow stays identical regardless of where it starts.
|
||||||
|
* @param {Object} recipe - The recipe to delete
|
||||||
|
*/
|
||||||
|
export function showRecipeDeleteConfirmation(recipe) {
|
||||||
|
try {
|
||||||
|
// Get recipe ID
|
||||||
|
const recipeId = recipe.id;
|
||||||
|
const filePath = recipe.file_path;
|
||||||
|
if (!recipeId) {
|
||||||
|
showToast('toast.recipes.cannotDelete', {}, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create delete modal content
|
||||||
|
const previewUrl = recipe.file_url || '/loras_static/images/no-preview.png';
|
||||||
|
const isVideo = previewUrl.endsWith('.mp4') || previewUrl.endsWith('.webm');
|
||||||
|
|
||||||
|
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">
|
||||||
|
${isVideo ?
|
||||||
|
`<video src="${previewUrl}" controls muted loop playsinline style="max-width: 100%;"></video>` :
|
||||||
|
`<img src="${previewUrl}" alt="${recipe.title}" onerror="this.onerror=null; this.src='/loras_static/images/no-preview.png'">`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="delete-info">
|
||||||
|
<h3>${recipe.title}</h3>
|
||||||
|
<p>${translate('modals.deleteRecipe.recoverableWarning')}</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;
|
||||||
|
deleteModal.dataset.filePath = filePath;
|
||||||
|
|
||||||
|
// Update button event handlers
|
||||||
|
cancelBtn.onclick = () => modalManager.closeModal('deleteModal');
|
||||||
|
deleteBtn.onclick = () => confirmRecipeDelete(recipe);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error showing delete confirmation:', error);
|
||||||
|
showToast('toast.recipes.deleteConfirmationError', {}, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the recipe deletion after the user confirms in the delete modal.
|
||||||
|
* @param {Object} recipe - The recipe being deleted (used for toast messaging)
|
||||||
|
*/
|
||||||
|
function confirmRecipeDelete(recipe) {
|
||||||
|
const deleteModal = document.getElementById('deleteModal');
|
||||||
|
const recipeId = deleteModal.dataset.recipeId;
|
||||||
|
|
||||||
|
if (!recipeId) {
|
||||||
|
showToast('toast.recipes.cannotDelete', {}, '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/lm/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 => {
|
||||||
|
if (data.batch_id) {
|
||||||
|
// Staged delete: offer undo instead of the plain success toast
|
||||||
|
const batchId = data.batch_id;
|
||||||
|
showActionToast('toast.undo.deleted', { name: recipe.title }, 'success', {
|
||||||
|
actionText: translate('toast.undo.action'),
|
||||||
|
onAction: () => handleUndoDelete(batchId, () => window.recipeManager.loadRecipes(true)),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
showToast('toast.recipes.deletedSuccessfully', {}, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
state.virtualScroller.removeItemByFilePath(deleteModal.dataset.filePath);
|
||||||
|
|
||||||
|
modalManager.closeModal('deleteModal');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error deleting recipe:', error);
|
||||||
|
showToast('toast.recipes.deleteFailed', { message: error.message }, 'error');
|
||||||
|
|
||||||
|
// Reset button state
|
||||||
|
deleteBtn.textContent = originalText;
|
||||||
|
deleteBtn.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export { RecipeCard };
|
export { RecipeCard };
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { fetchRecipeDetails, updateRecipeMetadata, sendRecipeWorkflow } from '..
|
|||||||
import { downloadManager } from '../managers/DownloadManager.js';
|
import { downloadManager } from '../managers/DownloadManager.js';
|
||||||
import { MODEL_TYPES } from '../api/apiConfig.js';
|
import { MODEL_TYPES } from '../api/apiConfig.js';
|
||||||
import { openMediaViewer } from './shared/MediaViewer.js';
|
import { openMediaViewer } from './shared/MediaViewer.js';
|
||||||
|
import { showRecipeDeleteConfirmation } from './RecipeCard.js';
|
||||||
import { renderCompactTags, setupTagTooltip } from './shared/utils.js';
|
import { renderCompactTags, setupTagTooltip } from './shared/utils.js';
|
||||||
import { setupTagEditMode } from './shared/ModelTags.js';
|
import { setupTagEditMode } from './shared/ModelTags.js';
|
||||||
|
|
||||||
@@ -123,6 +124,7 @@ class RecipeModal {
|
|||||||
this.setupStripLoraToggle();
|
this.setupStripLoraToggle();
|
||||||
this.setupPromptEditors();
|
this.setupPromptEditors();
|
||||||
this.setupNavigationControls();
|
this.setupNavigationControls();
|
||||||
|
this.setupDeleteControl();
|
||||||
// Set up tooltip positioning handlers after DOM is ready
|
// Set up tooltip positioning handlers after DOM is ready
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
this.setupTooltipPositioning();
|
this.setupTooltipPositioning();
|
||||||
@@ -180,6 +182,18 @@ class RecipeModal {
|
|||||||
this.updateNavigationControls();
|
this.updateNavigationControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupDeleteControl() {
|
||||||
|
const deleteBtn = document.getElementById('deleteRecipeBtn');
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.addEventListener('click', () => this.handleDeleteRecipe());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDeleteRecipe() {
|
||||||
|
if (!this.currentRecipe) return;
|
||||||
|
showRecipeDeleteConfirmation(this.currentRecipe);
|
||||||
|
}
|
||||||
|
|
||||||
shouldIgnoreNavigationKey(event) {
|
shouldIgnoreNavigationKey(event) {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
if (!target) return false;
|
if (!target) return false;
|
||||||
@@ -230,6 +244,9 @@ class RecipeModal {
|
|||||||
} else if (event.key === 'ArrowRight') {
|
} else if (event.key === 'ArrowRight') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.handleDirectionalNavigation('next');
|
this.handleDirectionalNavigation('next');
|
||||||
|
} else if (event.key === 'Delete') {
|
||||||
|
event.preventDefault();
|
||||||
|
this.handleDeleteRecipe();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -624,6 +641,10 @@ class RecipeModal {
|
|||||||
|
|
||||||
actionsContainer.querySelectorAll('.recipe-source-url-btn').forEach(btn => btn.remove());
|
actionsContainer.querySelectorAll('.recipe-source-url-btn').forEach(btn => btn.remove());
|
||||||
|
|
||||||
|
// Keep the delete button as the last (rightmost) header action;
|
||||||
|
// insertBefore with null falls back to appendChild if it is missing.
|
||||||
|
const deleteBtn = document.getElementById('deleteRecipeBtn');
|
||||||
|
|
||||||
if (this.currentRecipe?.has_workflow === true) {
|
if (this.currentRecipe?.has_workflow === true) {
|
||||||
const workflowBtn = document.createElement('button');
|
const workflowBtn = document.createElement('button');
|
||||||
workflowBtn.className = 'recipe-source-url-btn';
|
workflowBtn.className = 'recipe-source-url-btn';
|
||||||
@@ -633,7 +654,7 @@ class RecipeModal {
|
|||||||
workflowBtn.addEventListener('click', () => {
|
workflowBtn.addEventListener('click', () => {
|
||||||
this.sendWorkflowToComfyUI();
|
this.sendWorkflowToComfyUI();
|
||||||
});
|
});
|
||||||
actionsContainer.appendChild(workflowBtn);
|
actionsContainer.insertBefore(workflowBtn, deleteBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourcePath = this.currentRecipe?.source_path || '';
|
const sourcePath = this.currentRecipe?.source_path || '';
|
||||||
@@ -646,7 +667,7 @@ class RecipeModal {
|
|||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
window.open(sourcePath, '_blank');
|
window.open(sourcePath, '_blank');
|
||||||
});
|
});
|
||||||
actionsContainer.appendChild(btn);
|
actionsContainer.insertBefore(btn, deleteBtn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { parsePresets, renderPresetTags } from './PresetTags.js';
|
|||||||
import { initVersionsTab } from './ModelVersionsTab.js';
|
import { initVersionsTab } from './ModelVersionsTab.js';
|
||||||
import { loadRecipesForModel } from './RecipeTab.js';
|
import { loadRecipesForModel } from './RecipeTab.js';
|
||||||
import { translate } from '../../utils/i18nHelpers.js';
|
import { translate } from '../../utils/i18nHelpers.js';
|
||||||
|
import { showDeleteModal } from '../../utils/modalUtils.js';
|
||||||
import { state } from '../../state/index.js';
|
import { state } from '../../state/index.js';
|
||||||
|
|
||||||
function getModalFilePath(fallback = '') {
|
function getModalFilePath(fallback = '') {
|
||||||
@@ -441,9 +442,22 @@ export async function showModelModal(model, modelType) {
|
|||||||
if (creatorActionsMarkup) {
|
if (creatorActionsMarkup) {
|
||||||
headerActionItems.push(creatorActionsMarkup);
|
headerActionItems.push(creatorActionsMarkup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Destructive action docks immediately left of the license icons so the
|
||||||
|
// icons keep their original rightmost position; the delete button's
|
||||||
|
// auto margin right-anchors the whole cluster.
|
||||||
|
const deleteModelTitle = translate('modals.model.actions.deleteModelWithShortcut', {}, 'Delete model (Del)');
|
||||||
|
const deleteModelButton = `
|
||||||
|
<button class="modal-delete-btn" data-action="delete-model" title="${deleteModelTitle}" aria-label="${deleteModelTitle}">
|
||||||
|
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
`.trim();
|
||||||
|
headerActionItems.push(indentMarkup(deleteModelButton, 20));
|
||||||
|
|
||||||
if (licenseIcons) {
|
if (licenseIcons) {
|
||||||
headerActionItems.push(indentMarkup(licenseIcons.trim(), 20));
|
headerActionItems.push(indentMarkup(licenseIcons.trim(), 20));
|
||||||
}
|
}
|
||||||
|
|
||||||
const headerActionsMarkup = headerActionItems.length
|
const headerActionsMarkup = headerActionItems.length
|
||||||
? [
|
? [
|
||||||
' <div class="modal-header-actions">',
|
' <div class="modal-header-actions">',
|
||||||
@@ -944,6 +958,9 @@ function setupEventHandlers(filePath, modelType) {
|
|||||||
case 'send-to-workflow':
|
case 'send-to-workflow':
|
||||||
handleSendToWorkflow(target, modelType);
|
handleSendToWorkflow(target, modelType);
|
||||||
break;
|
break;
|
||||||
|
case 'delete-model':
|
||||||
|
handleDeleteModel();
|
||||||
|
break;
|
||||||
case 'copy-hash':
|
case 'copy-hash':
|
||||||
if (target.dataset.hash) {
|
if (target.dataset.hash) {
|
||||||
copyToClipboard(target.dataset.hash, 'Hash copied to clipboard');
|
copyToClipboard(target.dataset.hash, 'Hash copied to clipboard');
|
||||||
@@ -1218,12 +1235,26 @@ function setupNavigationShortcuts(modelType) {
|
|||||||
} else if (event.key === 'ArrowRight') {
|
} else if (event.key === 'ArrowRight') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
handleDirectionalNavigation('next', navigationModelType);
|
handleDirectionalNavigation('next', navigationModelType);
|
||||||
|
} else if (event.key === 'Delete') {
|
||||||
|
event.preventDefault();
|
||||||
|
handleDeleteModel();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('keydown', navigationKeyHandler);
|
document.addEventListener('keydown', navigationKeyHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the shared delete confirmation for the model currently shown in the
|
||||||
|
* modal. Showing the delete modal replaces this modal (ModalManager only
|
||||||
|
* keeps one modal open), which also unregisters these shortcuts.
|
||||||
|
*/
|
||||||
|
function handleDeleteModel() {
|
||||||
|
const filePath = getModalFilePath();
|
||||||
|
if (!filePath) return;
|
||||||
|
showDeleteModal(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
async function handleDirectionalNavigation(direction, modelType) {
|
async function handleDirectionalNavigation(direction, modelType) {
|
||||||
if (navigationInProgress) return;
|
if (navigationInProgress) return;
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,9 @@
|
|||||||
<button class="modal-send-btn" id="sendRecipeBtn" title="Send Recipe to ComfyUI">
|
<button class="modal-send-btn" id="sendRecipeBtn" title="Send Recipe to ComfyUI">
|
||||||
<i class="fas fa-paper-plane"></i> <span>{{ t('recipes.actions.sendRecipe') }}</span>
|
<i class="fas fa-paper-plane"></i> <span>{{ t('recipes.actions.sendRecipe') }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="modal-delete-btn" id="deleteRecipeBtn" title="{{ t('recipes.actions.deleteRecipeWithShortcut') }}" aria-label="{{ t('recipes.actions.deleteRecipeWithShortcut') }}">
|
||||||
|
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Recipe Tags Container (rendered by renderCompactTags) -->
|
<!-- Recipe Tags Container (rendered by renderCompactTags) -->
|
||||||
<div id="recipeTagsContainer"></div>
|
<div id="recipeTagsContainer"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user