/** * ShowcaseView.js * 处理LoRA模型展示内容(图片、视频)的功能模块 */ import { toggleShowcase, setupShowcaseScroll, scrollToTop } from '../../utils/uiHelpers.js'; import { state } from '../../state/index.js'; import { NSFW_LEVELS } from '../../utils/constants.js'; /** * 获取展示内容并进行渲染 * @param {Array} images - 要展示的图片/视频数组 * @param {Array} exampleFiles - Local example files already fetched * @returns {Promise} HTML内容 */ export function renderShowcaseContent(images, exampleFiles = []) { if (!images?.length) { // Replace empty message with import interface return renderImportInterface(true); } // 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
`; } /** * Render the import interface for example images * @param {boolean} isEmpty - Whether there are no existing examples * @returns {string} HTML content for import interface */ function renderImportInterface(isEmpty) { return `

${isEmpty ? 'No example images available' : 'Add more examples'}

Drag & drop images or videos here

or

Supported formats: jpg, png, gif, webp, mp4, webm

`; } /** * Initialize the import functionality for example images * @param {string} modelHash - The SHA256 hash of the model * @param {Element} container - The container element for the import area */ export function initExampleImport(modelHash, container) { if (!container) return; const importContainer = container.querySelector('#exampleImportContainer'); const fileInput = container.querySelector('#exampleFilesInput'); const selectFilesBtn = container.querySelector('#selectExampleFilesBtn'); // Set up file selection button if (selectFilesBtn) { selectFilesBtn.addEventListener('click', () => { fileInput.click(); }); } // Handle file selection if (fileInput) { fileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) { handleImportFiles(Array.from(e.target.files), modelHash, importContainer); } }); } // Set up drag and drop if (importContainer) { ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { importContainer.addEventListener(eventName, preventDefaults, false); }); function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); } // Highlight drop area on drag over ['dragenter', 'dragover'].forEach(eventName => { importContainer.addEventListener(eventName, () => { importContainer.classList.add('highlight'); }, false); }); // Remove highlight on drag leave ['dragleave', 'drop'].forEach(eventName => { importContainer.addEventListener(eventName, () => { importContainer.classList.remove('highlight'); }, false); }); // Handle dropped files importContainer.addEventListener('drop', (e) => { const files = Array.from(e.dataTransfer.files); handleImportFiles(files, modelHash, importContainer); }, false); } } /** * Handle the file import process * @param {File[]} files - Array of files to import * @param {string} modelHash - The SHA256 hash of the model * @param {Element} importContainer - The container element for import UI */ async function handleImportFiles(files, modelHash, importContainer) { // Filter for supported file types const supportedImages = ['.jpg', '.jpeg', '.png', '.gif', '.webp']; const supportedVideos = ['.mp4', '.webm']; const supportedExtensions = [...supportedImages, ...supportedVideos]; const validFiles = files.filter(file => { const ext = '.' + file.name.split('.').pop().toLowerCase(); return supportedExtensions.includes(ext); }); if (validFiles.length === 0) { alert('No supported files selected. Please select image or video files.'); return; } try { // Get file paths to send to backend const filePaths = validFiles.map(file => { // We need the full path, but we only have the filename // For security reasons, browsers don't provide full paths // This will only work if the backend can handle just filenames return URL.createObjectURL(file); }); // Use FileReader to get the file data for direct upload const formData = new FormData(); formData.append('model_hash', modelHash); validFiles.forEach(file => { formData.append('files', file); }); // Call API to import files const response = await fetch('/api/import-example-images', { method: 'POST', body: formData }); const result = await response.json(); if (!result.success) { throw new Error(result.error || 'Failed to import example files'); } // Get updated local files const updatedFilesResponse = await fetch(`/api/example-image-files?model_hash=${modelHash}`); const updatedFilesResult = await updatedFilesResponse.json(); if (!updatedFilesResult.success) { throw new Error(updatedFilesResult.error || 'Failed to get updated file list'); } // Re-render the showcase content const showcaseTab = document.getElementById('showcase-tab'); if (showcaseTab) { // Get the updated images from the result const regularImages = result.regular_images || []; const customImages = result.custom_images || []; // Combine both arrays for rendering const allImages = [...regularImages, ...customImages]; showcaseTab.innerHTML = renderShowcaseContent(allImages, updatedFilesResult.files); // Re-initialize showcase functionality const carousel = showcaseTab.querySelector('.carousel'); if (carousel) { if (!carousel.classList.contains('collapsed')) { initLazyLoading(carousel); initNsfwBlurHandlers(carousel); initMetadataPanelHandlers(carousel); } // Initialize the import UI for the new content initExampleImport(modelHash, showcaseTab); } // Update VirtualScroller if available if (state.virtualScroller && result.model_file_path) { // Create an update object with only the necessary properties const updateData = { civitai: { images: regularImages, customImages: customImages } }; // Update the item in the virtual scroller state.virtualScroller.updateSingleItem(result.model_file_path, updateData); console.log('Updated VirtualScroller item with new example images'); } } } catch (error) { console.error('Error importing examples:', error); } } /** * Generate metadata panel HTML */ function generateMetadataPanel(hasParams, hasPrompts, prompt, negativePrompt, size, seed, model, steps, sampler, cfgScale, clipSkip) { // Create unique IDs for prompt copying const promptIndex = Math.random().toString(36).substring(2, 15); const negPromptIndex = Math.random().toString(36).substring(2, 15); let content = '
'; return content; } /** * 生成视频包装HTML */ function generateVideoWrapper(img, heightPercent, shouldBlur, nsfwText, metadataPanel, localUrl, remoteUrl) { return `
${shouldBlur ? ` ` : ''} ${shouldBlur ? `

${nsfwText}

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

${nsfwText}

` : ''} ${metadataPanel}
`; } // Use the shared setupShowcaseScroll function with the correct modal ID export { setupShowcaseScroll, scrollToTop, toggleShowcase }; // Initialize the showcase scroll when this module is imported document.addEventListener('DOMContentLoaded', () => { setupShowcaseScroll('loraModal'); });