import { useEffect, useRef, useState, type ReactNode } from 'react' import { Link } from 'react-router-dom' import { AnimatePresence, motion } from 'framer-motion' import { ArrowRight, ChevronLeft, ChevronRight, Clock, Film, FolderOpen, Library as LibraryIcon, Music, Play, PlayCircle, Sparkles, Star, Tv, } from 'lucide-react' import { imageURL } from '../api/client' 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 (

您的家庭影视站暂无内容

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

前往媒体库
) } /* ========================================================================= 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, }: { libraries: Library[] libraryData?: Record libraryCounts: Record }) { return (

媒体库

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

全部媒体库
{/* Libraries Grid */}
{libraries.map((lib) => { const count = libraryCounts[lib.id] ?? 0 const cards = libraryData?.[lib.id]?.cards || [] const artwork = getLibraryArtworks(lib, 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[lib.type] || }
)} {/* 浮动类型标签 */}
{TYPE_LABELS[lib.type] || '自定义'}
{/* 媒体库信息 */}

{lib.name}

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

) })}
) } /* ========================================================================= 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 && (
)}
) }