From ed9bae6f6a436688b9263f1ab509c0bfbc78d663 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Thu, 26 Jun 2025 11:01:10 +0800 Subject: [PATCH] feat: enhance recipe metadata handling with NSFW level updates and context menu actions. FIxes #247 --- py/routes/recipe_routes.py | 4 +- static/css/components/card.css | 30 +- static/js/api/recipeApi.js | 41 +++ .../ContextMenu/RecipeContextMenu.js | 31 +- static/js/components/RecipeCard.js | 102 ++++++- static/js/components/RecipeModal.js | 285 +++++++++--------- static/js/components/loraModal/RecipeTab.js | 5 +- templates/recipes.html | 3 + 8 files changed, 315 insertions(+), 186 deletions(-) diff --git a/py/routes/recipe_routes.py b/py/routes/recipe_routes.py index f05a1292..ff9fabf2 100644 --- a/py/routes/recipe_routes.py +++ b/py/routes/recipe_routes.py @@ -1266,9 +1266,9 @@ class RecipeRoutes: data = await request.json() # Validate required fields - if 'title' not in data and 'tags' not in data and 'source_path' not in data: + if 'title' not in data and 'tags' not in data and 'source_path' not in data and 'preview_nsfw_level' not in data: return web.json_response({ - "error": "At least one field to update must be provided (title or tags or source_path)" + "error": "At least one field to update must be provided (title or tags or source_path or preview_nsfw_level)" }, status=400) # Use the recipe scanner's update method diff --git a/static/css/components/card.css b/static/css/components/card.css index 6a88d55e..acaeb5b6 100644 --- a/static/css/components/card.css +++ b/static/css/components/card.css @@ -254,15 +254,13 @@ /* New styles for hover reveal mode */ .hover-reveal .card-header, -.hover-reveal .card-footer, -.hover-reveal .recipe-indicator { +.hover-reveal .card-footer { opacity: 0; transition: opacity 0.2s ease; } .hover-reveal .lora-card:hover .card-header, -.hover-reveal .lora-card:hover .card-footer, -.hover-reveal .lora-card:hover .recipe-indicator { +.hover-reveal .lora-card:hover .card-footer { opacity: 1; } @@ -445,30 +443,6 @@ user-select: none; } -/* Recipe specific elements - migrated from recipe-card.css */ -.recipe-indicator { - position: absolute; - top: 6px; - left: 8px; - width: 24px; - height: 24px; - background: var(--lora-primary); - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - color: white; - font-weight: bold; - z-index: 2; -} - -.base-model-wrapper { - display: flex; - align-items: center; - gap: 8px; - margin-left: 32px; /* For accommodating the recipe indicator */ -} - .lora-count { display: flex; align-items: center; diff --git a/static/js/api/recipeApi.js b/static/js/api/recipeApi.js index bfcba3f7..0072879d 100644 --- a/static/js/api/recipeApi.js +++ b/static/js/api/recipeApi.js @@ -171,3 +171,44 @@ export function createRecipeCard(recipe) { }); return recipeCard.element; } + +/** + * Update recipe metadata on the server + * @param {string} filePath - The file path of the recipe (e.g. D:/Workspace/ComfyUI/models/loras/recipes/86b4c335-ecfc-4791-89d2-3746e55a7614.webp) + * @param {Object} updates - The metadata updates to apply + * @returns {Promise} The updated recipe data + */ +export async function updateRecipeMetadata(filePath, updates) { + try { + state.loadingManager.showSimpleLoading('Saving metadata...'); + + // Extract recipeId from filePath (basename without extension) + const basename = filePath.split('/').pop().split('\\').pop(); + const recipeId = basename.substring(0, basename.lastIndexOf('.')); + + const response = await fetch(`/api/recipe/${recipeId}/update`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(updates) + }); + + const data = await response.json(); + + if (!data.success) { + showToast(`Failed to update recipe: ${data.error}`, 'error'); + throw new Error(data.error || 'Failed to update recipe'); + } + + state.virtualScroller.updateSingleItem(filePath, updates); + + return data; + } catch (error) { + console.error('Error updating recipe:', error); + showToast(`Error updating recipe: ${error.message}`, 'error'); + throw error; + } finally { + state.loadingManager.hide(); + } +} diff --git a/static/js/components/ContextMenu/RecipeContextMenu.js b/static/js/components/ContextMenu/RecipeContextMenu.js index 1a3ca0c2..63d5795a 100644 --- a/static/js/components/ContextMenu/RecipeContextMenu.js +++ b/static/js/components/ContextMenu/RecipeContextMenu.js @@ -1,11 +1,31 @@ import { BaseContextMenu } from './BaseContextMenu.js'; +import { ModelContextMenuMixin } from './ModelContextMenuMixin.js'; import { showToast, copyToClipboard, sendLoraToWorkflow } from '../../utils/uiHelpers.js'; import { setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js'; +import { updateRecipeMetadata } from '../../api/recipeApi.js'; import { state } from '../../state/index.js'; export class RecipeContextMenu extends BaseContextMenu { constructor() { super('recipeContextMenu', '.lora-card'); + this.nsfwSelector = document.getElementById('nsfwLevelSelector'); + this.modelType = 'recipe'; + + // Initialize NSFW Level Selector events + if (this.nsfwSelector) { + this.initNSFWSelector(); + } + } + + // Use the updateRecipeMetadata implementation from recipeApi + async saveModelMetadata(filePath, data) { + return updateRecipeMetadata(filePath, data); + } + + // Override resetAndReload for recipe context + async resetAndReload() { + const { resetAndReload } = await import('../../api/recipeApi.js'); + return resetAndReload(); } showMenu(x, y, card) { @@ -31,6 +51,12 @@ export class RecipeContextMenu extends BaseContextMenu { } handleMenuAction(action) { + // First try to handle with common actions from ModelContextMenuMixin + if (ModelContextMenuMixin.handleCommonMenuActions.call(this, action)) { + return; + } + + // Handle recipe-specific actions const recipeId = this.currentCard.dataset.id; switch(action) { @@ -256,4 +282,7 @@ export class RecipeContextMenu extends BaseContextMenu { } } } -} \ No newline at end of file +} + +// Mix in shared methods from ModelContextMenuMixin +Object.assign(RecipeContextMenu.prototype, ModelContextMenuMixin); \ No newline at end of file diff --git a/static/js/components/RecipeCard.js b/static/js/components/RecipeCard.js index 5b838031..84edc925 100644 --- a/static/js/components/RecipeCard.js +++ b/static/js/components/RecipeCard.js @@ -3,6 +3,7 @@ import { showToast, copyToClipboard, sendLoraToWorkflow } from '../utils/uiHelpe import { modalManager } from '../managers/ModalManager.js'; import { getCurrentPageState } from '../state/index.js'; import { state } from '../state/index.js'; +import { NSFW_LEVELS } from '../utils/constants.js'; class RecipeCard { constructor(recipe, clickHandler) { @@ -17,8 +18,9 @@ class RecipeCard { createCardElement() { const card = document.createElement('div'); card.className = 'lora-card'; - card.dataset.filePath = this.recipe.file_path; + card.dataset.filepath = this.recipe.file_path; card.dataset.title = this.recipe.title; + card.dataset.nsfwLevel = this.recipe.preview_nsfw_level || 0; card.dataset.created = this.recipe.created_date; card.dataset.id = this.recipe.id || ''; @@ -42,15 +44,34 @@ class RecipeCard { const pageState = getCurrentPageState(); const isDuplicatesMode = pageState.duplicatesMode; + // NSFW blur logic - similar to LoraCard + const nsfwLevel = this.recipe.preview_nsfw_level !== undefined ? this.recipe.preview_nsfw_level : 0; + const shouldBlur = state.settings.blurMatureContent && nsfwLevel > NSFW_LEVELS.PG13; + + if (shouldBlur) { + card.classList.add('nsfw-content'); + } + + // Determine NSFW warning text based on level + let nsfwText = "Mature Content"; + if (nsfwLevel >= NSFW_LEVELS.XXX) { + nsfwText = "XXX-rated Content"; + } else if (nsfwLevel >= NSFW_LEVELS.X) { + nsfwText = "X-rated Content"; + } else if (nsfwLevel >= NSFW_LEVELS.R) { + nsfwText = "R-rated Content"; + } + card.innerHTML = ` - ${!isDuplicatesMode ? `
R
` : ''} -
+
${this.recipe.title} ${!isDuplicatesMode ? `
-
- ${baseModel ? `${baseModel}` : ''} -
+ ${shouldBlur ? + `` : ''} + ${baseModel ? `${baseModel}` : ''}
@@ -58,6 +79,14 @@ class RecipeCard {
` : ''} + ${shouldBlur ? ` +
+
+

${nsfwText}

+ +
+
+ ` : ''}