mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-24 14:42:11 -03:00
Refactor LoRA template with modular components and script updates
This commit is contained in:
210
static/js/components/LoraCard.js
Normal file
210
static/js/components/LoraCard.js
Normal file
@@ -0,0 +1,210 @@
|
||||
import { showToast } from '../utils/uiHelpers.js';
|
||||
import { modalManager } from '../managers/ModalManager.js';
|
||||
|
||||
export function createLoraCard(lora) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'lora-card';
|
||||
card.dataset.sha256 = lora.sha256;
|
||||
card.dataset.filepath = lora.file_path;
|
||||
card.dataset.name = lora.model_name;
|
||||
card.dataset.file_name = lora.file_name;
|
||||
card.dataset.folder = lora.folder;
|
||||
card.dataset.modified = lora.modified;
|
||||
card.dataset.from_civitai = lora.from_civitai;
|
||||
card.dataset.meta = JSON.stringify(lora.civitai || {});
|
||||
|
||||
// Add timestamp to preview URL to prevent caching
|
||||
const previewUrl = lora.preview_url ? `${lora.preview_url}?t=${Date.now()}` : '/loras_static/images/no-preview.png';
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="card-preview">
|
||||
${previewUrl.endsWith('.mp4') ?
|
||||
`<video controls autoplay muted loop>
|
||||
<source src="${previewUrl}" type="video/mp4">
|
||||
</video>` :
|
||||
`<img src="${previewUrl}" alt="${lora.model_name}">`
|
||||
}
|
||||
<div class="card-header">
|
||||
<span class="base-model-label" title="${lora.base_model}">
|
||||
${lora.base_model}
|
||||
</span>
|
||||
<div class="card-actions">
|
||||
<i class="fas fa-globe"
|
||||
title="${lora.from_civitai ? 'View on Civitai' : 'Not available from Civitai'}"
|
||||
${lora.from_civitai ?
|
||||
`onclick="event.stopPropagation(); openCivitai('${lora.model_name}')"` :
|
||||
'style="opacity: 0.5; cursor: not-allowed"'}>
|
||||
</i>
|
||||
<i class="fas fa-copy"
|
||||
title="Copy Model Name"
|
||||
onclick="event.stopPropagation(); navigator.clipboard.writeText('${lora.file_name}')">
|
||||
</i>
|
||||
<i class="fas fa-trash"
|
||||
title="Delete Model"
|
||||
onclick="event.stopPropagation(); deleteModel('${lora.file_path}')">
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="model-info">
|
||||
<span class="model-name">${lora.model_name}</span>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<i class="fas fa-image"
|
||||
title="Replace Preview Image"
|
||||
onclick="event.stopPropagation(); replacePreview('${lora.file_path}')">
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
card.addEventListener('click', () => {
|
||||
const meta = JSON.parse(card.dataset.meta || '{}');
|
||||
if (Object.keys(meta).length) {
|
||||
showLoraModal(meta);
|
||||
} else {
|
||||
showToast(
|
||||
card.dataset.from_civitai === 'true' ?
|
||||
'Click "Fetch" to retrieve metadata' :
|
||||
'No CivitAI information available',
|
||||
'info'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
export function updatePreviewInCard(filePath, file, previewUrl) {
|
||||
const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
|
||||
const previewContainer = card?.querySelector('.card-preview');
|
||||
const oldPreview = previewContainer?.querySelector('img, video');
|
||||
|
||||
if (oldPreview) {
|
||||
const newPreviewUrl = `${previewUrl}?t=${Date.now()}`;
|
||||
const newPreview = file.type.startsWith('video/')
|
||||
? createVideoPreview(newPreviewUrl)
|
||||
: createImagePreview(newPreviewUrl);
|
||||
oldPreview.replaceWith(newPreview);
|
||||
}
|
||||
}
|
||||
|
||||
export function showLoraModal(lora) {
|
||||
const escapedWords = lora.trainedWords?.length ?
|
||||
lora.trainedWords.map(word => word.replace(/'/g, '\\\'')) : [];
|
||||
|
||||
const categories = {};
|
||||
escapedWords.forEach(word => {
|
||||
const category = word.includes(':') ? word.split(':')[0] : 'General';
|
||||
if (!categories[category]) {
|
||||
categories[category] = [];
|
||||
}
|
||||
categories[category].push(word);
|
||||
});
|
||||
|
||||
const imageMarkup = lora.images.map(img => {
|
||||
if (img.type === 'video') {
|
||||
return `<video controls autoplay muted loop crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||
<source src="${img.url}" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>`;
|
||||
} else {
|
||||
return `<img src="${img.url}" alt="Preview"
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
loading="lazy">`;
|
||||
}
|
||||
}).join('');
|
||||
|
||||
const triggerWordsMarkup = escapedWords.length ? `
|
||||
<div class="trigger-words-container">
|
||||
<div class="trigger-words-title">Trigger Words</div>
|
||||
<div class="trigger-words-tags">
|
||||
${escapedWords.map(word => `
|
||||
<div class="trigger-word-tag" onclick="copyTriggerWord('${word}')">
|
||||
<span class="trigger-word-content">${word}</span>
|
||||
<span class="trigger-word-copy">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : '<div class="trigger-words-container">No trigger words</div>';
|
||||
|
||||
const content = `
|
||||
<div class="modal-content">
|
||||
<h2>${lora.model.name}</h2>
|
||||
<div class="carousel">
|
||||
${imageMarkup}
|
||||
</div>
|
||||
<div class="description">About this version: ${lora.description || 'N/A'}</div>
|
||||
${triggerWordsMarkup}
|
||||
<div class="model-link">
|
||||
<a href="https://civitai.com/models/${lora.modelId}?modelVersionId=${lora.id}"
|
||||
target="_blank">more details on CivitAI</a>
|
||||
</div>
|
||||
<button class="close" onclick="modalManager.closeModal('loraModal')">×</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modalManager.showModal('loraModal', content);
|
||||
|
||||
document.querySelectorAll('.trigger-category').forEach(category => {
|
||||
category.addEventListener('click', function() {
|
||||
const categoryName = this.dataset.category;
|
||||
document.querySelectorAll('.trigger-category').forEach(c => c.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
const wordsList = document.querySelector('.trigger-words-list');
|
||||
wordsList.innerHTML = categories[categoryName].map(word => `
|
||||
<div class="trigger-word-tag" onclick="copyTriggerWord('${word}')">
|
||||
<span class="trigger-word-content">${word}</span>
|
||||
<span class="trigger-word-copy">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
`).join('');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function initializeLoraCards() {
|
||||
document.querySelectorAll('.lora-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
const meta = JSON.parse(card.dataset.meta || '{}');
|
||||
if (Object.keys(meta).length) {
|
||||
showLoraModal(meta);
|
||||
} else {
|
||||
showToast(card.dataset.from_civitai === 'True'
|
||||
? 'Click "Fetch" to retrieve metadata'
|
||||
: 'No CivitAI information available', 'info');
|
||||
}
|
||||
});
|
||||
|
||||
card.querySelector('.fa-copy')?.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.writeText(card.dataset.file_name)
|
||||
.then(() => showToast('Model name copied', 'success'))
|
||||
.catch(() => showToast('Copy failed', 'error'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createVideoPreview(url) {
|
||||
const video = document.createElement('video');
|
||||
video.controls = video.autoplay = video.muted = video.loop = true;
|
||||
video.src = url;
|
||||
return video;
|
||||
}
|
||||
|
||||
function createImagePreview(url) {
|
||||
const img = document.createElement('img');
|
||||
img.src = url;
|
||||
return img;
|
||||
}
|
||||
Reference in New Issue
Block a user