diff --git a/web/src/index.css b/web/src/index.css index 1bcc267..aadda3f 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -430,7 +430,8 @@ video::cue(*) { .animate-overlay-in { animation: overlay-in 0.15s ease-out both; } .animate-drawer-in { animation: drawer-in 0.22s ease-out both; } -.animate-page-in { animation: page-in 0.25s ease-out both; } +/* 播放完成后不能保留 transform,否则长页面会成为 fixed 弹框的包含块。 */ +.animate-page-in { animation: page-in 0.25s ease-out backwards; } .animate-accordion-in { animation: accordion-in 0.18s ease-out both; } .animate-fade-slide-in { animation: fade-slide-in 0.2s ease-out both; } .animate-menu-in { animation: menu-in 0.15s ease-out both; } diff --git a/web/src/pages/HomePage.tsx b/web/src/pages/HomePage.tsx index 9444ddf..3777c3e 100644 --- a/web/src/pages/HomePage.tsx +++ b/web/src/pages/HomePage.tsx @@ -151,33 +151,30 @@ export function HomePage() { ) }, []) - // 3. 首屏只预取轮播和前三行所需的预览;入口卡片进入视口后再按批加载。 + // 3. 首屏只预取轮播和前三行所需的预览;入口卡片进入视口后自行按批加载。 useEffect(() => { if (sortedLibraries.length === 0) return const carouselLibIds = sortedLibraries .filter((l) => l.carousel_enabled === true) .map((l) => l.id) const topRowLibIds = sortedLibraries.slice(0, 3).map((l) => l.id) - // 入口网格仍然是一次展示 20 个库;没有自定义封面的库必须在首屏 - // 立即拉预览,否则会先出现占位图标再补图,体感反而更慢。 - const topGridLibIds = sortedLibraries - .slice(0, 20) - .filter((l) => !l.cover_url) - .map((l) => l.id) const shelfTargets = Array.from(new Set([...carouselLibIds, ...topRowLibIds])) - const shelfTargetSet = new Set(shelfTargets) - const gridTargets = topGridLibIds.filter((id) => !shelfTargetSet.has(id)) - // 入口网格只需要 2 张封面,横向货架才需要 10 张。分开请求可以避免 - // 为暂时不会出现的货架预取完整卡片窗口。 void fetchPreviews(shelfTargets, 10) - void fetchPreviews(gridTargets, 2) }, [sortedLibraries, fetchPreviews]) // 4. 媒体库展示行渐进流式加载:默认先检视前 3 个库,随向下滚动逐步检视后续库 const INITIAL_ROWS = 3 const STEP_ROWS = 2 const [visibleTargetCount, setVisibleTargetCount] = useState(INITIAL_ROWS) - const sentinelRef = useRef(null) + const sentinelRef = useRef(null) + const userScrolledRef = useRef(false) + + const revealMoreLibraries = useCallback(() => { + setVisibleTargetCount((prev) => { + if (prev >= sortedLibraries.length) return prev + return Math.min(prev + STEP_ROWS, sortedLibraries.length) + }) + }, [sortedLibraries.length]) // 随 visibleTargetCount 增加,按需触发后续库的预览加载 useEffect(() => { @@ -201,12 +198,11 @@ export function HomePage() { if (!scrollParent) return const handleCheckBottom = () => { + if (scrollParent.scrollTop <= 0) return + userScrolledRef.current = true const remaining = scrollParent.scrollHeight - scrollParent.scrollTop - scrollParent.clientHeight if (remaining < 600) { - setVisibleTargetCount((prev) => { - if (prev >= sortedLibraries.length) return prev - return Math.min(prev + STEP_ROWS, sortedLibraries.length) - }) + revealMoreLibraries() } } @@ -218,11 +214,8 @@ export function HomePage() { observer = new IntersectionObserver( (entries) => { const [entry] = entries - if (entry?.isIntersecting) { - setVisibleTargetCount((prev) => { - if (prev >= sortedLibraries.length) return prev - return Math.min(prev + STEP_ROWS, sortedLibraries.length) - }) + if (entry?.isIntersecting && userScrolledRef.current) { + revealMoreLibraries() } }, { @@ -234,15 +227,13 @@ export function HomePage() { observer.observe(sentinel) } - handleCheckBottom() - return () => { scrollParent.removeEventListener('scroll', handleCheckBottom) if (observer) { observer.disconnect() } } - }, [visibleLibraries.length, hasMoreLibraries, sortedLibraries.length]) + }, [hasMoreLibraries, revealMoreLibraries, visibleLibraries.length]) // Quick lookup map for libraries const libraryMap = useMemo(() => { @@ -333,7 +324,7 @@ export function HomePage() { )} {/* 4. 各媒体库内容展示行(向下滑动渐进流式加载,不受上方20个分页限制) */} - {visibleLibraries.length > 0 && ( + {(visibleLibraries.length > 0 || hasMoreLibraries) && (
{visibleLibraries.map((lib) => { const cards = libraryData[lib.id]?.cards || [] @@ -346,12 +337,17 @@ export function HomePage() { ) })} {hasMoreLibraries && ( -
+ )}
)} diff --git a/web/src/pages/LibrariesPageSections.tsx b/web/src/pages/LibrariesPageSections.tsx index d9c4f46..f5ad37c 100644 --- a/web/src/pages/LibrariesPageSections.tsx +++ b/web/src/pages/LibrariesPageSections.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Link } from 'react-router-dom' import { Library as LibraryIcon, RefreshCw, Sparkles } from 'lucide-react' @@ -95,7 +95,15 @@ export function LibrariesContent({ const INITIAL_SHELVES = 3 const STEP_SHELVES = 2 const [visibleCount, setVisibleCount] = useState(INITIAL_SHELVES) - const sentinelRef = useRef(null) + const sentinelRef = useRef(null) + const userScrolledRef = useRef(false) + + const revealMoreShelves = useCallback(() => { + setVisibleCount((prev) => { + if (prev >= previews.length) return prev + return Math.min(prev + STEP_SHELVES, previews.length) + }) + }, [previews.length]) useEffect(() => { const currentTargets = previews.slice(0, visibleCount).map((preview) => preview.library.id) @@ -107,12 +115,11 @@ export function LibrariesContent({ if (!scrollParent) return const handleCheckBottom = () => { + if (scrollParent.scrollTop <= 0) return + userScrolledRef.current = true const remaining = scrollParent.scrollHeight - scrollParent.scrollTop - scrollParent.clientHeight if (remaining < 600) { - setVisibleCount((prev) => { - if (prev >= previews.length) return prev - return Math.min(prev + STEP_SHELVES, previews.length) - }) + revealMoreShelves() } } @@ -124,11 +131,8 @@ export function LibrariesContent({ observer = new IntersectionObserver( (entries) => { const [entry] = entries - if (entry?.isIntersecting) { - setVisibleCount((prev) => { - if (prev >= previews.length) return prev - return Math.min(prev + STEP_SHELVES, previews.length) - }) + if (entry?.isIntersecting && userScrolledRef.current) { + revealMoreShelves() } }, { @@ -140,13 +144,11 @@ export function LibrariesContent({ observer.observe(sentinel) } - handleCheckBottom() - return () => { scrollParent.removeEventListener('scroll', handleCheckBottom) if (observer) observer.disconnect() } - }, [visibleCount, previews.length]) + }, [revealMoreShelves, visibleCount, previews.length]) const visiblePreviews = useMemo( () => previews.slice(0, visibleCount), @@ -196,12 +198,17 @@ export function LibrariesContent({ ))} {visibleCount < previews.length && ( -
+ )} )} diff --git a/web/src/pages/useLibraryData.ts b/web/src/pages/useLibraryData.ts index 7e04cd5..a697e2b 100644 --- a/web/src/pages/useLibraryData.ts +++ b/web/src/pages/useLibraryData.ts @@ -86,7 +86,7 @@ export function useLibraryData( const seriesMode = isSeriesLibraryType(lib.type) modeRef.current = seriesMode ? 'series' : 'media' - const pageSize = pageSizeFor(lib, seriesMode) + const pageSize = pageSizeFor(lib) try { if (seriesMode) { const data = await libraryAPI.listSeries(libraryID, 1, pageSize, { sort: sortField, order: sortOrder }) @@ -155,7 +155,7 @@ export function useLibraryData( try { const page = nextPageRef.current if (modeRef.current === 'series') { - const data = await libraryAPI.listSeries(libraryID, page, pageSizeFor(lib, true), { + const data = await libraryAPI.listSeries(libraryID, page, pageSizeFor(lib), { sort: sortField, order: sortOrder, }) @@ -167,7 +167,7 @@ export function useLibraryData( nextPageRef.current = page + 1 hasMoreRef.current = pageItems.length > 0 && loadedCountRef.current < totalRef.current } else { - const data = await libraryAPI.listMedia(libraryID, page, pageSizeFor(lib, false), { + const data = await libraryAPI.listMedia(libraryID, page, pageSizeFor(lib), { sort: sortField, order: sortOrder, }) @@ -238,9 +238,11 @@ function isSeriesLibraryType(type?: string) { return type === 'tv' || type === 'anime' || type === 'variety' } -function pageSizeFor(lib: Library, seriesMode: boolean): number { - if (lib.is_remote_emby) return 100 - return seriesMode ? 500 : 2000 +function pageSizeFor(lib: Library): number { + // 首屏保持一屏多一点,后续统一由底部哨兵按页追加。此前沿用旧的 + // 500/2000 批量值,会让中小型媒体库在第一次请求时就等于全量加载。 + if (lib.is_remote_emby) return 50 + return 60 } function yieldToBrowser(): Promise { @@ -251,4 +253,4 @@ function yieldToBrowser(): Promise { setTimeout(resolve, 0) } }) -} \ No newline at end of file +}