mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 15:15:44 -03:00
feat: enhance recipe metadata handling with NSFW level updates and context menu actions. FIxes #247
This commit is contained in:
@@ -1266,9 +1266,9 @@ class RecipeRoutes:
|
|||||||
data = await request.json()
|
data = await request.json()
|
||||||
|
|
||||||
# Validate required fields
|
# 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({
|
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)
|
}, status=400)
|
||||||
|
|
||||||
# Use the recipe scanner's update method
|
# Use the recipe scanner's update method
|
||||||
|
|||||||
@@ -254,15 +254,13 @@
|
|||||||
|
|
||||||
/* New styles for hover reveal mode */
|
/* New styles for hover reveal mode */
|
||||||
.hover-reveal .card-header,
|
.hover-reveal .card-header,
|
||||||
.hover-reveal .card-footer,
|
.hover-reveal .card-footer {
|
||||||
.hover-reveal .recipe-indicator {
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hover-reveal .lora-card:hover .card-header,
|
.hover-reveal .lora-card:hover .card-header,
|
||||||
.hover-reveal .lora-card:hover .card-footer,
|
.hover-reveal .lora-card:hover .card-footer {
|
||||||
.hover-reveal .lora-card:hover .recipe-indicator {
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,30 +443,6 @@
|
|||||||
user-select: none;
|
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 {
|
.lora-count {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -171,3 +171,44 @@ export function createRecipeCard(recipe) {
|
|||||||
});
|
});
|
||||||
return recipeCard.element;
|
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<Object>} 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,31 @@
|
|||||||
import { BaseContextMenu } from './BaseContextMenu.js';
|
import { BaseContextMenu } from './BaseContextMenu.js';
|
||||||
|
import { ModelContextMenuMixin } from './ModelContextMenuMixin.js';
|
||||||
import { showToast, copyToClipboard, sendLoraToWorkflow } from '../../utils/uiHelpers.js';
|
import { showToast, copyToClipboard, sendLoraToWorkflow } from '../../utils/uiHelpers.js';
|
||||||
import { setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js';
|
import { setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js';
|
||||||
|
import { updateRecipeMetadata } from '../../api/recipeApi.js';
|
||||||
import { state } from '../../state/index.js';
|
import { state } from '../../state/index.js';
|
||||||
|
|
||||||
export class RecipeContextMenu extends BaseContextMenu {
|
export class RecipeContextMenu extends BaseContextMenu {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('recipeContextMenu', '.lora-card');
|
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) {
|
showMenu(x, y, card) {
|
||||||
@@ -31,6 +51,12 @@ export class RecipeContextMenu extends BaseContextMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleMenuAction(action) {
|
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;
|
const recipeId = this.currentCard.dataset.id;
|
||||||
|
|
||||||
switch(action) {
|
switch(action) {
|
||||||
@@ -257,3 +283,6 @@ export class RecipeContextMenu extends BaseContextMenu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mix in shared methods from ModelContextMenuMixin
|
||||||
|
Object.assign(RecipeContextMenu.prototype, ModelContextMenuMixin);
|
||||||
@@ -3,6 +3,7 @@ import { showToast, copyToClipboard, sendLoraToWorkflow } from '../utils/uiHelpe
|
|||||||
import { modalManager } from '../managers/ModalManager.js';
|
import { modalManager } from '../managers/ModalManager.js';
|
||||||
import { getCurrentPageState } from '../state/index.js';
|
import { getCurrentPageState } from '../state/index.js';
|
||||||
import { state } from '../state/index.js';
|
import { state } from '../state/index.js';
|
||||||
|
import { NSFW_LEVELS } from '../utils/constants.js';
|
||||||
|
|
||||||
class RecipeCard {
|
class RecipeCard {
|
||||||
constructor(recipe, clickHandler) {
|
constructor(recipe, clickHandler) {
|
||||||
@@ -17,8 +18,9 @@ class RecipeCard {
|
|||||||
createCardElement() {
|
createCardElement() {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'lora-card';
|
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.title = this.recipe.title;
|
||||||
|
card.dataset.nsfwLevel = this.recipe.preview_nsfw_level || 0;
|
||||||
card.dataset.created = this.recipe.created_date;
|
card.dataset.created = this.recipe.created_date;
|
||||||
card.dataset.id = this.recipe.id || '';
|
card.dataset.id = this.recipe.id || '';
|
||||||
|
|
||||||
@@ -42,15 +44,34 @@ class RecipeCard {
|
|||||||
const pageState = getCurrentPageState();
|
const pageState = getCurrentPageState();
|
||||||
const isDuplicatesMode = pageState.duplicatesMode;
|
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 = `
|
card.innerHTML = `
|
||||||
${!isDuplicatesMode ? `<div class="recipe-indicator" title="Recipe">R</div>` : ''}
|
<div class="card-preview ${shouldBlur ? 'blurred' : ''}">
|
||||||
<div class="card-preview">
|
|
||||||
<img src="${imageUrl}" alt="${this.recipe.title}">
|
<img src="${imageUrl}" alt="${this.recipe.title}">
|
||||||
${!isDuplicatesMode ? `
|
${!isDuplicatesMode ? `
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="base-model-wrapper">
|
${shouldBlur ?
|
||||||
${baseModel ? `<span class="base-model-label" title="${baseModel}">${baseModel}</span>` : ''}
|
`<button class="toggle-blur-btn" title="Toggle blur">
|
||||||
</div>
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>` : ''}
|
||||||
|
${baseModel ? `<span class="base-model-label ${shouldBlur ? 'with-toggle' : ''}" title="${baseModel}">${baseModel}</span>` : ''}
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<i class="fas fa-share-alt" title="Share Recipe"></i>
|
<i class="fas fa-share-alt" title="Share Recipe"></i>
|
||||||
<i class="fas fa-paper-plane" title="Send Recipe to Workflow (Click: Append, Shift+Click: Replace)"></i>
|
<i class="fas fa-paper-plane" title="Send Recipe to Workflow (Click: Append, Shift+Click: Replace)"></i>
|
||||||
@@ -58,6 +79,14 @@ class RecipeCard {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
|
${shouldBlur ? `
|
||||||
|
<div class="nsfw-overlay">
|
||||||
|
<div class="nsfw-warning">
|
||||||
|
<p>${nsfwText}</p>
|
||||||
|
<button class="show-content-btn">Show</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<div class="model-info">
|
<div class="model-info">
|
||||||
<span class="model-name">${this.recipe.title}</span>
|
<span class="model-name">${this.recipe.title}</span>
|
||||||
@@ -72,7 +101,7 @@ class RecipeCard {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
this.attachEventListeners(card, isDuplicatesMode);
|
this.attachEventListeners(card, isDuplicatesMode, shouldBlur);
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +111,27 @@ class RecipeCard {
|
|||||||
return `${missingCount} of ${totalCount} LoRAs missing`;
|
return `${missingCount} of ${totalCount} LoRAs missing`;
|
||||||
}
|
}
|
||||||
|
|
||||||
attachEventListeners(card, isDuplicatesMode) {
|
attachEventListeners(card, isDuplicatesMode, shouldBlur) {
|
||||||
|
// Add blur toggle functionality if content should be blurred
|
||||||
|
if (shouldBlur) {
|
||||||
|
const toggleBtn = card.querySelector('.toggle-blur-btn');
|
||||||
|
const showBtn = card.querySelector('.show-content-btn');
|
||||||
|
|
||||||
|
if (toggleBtn) {
|
||||||
|
toggleBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.toggleBlurContent(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showBtn) {
|
||||||
|
showBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.showBlurredContent(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Recipe card click event - only attach if not in duplicates mode
|
// Recipe card click event - only attach if not in duplicates mode
|
||||||
if (!isDuplicatesMode) {
|
if (!isDuplicatesMode) {
|
||||||
card.addEventListener('click', () => {
|
card.addEventListener('click', () => {
|
||||||
@@ -109,7 +158,42 @@ class RecipeCard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace copyRecipeSyntax with sendRecipeToWorkflow
|
toggleBlurContent(card) {
|
||||||
|
const preview = card.querySelector('.card-preview');
|
||||||
|
const isBlurred = preview.classList.toggle('blurred');
|
||||||
|
const icon = card.querySelector('.toggle-blur-btn i');
|
||||||
|
|
||||||
|
// Update the icon based on blur state
|
||||||
|
if (isBlurred) {
|
||||||
|
icon.className = 'fas fa-eye';
|
||||||
|
} else {
|
||||||
|
icon.className = 'fas fa-eye-slash';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle the overlay visibility
|
||||||
|
const overlay = card.querySelector('.nsfw-overlay');
|
||||||
|
if (overlay) {
|
||||||
|
overlay.style.display = isBlurred ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showBlurredContent(card) {
|
||||||
|
const preview = card.querySelector('.card-preview');
|
||||||
|
preview.classList.remove('blurred');
|
||||||
|
|
||||||
|
// Update the toggle button icon
|
||||||
|
const toggleBtn = card.querySelector('.toggle-blur-btn');
|
||||||
|
if (toggleBtn) {
|
||||||
|
toggleBtn.querySelector('i').className = 'fas fa-eye-slash';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the overlay
|
||||||
|
const overlay = card.querySelector('.nsfw-overlay');
|
||||||
|
if (overlay) {
|
||||||
|
overlay.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sendRecipeToWorkflow(replaceMode = false) {
|
sendRecipeToWorkflow(replaceMode = false) {
|
||||||
try {
|
try {
|
||||||
// Get recipe ID
|
// Get recipe ID
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { showToast, copyToClipboard } from '../utils/uiHelpers.js';
|
|||||||
import { state } from '../state/index.js';
|
import { state } from '../state/index.js';
|
||||||
import { setSessionItem, removeSessionItem } from '../utils/storageHelpers.js';
|
import { setSessionItem, removeSessionItem } from '../utils/storageHelpers.js';
|
||||||
import { updateRecipeCard } from '../utils/cardUpdater.js';
|
import { updateRecipeCard } from '../utils/cardUpdater.js';
|
||||||
|
import { updateRecipeMetadata } from '../api/recipeApi.js';
|
||||||
|
|
||||||
class RecipeModal {
|
class RecipeModal {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -117,6 +118,7 @@ class RecipeModal {
|
|||||||
|
|
||||||
// Store the recipe ID for copy syntax API call
|
// Store the recipe ID for copy syntax API call
|
||||||
this.recipeId = recipe.id;
|
this.recipeId = recipe.id;
|
||||||
|
this.filePath = recipe.file_path;
|
||||||
|
|
||||||
// Set recipe tags if they exist
|
// Set recipe tags if they exist
|
||||||
const tagsCompactElement = document.getElementById('recipeTagsCompact');
|
const tagsCompactElement = document.getElementById('recipeTagsCompact');
|
||||||
@@ -522,7 +524,19 @@ class RecipeModal {
|
|||||||
titleContainer.querySelector('.content-text').textContent = newTitle;
|
titleContainer.querySelector('.content-text').textContent = newTitle;
|
||||||
|
|
||||||
// Update the recipe on the server
|
// Update the recipe on the server
|
||||||
this.updateRecipeMetadata({ title: newTitle });
|
updateRecipeMetadata(this.filePath, { title: newTitle })
|
||||||
|
.then(data => {
|
||||||
|
// Show success toast
|
||||||
|
showToast('Recipe name updated successfully', 'success');
|
||||||
|
|
||||||
|
// Update the current recipe object
|
||||||
|
this.currentRecipe.title = newTitle;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// Error is handled in the API function
|
||||||
|
// Reset the UI if needed
|
||||||
|
titleContainer.querySelector('.content-text').textContent = this.currentRecipe.title || '';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide editor
|
// Hide editor
|
||||||
@@ -580,64 +594,20 @@ class RecipeModal {
|
|||||||
|
|
||||||
if (tagsChanged) {
|
if (tagsChanged) {
|
||||||
// Update the recipe on the server
|
// Update the recipe on the server
|
||||||
this.updateRecipeMetadata({ tags: newTags });
|
updateRecipeMetadata(this.filePath, { tags: newTags })
|
||||||
|
.then(data => {
|
||||||
|
// Show success toast
|
||||||
|
showToast('Recipe tags updated successfully', 'success');
|
||||||
|
|
||||||
// Update tags in the UI
|
// Update the current recipe object
|
||||||
const tagsDisplay = tagsContainer.querySelector('.tags-display');
|
this.currentRecipe.tags = newTags;
|
||||||
tagsDisplay.innerHTML = '';
|
|
||||||
|
|
||||||
if (newTags.length > 0) {
|
// Update tags in the UI
|
||||||
// Limit displayed tags to 5, show a "+X more" button if needed
|
this.updateTagsDisplay(tagsContainer, newTags);
|
||||||
const maxVisibleTags = 5;
|
})
|
||||||
const visibleTags = newTags.slice(0, maxVisibleTags);
|
.catch(error => {
|
||||||
const remainingTags = newTags.length > maxVisibleTags ? newTags.slice(maxVisibleTags) : [];
|
// Error is handled in the API function
|
||||||
|
|
||||||
// Add visible tags
|
|
||||||
visibleTags.forEach(tag => {
|
|
||||||
const tagElement = document.createElement('div');
|
|
||||||
tagElement.className = 'recipe-tag-compact';
|
|
||||||
tagElement.textContent = tag;
|
|
||||||
tagsDisplay.appendChild(tagElement);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add "more" button if needed
|
|
||||||
if (remainingTags.length > 0) {
|
|
||||||
const moreButton = document.createElement('div');
|
|
||||||
moreButton.className = 'recipe-tag-more';
|
|
||||||
moreButton.textContent = `+${remainingTags.length} more`;
|
|
||||||
tagsDisplay.appendChild(moreButton);
|
|
||||||
|
|
||||||
// Update tooltip content
|
|
||||||
const tooltipContent = document.getElementById('recipeTagsTooltipContent');
|
|
||||||
if (tooltipContent) {
|
|
||||||
tooltipContent.innerHTML = '';
|
|
||||||
newTags.forEach(tag => {
|
|
||||||
const tooltipTag = document.createElement('div');
|
|
||||||
tooltipTag.className = 'tooltip-tag';
|
|
||||||
tooltipTag.textContent = tag;
|
|
||||||
tooltipContent.appendChild(tooltipTag);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-add tooltip functionality
|
|
||||||
moreButton.addEventListener('mouseenter', () => {
|
|
||||||
document.getElementById('recipeTagsTooltip').classList.add('visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
moreButton.addEventListener('mouseleave', () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!document.getElementById('recipeTagsTooltip').matches(':hover')) {
|
|
||||||
document.getElementById('recipeTagsTooltip').classList.remove('visible');
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tagsDisplay.innerHTML = '<div class="no-tags">No tags</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the current recipe object
|
|
||||||
this.currentRecipe.tags = newTags;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide editor
|
// Hide editor
|
||||||
@@ -646,6 +616,62 @@ class RecipeModal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper method to update tags display
|
||||||
|
updateTagsDisplay(tagsContainer, tags) {
|
||||||
|
const tagsDisplay = tagsContainer.querySelector('.tags-display');
|
||||||
|
tagsDisplay.innerHTML = '';
|
||||||
|
|
||||||
|
if (tags.length > 0) {
|
||||||
|
// Limit displayed tags to 5, show a "+X more" button if needed
|
||||||
|
const maxVisibleTags = 5;
|
||||||
|
const visibleTags = tags.slice(0, maxVisibleTags);
|
||||||
|
const remainingTags = tags.length > maxVisibleTags ? tags.slice(maxVisibleTags) : [];
|
||||||
|
|
||||||
|
// Add visible tags
|
||||||
|
visibleTags.forEach(tag => {
|
||||||
|
const tagElement = document.createElement('div');
|
||||||
|
tagElement.className = 'recipe-tag-compact';
|
||||||
|
tagElement.textContent = tag;
|
||||||
|
tagsDisplay.appendChild(tagElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add "more" button if needed
|
||||||
|
if (remainingTags.length > 0) {
|
||||||
|
const moreButton = document.createElement('div');
|
||||||
|
moreButton.className = 'recipe-tag-more';
|
||||||
|
moreButton.textContent = `+${remainingTags.length} more`;
|
||||||
|
tagsDisplay.appendChild(moreButton);
|
||||||
|
|
||||||
|
// Update tooltip content
|
||||||
|
const tooltipContent = document.getElementById('recipeTagsTooltipContent');
|
||||||
|
if (tooltipContent) {
|
||||||
|
tooltipContent.innerHTML = '';
|
||||||
|
tags.forEach(tag => {
|
||||||
|
const tooltipTag = document.createElement('div');
|
||||||
|
tooltipTag.className = 'tooltip-tag';
|
||||||
|
tooltipTag.textContent = tag;
|
||||||
|
tooltipContent.appendChild(tooltipTag);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-add tooltip functionality
|
||||||
|
moreButton.addEventListener('mouseenter', () => {
|
||||||
|
document.getElementById('recipeTagsTooltip').classList.add('visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
moreButton.addEventListener('mouseleave', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!document.getElementById('recipeTagsTooltip').matches(':hover')) {
|
||||||
|
document.getElementById('recipeTagsTooltip').classList.remove('visible');
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tagsDisplay.innerHTML = '<div class="no-tags">No tags</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cancelTagsEdit() {
|
cancelTagsEdit() {
|
||||||
const tagsContainer = document.getElementById('recipeTagsCompact');
|
const tagsContainer = document.getElementById('recipeTagsCompact');
|
||||||
if (tagsContainer) {
|
if (tagsContainer) {
|
||||||
@@ -660,41 +686,66 @@ class RecipeModal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update recipe metadata on the server
|
// Setup source URL handlers
|
||||||
async updateRecipeMetadata(updates) {
|
setupSourceUrlHandlers() {
|
||||||
try {
|
const sourceUrlContainer = document.querySelector('.source-url-container');
|
||||||
const response = await fetch(`/api/recipe/${this.recipeId}/update`, {
|
const sourceUrlEditor = document.querySelector('.source-url-editor');
|
||||||
method: 'PUT',
|
const sourceUrlText = sourceUrlContainer.querySelector('.source-url-text');
|
||||||
headers: {
|
const sourceUrlEditBtn = sourceUrlContainer.querySelector('.source-url-edit-btn');
|
||||||
'Content-Type': 'application/json',
|
const sourceUrlCancelBtn = sourceUrlEditor.querySelector('.source-url-cancel-btn');
|
||||||
},
|
const sourceUrlSaveBtn = sourceUrlEditor.querySelector('.source-url-save-btn');
|
||||||
body: JSON.stringify(updates)
|
const sourceUrlInput = sourceUrlEditor.querySelector('.source-url-input');
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
// Show editor on edit button click
|
||||||
|
sourceUrlEditBtn.addEventListener('click', () => {
|
||||||
|
sourceUrlContainer.classList.add('hide');
|
||||||
|
sourceUrlEditor.classList.add('active');
|
||||||
|
sourceUrlInput.focus();
|
||||||
|
});
|
||||||
|
|
||||||
if (data.success) {
|
// Cancel editing
|
||||||
// 显示保存成功的提示
|
sourceUrlCancelBtn.addEventListener('click', () => {
|
||||||
if (updates.title) {
|
sourceUrlEditor.classList.remove('active');
|
||||||
showToast('Recipe name updated successfully', 'success');
|
sourceUrlContainer.classList.remove('hide');
|
||||||
} else if (updates.tags) {
|
sourceUrlInput.value = this.currentRecipe.source_path || '';
|
||||||
showToast('Recipe tags updated successfully', 'success');
|
});
|
||||||
} else {
|
|
||||||
showToast('Recipe updated successfully', 'success');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新当前recipe对象的属性
|
// Save new source URL
|
||||||
Object.assign(this.currentRecipe, updates);
|
sourceUrlSaveBtn.addEventListener('click', () => {
|
||||||
|
const newSourceUrl = sourceUrlInput.value.trim();
|
||||||
|
if (newSourceUrl !== this.currentRecipe.source_path) {
|
||||||
|
// Update the recipe on the server
|
||||||
|
updateRecipeMetadata(this.filePath, { source_path: newSourceUrl })
|
||||||
|
.then(data => {
|
||||||
|
// Show success toast
|
||||||
|
showToast('Source URL updated successfully', 'success');
|
||||||
|
|
||||||
// Update the recipe card in the UI
|
// Update source URL in the UI
|
||||||
updateRecipeCard(this.recipeId, updates);
|
sourceUrlText.textContent = newSourceUrl || 'No source URL';
|
||||||
} else {
|
sourceUrlText.title = newSourceUrl && (newSourceUrl.startsWith('http://') ||
|
||||||
showToast(`Failed to update recipe: ${data.error}`, 'error');
|
newSourceUrl.startsWith('https://')) ?
|
||||||
|
'Click to open source URL' : 'No valid URL';
|
||||||
|
|
||||||
|
// Update the current recipe object
|
||||||
|
this.currentRecipe.source_path = newSourceUrl;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// Error is handled in the API function
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating recipe:', error);
|
// Hide editor
|
||||||
showToast(`Error updating recipe: ${error.message}`, 'error');
|
sourceUrlEditor.classList.remove('active');
|
||||||
}
|
sourceUrlContainer.classList.remove('hide');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open source URL in a new tab if it's valid
|
||||||
|
sourceUrlText.addEventListener('click', () => {
|
||||||
|
const url = sourceUrlText.textContent.trim();
|
||||||
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup copy buttons for prompts and recipe syntax
|
// Setup copy buttons for prompts and recipe syntax
|
||||||
@@ -1055,56 +1106,6 @@ class RecipeModal {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// New method to set up source URL handlers
|
|
||||||
setupSourceUrlHandlers() {
|
|
||||||
const sourceUrlContainer = document.querySelector('.source-url-container');
|
|
||||||
const sourceUrlEditor = document.querySelector('.source-url-editor');
|
|
||||||
const sourceUrlText = sourceUrlContainer.querySelector('.source-url-text');
|
|
||||||
const sourceUrlEditBtn = sourceUrlContainer.querySelector('.source-url-edit-btn');
|
|
||||||
const sourceUrlCancelBtn = sourceUrlEditor.querySelector('.source-url-cancel-btn');
|
|
||||||
const sourceUrlSaveBtn = sourceUrlEditor.querySelector('.source-url-save-btn');
|
|
||||||
const sourceUrlInput = sourceUrlEditor.querySelector('.source-url-input');
|
|
||||||
|
|
||||||
// Show editor on edit button click
|
|
||||||
sourceUrlEditBtn.addEventListener('click', () => {
|
|
||||||
sourceUrlContainer.classList.add('hide');
|
|
||||||
sourceUrlEditor.classList.add('active');
|
|
||||||
sourceUrlInput.focus();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cancel editing
|
|
||||||
sourceUrlCancelBtn.addEventListener('click', () => {
|
|
||||||
sourceUrlEditor.classList.remove('active');
|
|
||||||
sourceUrlContainer.classList.remove('hide');
|
|
||||||
sourceUrlInput.value = this.currentRecipe.source_path || '';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save new source URL
|
|
||||||
sourceUrlSaveBtn.addEventListener('click', () => {
|
|
||||||
const newSourceUrl = sourceUrlInput.value.trim();
|
|
||||||
if (newSourceUrl && newSourceUrl !== this.currentRecipe.source_path) {
|
|
||||||
// Update source URL in the UI
|
|
||||||
sourceUrlText.textContent = newSourceUrl;
|
|
||||||
sourceUrlText.title = newSourceUrl.startsWith('http://') || newSourceUrl.startsWith('https://') ? 'Click to open source URL' : 'No valid URL';
|
|
||||||
|
|
||||||
// Update the recipe on the server
|
|
||||||
this.updateRecipeMetadata({ source_path: newSourceUrl });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide editor
|
|
||||||
sourceUrlEditor.classList.remove('active');
|
|
||||||
sourceUrlContainer.classList.remove('hide');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Open source URL in a new tab if it's valid
|
|
||||||
sourceUrlText.addEventListener('click', () => {
|
|
||||||
const url = sourceUrlText.textContent.trim();
|
|
||||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
||||||
window.open(url, '_blank');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { RecipeModal };
|
export { RecipeModal };
|
||||||
@@ -102,13 +102,10 @@ function renderRecipes(tabElement, recipes, loraName, loraHash) {
|
|||||||
card.dataset.id = recipe.id || '';
|
card.dataset.id = recipe.id || '';
|
||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="recipe-indicator" title="Recipe">R</div>
|
|
||||||
<div class="card-preview">
|
<div class="card-preview">
|
||||||
<img src="${imageUrl}" alt="${recipe.title}" loading="lazy">
|
<img src="${imageUrl}" alt="${recipe.title}" loading="lazy">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="base-model-wrapper">
|
${baseModel ? `<span class="base-model-label" title="${baseModel}">${baseModel}</span>` : ''}
|
||||||
${baseModel ? `<span class="base-model-label" title="${baseModel}">${baseModel}</span>` : ''}
|
|
||||||
</div>
|
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<i class="fas fa-copy" title="Copy Recipe Syntax"></i>
|
<i class="fas fa-copy" title="Copy Recipe Syntax"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,6 +25,9 @@
|
|||||||
<div class="context-menu-item" data-action="sendreplace"><i class="fas fa-exchange-alt"></i> Send to Workflow (Replace)</div>
|
<div class="context-menu-item" data-action="sendreplace"><i class="fas fa-exchange-alt"></i> Send to Workflow (Replace)</div>
|
||||||
<div class="context-menu-item" data-action="viewloras"><i class="fas fa-layer-group"></i> View All LoRAs</div>
|
<div class="context-menu-item" data-action="viewloras"><i class="fas fa-layer-group"></i> View All LoRAs</div>
|
||||||
<div class="context-menu-item download-missing-item" data-action="download-missing"><i class="fas fa-download"></i> Download Missing LoRAs</div>
|
<div class="context-menu-item download-missing-item" data-action="download-missing"><i class="fas fa-download"></i> Download Missing LoRAs</div>
|
||||||
|
<div class="context-menu-item" data-action="set-nsfw">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i> Set Content Rating
|
||||||
|
</div>
|
||||||
<div class="context-menu-separator"></div>
|
<div class="context-menu-separator"></div>
|
||||||
<div class="context-menu-item delete-item" data-action="delete"><i class="fas fa-trash"></i> Delete Recipe</div>
|
<div class="context-menu-item delete-item" data-action="delete"><i class="fas fa-trash"></i> Delete Recipe</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user