import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import { Link, useLocation } from 'react-router-dom' import { ArrowRight, ChevronLeft, ChevronRight, Clock, Film, FolderOpen, Library as LibraryIcon, Music, Pin, Play, PlayCircle, Sparkles, Star, Tv, } from 'lucide-react' import { imageURL } from '../api/client' import { useInViewOnce } from '../hooks/useInViewOnce' import { useLazyPreviewBatch } from '../hooks/useLazyPreviewBatch' import { useRememberedListPosition } from '../hooks/useListPositionMemory' import { MediaCard } from '../components/MediaCard' import type { HistoryItem } from '../api/playback' import type { Library, Media } from '../types' import type { SeriesCard } from '../utils/groupSeries' import { seriesCardLink } from '../utils/groupSeries' import { isRemoteEmbyID } from '../utils/remoteEmby' import { getLibraryArtworks } from './librariesPageModel' const TYPE_ICONS: Record = { movie: , movies: , tv: , series: , anime: , shows: , variety: , music: , adult: , } const TYPE_LABELS: Record = { movie: '电影', movies: '电影', tv: '剧集', series: '剧集', anime: '动漫', shows: '综艺', variety: '综艺', music: '音乐', adult: 'Adult', } export function HomeLoadingState() { return (
首页内容准备中…
) } export function HomeEmptyState() { return (

您的家庭影视站暂无内容

前往媒体库添加媒体目录,扫描后首页将展示海报轮播、媒体库和最新入库。

前往媒体库
) } // ContinueWatchingSkeleton 与 ContinueWatchingSection 同构的占位块: // 播放记录请求独立渐进加载时,避免区块突然弹入造成的布局跳动。 export function ContinueWatchingSkeleton() { return (
{[0, 1, 2, 3, 4, 5].map((i) => (
))}
) } /* ========================================================================= 1. 海报轮播区 (Hero Carousel Section) ========================================================================= */ export function HomeCarouselSection({ items, libraryMap, }: { items: Media[] libraryMap: Map }) { const [currentIndex, setCurrentIndex] = useState(0) const [isPaused, setIsPaused] = useState(false) const currentItem = items[currentIndex] || items[0] const count = items.length useEffect(() => { if (count <= 1 || isPaused) return const timer = setInterval(() => { setCurrentIndex((prev) => (prev + 1) % count) }, 5000) return () => clearInterval(timer) }, [count, isPaused]) if (!currentItem) return null const handlePrev = () => { setCurrentIndex((prev) => (prev - 1 + count) % count) } const handleNext = () => { setCurrentIndex((prev) => (prev + 1) % count) } const visual = currentItem.backdrop_url || currentItem.poster_url || '' const poster = currentItem.poster_url || currentItem.backdrop_url || '' const lib = libraryMap.get(currentItem.display_library_id || currentItem.library_id) return (
setIsPaused(true)} onMouseLeave={() => setIsPaused(false)} className="group relative overflow-hidden rounded-[2rem] border border-[var(--app-border)] bg-[var(--app-panel)] shadow-[0_24px_80px_var(--app-shadow)] min-h-[420px] md:min-h-[460px] flex flex-col justify-end" > {/* Background Backdrop Image with Crossfade */}
{visual && ( { e.currentTarget.style.display = 'none' }} /> )}
{/* Main Content Layout */}
{/* Left Column: Metadata & Actions */}
{/* Tag / Library Label */}
焦点推荐
{lib && ( {TYPE_ICONS[lib.type] || } {lib.name} )}
{/* Title */}

{currentItem.title}

{currentItem.original_name && currentItem.original_name !== currentItem.title && (

{currentItem.original_name}

)}
{/* Metadata Chips */}
{currentItem.rating > 0 && ( {currentItem.rating.toFixed(1)} )} {currentItem.year > 0 && ( {currentItem.year} 年 )} {currentItem.video_codec && ( {currentItem.video_codec} )} {currentItem.container && ( {currentItem.container} )}
{/* Overview */}

{currentItem.overview || '家庭私人媒体中心收藏。支持多端播放、高码率串流与智能刮削。'}

{/* Action Buttons */}
立即播放 查看详情
{/* Right Column: Floating 3D Poster Card */}
{currentItem.title}
{poster && ( {currentItem.title} { e.currentTarget.style.display = 'none' }} /> )}
{/* Carousel Controls & Dots */} {count > 1 && (
{/* Navigation Indicators */}
{items.map((_, idx) => (
{/* Slide Number & Arrow Buttons */}
{String(currentIndex + 1).padStart(2, '0')} / {String(count).padStart(2, '0')}
)}
) } /* ========================================================================= 2. 媒体库卡片区 (Libraries Grid) ========================================================================= */ export function HomeLibrariesSection({ libraries, libraryData, libraryCounts, onNeedPreviews, pinnedIds = [], onTogglePin, showAllLink = true, title = '媒体库', }: { libraries: Library[] libraryData?: Record libraryCounts: Record onNeedPreviews?: (ids: string[], limit?: number) => void pinnedIds?: string[] onTogglePin?: (libraryId: string) => void showAllLink?: boolean title?: string }) { const PAGE_SIZE = 20 // 分页位置按路由分别记忆:首页「媒体库」和 /libraries 的「媒体库入口」 // 各自保留自己的页码,进入媒体库详情再返回时不会掉回第一页。 const { pathname } = useLocation() const [currentPage, setCurrentPage] = useRememberedListPosition( `libraries-grid:${pathname}`, 1, ) const totalPages = Math.max(1, Math.ceil(libraries.length / PAGE_SIZE)) const effectivePage = Math.min(currentPage, totalPages) const queuePreview = useLazyPreviewBatch((ids) => onNeedPreviews?.(ids, 2)) const pagedLibraries = useMemo(() => { const start = (effectivePage - 1) * PAGE_SIZE return libraries.slice(start, start + PAGE_SIZE) }, [libraries, effectivePage]) return (

{title}

{libraries.length > PAGE_SIZE && ( 共 {libraries.length} 个 · 每页 {PAGE_SIZE} 个 )}

点击卡片浏览对应媒体库精选内容

{(totalPages > 1 || showAllLink) && (
{totalPages > 1 && (
{effectivePage} / {totalPages}
)} {showAllLink && ( 全部媒体库 )}
)}
{pagedLibraries.map((lib) => ( onTogglePin(lib.id) : undefined} onVisible={() => { if (!lib.cover_url) { queuePreview(lib.id) } }} /> ))}
) } export function HomeLibraryCard({ library, count, cards, pinned = false, onTogglePin, onVisible, }: { library: Library count: number cards: SeriesCard[] pinned?: boolean onTogglePin?: () => void onVisible: () => void }) { const ref = useInViewOnce(onVisible) const artwork = getLibraryArtworks(library, cards) return (
1 ? 'grid grid-cols-2 gap-0.5' : '' }`} > {artwork.length > 0 ? ( artwork.map(({ src, version }, index) => ( { event.currentTarget.style.visibility = 'hidden' }} /> )) ) : (
{TYPE_ICONS[library.type] || }
)}
{TYPE_LABELS[library.type] || '自定义'}

{library.name}

{count > 0 ? `${count} 部媒体` : '暂无条目'}

{onTogglePin && ( )}
) } /* ========================================================================= 3. 单个媒体库内容行 (Horizontal Scroll Row) ========================================================================= */ export function HomeLibraryRowSection({ library, cards, }: { library: Library cards: SeriesCard[] }) { const scrollRef = useRef(null) if (!cards || cards.length === 0) return null const scroll = (direction: 'left' | 'right') => { if (scrollRef.current) { const scrollAmount = direction === 'left' ? -480 : 480 scrollRef.current.scrollBy({ left: scrollAmount, behavior: 'smooth' }) } } return (
{/* Row Header */}
{TYPE_ICONS[library.type] || }

{library.name}

共 {cards.length} 部精选内容
查看全部 {/* Scroll Buttons */}
{/* Horizontal Carousel Row */}
{cards.map((card) => (
))}
) } /* ========================================================================= 4. 继续观看行 (Continue Watching) ========================================================================= */ export function ContinueWatchingSection({ history }: { history: HistoryItem[] }) { const scrollRef = useRef(null) const scroll = (direction: 'left' | 'right') => { if (scrollRef.current) { const scrollAmount = direction === 'left' ? -400 : 400 scrollRef.current.scrollBy({ left: scrollAmount, behavior: 'smooth' }) } } return (

继续观看

{history.length} 条播放记录
{history.slice(0, 12).map((h) => { const media: Media = h.media ?? ({ id: h.media_id, title: isRemoteEmbyID(h.media_id) ? '远程媒体' : '媒体', poster_url: '', updated_at: h.watched_at, } as Media) const progress = h.duration_ms > 0 ? h.position_ms / h.duration_ms : 0 return (
) })}
) } function ContinueCard({ media, progress }: { media: Media; progress: number }) { return (
{media.poster_url ? ( ) : (
)}

{media.title}

{media.year > 0 && {media.year}} {media.season_num !== undefined && media.episode_num !== undefined && ( S{media.season_num}E{media.episode_num} )}
{/* Progress Bar */} {progress > 0 && progress < 1 && (
)}
) }