diff --git a/static/css/components/lora-modal/lora-modal.css b/static/css/components/lora-modal/lora-modal.css index 35a988d2..7f21d169 100644 --- a/static/css/components/lora-modal/lora-modal.css +++ b/static/css/components/lora-modal/lora-modal.css @@ -123,42 +123,6 @@ } } -/* 修改 back-to-top 按钮样式,使其固定在 modal 内部 */ -.modal-content .back-to-top { - position: sticky; /* 改用 sticky 定位 */ - float: right; /* 使用 float 确保按钮在右侧 */ - bottom: 20px; /* 距离底部的距离 */ - margin-right: 20px; /* 右侧间距 */ - margin-top: -56px; /* 负边距确保不占用额外空间 */ - width: 36px; - height: 36px; - border-radius: 50%; - background: var(--card-bg); - border: 1px solid var(--border-color); - color: var(--text-color); - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - opacity: 0; - visibility: hidden; - transform: translateY(10px); - transition: all 0.3s ease; - z-index: 10; -} - -.modal-content .back-to-top.visible { - opacity: 1; - visibility: visible; - transform: translateY(0); -} - -.modal-content .back-to-top:hover { - background: var(--lora-accent); - color: white; - transform: translateY(-2px); -} - /* File name copy styles */ .file-name-wrapper { display: flex; diff --git a/static/css/components/lora-modal/showcase.css b/static/css/components/lora-modal/showcase.css index 252c927a..965efd0a 100644 --- a/static/css/components/lora-modal/showcase.css +++ b/static/css/components/lora-modal/showcase.css @@ -4,31 +4,254 @@ margin-top: var(--space-4); } -.carousel { - transition: max-height 0.3s ease-in-out; +/* Main showcase container */ +.showcase-container { + display: flex; + height: 750px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius-sm); overflow: hidden; + background: var(--lora-surface); } -.carousel.collapsed { - max-height: 0; +.showcase-container.empty { + height: 400px; } -.carousel-container { +/* Thumbnail Sidebar */ +.thumbnail-sidebar { + width: 200px; + background: var(--bg-color); + border-right: 1px solid var(--border-color); + display: flex; + flex-direction: column; +} + +.thumbnail-grid { + flex: 1; + overflow-y: auto; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* Internet Explorer 10+ */ + padding: var(--space-2); display: flex; flex-direction: column; gap: var(--space-2); } +.thumbnail-grid::-webkit-scrollbar { + display: none; /* WebKit */ +} + +.thumbnail-item { + position: relative; + aspect-ratio: 1; + border-radius: var(--border-radius-xs); + overflow: hidden; + cursor: pointer; + border: 2px solid transparent; + transition: all 0.2s ease; + background: var(--lora-surface); +} + +.thumbnail-item:hover { + border-color: var(--lora-accent); + transform: scale(1.02); +} + +.thumbnail-item.active { + border-color: var(--lora-accent); + box-shadow: 0 0 0 1px var(--lora-accent); +} + +.thumbnail-media { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.thumbnail-media.blurred { + filter: blur(8px); +} + +.video-indicator { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: white; + background: rgba(0, 0, 0, 0.6); + border-radius: 50%; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.7em; + pointer-events: none; +} + +.thumbnail-nsfw-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 1.2em; +} + +/* Import Section */ +.import-section { + padding: var(--space-2); + border-top: 1px solid var(--border-color); + background: var(--bg-color); +} + +.select-files-btn { + width: 100%; + background: var(--lora-accent); + color: var(--lora-text); + border: none; + border-radius: var(--border-radius-xs); + padding: var(--space-2); + cursor: pointer; + font-size: 0.9em; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + transition: all 0.2s; + margin-bottom: var(--space-2); +} + +.select-files-btn:hover { + opacity: 0.9; + transform: translateY(-1px); +} + +.import-drop-zone { + border: 2px dashed var(--border-color); + border-radius: var(--border-radius-xs); + padding: var(--space-2); + text-align: center; + transition: all 0.3s ease; + background: var(--lora-surface); + min-height: 60px; + display: flex; + align-items: center; + justify-content: center; +} + +.import-drop-zone.highlight { + border-color: var(--lora-accent); + background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.1); +} + +.drop-zone-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + color: var(--text-color); + opacity: 0.6; + font-size: 0.8em; +} + +.drop-zone-content i { + font-size: 1.2em; + margin-bottom: 2px; +} + +/* Main Display Area */ +.main-display-area { + flex: 1; + position: relative; + background: var(--card-bg); + overflow: hidden; +} + +.main-display-area.empty { + display: flex; + align-items: center; + justify-content: center; +} + +.empty-state { + text-align: center; + color: var(--text-color); + opacity: 0.6; +} + +.empty-state i { + font-size: 3em; + margin-bottom: var(--space-2); + opacity: 0.5; +} + +.empty-state h3 { + margin: 0 0 var(--space-1); + font-weight: 500; +} + +.empty-state p { + margin: 0; + font-size: 0.9em; +} + +.navigation-controls { + position: absolute; + top: var(--space-2); + right: var(--space-2); + display: flex; + gap: 6px; + z-index: 10; +} + +.nav-btn { + width: 36px; + height: 36px; + border-radius: 50%; + background: var(--bg-color); + border: 1px solid var(--border-color); + color: var(--text-color); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + opacity: 0.8; +} + +.nav-btn:hover { + opacity: 1; + transform: translateY(-1px); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.15); +} + +.nav-btn.info-btn.active { + background: var(--lora-accent); + color: var(--lora-text); + border-color: var(--lora-accent); +} + +.main-media-container { + position: relative; + width: 100%; + height: 100%; +} + .media-wrapper { position: relative; width: 100%; + height: 100%; background: var(--lora-surface); - margin-bottom: var(--space-2); - overflow: hidden; /* Ensure metadata panel is contained */ -} - -.media-wrapper:last-child { - margin-bottom: 0; + overflow: hidden; } .media-wrapper img, @@ -41,50 +264,11 @@ object-fit: contain; } -.no-examples { - text-align: center; - padding: var(--space-3); - color: var(--text-color); - opacity: 0.7; -} - -/* Adjust the media wrapper for tab system */ -#showcase-tab .carousel-container { - margin-top: var(--space-2); -} - -/* Add styles for blurred showcase content */ -.nsfw-media-wrapper { - position: relative; -} - -.media-wrapper img.blurred, -.media-wrapper video.blurred { - filter: blur(25px); -} - -.media-wrapper .nsfw-overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: flex; - align-items: center; - justify-content: center; - z-index: 2; - pointer-events: none; -} - -/* Position the toggle button at the top left of showcase media */ -.showcase-toggle-btn { - position: absolute; - z-index: 3; -} - -/* Add styles for showcase media controls */ +/* Media Controls for main display */ .media-controls { position: absolute; + top: var(--space-2); + left: var(--space-2); display: flex; gap: 6px; z-index: 4; @@ -94,15 +278,15 @@ pointer-events: none; } -.media-controls.visible { +.media-wrapper:hover .media-controls { opacity: 1; transform: translateY(0); pointer-events: auto; } .media-control-btn { - width: 28px; - height: 28px; + width: 32px; + height: 32px; border-radius: 50%; background: var(--bg-color); border: 1px solid var(--border-color); @@ -135,13 +319,11 @@ border-color: var(--lora-error); } -/* Disabled state for delete button */ .media-control-btn.example-delete-btn.disabled { opacity: 0.5; cursor: not-allowed; } -/* Two-step confirmation for delete button */ .media-control-btn.example-delete-btn .confirm-icon { position: absolute; top: 0; @@ -172,16 +354,29 @@ border-color: var(--lora-error); } -@keyframes pulse { - 0% { - box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.7); - } - 70% { - box-shadow: 0 0 0 5px rgba(220, 53, 69, 0); - } - 100% { - box-shadow: 0 0 0 0 rgba(220, 53, 69, 0); - } +/* Toggle blur button for main display */ +.showcase-toggle-btn { + position: absolute; + top: calc(var(--space-2) + 44px); + left: var(--space-2); + z-index: 3; + width: 32px; + height: 32px; + border-radius: 50%; + background: var(--bg-color); + border: 1px solid var(--border-color); + color: var(--text-color); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15); + opacity: 0; +} + +.media-wrapper:hover .showcase-toggle-btn { + opacity: 1; } /* Image Metadata Panel Styles */ @@ -195,22 +390,20 @@ padding: var(--space-2); transform: translateY(100%); transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.25s ease; - z-index: 5; - max-height: 50%; /* Reduced to take less space */ + z-index: 15; + max-height: 50%; overflow-y: auto; box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1); opacity: 0; pointer-events: none; } -/* Show metadata panel only when the 'visible' class is added */ -.media-wrapper .image-metadata-panel.visible { +.image-metadata-panel.visible { transform: translateY(0); opacity: 0.98; pointer-events: auto; } -/* Adjust to dark theme */ [data-theme="dark"] .image-metadata-panel { background: var(--card-bg); box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.3); @@ -222,7 +415,6 @@ gap: 10px; } -/* Styling for parameters tags */ .params-tags { display: flex; flex-wrap: wrap; @@ -255,7 +447,6 @@ color: var(--lora-accent); } -/* Special styling for prompt row */ .metadata-row.prompt-row { flex-direction: column; padding-top: 0; @@ -281,7 +472,7 @@ border-radius: var(--border-radius-xs); padding: 6px 30px 6px 8px; margin-top: 2px; - max-height: 80px; /* Reduced from 120px */ + max-height: 80px; overflow-y: auto; word-break: break-word; width: 100%; @@ -313,27 +504,6 @@ color: var(--lora-accent); } -/* Scrollbar styling for metadata panel */ -.image-metadata-panel::-webkit-scrollbar { - width: 6px; -} - -.image-metadata-panel::-webkit-scrollbar-track { - background: transparent; -} - -.image-metadata-panel::-webkit-scrollbar-thumb { - background-color: var(--border-color); - border-radius: 3px; -} - -/* For Firefox */ -.image-metadata-panel { - scrollbar-width: thin; - scrollbar-color: var(--border-color) transparent; -} - -/* No metadata message styling */ .no-metadata-message { display: flex; align-items: center; @@ -352,31 +522,66 @@ opacity: 0.8; } -/* Scroll Indicator */ -.scroll-indicator { - cursor: pointer; - padding: var(--space-2); - background: var(--lora-surface); - border: 1px solid var(--lora-border); - border-radius: var(--border-radius-sm); +/* Scrollbar styling for metadata panel */ +.image-metadata-panel::-webkit-scrollbar { + width: 6px; +} + +.image-metadata-panel::-webkit-scrollbar-track { + background: transparent; +} + +.image-metadata-panel::-webkit-scrollbar-thumb { + background-color: var(--border-color); + border-radius: 3px; +} + +.image-metadata-panel { + scrollbar-width: thin; + scrollbar-color: var(--border-color) transparent; +} + +/* NSFW Content Styles */ +.media-wrapper img.blurred, +.media-wrapper video.blurred { + filter: blur(25px); +} + +.media-wrapper .nsfw-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; display: flex; align-items: center; justify-content: center; - gap: 8px; + z-index: 2; + pointer-events: none; +} + +/* NSFW Filter Notification */ +.nsfw-filter-notification { + background: var(--lora-warning); + color: var(--lora-text); + padding: var(--space-2); + border-radius: var(--border-radius-xs); margin-bottom: var(--space-2); - transition: background-color 0.2s, transform 0.2s; -} - -.scroll-indicator:hover { - background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.1); - transform: translateY(-1px); -} - -.scroll-indicator span { + display: flex; + align-items: center; + gap: 8px; font-size: 0.9em; - color: var(--text-color); } +/* No examples message */ +.no-examples { + text-align: center; + padding: var(--space-4); + color: var(--text-color); + opacity: 0.7; +} + +/* Lazy loading */ .lazy { opacity: 0; transition: opacity 0.3s; @@ -386,93 +591,24 @@ opacity: 1; } -/* Example Import Area */ -.example-import-area { - margin-top: var(--space-4); - padding: var(--space-2); -} - -.example-import-area.empty { - margin-top: var(--space-2); - padding: var(--space-4) var(--space-2); -} - -.import-container { - border: 2px dashed var(--border-color); - border-radius: var(--border-radius-sm); - padding: var(--space-4); - text-align: center; - transition: all 0.3s ease; - background: var(--lora-surface); - cursor: pointer; -} - -.import-container.highlight { - border-color: var(--lora-accent); - background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.1); - transform: scale(1.01); -} - -.import-placeholder { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--space-1); - padding-top: var(--space-1); -} - -.import-placeholder i { - font-size: 2.5rem; - /* color: var(--lora-accent); */ - opacity: 0.8; - margin-bottom: var(--space-1); -} - -.import-placeholder h3 { - margin: 0 0 var(--space-1); - font-size: 1.2rem; - font-weight: 500; - color: var(--text-color); -} - -.import-placeholder p { - margin: var(--space-1) 0; - color: var(--text-color); - opacity: 0.8; -} - -.import-placeholder .sub-text { - font-size: 0.9em; - opacity: 0.6; - margin: var(--space-1) 0; -} - -.import-formats { - font-size: 0.8em !important; - opacity: 0.6 !important; - margin-top: var(--space-2) !important; -} - -.select-files-btn { - background: var(--lora-accent); - color: var(--lora-text); - border: none; - border-radius: var(--border-radius-xs); - padding: var(--space-2) var(--space-3); - cursor: pointer; - font-size: 0.9em; - display: flex; - align-items: center; - gap: 8px; - transition: all 0.2s; -} - -.select-files-btn:hover { - opacity: 0.9; - transform: translateY(-1px); -} - /* For dark theme */ -[data-theme="dark"] .import-container { +[data-theme="dark"] .import-drop-zone { background: rgba(255, 255, 255, 0.03); +} + +/* Responsive design for smaller screens */ +@media (max-width: 768px) { + .thumbnail-sidebar { + width: 160px; + } + + .navigation-controls { + top: var(--space-1); + right: var(--space-1); + } + + .nav-btn { + width: 32px; + height: 32px; + } } \ No newline at end of file diff --git a/static/js/components/shared/ModelCard.js b/static/js/components/shared/ModelCard.js index 492f16d2..1ed1d5aa 100644 --- a/static/js/components/shared/ModelCard.js +++ b/static/js/components/shared/ModelCard.js @@ -1,7 +1,6 @@ import { showToast, openCivitai, copyToClipboard, sendLoraToWorkflow, openExampleImagesFolder } from '../../utils/uiHelpers.js'; import { state, getCurrentPageState } from '../../state/index.js'; import { showModelModal } from './ModelModal.js'; -import { toggleShowcase } from './showcase/ShowcaseView.js'; import { bulkManager } from '../../managers/BulkManager.js'; import { modalManager } from '../../managers/ModalManager.js'; import { NSFW_LEVELS } from '../../utils/constants.js'; @@ -340,15 +339,6 @@ function showExampleAccessModal(card, modelType) { tabBtn.click(); } - // Then toggle showcase if collapsed - const carousel = showcaseTab.querySelector('.carousel'); - if (carousel && carousel.classList.contains('collapsed')) { - const scrollIndicator = showcaseTab.querySelector('.scroll-indicator'); - if (scrollIndicator) { - toggleShowcase(scrollIndicator); - } - } - // Finally scroll to the import area importArea.scrollIntoView({ behavior: 'smooth' }); } diff --git a/static/js/components/shared/ModelModal.js b/static/js/components/shared/ModelModal.js index d6246487..006ce2b5 100644 --- a/static/js/components/shared/ModelModal.js +++ b/static/js/components/shared/ModelModal.js @@ -1,9 +1,6 @@ import { showToast, openCivitai } from '../../utils/uiHelpers.js'; import { modalManager } from '../../managers/ModalManager.js'; import { - toggleShowcase, - setupShowcaseScroll, - scrollToTop, loadExampleImages } from './showcase/ShowcaseView.js'; import { setupTabSwitching } from './ModelDescription.js'; @@ -33,7 +30,6 @@ export function showModelModal(model, modelType) { model.civitai.trainedWords.map(word => word.replace(/'/g, '\\\'')) : []; // Generate model type specific content - // const typeSpecificContent = modelType === 'loras' ? renderLoraSpecificContent(model, escapedWords) : ''; let typeSpecificContent; if (modelType === 'loras') { typeSpecificContent = renderLoraSpecificContent(model, escapedWords); @@ -184,17 +180,12 @@ export function showModelModal(model, modelType) {
${tabPanesContent}
- - `; const onCloseCallback = function() { - // Clean up all handlers when modal closes for LoRA const modalElement = document.getElementById(modalId); if (modalElement && modalElement._clickHandler) { modalElement.removeEventListener('click', modalElement._clickHandler); @@ -204,7 +195,6 @@ export function showModelModal(model, modelType) { modalManager.showModal(modalId, content, null, onCloseCallback); setupEditableFields(model.file_path, modelType); - setupShowcaseScroll(modalId); setupTabSwitching(); setupTagTooltip(); setupTagEditMode(); @@ -223,10 +213,9 @@ export function showModelModal(model, modelType) { } } - // Load example images asynchronously - merge regular and custom images + // Load example images asynchronously const regularImages = model.civitai?.images || []; const customImages = model.civitai?.customImages || []; - // Combine images - regular images first, then custom images const allImages = [...regularImages, ...customImages]; loadExampleImages(allImages, model.sha256); } @@ -261,16 +250,14 @@ function renderEmbeddingSpecificContent(embedding, escapedWords) { } /** - * Sets up event handlers using event delegation for LoRA modal + * Sets up event handlers using event delegation for modal * @param {string} filePath - Path to the model file */ function setupEventHandlers(filePath) { const modalElement = document.getElementById('modelModal'); - // Remove existing event listeners first modalElement.removeEventListener('click', handleModalClick); - // Create and store the handler function function handleModalClick(event) { const target = event.target.closest('[data-action]'); if (!target) return; @@ -281,9 +268,6 @@ function setupEventHandlers(filePath) { case 'close-modal': modalManager.closeModal('modelModal'); break; - case 'scroll-to-top': - scrollToTop(target); - break; case 'view-civitai': openCivitai(target.dataset.filepath); break; @@ -296,10 +280,7 @@ function setupEventHandlers(filePath) { } } - // Add the event listener with the named function modalElement.addEventListener('click', handleModalClick); - - // Store reference to the handler on the element for potential cleanup modalElement._clickHandler = handleModalClick; } @@ -421,9 +402,7 @@ async function saveNotes(filePath) { // Export the model modal API const modelModal = { - show: showModelModal, - toggleShowcase, - scrollToTop + show: showModelModal }; export { modelModal }; \ No newline at end of file diff --git a/static/js/components/shared/showcase/MediaUtils.js b/static/js/components/shared/showcase/MediaUtils.js index 5a19749f..17d61f87 100644 --- a/static/js/components/shared/showcase/MediaUtils.js +++ b/static/js/components/shared/showcase/MediaUtils.js @@ -182,119 +182,46 @@ export function getRenderedMediaRect(mediaElement, containerWidth, containerHeig * @param {HTMLElement} container - Container element with media wrappers */ export function initMetadataPanelHandlers(container) { - const mediaWrappers = container.querySelectorAll('.media-wrapper'); + // Metadata panel interaction is now handled by the info button + // Keep the existing copy functionality but remove hover-based visibility + const metadataPanel = container.querySelector('.image-metadata-panel'); - mediaWrappers.forEach(wrapper => { - // Get the metadata panel and media element (img or video) - const metadataPanel = wrapper.querySelector('.image-metadata-panel'); - const mediaControls = wrapper.querySelector('.media-controls'); - const mediaElement = wrapper.querySelector('img, video'); - - if (!mediaElement) return; - - let isOverMetadataPanel = false; - - // Add event listeners to the wrapper for mouse tracking - wrapper.addEventListener('mousemove', (e) => { - // Get mouse position relative to wrapper - const rect = wrapper.getBoundingClientRect(); - const mouseX = e.clientX - rect.left; - const mouseY = e.clientY - rect.top; - - // Get the actual displayed dimensions of the media element - const mediaRect = getRenderedMediaRect(mediaElement, rect.width, rect.height); - - // Check if mouse is over the actual media content - const isOverMedia = ( - mouseX >= mediaRect.left && - mouseX <= mediaRect.right && - mouseY >= mediaRect.top && - mouseY <= mediaRect.bottom - ); - - // Show metadata panel and controls when over media content or metadata panel itself - if (isOverMedia || isOverMetadataPanel) { - if (metadataPanel) metadataPanel.classList.add('visible'); - if (mediaControls) mediaControls.classList.add('visible'); - } else { - if (metadataPanel) metadataPanel.classList.remove('visible'); - if (mediaControls) mediaControls.classList.remove('visible'); - } + if (metadataPanel) { + // Prevent events from bubbling + metadataPanel.addEventListener('click', (e) => { + e.stopPropagation(); }); - wrapper.addEventListener('mouseleave', () => { - if (!isOverMetadataPanel) { - if (metadataPanel) metadataPanel.classList.remove('visible'); - if (mediaControls) mediaControls.classList.remove('visible'); - } - }); - - // Add mouse enter/leave events for the metadata panel itself - if (metadataPanel) { - metadataPanel.addEventListener('mouseenter', () => { - isOverMetadataPanel = true; - metadataPanel.classList.add('visible'); - if (mediaControls) mediaControls.classList.add('visible'); - }); + // Handle copy prompt buttons + const copyBtns = metadataPanel.querySelectorAll('.copy-prompt-btn'); + copyBtns.forEach(copyBtn => { + const promptIndex = copyBtn.dataset.promptIndex; + const promptElement = container.querySelector(`#prompt-${promptIndex}`); - metadataPanel.addEventListener('mouseleave', () => { - isOverMetadataPanel = false; - // Only hide if mouse is not over the media - const rect = wrapper.getBoundingClientRect(); - const mediaRect = getRenderedMediaRect(mediaElement, rect.width, rect.height); - const mouseX = event.clientX - rect.left; - const mouseY = event.clientY - rect.top; - - const isOverMedia = ( - mouseX >= mediaRect.left && - mouseX <= mediaRect.right && - mouseY >= mediaRect.top && - mouseY <= mediaRect.bottom - ); - - if (!isOverMedia) { - metadataPanel.classList.remove('visible'); - if (mediaControls) mediaControls.classList.remove('visible'); - } - }); - - // Prevent events from bubbling - metadataPanel.addEventListener('click', (e) => { + copyBtn.addEventListener('click', async (e) => { e.stopPropagation(); - }); - - // Handle copy prompt buttons - 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(); - - if (!promptElement) return; - - try { - await copyToClipboard(promptElement.textContent, 'Prompt copied to clipboard'); - } catch (err) { - console.error('Copy failed:', err); - showToast('Copy failed', 'error'); - } - }); - }); - - // Prevent panel scroll from causing modal scroll - metadataPanel.addEventListener('wheel', (e) => { - const isAtTop = metadataPanel.scrollTop === 0; - const isAtBottom = metadataPanel.scrollHeight - metadataPanel.scrollTop === metadataPanel.clientHeight; + if (!promptElement) return; - // Only prevent default if scrolling would cause the panel to scroll - if ((e.deltaY < 0 && !isAtTop) || (e.deltaY > 0 && !isAtBottom)) { - e.stopPropagation(); + try { + await copyToClipboard(promptElement.textContent, 'Prompt copied to clipboard'); + } catch (err) { + console.error('Copy failed:', err); + showToast('Copy failed', 'error'); } - }, { passive: true }); - } - }); + }); + }); + + // Prevent panel scroll from causing modal scroll + metadataPanel.addEventListener('wheel', (e) => { + const isAtTop = metadataPanel.scrollTop === 0; + const isAtBottom = metadataPanel.scrollHeight - metadataPanel.scrollTop === metadataPanel.clientHeight; + + if ((e.deltaY < 0 && !isAtTop) || (e.deltaY > 0 && !isAtBottom)) { + e.stopPropagation(); + } + }, { passive: true }); + } } /** @@ -366,9 +293,8 @@ export function initMediaControlHandlers(container) { btn.addEventListener('click', async function(e) { e.stopPropagation(); - // Explicitly check for disabled state if (this.classList.contains('disabled')) { - return; // Don't do anything if button is disabled + return; } const shortId = this.dataset.shortId; @@ -376,14 +302,11 @@ export function initMediaControlHandlers(container) { if (!shortId) return; - // Handle two-step confirmation if (btnState === 'initial') { - // First click: show confirmation state this.dataset.state = 'confirm'; this.classList.add('confirm'); this.title = 'Click again to confirm deletion'; - // Auto-reset after 3 seconds setTimeout(() => { if (this.dataset.state === 'confirm') { this.dataset.state = 'initial'; @@ -395,19 +318,16 @@ export function initMediaControlHandlers(container) { return; } - // Second click within 3 seconds: proceed with deletion if (btnState === 'confirm') { this.disabled = true; this.classList.remove('confirm'); this.innerHTML = ''; - // Get model hash from URL or data attribute const mediaWrapper = this.closest('.media-wrapper'); const modelHashAttr = document.querySelector('.showcase-section')?.dataset; const modelHash = modelHashAttr?.modelHash; try { - // Call the API to delete the custom example const response = await fetch('/api/delete-example-image', { method: 'POST', headers: { @@ -422,32 +342,45 @@ export function initMediaControlHandlers(container) { const result = await response.json(); if (result.success) { - // Success: remove the media wrapper from the DOM - mediaWrapper.style.opacity = '0'; - mediaWrapper.style.height = '0'; - mediaWrapper.style.transition = 'opacity 0.3s ease, height 0.3s ease 0.3s'; + // Remove the corresponding thumbnail and update main display + const thumbnailItem = container.querySelector(`.thumbnail-item[data-short-id="${shortId}"]`); + if (thumbnailItem) { + const wasActive = thumbnailItem.classList.contains('active'); + thumbnailItem.remove(); + + // If the deleted item was active, select next item + if (wasActive) { + const remainingThumbnails = container.querySelectorAll('.thumbnail-item'); + if (remainingThumbnails.length > 0) { + remainingThumbnails[0].click(); + } else { + // No more items, show empty state + const mainContainer = container.querySelector('#mainMediaContainer'); + if (mainContainer) { + mainContainer.innerHTML = ` +
+ +

No example images available

+

Import images or videos using the sidebar

+
+ `; + } + } + } + } - setTimeout(() => { - mediaWrapper.remove(); - }, 600); - - // Show success toast showToast('Example image deleted', 'success'); - // Create an update object with only the necessary properties const updateData = { civitai: { customImages: result.custom_images || [] } }; - // Update the item in the virtual scroller state.virtualScroller.updateSingleItem(result.model_file_path, updateData); } else { - // Show error message showToast(result.error || 'Failed to delete example image', 'error'); - // Reset button state this.disabled = false; this.dataset.state = 'initial'; this.classList.remove('confirm'); @@ -458,7 +391,6 @@ export function initMediaControlHandlers(container) { console.error('Error deleting example image:', error); showToast('Failed to delete example image', 'error'); - // Reset button state this.disabled = false; this.dataset.state = 'initial'; this.classList.remove('confirm'); @@ -469,11 +401,7 @@ export function initMediaControlHandlers(container) { }); }); - // Initialize set preview buttons initSetPreviewHandlers(container); - - // Media control visibility is now handled in initMetadataPanelHandlers - // Any click handlers or other functionality can still be added here } /** @@ -544,50 +472,4 @@ function initSetPreviewHandlers(container) { } }); }); -} - -/** - * Position media controls within the actual rendered media rectangle - * @param {HTMLElement} mediaWrapper - The wrapper containing the media and controls - */ -export function positionMediaControlsInMediaRect(mediaWrapper) { - const mediaElement = mediaWrapper.querySelector('img, video'); - const controlsElement = mediaWrapper.querySelector('.media-controls'); - - if (!mediaElement || !controlsElement) return; - - // Get wrapper dimensions - const wrapperRect = mediaWrapper.getBoundingClientRect(); - - // Calculate the actual rendered media rectangle - const mediaRect = getRenderedMediaRect( - mediaElement, - wrapperRect.width, - wrapperRect.height - ); - - // Calculate the position for controls - place them inside the actual media area - const padding = 8; // Padding from the edge of the media - - // Position at top-right inside the actual media rectangle - controlsElement.style.top = `${mediaRect.top + padding}px`; - controlsElement.style.right = `${wrapperRect.width - mediaRect.right + padding}px`; - - // Also position any toggle blur buttons in the same way but on the left - const toggleBlurBtn = mediaWrapper.querySelector('.toggle-blur-btn'); - if (toggleBlurBtn) { - toggleBlurBtn.style.top = `${mediaRect.top + padding}px`; - toggleBlurBtn.style.left = `${mediaRect.left + padding}px`; - } -} - -/** - * Position all media controls in a container - * @param {HTMLElement} container - Container with media wrappers - */ -export function positionAllMediaControls(container) { - const mediaWrappers = container.querySelectorAll('.media-wrapper'); - mediaWrappers.forEach(wrapper => { - positionMediaControlsInMediaRect(wrapper); - }); } \ No newline at end of file diff --git a/static/js/components/shared/showcase/MetadataPanel.js b/static/js/components/shared/showcase/MetadataPanel.js index f34d9b03..96079c36 100644 --- a/static/js/components/shared/showcase/MetadataPanel.js +++ b/static/js/components/shared/showcase/MetadataPanel.js @@ -23,6 +23,7 @@ export function generateMetadataPanel(hasParams, hasPrompts, prompt, negativePro const promptIndex = Math.random().toString(36).substring(2, 15); const negPromptIndex = Math.random().toString(36).substring(2, 15); + // Note: Panel visibility is now controlled by the info button, not hover let content = '
'; if (hasParams) { diff --git a/static/js/components/shared/showcase/ShowcaseView.js b/static/js/components/shared/showcase/ShowcaseView.js index d57725e8..8120c1b3 100644 --- a/static/js/components/shared/showcase/ShowcaseView.js +++ b/static/js/components/shared/showcase/ShowcaseView.js @@ -9,8 +9,7 @@ import { initLazyLoading, initNsfwBlurHandlers, initMetadataPanelHandlers, - initMediaControlHandlers, - positionAllMediaControls + initMediaControlHandlers } from './MediaUtils.js'; import { generateMetadataPanel } from './MetadataPanel.js'; import { generateImageWrapper, generateVideoWrapper } from './MediaRenderers.js'; @@ -46,13 +45,10 @@ export async function loadExampleImages(images, modelHash) { showcaseTab.innerHTML = renderShowcaseContent(images, localFiles); // Re-initialize the showcase event listeners - const carousel = showcaseTab.querySelector('.carousel'); - if (carousel && !carousel.classList.contains('collapsed')) { - initShowcaseContent(carousel); - } + initShowcaseContent(showcaseTab); // Initialize the example import functionality - initExampleImport(modelHash, showcaseTab); + // initExampleImport(modelHash, showcaseTab); } catch (error) { console.error('Error loading example images:', error); const showcaseTab = document.getElementById('showcase-tab'); @@ -71,13 +67,13 @@ export async function loadExampleImages(images, modelHash) { * Render showcase content * @param {Array} images - Array of images/videos to show * @param {Array} exampleFiles - Local example files - * @param {boolean} startExpanded - Whether to start in expanded state + * @param {boolean} startExpanded - Whether to start in expanded state (unused in new design) * @returns {string} HTML content */ export function renderShowcaseContent(images, exampleFiles = [], startExpanded = false) { if (!images?.length) { // Show empty state with import interface - return renderImportInterface(true); + return renderEmptyShowcase(); } // Filter images based on SFW setting @@ -112,29 +108,69 @@ export function renderShowcaseContent(images, exampleFiles = [], startExpanded =
` : ''; return ` -
- - Scroll or click to ${startExpanded ? 'hide' : 'show'} ${filteredImages.length} examples -
-