import { useEffect, useMemo, useState } from 'react' import { libraryAPI } from '../api/library' import { playbackAPI, type HistoryItem } from '../api/playback' import type { Library, Media } from '../types' import { groupSeries, type SeriesCard } from '../utils/groupSeries' import { ContinueWatchingSection, HomeCarouselSection, HomeEmptyState, HomeLibrariesSection, HomeLibraryRowSection, HomeLoadingState, } from './HomePageSections' const hasArtwork = (media?: Media | null) => !!(media?.poster_url || media?.backdrop_url) const asArray = (value: unknown): T[] => (Array.isArray(value) ? (value as T[]) : []) export function HomePage() { const [libraries, setLibraries] = useState([]) const [libraryData, setLibraryData] = useState>({}) const [history, setHistory] = useState([]) const [loading, setLoading] = useState(true) useEffect(() => { let cancelled = false async function load() { setLoading(true) try { const [libs, hist] = await Promise.all([ libraryAPI.list().then((rows) => asArray(rows)).catch(() => [] as Library[]), playbackAPI.recentHistory().then((rows) => asArray(rows)).catch(() => [] as HistoryItem[]), ]) if (cancelled) return setLibraries(libs) setHistory(hist.filter((h) => h && !h.completed && !!h.media)) // Fetch media items for all libraries in parallel const isSeriesType = (type?: string) => type === 'tv' || type === 'anime' || type === 'variety' const results = await Promise.allSettled( libs.map(async (lib) => { // 剧集类媒体库(tv/anime/variety):后端 /series 已按剧聚合, // 首页若用 episode 级 /media 的前 30 行再 groupSeries,同一部剧的 // 多集会折叠成 1 张卡,导致整行只显示 1 个条目。 // 改用 /series 分页拉取全部聚合后的剧集卡片。 if (isSeriesType(lib.type)) { const cards: SeriesCard[] = [] let total = 0 const pageSize = 200 for (let page = 1; page <= 10; page++) { const data = await libraryAPI.listSeries(lib.id, page, pageSize) const pageItems = asArray(data?.items) cards.push(...pageItems) total = data?.total ?? cards.length if (cards.length >= total || pageItems.length < pageSize) break } return { id: lib.id, cards, items: [], total } } const page = await libraryAPI.listMedia(lib.id, 1, 30) const items = asArray(page?.items) const cards = groupSeries(items) return { id: lib.id, cards, items, total: page?.total ?? items.length, } }), ) if (cancelled) return const mapData: Record = {} for (const res of results) { if (res.status === 'fulfilled' && res.value) { mapData[res.value.id] = { cards: res.value.cards, items: res.value.items, total: res.value.total, } } } setLibraryData(mapData) } finally { if (!cancelled) setLoading(false) } } load() return () => { cancelled = true } }, []) // Quick lookup map for libraries const libraryMap = useMemo(() => { const map = new Map() for (const lib of libraries) { map.set(lib.id, lib) } return map }, [libraries]) // Counts lookup const libraryCounts = useMemo(() => { const counts: Record = {} for (const lib of libraries) { counts[lib.id] = libraryData[lib.id]?.total ?? 0 } return counts }, [libraries, libraryData]) // Compute items to show in the Hero Carousel. Only libraries flagged // carousel_enabled contribute; a library's items array is empty for // series-type libs (loaded via /series), so gate on cards instead. const carouselItems = useMemo(() => { const candidateMedia: Media[] = [] const effectiveSelectedIds = libraries .filter((l) => l.carousel_enabled !== false) .map((l) => l.id) for (const libId of effectiveSelectedIds) { const data = libraryData[libId] if (data && data.cards.length > 0) { for (const card of data.cards) { if (hasArtwork(card.rep)) { candidateMedia.push(card.rep) } } } } // Fallback to all loaded items with artwork if (candidateMedia.length === 0) { for (const lib of libraries) { const data = libraryData[lib.id] if (data) { for (const card of data.cards) { candidateMedia.push(card.rep) } } } } return candidateMedia.slice(0, 10) }, [libraries, libraryData]) const empty = !loading && libraries.length === 0 && history.length === 0 if (loading) { return } if (empty) { return } return (
{/* 1. 顶部海报轮播区 */} {carouselItems.length > 0 && ( )} {/* 2. 继续观看(若有历史) */} {history.length > 0 && } {/* 3. 媒体库卡片区 */} {libraries.length > 0 && ( )} {/* 4. 各媒体库内容展示行 */}
{libraries.map((lib) => { const cards = libraryData[lib.id]?.cards || [] if (cards.length === 0) return null return ( ) })}
) }