// Debounce function
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// Sorting functionality
function sortCards(sortBy) {
const grid = document.getElementById('loraGrid');
if (!grid) return;
const fragment = document.createDocumentFragment();
const cards = Array.from(grid.children);
requestAnimationFrame(() => {
cards.sort((a, b) => sortBy === 'date'
? parseFloat(b.dataset.modified) - parseFloat(a.dataset.modified)
: a.dataset.name.localeCompare(b.dataset.name)
).forEach(card => fragment.appendChild(card));
grid.appendChild(fragment);
});
}
// Loading management
class LoadingManager {
constructor() {
this.overlay = document.getElementById('loading-overlay');
this.progressBar = this.overlay.querySelector('.progress-bar');
this.statusText = this.overlay.querySelector('.loading-status');
}
show(message = 'Loading...', progress = 0) {
this.overlay.style.display = 'flex';
this.setProgress(progress);
this.setStatus(message);
}
hide() {
this.overlay.style.display = 'none';
this.reset();
}
setProgress(percent) {
this.progressBar.style.width = `${percent}%`;
this.progressBar.setAttribute('aria-valuenow', percent);
}
setStatus(message) {
this.statusText.textContent = message;
}
reset() {
this.setProgress(0);
this.setStatus('');
}
async showWithProgress(callback, options = {}) {
const { initialMessage = 'Processing...', completionMessage = 'Complete' } = options;
try {
this.show(initialMessage);
await callback(this);
this.setProgress(100);
this.setStatus(completionMessage);
await new Promise(resolve => setTimeout(resolve, 500));
} finally {
this.hide();
}
}
showSimpleLoading(message = 'Loading...') {
this.overlay.style.display = 'flex';
this.progressBar.style.display = 'none';
this.setStatus(message);
}
restoreProgressBar() {
this.progressBar.style.display = 'block';
}
}
const loadingManager = new LoadingManager();
// Media preview handling
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;
}
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);
}
}
// Modal management
class ModalManager {
constructor() {
this.modals = new Map();
this.boundHandleEscape = this.handleEscape.bind(this);
// 注册所有模态窗口
this.registerModal('loraModal', {
element: document.getElementById('loraModal'),
onClose: () => {
this.getModal('loraModal').element.style.display = 'none';
document.body.classList.remove('modal-open');
}
});
this.registerModal('deleteModal', {
element: document.getElementById('deleteModal'),
onClose: () => {
this.getModal('deleteModal').element.classList.remove('show');
document.body.classList.remove('modal-open');
pendingDeletePath = null;
}
});
// 添加全局事件监听
document.addEventListener('keydown', this.boundHandleEscape);
}
registerModal(id, config) {
this.modals.set(id, {
element: config.element,
onClose: config.onClose,
isOpen: false
});
// 为每个模态窗口添加点击外部关闭事件
config.element.addEventListener('click', (e) => {
if (e.target === config.element) {
this.closeModal(id);
}
});
}
getModal(id) {
return this.modals.get(id);
}
showModal(id, content = null) {
const modal = this.getModal(id);
if (!modal) return;
if (content) {
modal.element.innerHTML = content;
}
if (id === 'loraModal') {
modal.element.style.display = 'block';
} else if (id === 'deleteModal') {
modal.element.classList.add('show');
}
modal.isOpen = true;
document.body.classList.add('modal-open');
}
closeModal(id) {
const modal = this.getModal(id);
if (!modal) return;
modal.onClose();
modal.isOpen = false;
}
handleEscape(e) {
if (e.key === 'Escape') {
// 关闭最后打开的模态窗口
for (const [id, modal] of this.modals) {
if (modal.isOpen) {
this.closeModal(id);
break;
}
}
}
}
}
const modalManager = new ModalManager();
// Data management functions
async function refreshLoras() {
const loraGrid = document.getElementById('loraGrid');
const currentSort = document.getElementById('sortSelect').value;
const activeFolder = document.querySelector('.tag.active')?.dataset.folder;
try {
loadingManager.showSimpleLoading('Refreshing loras...');
const response = await fetch('/loras?refresh=true');
if (!response.ok) throw new Error('Refresh failed');
const doc = new DOMParser().parseFromString(await response.text(), 'text/html');
loraGrid.innerHTML = doc.getElementById('loraGrid').innerHTML;
initializeLoraCards();
sortCards(currentSort);
if (activeFolder) filterByFolder(activeFolder);
showToast('Refresh complete', 'success');
} catch (error) {
console.error('Refresh failed:', error);
showToast('Failed to refresh loras', 'error');
} finally {
loadingManager.hide();
loadingManager.restoreProgressBar();
}
}
async function fetchCivitai() {
const loraCards = document.querySelectorAll('.lora-card');
const totalCards = loraCards.length;
await loadingManager.showWithProgress(async (loading) => {
for (let i = 0; i < totalCards; i++) {
const card = loraCards[i];
if (card.dataset.meta?.length > 2) continue;
const { sha256, filepath: filePath } = card.dataset;
if (!sha256 || !filePath) continue;
loading.setProgress((i / totalCards * 100).toFixed(1));
loading.setStatus(`Processing (${i+1}/${totalCards}) ${card.dataset.name}`);
try {
await fetch('/api/fetch-civitai', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sha256, file_path: filePath })
});
} catch (error) {
console.error(`Failed to fetch ${card.dataset.name}:`, error);
}
}
localStorage.setItem('scrollPosition', window.scrollY.toString());
window.location.reload();
}, {
initialMessage: 'Fetching metadata...',
completionMessage: 'Metadata update complete'
});
}
// UI interaction functions
function showLoraModal(lora) {
const escapedWords = lora.trainedWords?.length ?
lora.trainedWords.map(word => word.replace(/'/g, '\\\'')) : [];
// Organize trigger words by categories
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 ``;
} else {
return ``;
}
}).join('');
const triggerWordsMarkup = escapedWords.length ? `