From c157e794f0cf2737702d2314405849dc39e9ba4f Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Thu, 26 Jun 2025 11:42:43 +0800 Subject: [PATCH] feat: implement event delegation for checkpoint cards and enhance Civitai link handling --- static/js/components/CheckpointCard.js | 391 +++++++++++++------------ static/js/components/LoraCard.js | 2 +- static/js/utils/infiniteScroll.js | 4 +- static/js/utils/uiHelpers.js | 11 +- 4 files changed, 207 insertions(+), 201 deletions(-) diff --git a/static/js/components/CheckpointCard.js b/static/js/components/CheckpointCard.js index faa5dedf..66e2c18b 100644 --- a/static/js/components/CheckpointCard.js +++ b/static/js/components/CheckpointCard.js @@ -1,10 +1,203 @@ -import { showToast, copyToClipboard, openExampleImagesFolder } from '../utils/uiHelpers.js'; +import { showToast, copyToClipboard, openExampleImagesFolder, openCivitai } from '../utils/uiHelpers.js'; import { state } from '../state/index.js'; import { showCheckpointModal } from './checkpointModal/index.js'; import { NSFW_LEVELS } from '../utils/constants.js'; import { replaceCheckpointPreview as apiReplaceCheckpointPreview, saveModelMetadata } from '../api/checkpointApi.js'; import { showDeleteModal } from '../utils/modalUtils.js'; +// Add a global event delegation handler +export function setupCheckpointCardEventDelegation() { + const gridElement = document.getElementById('checkpointGrid'); + if (!gridElement) return; + + // Remove any existing event listener to prevent duplication + gridElement.removeEventListener('click', handleCheckpointCardEvent); + + // Add the event delegation handler + gridElement.addEventListener('click', handleCheckpointCardEvent); +} + +// Event delegation handler for all checkpoint card events +function handleCheckpointCardEvent(event) { + // Find the closest card element + const card = event.target.closest('.lora-card'); + if (!card) return; + + // Handle specific elements within the card + if (event.target.closest('.toggle-blur-btn')) { + event.stopPropagation(); + toggleBlurContent(card); + return; + } + + if (event.target.closest('.show-content-btn')) { + event.stopPropagation(); + showBlurredContent(card); + return; + } + + if (event.target.closest('.fa-star')) { + event.stopPropagation(); + toggleFavorite(card); + return; + } + + if (event.target.closest('.fa-globe')) { + event.stopPropagation(); + if (card.dataset.from_civitai === 'true') { + openCivitai(card.dataset.filepath); + } + return; + } + + if (event.target.closest('.fa-copy')) { + event.stopPropagation(); + copyCheckpointName(card); + return; + } + + if (event.target.closest('.fa-trash')) { + event.stopPropagation(); + showDeleteModal(card.dataset.filepath); + return; + } + + if (event.target.closest('.fa-image')) { + event.stopPropagation(); + replaceCheckpointPreview(card.dataset.filepath); + return; + } + + if (event.target.closest('.fa-folder-open')) { + event.stopPropagation(); + openExampleImagesFolder(card.dataset.sha256); + return; + } + + // If no specific element was clicked, handle the card click (show modal) + showCheckpointModalFromCard(card); +} + +// Helper functions for event handling +function 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'; + } +} + +function 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'; + } +} + +async function toggleFavorite(card) { + const starIcon = card.querySelector('.fa-star'); + const isFavorite = starIcon.classList.contains('fas'); + const newFavoriteState = !isFavorite; + + try { + // Save the new favorite state to the server + await saveModelMetadata(card.dataset.filepath, { + favorite: newFavoriteState + }); + + if (newFavoriteState) { + showToast('Added to favorites', 'success'); + } else { + showToast('Removed from favorites', 'success'); + } + } catch (error) { + console.error('Failed to update favorite status:', error); + showToast('Failed to update favorite status', 'error'); + } +} + +async function copyCheckpointName(card) { + const checkpointName = card.dataset.file_name; + + try { + await copyToClipboard(checkpointName, 'Checkpoint name copied'); + } catch (err) { + console.error('Copy failed:', err); + showToast('Copy failed', 'error'); + } +} + +function showCheckpointModalFromCard(card) { + // Get the page-specific previewVersions map + const previewVersions = state.pages.checkpoints.previewVersions || new Map(); + const version = previewVersions.get(card.dataset.filepath); + const previewUrl = card.dataset.preview_url || '/loras_static/images/no-preview.png'; + const versionedPreviewUrl = version ? `${previewUrl}?t=${version}` : previewUrl; + + // Show checkpoint details modal + const checkpointMeta = { + sha256: card.dataset.sha256, + file_path: card.dataset.filepath, + model_name: card.dataset.name, + file_name: card.dataset.file_name, + folder: card.dataset.folder, + modified: card.dataset.modified, + file_size: parseInt(card.dataset.file_size || '0'), + from_civitai: card.dataset.from_civitai === 'true', + base_model: card.dataset.base_model, + notes: card.dataset.notes || '', + preview_url: versionedPreviewUrl, + // Parse civitai metadata from the card's dataset + civitai: (() => { + try { + return JSON.parse(card.dataset.meta || '{}'); + } catch (e) { + console.error('Failed to parse civitai metadata:', e); + return {}; // Return empty object on error + } + })(), + tags: (() => { + try { + return JSON.parse(card.dataset.tags || '[]'); + } catch (e) { + console.error('Failed to parse tags:', e); + return []; // Return empty array on error + } + })(), + modelDescription: card.dataset.modelDescription || '' + }; + showCheckpointModal(checkpointMeta); +} + +function replaceCheckpointPreview(filePath) { + if (window.replaceCheckpointPreview) { + window.replaceCheckpointPreview(filePath); + } else { + apiReplaceCheckpointPreview(filePath); + } +} + export function createCheckpointCard(checkpoint) { const card = document.createElement('div'); card.className = 'lora-card'; // Reuse the same class for styling @@ -123,153 +316,7 @@ export function createCheckpointCard(checkpoint) { `; - // Main card click event - card.addEventListener('click', () => { - // Show checkpoint details modal - const checkpointMeta = { - sha256: card.dataset.sha256, - file_path: card.dataset.filepath, - model_name: card.dataset.name, - file_name: card.dataset.file_name, - folder: card.dataset.folder, - modified: card.dataset.modified, - file_size: parseInt(card.dataset.file_size || '0'), - from_civitai: card.dataset.from_civitai === 'true', - base_model: card.dataset.base_model, - notes: card.dataset.notes || '', - preview_url: versionedPreviewUrl, - // Parse civitai metadata from the card's dataset - civitai: (() => { - try { - return JSON.parse(card.dataset.meta || '{}'); - } catch (e) { - console.error('Failed to parse civitai metadata:', e); - return {}; // Return empty object on error - } - })(), - tags: (() => { - try { - return JSON.parse(card.dataset.tags || '[]'); - } catch (e) { - console.error('Failed to parse tags:', e); - return []; // Return empty array on error - } - })(), - modelDescription: card.dataset.modelDescription || '' - }; - showCheckpointModal(checkpointMeta); - }); - - // Toggle blur button functionality - const toggleBlurBtn = card.querySelector('.toggle-blur-btn'); - if (toggleBlurBtn) { - toggleBlurBtn.addEventListener('click', (e) => { - e.stopPropagation(); - const preview = card.querySelector('.card-preview'); - const isBlurred = preview.classList.toggle('blurred'); - const icon = toggleBlurBtn.querySelector('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'; - } - }); - } - - // Show content button functionality - const showContentBtn = card.querySelector('.show-content-btn'); - if (showContentBtn) { - showContentBtn.addEventListener('click', (e) => { - e.stopPropagation(); - 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'; - } - }); - } - - // Favorite button click event - card.querySelector('.fa-star')?.addEventListener('click', async e => { - e.stopPropagation(); - const starIcon = e.currentTarget; - const isFavorite = starIcon.classList.contains('fas'); - const newFavoriteState = !isFavorite; - - try { - // Save the new favorite state to the server - await saveModelMetadata(card.dataset.filepath, { - favorite: newFavoriteState - }); - - if (newFavoriteState) { - showToast('Added to favorites', 'success'); - } else { - showToast('Removed from favorites', 'success'); - } - } catch (error) { - console.error('Failed to update favorite status:', error); - showToast('Failed to update favorite status', 'error'); - } - }); - - // Copy button click event - card.querySelector('.fa-copy')?.addEventListener('click', async e => { - e.stopPropagation(); - const checkpointName = card.dataset.file_name; - - try { - await copyToClipboard(checkpointName, 'Checkpoint name copied'); - } catch (err) { - console.error('Copy failed:', err); - showToast('Copy failed', 'error'); - } - }); - - // Civitai button click event - if (checkpoint.from_civitai) { - card.querySelector('.fa-globe')?.addEventListener('click', e => { - e.stopPropagation(); - openCivitai(checkpoint.model_name); - }); - } - - // Delete button click event - card.querySelector('.fa-trash')?.addEventListener('click', e => { - e.stopPropagation(); - showDeleteModal(checkpoint.file_path); - }); - - // Replace preview button click event - card.querySelector('.fa-image')?.addEventListener('click', e => { - e.stopPropagation(); - replaceCheckpointPreview(checkpoint.file_path); - }); - - // Open example images folder button click event - card.querySelector('.fa-folder-open')?.addEventListener('click', e => { - e.stopPropagation(); - openExampleImagesFolder(checkpoint.sha256); - }); - - // Add autoplayOnHover handlers for video elements if needed + // Add video auto-play on hover functionality if needed const videoElement = card.querySelector('video'); if (videoElement && autoplayOnHover) { const cardPreview = card.querySelector('.card-preview'); @@ -278,52 +325,10 @@ export function createCheckpointCard(checkpoint) { videoElement.removeAttribute('autoplay'); videoElement.pause(); - // Add mouse events to trigger play/pause - cardPreview.addEventListener('mouseenter', () => { - videoElement.play(); - }); - - cardPreview.addEventListener('mouseleave', () => { - videoElement.pause(); - videoElement.currentTime = 0; - }); + // Add mouse events to trigger play/pause using event attributes + cardPreview.setAttribute('onmouseenter', 'this.querySelector("video")?.play()'); + cardPreview.setAttribute('onmouseleave', 'const v=this.querySelector("video"); if(v){v.pause();v.currentTime=0;}'); } return card; -} - -// These functions will be implemented in checkpointApi.js -function openCivitai(modelName) { - // Check if the global function exists (registered by PageControls) - if (window.openCivitai) { - window.openCivitai(modelName); - } else { - // Fallback implementation - const card = document.querySelector(`.lora-card[data-name="${modelName}"]`); - if (!card) return; - - const metaData = JSON.parse(card.dataset.meta || '{}'); - const civitaiId = metaData.modelId; - const versionId = metaData.id; - - // Build URL - if (civitaiId) { - let url = `https://civitai.com/models/${civitaiId}`; - if (versionId) { - url += `?modelVersionId=${versionId}`; - } - window.open(url, '_blank'); - } else { - // If no ID, try searching by name - window.open(`https://civitai.com/models?query=${encodeURIComponent(modelName)}`, '_blank'); - } - } -} - -function replaceCheckpointPreview(filePath) { - if (window.replaceCheckpointPreview) { - window.replaceCheckpointPreview(filePath); - } else { - apiReplaceCheckpointPreview(filePath); - } } \ No newline at end of file diff --git a/static/js/components/LoraCard.js b/static/js/components/LoraCard.js index 0fcefc18..fa83e0c6 100644 --- a/static/js/components/LoraCard.js +++ b/static/js/components/LoraCard.js @@ -45,7 +45,7 @@ function handleLoraCardEvent(event) { if (event.target.closest('.fa-globe')) { event.stopPropagation(); if (card.dataset.from_civitai === 'true') { - openCivitai(card.dataset.name); + openCivitai(card.dataset.filepath); } return; } diff --git a/static/js/utils/infiniteScroll.js b/static/js/utils/infiniteScroll.js index 2b0c72d4..f92f5849 100644 --- a/static/js/utils/infiniteScroll.js +++ b/static/js/utils/infiniteScroll.js @@ -1,7 +1,7 @@ import { state, getCurrentPageState } from '../state/index.js'; import { VirtualScroller } from './VirtualScroller.js'; import { createLoraCard, setupLoraCardEventDelegation } from '../components/LoraCard.js'; -import { createCheckpointCard } from '../components/CheckpointCard.js'; +import { createCheckpointCard, setupCheckpointCardEventDelegation } from '../components/CheckpointCard.js'; import { fetchLorasPage } from '../api/loraApi.js'; import { fetchCheckpointsPage } from '../api/checkpointApi.js'; import { showToast } from './uiHelpers.js'; @@ -68,6 +68,8 @@ export async function initializeInfiniteScroll(pageType = 'loras') { // Setup event delegation for lora cards if on the loras page if (pageType === 'loras') { setupLoraCardEventDelegation(); + } else if (pageType === 'checkpoints') { + setupCheckpointCardEventDelegation(); } } diff --git a/static/js/utils/uiHelpers.js b/static/js/utils/uiHelpers.js index 5cb405ab..b3af63da 100644 --- a/static/js/utils/uiHelpers.js +++ b/static/js/utils/uiHelpers.js @@ -196,16 +196,14 @@ function filterByFolder(folderPath) { }); } -export function openCivitai(modelName) { - // 从卡片的data-meta属性中获取civitai ID - const loraCard = document.querySelector(`.lora-card[data-name="${modelName}"]`); +export function openCivitai(filePath) { + const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); if (!loraCard) return; const metaData = JSON.parse(loraCard.dataset.meta); - const civitaiId = metaData.modelId; // 使用modelId作为civitai模型ID - const versionId = metaData.id; // 使用id作为版本ID + const civitaiId = metaData.modelId; + const versionId = metaData.id; - // 构建URL if (civitaiId) { let url = `https://civitai.com/models/${civitaiId}`; if (versionId) { @@ -214,6 +212,7 @@ export function openCivitai(modelName) { window.open(url, '_blank'); } else { // 如果没有ID,尝试使用名称搜索 + const modelName = loraCard.dataset.name; window.open(`https://civitai.com/models?query=${encodeURIComponent(modelName)}`, '_blank'); } }