import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import { Link } from 'react-router-dom' import { motion } from 'framer-motion' import { ArrowRight, ChevronLeft, ChevronRight, Film, FolderOpen, Library as LibraryIcon, Music, Pin, PlayCircle, RefreshCw, Sparkles, Tv } from 'lucide-react' import { imageURL } from '../api/client' import { EpisodeArtworkToggle } from '../components/EpisodeArtworkToggle' import { MediaCard } from '../components/MediaCard' import { seriesCardLink } from '../utils/groupSeries' import { libraryDisplayPath } from './libraryDisplayModel' import { libraryArtworkItems, type LibraryPreview } from './librariesPageModel' import { isLibraryPinned } from '../utils/pinnedLibraries' const TYPE_ICONS: Record = { movie: , tv: , anime: , variety: , music: , adult: , } const TYPE_LABELS: Record = { movie: '电影', tv: '剧集', anime: '动漫', variety: '综艺', music: '音乐', adult: '成人', } export function LibrariesHeader({ previewCount, total, repairMsg, repairEpisodeArtwork, repairing, onRepairEpisodeArtworkChange, onRepairRescrape, onManageLibraries, }: { previewCount: number total: number repairMsg: string repairEpisodeArtwork: boolean repairing: boolean onRepairEpisodeArtworkChange: (value: boolean) => void onRepairRescrape: () => void onManageLibraries: () => void }) { return (

媒体库

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

{repairMsg && {repairMsg}} 刮削队列
) } export function LibrariesEmptyState() { return (

暂无媒体库,请到管理后台添加目录。

) } export function LibrariesContent({ previews, pinnedIds, onTogglePin, onNeedPreviews, }: { previews: LibraryPreview[] pinnedIds: string[] onTogglePin: (libraryId: string) => void onNeedPreviews?: (ids: string[]) => void }) { const pinnedCount = previews.filter((preview) => isLibraryPinned(preview.library.id, pinnedIds)).length // 下方媒体库货架支持向下滑动渐进流式加载:默认先展示前 3 个库货架, // 随着用户向下滑动接近底部,通过 IntersectionObserver 动态解锁后续媒体库货架。 const INITIAL_SHELVES = 3 const STEP_SHELVES = 2 const [visibleCount, setVisibleCount] = useState(INITIAL_SHELVES) const sentinelRef = useRef(null) useEffect(() => { const currentTargets = previews.slice(0, visibleCount).map((p) => p.library.id) onNeedPreviews?.(currentTargets) }, [previews, visibleCount, onNeedPreviews]) useEffect(() => { const sentinel = sentinelRef.current if (!sentinel) return const scrollParent = document.getElementById('app-main-scroll') const 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) }) } }, { root: scrollParent, rootMargin: '400px 0px', threshold: 0, }, ) observer.observe(sentinel) return () => { observer.disconnect() } }, [previews.length]) const visiblePreviews = useMemo(() => { return previews.slice(0, visibleCount) }, [previews, visibleCount]) const ENTRY_PAGE_SIZE = 20 const [entryPage, setEntryPage] = useState(1) const totalEntryPages = Math.max(1, Math.ceil(previews.length / ENTRY_PAGE_SIZE)) const effectiveEntryPage = Math.min(entryPage, totalEntryPages) const pagedPreviews = useMemo(() => { const start = (effectiveEntryPage - 1) * ENTRY_PAGE_SIZE return previews.slice(start, start + ENTRY_PAGE_SIZE) }, [previews, effectiveEntryPage]) return ( <>

媒体库入口

{previews.length > ENTRY_PAGE_SIZE && ( 共 {previews.length} 个 · 每页 {ENTRY_PAGE_SIZE} 个 )}

按目录进入完整媒体库;下方每个目录也会直接展示最新内容。 {pinnedCount > 0 ? ` 已置顶 ${pinnedCount} 个媒体库。` : ' 点击卡片右上角图钉可置顶常用媒体库。'}

{totalEntryPages > 1 && (
{effectiveEntryPage} / {totalEntryPages}
)}
{pagedPreviews.map((preview, index) => ( onTogglePin(preview.library.id)} /> ))}
{visiblePreviews.length > 0 && (
{visiblePreviews.map((preview, index) => ( ))} {visibleCount < previews.length && (
加载更多媒体库货架…
)}
)} ) } function LibraryEntryCard({ preview, pinned, onTogglePin, }: { preview: LibraryPreview pinned: boolean onTogglePin: () => void }) { const library = preview.library const artwork = library.cover_url ? [{ src: library.cover_url, version: library.updated_at }] : libraryArtworkItems(preview.cards) const displayPath = libraryDisplayPath(library.path) return (
1 ? 'grid-cols-2' : 'grid-cols-1'}`}> {artwork.length > 0 ? ( artwork.map(({ src, version }, index) => ( { event.currentTarget.style.visibility = 'hidden' }} /> )) ) : (
{TYPE_ICONS[library.type] ?? }
)}
{TYPE_LABELS[library.type] ?? library.type} {preview.total.toLocaleString()} 个条目

{library.name} {pinned ? 置顶 : null}

{displayPath}

{preview.total.toLocaleString()} 个条目 浏览全部
) } function LibraryShelf({ preview, pinned }: { preview: LibraryPreview; pinned?: boolean }) { const library = preview.library const cards = preview.cards.slice(0, 10) const displayPath = libraryDisplayPath(library.path) return (
{TYPE_ICONS[library.type] ?? } {TYPE_LABELS[library.type] ?? library.type}

{library.name} {pinned ? 置顶 : null}

{displayPath} · {preview.total.toLocaleString()} 个条目 · 最新 {cards.length} 部

浏览全部
{cards.length > 0 ? (
{cards.map((card) => (
))}
) : (
该目录暂无可展示内容,扫描媒体库后会出现在这里。
)}
) }