feat(ui): add delete button and Del shortcut to model and recipe modals

This commit is contained in:
Will Miao
2026-08-24 09:27:00 +08:00
parent 90be5799e4
commit 8ca66e72eb
15 changed files with 258 additions and 140 deletions
+4 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 };
+23 -2
View File
@@ -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);
} }
} }
+31
View File
@@ -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;
+3
View File
@@ -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>