mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-24 06:21: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",
|
||||
"actions": {
|
||||
"sendCheckpoint": "Send to ComfyUI",
|
||||
"sendRecipe": "Send to ComfyUI"
|
||||
"sendRecipe": "Send to ComfyUI",
|
||||
"deleteRecipeWithShortcut": "Rezept löschen (Del)"
|
||||
},
|
||||
"navigation": {
|
||||
"label": "Rezeptnavigation",
|
||||
@@ -1440,7 +1441,8 @@
|
||||
"openFileLocation": "Dateispeicherort öffnen",
|
||||
"sendToWorkflow": "An ComfyUI senden",
|
||||
"sendToWorkflowText": "An ComfyUI senden",
|
||||
"copyHash": "Hash kopieren"
|
||||
"copyHash": "Hash kopieren",
|
||||
"deleteModelWithShortcut": "Modell löschen (Del)"
|
||||
},
|
||||
"openFileLocation": {
|
||||
"success": "Dateispeicherort erfolgreich geöffnet",
|
||||
|
||||
+4
-2
@@ -855,7 +855,8 @@
|
||||
"title": "LoRA Recipes",
|
||||
"actions": {
|
||||
"sendCheckpoint": "Send to ComfyUI",
|
||||
"sendRecipe": "Send to ComfyUI"
|
||||
"sendRecipe": "Send to ComfyUI",
|
||||
"deleteRecipeWithShortcut": "Delete recipe (Del)"
|
||||
},
|
||||
"navigation": {
|
||||
"label": "Recipe navigation",
|
||||
@@ -1440,7 +1441,8 @@
|
||||
"openFileLocation": "Open File Location",
|
||||
"sendToWorkflow": "Send to ComfyUI",
|
||||
"sendToWorkflowText": "Send to ComfyUI",
|
||||
"copyHash": "Copy hash"
|
||||
"copyHash": "Copy hash",
|
||||
"deleteModelWithShortcut": "Delete model (Del)"
|
||||
},
|
||||
"openFileLocation": {
|
||||
"success": "File location opened successfully",
|
||||
|
||||
+4
-2
@@ -855,7 +855,8 @@
|
||||
"title": "Recetas de LoRA",
|
||||
"actions": {
|
||||
"sendCheckpoint": "Enviar a ComfyUI",
|
||||
"sendRecipe": "Enviar a ComfyUI"
|
||||
"sendRecipe": "Enviar a ComfyUI",
|
||||
"deleteRecipeWithShortcut": "Eliminar receta (Del)"
|
||||
},
|
||||
"navigation": {
|
||||
"label": "Navegación de recetas",
|
||||
@@ -1440,7 +1441,8 @@
|
||||
"openFileLocation": "Abrir ubicación del archivo",
|
||||
"sendToWorkflow": "Enviar a ComfyUI",
|
||||
"sendToWorkflowText": "Enviar a ComfyUI",
|
||||
"copyHash": "Copiar hash"
|
||||
"copyHash": "Copiar hash",
|
||||
"deleteModelWithShortcut": "Eliminar modelo (Del)"
|
||||
},
|
||||
"openFileLocation": {
|
||||
"success": "Ubicación del archivo abierta exitosamente",
|
||||
|
||||
+4
-2
@@ -855,7 +855,8 @@
|
||||
"title": "LoRA Recipes",
|
||||
"actions": {
|
||||
"sendCheckpoint": "Envoyer vers ComfyUI",
|
||||
"sendRecipe": "Envoyer vers ComfyUI"
|
||||
"sendRecipe": "Envoyer vers ComfyUI",
|
||||
"deleteRecipeWithShortcut": "Supprimer la recette (Del)"
|
||||
},
|
||||
"navigation": {
|
||||
"label": "Navigation des recettes",
|
||||
@@ -1440,7 +1441,8 @@
|
||||
"openFileLocation": "Ouvrir l'emplacement du fichier",
|
||||
"sendToWorkflow": "Envoyer vers ComfyUI",
|
||||
"sendToWorkflowText": "Envoyer vers ComfyUI",
|
||||
"copyHash": "Copier le hash"
|
||||
"copyHash": "Copier le hash",
|
||||
"deleteModelWithShortcut": "Supprimer le modèle (Del)"
|
||||
},
|
||||
"openFileLocation": {
|
||||
"success": "Emplacement du fichier ouvert avec succès",
|
||||
|
||||
+4
-2
@@ -855,7 +855,8 @@
|
||||
"title": "מתכוני LoRA",
|
||||
"actions": {
|
||||
"sendCheckpoint": "שלח ל-ComfyUI",
|
||||
"sendRecipe": "שלח ל-ComfyUI"
|
||||
"sendRecipe": "שלח ל-ComfyUI",
|
||||
"deleteRecipeWithShortcut": "מחק מתכון (Del)"
|
||||
},
|
||||
"navigation": {
|
||||
"label": "ניווט מתכונים",
|
||||
@@ -1440,7 +1441,8 @@
|
||||
"openFileLocation": "פתח מיקום קובץ",
|
||||
"sendToWorkflow": "שלח ל-ComfyUI",
|
||||
"sendToWorkflowText": "שלח ל-ComfyUI",
|
||||
"copyHash": "העתק האש"
|
||||
"copyHash": "העתק האש",
|
||||
"deleteModelWithShortcut": "מחק מודל (Del)"
|
||||
},
|
||||
"openFileLocation": {
|
||||
"success": "מיקום הקובץ נפתח בהצלחה",
|
||||
|
||||
+4
-2
@@ -855,7 +855,8 @@
|
||||
"title": "LoRAレシピ",
|
||||
"actions": {
|
||||
"sendCheckpoint": "ComfyUIへ送信",
|
||||
"sendRecipe": "ComfyUIへ送信"
|
||||
"sendRecipe": "ComfyUIへ送信",
|
||||
"deleteRecipeWithShortcut": "レシピを削除(Del)"
|
||||
},
|
||||
"navigation": {
|
||||
"label": "レシピナビゲーション",
|
||||
@@ -1440,7 +1441,8 @@
|
||||
"openFileLocation": "ファイルの場所を開く",
|
||||
"sendToWorkflow": "ComfyUI に送信",
|
||||
"sendToWorkflowText": "ComfyUI に送信",
|
||||
"copyHash": "ハッシュをコピー"
|
||||
"copyHash": "ハッシュをコピー",
|
||||
"deleteModelWithShortcut": "モデルを削除(Del)"
|
||||
},
|
||||
"openFileLocation": {
|
||||
"success": "ファイルの場所を正常に開きました",
|
||||
|
||||
+4
-2
@@ -855,7 +855,8 @@
|
||||
"title": "LoRA 레시피",
|
||||
"actions": {
|
||||
"sendCheckpoint": "ComfyUI로 보내기",
|
||||
"sendRecipe": "ComfyUI로 보내기"
|
||||
"sendRecipe": "ComfyUI로 보내기",
|
||||
"deleteRecipeWithShortcut": "레시피 삭제(Del)"
|
||||
},
|
||||
"navigation": {
|
||||
"label": "레시피 탐색",
|
||||
@@ -1440,7 +1441,8 @@
|
||||
"openFileLocation": "파일 위치 열기",
|
||||
"sendToWorkflow": "ComfyUI로 보내기",
|
||||
"sendToWorkflowText": "ComfyUI로 보내기",
|
||||
"copyHash": "해시 복사"
|
||||
"copyHash": "해시 복사",
|
||||
"deleteModelWithShortcut": "모델 삭제(Del)"
|
||||
},
|
||||
"openFileLocation": {
|
||||
"success": "파일 위치가 성공적으로 열렸습니다",
|
||||
|
||||
+4
-2
@@ -855,7 +855,8 @@
|
||||
"title": "Рецепты LoRA",
|
||||
"actions": {
|
||||
"sendCheckpoint": "Отправить в ComfyUI",
|
||||
"sendRecipe": "Отправить в ComfyUI"
|
||||
"sendRecipe": "Отправить в ComfyUI",
|
||||
"deleteRecipeWithShortcut": "Удалить рецепт (Del)"
|
||||
},
|
||||
"navigation": {
|
||||
"label": "Навигация по рецептам",
|
||||
@@ -1440,7 +1441,8 @@
|
||||
"openFileLocation": "Открыть расположение файла",
|
||||
"sendToWorkflow": "Отправить в ComfyUI",
|
||||
"sendToWorkflowText": "Отправить в ComfyUI",
|
||||
"copyHash": "Копировать хэш"
|
||||
"copyHash": "Копировать хэш",
|
||||
"deleteModelWithShortcut": "Удалить модель (Del)"
|
||||
},
|
||||
"openFileLocation": {
|
||||
"success": "Расположение файла успешно открыто",
|
||||
|
||||
+4
-2
@@ -855,7 +855,8 @@
|
||||
"title": "LoRA 配方",
|
||||
"actions": {
|
||||
"sendCheckpoint": "发送到 ComfyUI",
|
||||
"sendRecipe": "发送到 ComfyUI"
|
||||
"sendRecipe": "发送到 ComfyUI",
|
||||
"deleteRecipeWithShortcut": "删除配方(Del)"
|
||||
},
|
||||
"navigation": {
|
||||
"label": "配方导航",
|
||||
@@ -1440,7 +1441,8 @@
|
||||
"openFileLocation": "打开文件位置",
|
||||
"sendToWorkflow": "发送到 ComfyUI",
|
||||
"sendToWorkflowText": "发送到 ComfyUI",
|
||||
"copyHash": "复制哈希值"
|
||||
"copyHash": "复制哈希值",
|
||||
"deleteModelWithShortcut": "删除模型(Del)"
|
||||
},
|
||||
"openFileLocation": {
|
||||
"success": "文件位置已成功打开",
|
||||
|
||||
+4
-2
@@ -855,7 +855,8 @@
|
||||
"title": "LoRA 配方",
|
||||
"actions": {
|
||||
"sendCheckpoint": "傳送到 ComfyUI",
|
||||
"sendRecipe": "傳送到 ComfyUI"
|
||||
"sendRecipe": "傳送到 ComfyUI",
|
||||
"deleteRecipeWithShortcut": "刪除配方(Del)"
|
||||
},
|
||||
"navigation": {
|
||||
"label": "配方導覽",
|
||||
@@ -1440,7 +1441,8 @@
|
||||
"openFileLocation": "開啟檔案位置",
|
||||
"sendToWorkflow": "傳送到 ComfyUI",
|
||||
"sendToWorkflowText": "傳送到 ComfyUI",
|
||||
"copyHash": "複製雜湊值"
|
||||
"copyHash": "複製雜湊值",
|
||||
"deleteModelWithShortcut": "刪除模型(Del)"
|
||||
},
|
||||
"openFileLocation": {
|
||||
"success": "檔案位置已成功開啟",
|
||||
|
||||
@@ -68,14 +68,40 @@
|
||||
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;
|
||||
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 {
|
||||
margin-left: auto;
|
||||
.modal-delete-btn:hover,
|
||||
.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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
+132
-115
@@ -339,124 +339,11 @@ class RecipeCard {
|
||||
}
|
||||
|
||||
showDeleteConfirmation() {
|
||||
try {
|
||||
// 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');
|
||||
}
|
||||
showRecipeDeleteConfirmation(this.recipe);
|
||||
}
|
||||
|
||||
confirmDeleteRecipe() {
|
||||
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: 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;
|
||||
});
|
||||
confirmRecipeDelete(this.recipe);
|
||||
}
|
||||
|
||||
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 };
|
||||
|
||||
@@ -8,6 +8,7 @@ import { fetchRecipeDetails, updateRecipeMetadata, sendRecipeWorkflow } from '..
|
||||
import { downloadManager } from '../managers/DownloadManager.js';
|
||||
import { MODEL_TYPES } from '../api/apiConfig.js';
|
||||
import { openMediaViewer } from './shared/MediaViewer.js';
|
||||
import { showRecipeDeleteConfirmation } from './RecipeCard.js';
|
||||
import { renderCompactTags, setupTagTooltip } from './shared/utils.js';
|
||||
import { setupTagEditMode } from './shared/ModelTags.js';
|
||||
|
||||
@@ -123,6 +124,7 @@ class RecipeModal {
|
||||
this.setupStripLoraToggle();
|
||||
this.setupPromptEditors();
|
||||
this.setupNavigationControls();
|
||||
this.setupDeleteControl();
|
||||
// Set up tooltip positioning handlers after DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
this.setupTooltipPositioning();
|
||||
@@ -180,6 +182,18 @@ class RecipeModal {
|
||||
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) {
|
||||
const target = event.target;
|
||||
if (!target) return false;
|
||||
@@ -230,6 +244,9 @@ class RecipeModal {
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
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());
|
||||
|
||||
// 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) {
|
||||
const workflowBtn = document.createElement('button');
|
||||
workflowBtn.className = 'recipe-source-url-btn';
|
||||
@@ -633,7 +654,7 @@ class RecipeModal {
|
||||
workflowBtn.addEventListener('click', () => {
|
||||
this.sendWorkflowToComfyUI();
|
||||
});
|
||||
actionsContainer.appendChild(workflowBtn);
|
||||
actionsContainer.insertBefore(workflowBtn, deleteBtn);
|
||||
}
|
||||
|
||||
const sourcePath = this.currentRecipe?.source_path || '';
|
||||
@@ -646,7 +667,7 @@ class RecipeModal {
|
||||
btn.addEventListener('click', () => {
|
||||
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 { loadRecipesForModel } from './RecipeTab.js';
|
||||
import { translate } from '../../utils/i18nHelpers.js';
|
||||
import { showDeleteModal } from '../../utils/modalUtils.js';
|
||||
import { state } from '../../state/index.js';
|
||||
|
||||
function getModalFilePath(fallback = '') {
|
||||
@@ -441,9 +442,22 @@ export async function showModelModal(model, modelType) {
|
||||
if (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) {
|
||||
headerActionItems.push(indentMarkup(licenseIcons.trim(), 20));
|
||||
}
|
||||
|
||||
const headerActionsMarkup = headerActionItems.length
|
||||
? [
|
||||
' <div class="modal-header-actions">',
|
||||
@@ -944,6 +958,9 @@ function setupEventHandlers(filePath, modelType) {
|
||||
case 'send-to-workflow':
|
||||
handleSendToWorkflow(target, modelType);
|
||||
break;
|
||||
case 'delete-model':
|
||||
handleDeleteModel();
|
||||
break;
|
||||
case 'copy-hash':
|
||||
if (target.dataset.hash) {
|
||||
copyToClipboard(target.dataset.hash, 'Hash copied to clipboard');
|
||||
@@ -1218,12 +1235,26 @@ function setupNavigationShortcuts(modelType) {
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
handleDirectionalNavigation('next', navigationModelType);
|
||||
} else if (event.key === 'Delete') {
|
||||
event.preventDefault();
|
||||
handleDeleteModel();
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
if (navigationInProgress) return;
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
<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>
|
||||
</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>
|
||||
<!-- Recipe Tags Container (rendered by renderCompactTags) -->
|
||||
<div id="recipeTagsContainer"></div>
|
||||
|
||||
Reference in New Issue
Block a user