mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 15:15:44 -03:00
Enhance LoraModal to include image metadata panel
- Added a new image metadata panel to display generation parameters and prompts for images and videos. - Implemented styles for the metadata panel in lora-modal.css, ensuring it is responsive and visually integrated. - Introduced functionality to copy prompts to the clipboard and handle metadata interactions within the modal. - Updated media rendering logic in LoraModal.js to incorporate metadata display and improve user experience.
This commit is contained in:
@@ -99,6 +99,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--lora-surface);
|
background: var(--lora-surface);
|
||||||
margin-bottom: var(--space-2);
|
margin-bottom: var(--space-2);
|
||||||
|
overflow: hidden; /* Ensure metadata panel is contained */
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-wrapper:last-child {
|
.media-wrapper:last-child {
|
||||||
@@ -1084,3 +1085,171 @@
|
|||||||
.carousel .media-wrapper {
|
.carousel .media-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Image Metadata Panel Styles */
|
||||||
|
.image-metadata-panel {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--bg-color);
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding: var(--space-2);
|
||||||
|
transform: translateY(100%);
|
||||||
|
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.25s ease;
|
||||||
|
z-index: 5;
|
||||||
|
max-height: 50%; /* Reduced to take less space */
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show metadata panel only on hover */
|
||||||
|
.media-wrapper:hover .image-metadata-panel {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 0.98;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adjust to dark theme */
|
||||||
|
[data-theme="dark"] .image-metadata-panel {
|
||||||
|
background: var(--card-bg);
|
||||||
|
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styling for parameters tags */
|
||||||
|
.params-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
padding-bottom: var(--space-1);
|
||||||
|
border-bottom: 1px solid var(--lora-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--lora-surface);
|
||||||
|
border: 1px solid var(--lora-border);
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-tag .param-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin-right: 4px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-tag .param-value {
|
||||||
|
color: var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Special styling for prompt row */
|
||||||
|
.metadata-row.prompt-row {
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-row.prompt-row + .metadata-row.prompt-row {
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.8;
|
||||||
|
font-size: 0.85em;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-prompt-wrapper {
|
||||||
|
position: relative;
|
||||||
|
background: var(--lora-surface);
|
||||||
|
border: 1px solid var(--lora-border);
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
padding: 6px 30px 6px 8px;
|
||||||
|
margin-top: 2px;
|
||||||
|
max-height: 80px; /* Reduced from 120px */
|
||||||
|
overflow-y: auto;
|
||||||
|
word-break: break-word;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-prompt {
|
||||||
|
color: var(--text-color);
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.85em;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-prompt-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
right: 6px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 3px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-prompt-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling for metadata panel */
|
||||||
|
.image-metadata-panel::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-metadata-panel::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-metadata-panel::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--border-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For Firefox */
|
||||||
|
.image-metadata-panel {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--border-color) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No metadata message styling */
|
||||||
|
.no-metadata-message {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--space-2);
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.7;
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-metadata-message i {
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: var(--lora-accent);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
@@ -208,61 +208,165 @@ function renderShowcaseContent(images) {
|
|||||||
nsfwText = "R-rated Content";
|
nsfwText = "R-rated Content";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (img.type === 'video') {
|
// Extract metadata from the image
|
||||||
return `
|
const meta = img.meta || {};
|
||||||
<div class="media-wrapper ${shouldBlur ? 'nsfw-media-wrapper' : ''}" style="padding-bottom: ${heightPercent}%">
|
const prompt = meta.prompt || '';
|
||||||
${shouldBlur ? `
|
const negativePrompt = meta.negative_prompt || meta.negativePrompt || '';
|
||||||
<button class="toggle-blur-btn showcase-toggle-btn" title="Toggle blur">
|
const size = meta.Size || `${img.width}x${img.height}`;
|
||||||
<i class="fas fa-eye"></i>
|
const seed = meta.seed || '';
|
||||||
</button>
|
const model = meta.Model || '';
|
||||||
` : ''}
|
const steps = meta.steps || '';
|
||||||
<video controls autoplay muted loop crossorigin="anonymous"
|
const sampler = meta.sampler || '';
|
||||||
referrerpolicy="no-referrer" data-src="${img.url}"
|
const cfgScale = meta.cfgScale || '';
|
||||||
class="lazy ${shouldBlur ? 'blurred' : ''}">
|
const clipSkip = meta.clipSkip || '';
|
||||||
<source data-src="${img.url}" type="video/mp4">
|
|
||||||
Your browser does not support video playback
|
// Check if we have any meaningful generation parameters
|
||||||
</video>
|
const hasParams = seed || model || steps || sampler || cfgScale || clipSkip;
|
||||||
${shouldBlur ? `
|
const hasPrompts = prompt || negativePrompt;
|
||||||
<div class="nsfw-overlay">
|
|
||||||
<div class="nsfw-warning">
|
// If no metadata available, show a message
|
||||||
<p>${nsfwText}</p>
|
if (!hasParams && !hasPrompts) {
|
||||||
<button class="show-content-btn">Show</button>
|
const metadataPanel = `
|
||||||
</div>
|
<div class="image-metadata-panel">
|
||||||
</div>
|
<div class="metadata-content">
|
||||||
` : ''}
|
<div class="no-metadata-message">
|
||||||
</div>
|
<i class="fas fa-info-circle"></i>
|
||||||
`;
|
<span>No generation parameters available</span>
|
||||||
}
|
|
||||||
return `
|
|
||||||
<div class="media-wrapper ${shouldBlur ? 'nsfw-media-wrapper' : ''}" style="padding-bottom: ${heightPercent}%">
|
|
||||||
${shouldBlur ? `
|
|
||||||
<button class="toggle-blur-btn showcase-toggle-btn" title="Toggle blur">
|
|
||||||
<i class="fas fa-eye"></i>
|
|
||||||
</button>
|
|
||||||
` : ''}
|
|
||||||
<img data-src="${img.url}"
|
|
||||||
alt="Preview"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
referrerpolicy="no-referrer"
|
|
||||||
width="${img.width}"
|
|
||||||
height="${img.height}"
|
|
||||||
class="lazy ${shouldBlur ? 'blurred' : ''}">
|
|
||||||
${shouldBlur ? `
|
|
||||||
<div class="nsfw-overlay">
|
|
||||||
<div class="nsfw-warning">
|
|
||||||
<p>${nsfwText}</p>
|
|
||||||
<button class="show-content-btn">Show</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (img.type === 'video') {
|
||||||
|
return generateVideoWrapper(img, heightPercent, shouldBlur, nsfwText, metadataPanel);
|
||||||
|
}
|
||||||
|
return generateImageWrapper(img, heightPercent, shouldBlur, nsfwText, metadataPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a data attribute with the prompt for copying instead of trying to handle it in the onclick
|
||||||
|
// This avoids issues with quotes and special characters
|
||||||
|
const promptIndex = Math.random().toString(36).substring(2, 15);
|
||||||
|
const negPromptIndex = Math.random().toString(36).substring(2, 15);
|
||||||
|
|
||||||
|
// Create parameter tags HTML
|
||||||
|
const paramTags = `
|
||||||
|
<div class="params-tags">
|
||||||
|
${size ? `<div class="param-tag"><span class="param-name">Size:</span><span class="param-value">${size}</span></div>` : ''}
|
||||||
|
${seed ? `<div class="param-tag"><span class="param-name">Seed:</span><span class="param-value">${seed}</span></div>` : ''}
|
||||||
|
${model ? `<div class="param-tag"><span class="param-name">Model:</span><span class="param-value">${model}</span></div>` : ''}
|
||||||
|
${steps ? `<div class="param-tag"><span class="param-name">Steps:</span><span class="param-value">${steps}</span></div>` : ''}
|
||||||
|
${sampler ? `<div class="param-tag"><span class="param-name">Sampler:</span><span class="param-value">${sampler}</span></div>` : ''}
|
||||||
|
${cfgScale ? `<div class="param-tag"><span class="param-name">CFG:</span><span class="param-value">${cfgScale}</span></div>` : ''}
|
||||||
|
${clipSkip ? `<div class="param-tag"><span class="param-name">Clip Skip:</span><span class="param-value">${clipSkip}</span></div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Metadata panel HTML
|
||||||
|
const metadataPanel = `
|
||||||
|
<div class="image-metadata-panel">
|
||||||
|
<div class="metadata-content">
|
||||||
|
${hasParams ? paramTags : ''}
|
||||||
|
${!hasParams && !hasPrompts ? `
|
||||||
|
<div class="no-metadata-message">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<span>No generation parameters available</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
${prompt ? `
|
||||||
|
<div class="metadata-row prompt-row">
|
||||||
|
<span class="metadata-label">Prompt:</span>
|
||||||
|
<div class="metadata-prompt-wrapper">
|
||||||
|
<div class="metadata-prompt">${prompt}</div>
|
||||||
|
<button class="copy-prompt-btn" data-prompt-index="${promptIndex}">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hidden-prompt" id="prompt-${promptIndex}" style="display:none;">${prompt}</div>
|
||||||
|
` : ''}
|
||||||
|
${negativePrompt ? `
|
||||||
|
<div class="metadata-row prompt-row">
|
||||||
|
<span class="metadata-label">Negative Prompt:</span>
|
||||||
|
<div class="metadata-prompt-wrapper">
|
||||||
|
<div class="metadata-prompt">${negativePrompt}</div>
|
||||||
|
<button class="copy-prompt-btn" data-prompt-index="${negPromptIndex}">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hidden-prompt" id="prompt-${negPromptIndex}" style="display:none;">${negativePrompt}</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (img.type === 'video') {
|
||||||
|
return generateVideoWrapper(img, heightPercent, shouldBlur, nsfwText, metadataPanel);
|
||||||
|
}
|
||||||
|
return generateImageWrapper(img, heightPercent, shouldBlur, nsfwText, metadataPanel);
|
||||||
}).join('')}
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to generate video wrapper HTML
|
||||||
|
function generateVideoWrapper(img, heightPercent, shouldBlur, nsfwText, metadataPanel) {
|
||||||
|
return `
|
||||||
|
<div class="media-wrapper ${shouldBlur ? 'nsfw-media-wrapper' : ''}" style="padding-bottom: ${heightPercent}%">
|
||||||
|
${shouldBlur ? `
|
||||||
|
<button class="toggle-blur-btn showcase-toggle-btn" title="Toggle blur">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
<video controls autoplay muted loop crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer" data-src="${img.url}"
|
||||||
|
class="lazy ${shouldBlur ? 'blurred' : ''}">
|
||||||
|
<source data-src="${img.url}" type="video/mp4">
|
||||||
|
Your browser does not support video playback
|
||||||
|
</video>
|
||||||
|
${shouldBlur ? `
|
||||||
|
<div class="nsfw-overlay">
|
||||||
|
<div class="nsfw-warning">
|
||||||
|
<p>${nsfwText}</p>
|
||||||
|
<button class="show-content-btn">Show</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
${metadataPanel}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to generate image wrapper HTML
|
||||||
|
function generateImageWrapper(img, heightPercent, shouldBlur, nsfwText, metadataPanel) {
|
||||||
|
return `
|
||||||
|
<div class="media-wrapper ${shouldBlur ? 'nsfw-media-wrapper' : ''}" style="padding-bottom: ${heightPercent}%">
|
||||||
|
${shouldBlur ? `
|
||||||
|
<button class="toggle-blur-btn showcase-toggle-btn" title="Toggle blur">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
<img data-src="${img.url}"
|
||||||
|
alt="Preview"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
width="${img.width}"
|
||||||
|
height="${img.height}"
|
||||||
|
class="lazy ${shouldBlur ? 'blurred' : ''}">
|
||||||
|
${shouldBlur ? `
|
||||||
|
<div class="nsfw-overlay">
|
||||||
|
<div class="nsfw-warning">
|
||||||
|
<p>${nsfwText}</p>
|
||||||
|
<button class="show-content-btn">Show</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
${metadataPanel}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// New function to handle tab switching
|
// New function to handle tab switching
|
||||||
function setupTabSwitching() {
|
function setupTabSwitching() {
|
||||||
const tabButtons = document.querySelectorAll('.showcase-tabs .tab-btn');
|
const tabButtons = document.querySelectorAll('.showcase-tabs .tab-btn');
|
||||||
@@ -626,13 +730,74 @@ export function toggleShowcase(element) {
|
|||||||
|
|
||||||
// Initialize NSFW content blur toggle handlers
|
// Initialize NSFW content blur toggle handlers
|
||||||
initNsfwBlurHandlers(carousel);
|
initNsfwBlurHandlers(carousel);
|
||||||
|
|
||||||
|
// Initialize metadata panel interaction handlers
|
||||||
|
initMetadataPanelHandlers(carousel);
|
||||||
} else {
|
} else {
|
||||||
const count = carousel.querySelectorAll('.media-wrapper').length;
|
const count = carousel.querySelectorAll('.media-wrapper').length;
|
||||||
indicator.textContent = `Scroll or click to show ${count} examples`;
|
indicator.textContent = `Scroll or click to show ${count} examples`;
|
||||||
icon.classList.replace('fa-chevron-up', 'fa-chevron-down');
|
icon.classList.replace('fa-chevron-up', 'fa-chevron-down');
|
||||||
|
|
||||||
|
// Make sure any open metadata panels get closed
|
||||||
|
const carouselContainer = carousel.querySelector('.carousel-container');
|
||||||
|
if (carouselContainer) {
|
||||||
|
carouselContainer.style.height = '0';
|
||||||
|
setTimeout(() => {
|
||||||
|
carouselContainer.style.height = '';
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to initialize metadata panel interactions
|
||||||
|
function initMetadataPanelHandlers(container) {
|
||||||
|
// Find all media wrappers
|
||||||
|
const mediaWrappers = container.querySelectorAll('.media-wrapper');
|
||||||
|
|
||||||
|
mediaWrappers.forEach(wrapper => {
|
||||||
|
// Get the metadata panel
|
||||||
|
const metadataPanel = wrapper.querySelector('.image-metadata-panel');
|
||||||
|
if (!metadataPanel) return;
|
||||||
|
|
||||||
|
// Prevent events from the metadata panel from bubbling
|
||||||
|
metadataPanel.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle copy prompt button clicks
|
||||||
|
const copyBtns = metadataPanel.querySelectorAll('.copy-prompt-btn');
|
||||||
|
copyBtns.forEach(copyBtn => {
|
||||||
|
const promptIndex = copyBtn.dataset.promptIndex;
|
||||||
|
const promptElement = wrapper.querySelector(`#prompt-${promptIndex}`);
|
||||||
|
|
||||||
|
copyBtn.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation(); // Prevent bubbling
|
||||||
|
|
||||||
|
if (!promptElement) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(promptElement.textContent);
|
||||||
|
showToast('Prompt copied to clipboard', 'success');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Copy failed:', err);
|
||||||
|
showToast('Copy failed', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent scrolling in the metadata panel from scrolling the whole modal
|
||||||
|
metadataPanel.addEventListener('wheel', (e) => {
|
||||||
|
const isAtTop = metadataPanel.scrollTop === 0;
|
||||||
|
const isAtBottom = metadataPanel.scrollHeight - metadataPanel.scrollTop === metadataPanel.clientHeight;
|
||||||
|
|
||||||
|
// Only prevent default if scrolling would cause the panel to scroll
|
||||||
|
if ((e.deltaY < 0 && !isAtTop) || (e.deltaY > 0 && !isAtBottom)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}, { passive: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// New function to initialize blur toggle handlers for showcase images/videos
|
// New function to initialize blur toggle handlers for showcase images/videos
|
||||||
function initNsfwBlurHandlers(container) {
|
function initNsfwBlurHandlers(container) {
|
||||||
// Handle toggle blur buttons
|
// Handle toggle blur buttons
|
||||||
|
|||||||
Reference in New Issue
Block a user