import { useCallback, useEffect, useMemo, useRef } from 'react' import { Link } from 'react-router-dom' import { Library as LibraryIcon, RefreshCw, Sparkles } from 'lucide-react' import { EpisodeArtworkToggle } from '../components/EpisodeArtworkToggle' import { LibraryListSortDropdown } from '../components/LibraryListSortDropdown' import { useRememberedListPosition } from '../hooks/useListPositionMemory' import type { LibraryListSortField, LibraryListSortOrder } from '../utils/libraryListSort' import { HomeLibrariesSection, HomeLibraryRowSection } from './HomePageSections' import type { LibraryPreview } from './librariesPageModel' // 返回媒体库列表时最多一次性解锁的货架数量:滚动到很深的位置后回来 // 仍能落到原处,同时避免一次渲染过多货架。上限之外的继续按需解锁。 const MAX_RESTORED_SHELVES = 15 export function LibrariesHeader({ previewCount, total, isAdmin, repairMsg, repairEpisodeArtwork, repairing, sortField, sortOrder, onSortChange, onRepairEpisodeArtworkChange, onRepairRescrape, onManageLibraries, }: { previewCount: number total: number isAdmin: boolean repairMsg: string repairEpisodeArtwork: boolean repairing: boolean sortField: LibraryListSortField sortOrder: LibraryListSortOrder onSortChange: (field: LibraryListSortField, order: LibraryListSortOrder) => void onRepairEpisodeArtworkChange: (value: boolean) => void onRepairRescrape: () => void onManageLibraries: () => void }) { return (

媒体库

共 {previewCount} 个目录 · {total.toLocaleString()} 个条目。每个目录直接展示最新入库内容。

{isAdmin && ( <> {repairMsg && {repairMsg}} 刮削队列 )}
) } export function LibrariesEmptyState({ isAdmin = false }: { isAdmin?: boolean }) { return (

{isAdmin ? '暂无媒体库,请到管理后台添加目录。' : '暂无可用媒体库。'}

) } export function LibrariesContent({ previews, pinnedIds, onTogglePin, onNeedPreviews, }: { previews: LibraryPreview[] pinnedIds: string[] onTogglePin: (libraryId: string) => void onNeedPreviews?: (ids: string[], limit?: number) => void }) { // 下方媒体库货架继续按需解锁:首屏先展示前 3 个,滚动接近底部再加载 2 个。 const INITIAL_SHELVES = 3 const STEP_SHELVES = 2 // 货架解锁数量同样跨路由记忆:进入媒体库详情再返回不会从头解锁。 const [visibleCount, setVisibleCount] = useRememberedListPosition( 'libraries-shelves', INITIAL_SHELVES, MAX_RESTORED_SHELVES, ) 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, setVisibleCount]) useEffect(() => { const currentTargets = previews.slice(0, visibleCount).map((preview) => preview.library.id) onNeedPreviews?.(currentTargets, 10) }, [previews, visibleCount, onNeedPreviews]) useEffect(() => { const scrollParent = document.getElementById('app-main-scroll') if (!scrollParent) return const handleCheckBottom = () => { if (scrollParent.scrollTop <= 0) return userScrolledRef.current = true const remaining = scrollParent.scrollHeight - scrollParent.scrollTop - scrollParent.clientHeight if (remaining < 600) { revealMoreShelves() } } scrollParent.addEventListener('scroll', handleCheckBottom, { passive: true }) const sentinel = sentinelRef.current let observer: IntersectionObserver | null = null if (sentinel) { observer = new IntersectionObserver( (entries) => { const [entry] = entries if (entry?.isIntersecting && userScrolledRef.current) { revealMoreShelves() } }, { root: scrollParent, rootMargin: '600px 0px', threshold: 0, }, ) observer.observe(sentinel) } return () => { scrollParent.removeEventListener('scroll', handleCheckBottom) if (observer) observer.disconnect() } }, [revealMoreShelves, visibleCount, previews.length]) const visiblePreviews = useMemo( () => previews.slice(0, visibleCount), [previews, visibleCount], ) const libraries = useMemo(() => previews.map((preview) => preview.library), [previews]) const libraryData = useMemo( () => Object.fromEntries( previews.map((preview) => [ preview.library.id, { cards: preview.cards, items: [], total: preview.total }, ]), ), [previews], ) const libraryCounts = useMemo( () => Object.fromEntries( previews.map((preview) => [preview.library.id, preview.total]), ), [previews], ) return ( <> {visiblePreviews.length > 0 && (
{visiblePreviews.map((preview) => (
))} {visibleCount < previews.length && ( )}
)} ) }