diff --git a/static/css/components/modal.css b/static/css/components/modal.css index a02a8575..2420a73f 100644 --- a/static/css/components/modal.css +++ b/static/css/components/modal.css @@ -29,6 +29,8 @@ body.modal-open { border: 1px solid var(--lora-border); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); transform: translateZ(0); /* 创建新的堆叠上下文 */ + max-height: 90vh; /* 限制最大高度为视窗高度的90% */ + overflow-y: auto; /* 添加垂直滚动条 */ } /* Delete Modal specific styles */ @@ -617,44 +619,66 @@ body.modal-open { } .carousel { - display: flex; - flex-direction: column; /* 改为垂直布局 */ - gap: var(--space-3); - overflow-y: auto; /* 垂直滚动 */ + transition: max-height 0.3s ease-in-out; + overflow: hidden; } -.carousel img, -.carousel video { +.carousel.collapsed { + max-height: 0; +} + +.carousel-container { + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.media-wrapper { + position: relative; width: 100%; - height: auto; - max-height: 70vh; /* 限制单张图片最大高度 */ + background: var(--lora-surface); + margin-bottom: var(--space-2); +} + +.media-wrapper:last-child { + margin-bottom: 0; +} + +.media-wrapper img, +.media-wrapper video { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; object-fit: contain; - border-radius: var(--border-radius-base); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } /* Scroll Indicator */ .scroll-indicator { - position: sticky; - top: 0; - left: 0; - right: 0; - text-align: center; + cursor: pointer; padding: var(--space-2); - background: linear-gradient(to bottom, - var(--lora-surface) 0%, - transparent 100%); - opacity: 0.8; - transition: opacity 0.3s; - pointer-events: none; - z-index: 1; /* 确保滚动提示在图片上层 */ + background: var(--lora-surface); + border: 1px solid var(--lora-border); + border-radius: var(--border-radius-sm); + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + margin-bottom: var(--space-2); } -/* Remove horizontal scrolling styles */ -.carousel { - scroll-snap-type: none; - grid-auto-flow: initial; - overflow-x: initial; +.scroll-indicator:hover { + background: oklch(var(--lora-accent) / 0.1); +} + +.lazy { + opacity: 0; + transition: opacity 0.3s; +} + +.lazy[src] { + opacity: 1; } /* Update Trigger Words styles */ diff --git a/static/js/components/LoraCard.js b/static/js/components/LoraCard.js index fa648172..99a35660 100644 --- a/static/js/components/LoraCard.js +++ b/static/js/components/LoraCard.js @@ -171,15 +171,7 @@ export function showLoraModal(lora) { -
-
- - Scroll for more examples -
- -
+ ${renderShowcaseImages(lora.civitai.images)} `; @@ -275,21 +267,101 @@ function renderTriggerWords(words) { function renderShowcaseImages(images) { if (!images?.length) return ''; - return images.map(img => { - if (img.type === 'video') { - return ` - - `; + return ` +
+
+ + Show ${images.length} examples +
+ +
+ `; +} + +// Add this to the window object for global access +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) { + indicator.textContent = 'Hide examples'; + icon.classList.replace('fa-chevron-down', 'fa-chevron-up'); + initLazyLoading(carousel); + } else { + const count = carousel.querySelectorAll('.media-wrapper').length; + indicator.textContent = `Show ${count} examples`; + icon.classList.replace('fa-chevron-up', 'fa-chevron-down'); + } +}; + +// Add lazy loading initialization +export 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; } - return ` - Preview - `; - }).join(''); + 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)); } \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js index 2bc4ad1e..bbc2ab9b 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -2,7 +2,7 @@ import { debounce } from './utils/debounce.js'; import { LoadingManager } from './managers/LoadingManager.js'; import { modalManager } from './managers/ModalManager.js'; import { state } from './state/index.js'; -import { showLoraModal } from './components/LoraCard.js'; +import { showLoraModal, toggleShowcase, initLazyLoading } from './components/LoraCard.js'; import { loadMoreLoras, fetchCivitai, deleteModel, replacePreview, resetAndReload, refreshLoras } from './api/loraApi.js'; import { showToast, @@ -45,6 +45,7 @@ window.toggleFolderTags = toggleFolderTags; window.settingsManager = new SettingsManager(); window.toggleApiKeyVisibility = toggleApiKeyVisibility; window.moveManager = moveManager; +window.toggleShowcase = toggleShowcase; // Initialize everything when DOM is ready document.addEventListener('DOMContentLoaded', () => { @@ -58,6 +59,7 @@ document.addEventListener('DOMContentLoaded', () => { initTheme(); initFolderTagsVisibility(); initBackToTop(); + initLazyLoading(); window.searchManager = new SearchManager(); new LoraContextMenu(); });