diff --git a/static/js/script.js b/static/js/script.js index 83a13c23..269dae81 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -1,4 +1,4 @@ -// 添加防抖函数 +// Debounce function function debounce(func, wait) { let timeout; return function(...args) { @@ -7,7 +7,7 @@ function debounce(func, wait) { }; } -// 优化排序功能 +// Sorting functionality function sortCards(sortBy) { const grid = document.getElementById('loraGrid'); if (!grid) return; @@ -16,90 +16,104 @@ function sortCards(sortBy) { const cards = Array.from(grid.children); requestAnimationFrame(() => { - cards.sort((a, b) => { - if (sortBy === 'date') { - // 将字符串转换为浮点数进行比较 - return parseFloat(b.dataset.modified) - parseFloat(a.dataset.modified); - } else { - // 名称排序保持不变 - return a.dataset.name.localeCompare(b.dataset.name); - } - }).forEach(card => fragment.appendChild(card)); + 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); }); } -// 改进WebSocket处理 -class WebSocketClient { - constructor(url, options = {}) { - this.url = url; - this.options = { - reconnectDelay: 1000, - maxReconnectAttempts: 5, - ...options - }; - this.reconnectAttempts = 0; - this.connect(); +// 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'); } - connect() { - this.ws = new WebSocket(this.url); - this.ws.onopen = this.onOpen.bind(this); - this.ws.onclose = this.onClose.bind(this); - this.ws.onerror = this.onError.bind(this); - this.ws.onmessage = this.onMessage.bind(this); + show(message = 'Loading...', progress = 0) { + this.overlay.style.display = 'flex'; + this.setProgress(progress); + this.setStatus(message); } - onOpen() { - this.reconnectAttempts = 0; - console.log('WebSocket connected'); + hide() { + this.overlay.style.display = 'none'; + this.reset(); } - onClose() { - if (this.reconnectAttempts < this.options.maxReconnectAttempts) { - setTimeout(() => { - this.reconnectAttempts++; - this.connect(); - }, this.options.reconnectDelay); - } + setProgress(percent) { + this.progressBar.style.width = `${percent}%`; + this.progressBar.setAttribute('aria-valuenow', percent); } - onError(error) { - console.error('WebSocket error:', error); + setStatus(message) { + this.statusText.textContent = message; } - onMessage(event) { + reset() { + this.setProgress(0); + this.setStatus(''); + } + + async showWithProgress(callback, options = {}) { + const { initialMessage = 'Processing...', completionMessage = 'Complete' } = options; + try { - const data = JSON.parse(event.data); - window.api.dispatchEvent(new CustomEvent('lora-scan-progress', { detail: data })); - } catch (error) { - console.error('Failed to parse WebSocket message:', error); + 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'; + } } -// 优化图片加载 -function lazyLoadImages() { - const imageObserver = new IntersectionObserver((entries, observer) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - const img = entry.target; - if (img.dataset.src) { - img.src = img.dataset.src; - img.removeAttribute('data-src'); - } - observer.unobserve(img); - } - }); - }); +const loadingManager = new LoadingManager(); - document.querySelectorAll('img[data-src]').forEach(img => { - imageObserver.observe(img); - }); +// 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(); @@ -185,10 +199,71 @@ class ModalManager { } } -// 创建全局 modalManager 实例 const modalManager = new ModalManager(); -// 修改现有的 showModal 函数为 showLoraModal +// 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, '\\\'')) : []; @@ -275,211 +350,135 @@ function showLoraModal(lora) { }); } -function copyTriggerWord(word) { - navigator.clipboard.writeText(word).then(() => { - const toast = document.createElement('div'); - toast.className = 'toast toast-copy'; - toast.textContent = 'Copied!'; - document.body.appendChild(toast); - - requestAnimationFrame(() => { - toast.classList.add('show'); - setTimeout(() => { - toast.classList.remove('show'); - setTimeout(() => toast.remove(), 300); - }, 1000); +function filterByFolder(folderPath) { + document.querySelectorAll('.lora-card').forEach(card => { + card.style.display = card.dataset.folder === folderPath ? '' : 'none'; + }); +} + +// Initialization +document.addEventListener('DOMContentLoaded', () => { + const searchHandler = debounce(term => { + document.querySelectorAll('.lora-card').forEach(card => { + card.style.display = [card.dataset.name, card.dataset.folder] + .some(text => text.toLowerCase().includes(term)) + ? 'block' + : 'none'; + }); + }, 250); + + document.getElementById('searchInput')?.addEventListener('input', e => { + searchHandler(e.target.value.toLowerCase()); + }); + + document.getElementById('sortSelect')?.addEventListener('change', e => { + sortCards(e.target.value); + }); + + lazyLoadImages(); + restoreFolderFilter(); + initializeLoraCards(); + initTheme(); +}); + +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')); }); }); } -// Add new functions for trigger word handling -function toggleTriggerWord(element) { - element.classList.toggle('selected'); -} - -function copySelectedTriggerWords() { - const selectedWords = Array.from(document.querySelectorAll('.trigger-word-tag.selected')) - .map(el => el.textContent.trim()); - - if (selectedWords.length === 0) { - // If no words are selected, select and copy all words - const allWords = Array.from(document.querySelectorAll('.trigger-word-tag')) - .map(el => el.textContent.trim()); - navigator.clipboard.writeText(allWords.join(', ')) - .then(() => showToast(`Copied all ${allWords.length} trigger words to clipboard`, 'success')) - .catch(() => showToast('Failed to copy trigger words', 'error')); - } else { - navigator.clipboard.writeText(selectedWords.join(', ')) - .then(() => showToast(`Copied ${selectedWords.length} selected trigger words to clipboard`, 'success')) - .catch(() => showToast('Failed to copy trigger words', 'error')); - } -} - -// 修改现有的 showDeleteModal 函数 -function showDeleteModal(filePath) { - event.stopPropagation(); - pendingDeletePath = filePath; - - const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); - const modelName = card.dataset.name; - const modal = modalManager.getModal('deleteModal').element; - const modelInfo = modal.querySelector('.delete-model-info'); - - modelInfo.innerHTML = ` - Model: ${modelName} -
- File: ${filePath} - `; - - modalManager.showModal('deleteModal'); -} - -// 修改现有的 closeDeleteModal 函数 -function closeDeleteModal() { - modalManager.closeModal('deleteModal'); -} - -// 添加 toast 通知功能 +// Helper functions function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; - - // 移除任何现有的 toast - document.querySelectorAll('.toast').forEach(t => t.remove()); - - document.body.appendChild(toast); + document.body.append(toast); - // 如果模态窗口打开,调整 toast 位置 - if (document.body.classList.contains('modal-open')) { - toast.style.transform = 'translate(-50%, 50%)'; // 在屏幕中间显示 - } - - // 触发动画 requestAnimationFrame(() => { toast.classList.add('show'); - setTimeout(() => { - toast.classList.remove('show'); - setTimeout(() => toast.remove(), 300); - }, 2000); + setTimeout(() => toast.remove(), 2300); }); } -// 初始化 -document.addEventListener('DOMContentLoaded', () => { - const wsClient = new WebSocketClient(`ws://${window.location.host}/ws`); - - // 优化搜索功能,添加防抖 - const debouncedSearch = debounce((term) => { - const cards = document.querySelectorAll('.lora-card'); - requestAnimationFrame(() => { - cards.forEach(card => { - const match = card.dataset.name.toLowerCase().includes(term) || - card.dataset.folder.toLowerCase().includes(term); - card.style.display = match ? 'block' : 'none'; - }); +function lazyLoadImages() { + const observer = new IntersectionObserver(entries => { + entries.forEach(entry => { + if (entry.isIntersecting && entry.target.dataset.src) { + entry.target.src = entry.target.dataset.src; + observer.unobserve(entry.target); + } }); - }, 250); - - document.getElementById('searchInput')?.addEventListener('input', (e) => { - debouncedSearch(e.target.value.toLowerCase()); }); - // 初始化懒加载 - lazyLoadImages(); - - // 优化滚动性能 - document.addEventListener('scroll', debounce(() => { - lazyLoadImages(); - }, 100), { passive: true }); -}); + document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img)); +} -// 刷新功能 -async function refreshLoras() { - const loadingOverlay = document.getElementById('loading-overlay'); - const loraGrid = document.getElementById('loraGrid'); - const currentSort = document.getElementById('sortSelect').value; - const activeFolder = document.querySelector('.tag.active')?.dataset.folder; - - try { - // Show loading overlay - loadingOverlay.style.display = 'flex'; - - // Fetch new data - const response = await fetch('/loras?refresh=true'); - if (!response.ok) throw new Error('Refresh failed'); - - // Parse the HTML response - const parser = new DOMParser(); - const doc = parser.parseFromString(await response.text(), 'text/html'); - - // Get the new lora cards - const newLoraGrid = doc.getElementById('loraGrid'); - - // Update the grid content - loraGrid.innerHTML = newLoraGrid.innerHTML; - - // Re-attach click listeners to new cards - document.querySelectorAll('.lora-card').forEach(card => { - card.addEventListener('click', () => { - const meta = JSON.parse(card.dataset.meta || '{}'); - if (Object.keys(meta).length > 0) { - showModal(meta); - } - }); - }); - - // Re-apply current sorting - sortCards(currentSort); - - // Modified folder filtering logic - if (activeFolder !== undefined) { // Check if there's an active folder - document.querySelectorAll('.lora-card').forEach(card => { - const cardFolder = card.getAttribute('data-folder'); - // For empty folder (root directory), only show cards with empty folder path - if (activeFolder === '') { - card.style.display = cardFolder === '' ? '' : 'none'; - } else { - // For other folders, show cards matching the folder path - card.style.display = cardFolder === activeFolder ? '' : 'none'; - } - }); - } - - } catch (error) { - console.error('Refresh failed:', error); - alert('Failed to refresh loras'); - } finally { - // Hide loading overlay - loadingOverlay.style.display = 'none'; +function restoreFolderFilter() { + const activeFolder = localStorage.getItem('activeFolder'); + const folderTag = activeFolder && document.querySelector(`.tag[data-folder="${activeFolder}"]`); + if (folderTag) { + folderTag.classList.add('active'); + filterByFolder(activeFolder); } } -// 占位功能函数 -function openCivitai(modelName) { - // 从卡片的data-meta属性中获取civitai ID - const loraCard = document.querySelector(`.lora-card[data-name="${modelName}"]`); - if (!loraCard) return; - - const metaData = JSON.parse(loraCard.dataset.meta); - const civitaiId = metaData.modelId; // 使用modelId作为civitai模型ID - const versionId = metaData.id; // 使用id作为版本ID - - // 构建URL - if (civitaiId) { - let url = `https://civitai.com/models/${civitaiId}`; - if (versionId) { - url += `?modelVersionId=${versionId}`; - } - window.open(url, '_blank'); - } else { - // 如果没有ID,尝试使用名称搜索 - window.open(`https://civitai.com/models?query=${encodeURIComponent(modelName)}`, '_blank'); - } +function initTheme() { + document.body.dataset.theme = localStorage.getItem('theme') || 'dark'; +} + +// Theme toggle +function toggleTheme() { + const theme = document.body.dataset.theme === 'light' ? 'dark' : 'light'; + document.body.dataset.theme = theme; + localStorage.setItem('theme', theme); } let pendingDeletePath = null; +function toggleFolder(element) { + // Store the previous state + const wasActive = element.classList.contains('active'); + + // Remove active class from all tags + document.querySelectorAll('.tag').forEach(tag => tag.classList.remove('active')); + + if (!wasActive) { + // Add active class to clicked tag + element.classList.add('active'); + // Store active folder in localStorage + localStorage.setItem('activeFolder', element.getAttribute('data-folder')); + // Hide all cards first + document.querySelectorAll('.lora-card').forEach(card => { + if (card.getAttribute('data-folder') === element.getAttribute('data-folder')) { + card.style.display = ''; + } else { + card.style.display = 'none'; + } + }); + } else { + // Clear stored folder when deactivating + localStorage.removeItem('activeFolder'); + // Show all cards + document.querySelectorAll('.lora-card').forEach(card => card.style.display = ''); + } +} + async function confirmDelete() { if (!pendingDeletePath) return; @@ -516,285 +515,64 @@ async function deleteModel(filePath) { showDeleteModal(filePath); } -// 初始化排序 -document.getElementById('sortSelect')?.addEventListener('change', (e) => { - sortCards(e.target.value); -}); - -// 立即执行初始排序 -const sortSelect = document.getElementById('sortSelect'); -if (sortSelect) { - sortCards(sortSelect.value); +function showDeleteModal(filePath) { + event.stopPropagation(); + pendingDeletePath = filePath; + + const card = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); + const modelName = card.dataset.name; + const modal = modalManager.getModal('deleteModal').element; + const modelInfo = modal.querySelector('.delete-model-info'); + + modelInfo.innerHTML = ` + Model: ${modelName} +
+ File: ${filePath} + `; + + modalManager.showModal('deleteModal'); } -// 添加搜索功能 -document.getElementById('searchInput')?.addEventListener('input', (e) => { - const term = e.target.value.toLowerCase(); - document.querySelectorAll('.lora-card').forEach(card => { - const match = card.dataset.name.toLowerCase().includes(term) || - card.dataset.folder.toLowerCase().includes(term); - card.style.display = match ? 'block' : 'none'; - }); -}); - -// 模态窗口管理 -let currentLora = null; -let currentImageIndex = 0; - -document.querySelectorAll('.lora-card').forEach(card => { - card.addEventListener('click', () => { - if (card.dataset.meta && Object.keys(JSON.parse(card.dataset.meta)).length > 0) { - currentLora = JSON.parse(card.dataset.meta); - showLoraModal(currentLora); - } - }); -}); - -// 更新卡片复制操作 -document.querySelectorAll('.lora-card').forEach(card => { - const copyBtn = card.querySelector('.fa-copy'); - if (copyBtn) { - copyBtn.onclick = (event) => { - event.stopPropagation(); - navigator.clipboard.writeText(card.dataset.file_name) - .then(() => showToast('Model name copied to clipboard', 'success')) - .catch(() => showToast('Failed to copy model name', 'error')); - }; - } - - // 为卡片添加点击反馈,根据不同情况显示不同的提示 - card.addEventListener('click', () => { - const meta = JSON.parse(card.dataset.meta || '{}'); - const fromCivitai = card.dataset.from_civitai === 'True'; - - if (Object.keys(meta).length === 0) { - if (fromCivitai) { - showToast('Model is available on Civitai. Please click "Fetch" to retrieve metadata.', 'info'); - } else { - showToast('This model is not available on Civitai. No additional information to display.', 'info'); - } - } - }); -}); - -function copyTriggerWords(words) { - if (!words) return; - - navigator.clipboard.writeText(words) - .then(() => { - const toast = document.createElement('div'); - toast.className = 'toast toast-success'; - toast.textContent = 'Trigger words copied to clipboard'; - document.body.appendChild(toast); - - // Force recalculation of toast position for modal context - toast.style.position = 'fixed'; - toast.style.zIndex = '9999'; // 确保显示在最上层 - toast.style.bottom = '50%'; - toast.style.transform = 'translate(-50%, 50%)'; - - requestAnimationFrame(() => { - toast.classList.add('show'); - setTimeout(() => { - toast.classList.remove('show'); - setTimeout(() => toast.remove(), 300); - }, 2000); - }); - }) - .catch(() => { - const toast = document.createElement('div'); - toast.className = 'toast toast-error'; - toast.textContent = 'Failed to copy trigger words'; - // ... 相同的 toast 显示逻辑 - }); -} - -// WebSocket handling for progress updates -document.addEventListener('DOMContentLoaded', function() { - const loadingOverlay = document.getElementById('loading-overlay'); - const progressBar = document.querySelector('.progress-bar'); - const loadingStatus = document.querySelector('.loading-status'); - - // 默认隐藏 loading overlay - loadingOverlay.style.display = 'none'; - - const api = new EventTarget(); - window.api = api; - - const ws = new WebSocket(`ws://${window.location.host}/ws`); - - ws.onmessage = function(event) { - const data = JSON.parse(event.data); - if (data.type === 'lora-scan-progress') { - // 当收到扫描进度消息时显示 overlay - loadingOverlay.style.display = 'flex'; - api.dispatchEvent(new CustomEvent('lora-scan-progress', { detail: data })); - } - }; - - api.addEventListener("lora-scan-progress", (event) => { - const data = event.detail; - const progress = (data.value / data.max) * 100; +function copyTriggerWord(word) { + navigator.clipboard.writeText(word).then(() => { + const toast = document.createElement('div'); + toast.className = 'toast toast-copy'; + toast.textContent = 'Copied!'; + document.body.appendChild(toast); - progressBar.style.width = `${progress}%`; - progressBar.setAttribute('aria-valuenow', progress); - loadingStatus.textContent = data.status; - - if (data.value === data.max) { - // 确保在扫描完成时隐藏 overlay + requestAnimationFrame(() => { + toast.classList.add('show'); setTimeout(() => { - loadingOverlay.style.display = 'none'; - // 重置进度条 - progressBar.style.width = '0%'; - progressBar.setAttribute('aria-valuenow', 0); - }, 500); - } - }); - - // Restore folder filter state - restoreFolderFilter(); - - // Restore scroll position if exists - const savedScrollPos = localStorage.getItem('scrollPosition'); - if (savedScrollPos !== null) { - window.scrollTo(0, parseInt(savedScrollPos)); - localStorage.removeItem('scrollPosition'); - } -}); - -function toggleFolder(element) { - // Store the previous state - const wasActive = element.classList.contains('active'); - - // Remove active class from all tags - document.querySelectorAll('.tag').forEach(tag => tag.classList.remove('active')); - - if (!wasActive) { - // Add active class to clicked tag - element.classList.add('active'); - // Store active folder in localStorage - localStorage.setItem('activeFolder', element.getAttribute('data-folder')); - // Hide all cards first - document.querySelectorAll('.lora-card').forEach(card => { - if (card.getAttribute('data-folder') === element.getAttribute('data-folder')) { - card.style.display = ''; - } else { - card.style.display = 'none'; - } + toast.classList.remove('show'); + setTimeout(() => toast.remove(), 300); + }, 1000); }); - } else { - // Clear stored folder when deactivating - localStorage.removeItem('activeFolder'); - // Show all cards - document.querySelectorAll('.lora-card').forEach(card => card.style.display = ''); - } -} - -// Add this function to restore folder filter state -function restoreFolderFilter() { - const activeFolder = localStorage.getItem('activeFolder'); - if (activeFolder !== null) { - const folderTag = document.querySelector(`.tag[data-folder="${activeFolder}"]`); - if (folderTag) { - folderTag.classList.add('active'); - document.querySelectorAll('.lora-card').forEach(card => { - if (card.getAttribute('data-folder') === activeFolder) { - card.style.display = ''; - } else { - card.style.display = 'none'; - } - }); - } - } -} - -// 主题切换 -function toggleTheme() { - const theme = document.body.dataset.theme || 'dark'; - document.body.dataset.theme = theme === 'light' ? 'dark' : 'light'; - localStorage.setItem('theme', document.body.dataset.theme); -} - -// 初始化主题 -function initTheme() { - const savedTheme = localStorage.getItem('theme') || 'dark'; - document.body.dataset.theme = savedTheme; -} - -// 键盘导航 -document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') modalManager.closeModal('loraModal'); -}); - -// 图片预加载 -function preloadImages(urls) { - urls.forEach(url => { - new Image().src = url; }); } -// 新增 fetchCivitai 函数 -async function fetchCivitai() { - const loadingOverlay = document.getElementById('loading-overlay'); - const progressBar = document.querySelector('.progress-bar'); - const loadingStatus = document.querySelector('.loading-status'); - const loraCards = document.querySelectorAll('.lora-card'); +function closeDeleteModal() { + modalManager.closeModal('deleteModal'); +} + +function openCivitai(modelName) { + // 从卡片的data-meta属性中获取civitai ID + const loraCard = document.querySelector(`.lora-card[data-name="${modelName}"]`); + if (!loraCard) return; - // 显示进度条 - loadingOverlay.style.display = 'flex'; - loadingStatus.textContent = 'Fetching metadata...'; + const metaData = JSON.parse(loraCard.dataset.meta); + const civitaiId = metaData.modelId; // 使用modelId作为civitai模型ID + const versionId = metaData.id; // 使用id作为版本ID - try { - // Iterate through all lora cards - for(let i = 0; i < loraCards.length; i++) { - const card = loraCards[i]; - // Skip if already has metadata - if (card.dataset.meta && Object.keys(JSON.parse(card.dataset.meta)).length > 0) { - continue; - } - - // Make sure these data attributes exist on your lora-card elements - const sha256 = card.dataset.sha256; - const filePath = card.dataset.filepath; - - // Add validation - if (!sha256 || !filePath) { - console.warn(`Missing data for card ${card.dataset.name}:`, { sha256, filePath }); - continue; - } - - // Update progress - const progress = (i / loraCards.length * 100).toFixed(1); - progressBar.style.width = `${progress}%`; - loadingStatus.textContent = `Processing (${i+1}/${loraCards.length}) ${card.dataset.name}`; - - // Call backend API - const response = await fetch('/api/fetch-civitai', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - sha256: sha256, - file_path: filePath - }) - }); + // 构建URL + if (civitaiId) { + let url = `https://civitai.com/models/${civitaiId}`; + if (versionId) { + url += `?modelVersionId=${versionId}`; } - - // Completion handling - progressBar.style.width = '100%'; - loadingStatus.textContent = 'Metadata update complete'; - setTimeout(() => { - loadingOverlay.style.display = 'none'; - // Store current scroll position - const scrollPos = window.scrollY; - localStorage.setItem('scrollPosition', scrollPos.toString()); - // Reload the page - window.location.reload(); - }, 2000); - - } catch (error) { - console.warn('Error fetching metadata:', error); + window.open(url, '_blank'); + } else { + // 如果没有ID,尝试使用名称搜索 + window.open(`https://civitai.com/models?query=${encodeURIComponent(modelName)}`, '_blank'); } } @@ -864,6 +642,4 @@ async function replacePreview(filePath) { // Trigger file selection input.click(); -} - -initTheme(); \ No newline at end of file +} \ No newline at end of file