/** * ShowcaseView.js * 处理LoRA模型展示内容(图片、视频)的功能模块 */ import { showToast, copyToClipboard } from '../../utils/uiHelpers.js'; import { state } from '../../state/index.js'; import { NSFW_LEVELS } from '../../utils/constants.js'; /** * 渲染展示内容 * @param {Array} images - 要展示的图片/视频数组 * @returns {string} HTML内容 */ export function renderShowcaseContent(images) { if (!images?.length) return '
No example images available
'; // Filter images based on SFW setting const showOnlySFW = state.settings.show_only_sfw; let filteredImages = images; let hiddenCount = 0; if (showOnlySFW) { filteredImages = images.filter(img => { const nsfwLevel = img.nsfwLevel !== undefined ? img.nsfwLevel : 0; const isSfw = nsfwLevel < NSFW_LEVELS.R; if (!isSfw) hiddenCount++; return isSfw; }); } // Show message if no images are available after filtering if (filteredImages.length === 0) { return `

All example images are filtered due to NSFW content settings

Your settings are currently set to show only safe-for-work content

You can change this in Settings

`; } // Show hidden content notification if applicable const hiddenNotification = hiddenCount > 0 ? `
${hiddenCount} ${hiddenCount === 1 ? 'image' : 'images'} hidden due to SFW-only setting
` : ''; return `
Scroll or click to show ${filteredImages.length} examples
`; } /** * 生成视频包装HTML */ function generateVideoWrapper(img, heightPercent, shouldBlur, nsfwText, metadataPanel) { return `
${shouldBlur ? ` ` : ''} ${shouldBlur ? `

${nsfwText}

` : ''} ${metadataPanel}
`; } /** * 生成图片包装HTML */ function generateImageWrapper(img, heightPercent, shouldBlur, nsfwText, metadataPanel) { return `
${shouldBlur ? ` ` : ''} Preview ${shouldBlur ? `

${nsfwText}

` : ''} ${metadataPanel}
`; } /** * 切换展示区域的显示状态 */ export function toggleShowcase(element) { const carousel = element.nextElementSibling; const isCollapsed = carousel.classList.contains('collapsed'); const indicator = element.querySelector('span'); const icon = element.querySelector('i'); carousel.classList.toggle('collapsed'); if (isCollapsed) { const count = carousel.querySelectorAll('.media-wrapper').length; indicator.textContent = `Scroll or click to hide examples`; icon.classList.replace('fa-chevron-down', 'fa-chevron-up'); initLazyLoading(carousel); // Initialize NSFW content blur toggle handlers initNsfwBlurHandlers(carousel); // Initialize metadata panel interaction handlers initMetadataPanelHandlers(carousel); } else { const count = carousel.querySelectorAll('.media-wrapper').length; indicator.textContent = `Scroll or click to show ${count} examples`; 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 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 copyToClipboard(promptElement.textContent, 'Prompt copied to clipboard'); } 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 }); }); } /** * 初始化模糊切换处理 */ function initNsfwBlurHandlers(container) { // Handle toggle blur buttons const toggleButtons = container.querySelectorAll('.toggle-blur-btn'); toggleButtons.forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const wrapper = btn.closest('.media-wrapper'); const media = wrapper.querySelector('img, video'); const isBlurred = media.classList.toggle('blurred'); const icon = btn.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 = wrapper.querySelector('.nsfw-overlay'); if (overlay) { overlay.style.display = isBlurred ? 'flex' : 'none'; } }); }); // Handle "Show" buttons in overlays const showButtons = container.querySelectorAll('.show-content-btn'); showButtons.forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const wrapper = btn.closest('.media-wrapper'); const media = wrapper.querySelector('img, video'); media.classList.remove('blurred'); // Update the toggle button icon const toggleBtn = wrapper.querySelector('.toggle-blur-btn'); if (toggleBtn) { toggleBtn.querySelector('i').className = 'fas fa-eye-slash'; } // Hide the overlay const overlay = wrapper.querySelector('.nsfw-overlay'); if (overlay) { overlay.style.display = 'none'; } }); }); } /** * 初始化延迟加载 */ function initLazyLoading(container) { const lazyElements = container.querySelectorAll('.lazy'); const lazyLoad = (element) => { if (element.tagName.toLowerCase() === 'video') { element.src = element.dataset.src; element.querySelector('source').src = element.dataset.src; element.load(); } else { element.src = element.dataset.src; } element.classList.remove('lazy'); }; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { lazyLoad(entry.target); observer.unobserve(entry.target); } }); }); lazyElements.forEach(element => observer.observe(element)); } /** * 设置展示区域的滚动处理 */ export function setupShowcaseScroll() { // Add event listener to document for wheel events document.addEventListener('wheel', (event) => { // Find the active modal content const modalContent = document.querySelector('#loraModal .modal-content'); if (!modalContent) return; const showcase = modalContent.querySelector('.showcase-section'); if (!showcase) return; const carousel = showcase.querySelector('.carousel'); const scrollIndicator = showcase.querySelector('.scroll-indicator'); if (carousel?.classList.contains('collapsed') && event.deltaY > 0) { const isNearBottom = modalContent.scrollHeight - modalContent.scrollTop - modalContent.clientHeight < 100; if (isNearBottom) { toggleShowcase(scrollIndicator); event.preventDefault(); } } }, { passive: false }); // Use MutationObserver instead of deprecated DOMNodeInserted const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length) { // Check if loraModal content was added const loraModal = document.getElementById('loraModal'); if (loraModal && loraModal.querySelector('.modal-content')) { setupBackToTopButton(loraModal.querySelector('.modal-content')); } } } }); // Start observing the document body for changes observer.observe(document.body, { childList: true, subtree: true }); // Also try to set up the button immediately in case the modal is already open const modalContent = document.querySelector('#loraModal .modal-content'); if (modalContent) { setupBackToTopButton(modalContent); } } /** * 设置返回顶部按钮 */ function setupBackToTopButton(modalContent) { // Remove any existing scroll listeners to avoid duplicates modalContent.onscroll = null; // Add new scroll listener modalContent.addEventListener('scroll', () => { const backToTopBtn = modalContent.querySelector('.back-to-top'); if (backToTopBtn) { if (modalContent.scrollTop > 300) { backToTopBtn.classList.add('visible'); } else { backToTopBtn.classList.remove('visible'); } } }); // Trigger a scroll event to check initial position modalContent.dispatchEvent(new Event('scroll')); } /** * 滚动到顶部 */ export function scrollToTop(button) { const modalContent = button.closest('.modal-content'); if (modalContent) { modalContent.scrollTo({ top: 0, behavior: 'smooth' }); } }