mirror of https://github.com/djteang/OrangeTV.git
369 lines
12 KiB
TypeScript
369 lines
12 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any, react-hooks/exhaustive-deps, no-console */
|
|
|
|
'use client';
|
|
|
|
import {
|
|
Button,
|
|
Card,
|
|
EmptyState,
|
|
Link as HeroLink,
|
|
Skeleton,
|
|
} from '@heroui/react';
|
|
import { Suspense, useEffect, useState } from 'react';
|
|
|
|
// 客户端收藏 API
|
|
import {
|
|
clearAllFavorites,
|
|
getAllFavorites,
|
|
getAllPlayRecords,
|
|
subscribeToDataUpdates,
|
|
} from '@/lib/db.client';
|
|
import { getDoubanCategories } from '@/lib/douban.client';
|
|
import { DoubanItem } from '@/lib/types';
|
|
|
|
import { AnnouncementBanner } from '@/components/AnnouncementBanner';
|
|
import CapsuleSwitch from '@/components/CapsuleSwitch';
|
|
import ContinueWatching from '@/components/ContinueWatching';
|
|
import PageLayout from '@/components/PageLayout';
|
|
import ScrollableRow from '@/components/ScrollableRow';
|
|
import { useSite } from '@/components/SiteProvider';
|
|
import VideoCard from '@/components/VideoCard';
|
|
|
|
function HomeClient() {
|
|
const [activeTab, setActiveTab] = useState<'home' | 'favorites'>('home');
|
|
const [hotMovies, setHotMovies] = useState<DoubanItem[]>([]);
|
|
const [hotTvShows, setHotTvShows] = useState<DoubanItem[]>([]);
|
|
const [hotVarietyShows, setHotVarietyShows] = useState<DoubanItem[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const { announcement } = useSite();
|
|
|
|
const [showAnnouncement, setShowAnnouncement] = useState(false);
|
|
|
|
// 检查公告弹窗状态
|
|
useEffect(() => {
|
|
if (typeof window !== 'undefined' && announcement) {
|
|
const hasSeenAnnouncement = localStorage.getItem('hasSeenAnnouncement');
|
|
if (hasSeenAnnouncement !== announcement) {
|
|
setShowAnnouncement(true);
|
|
} else {
|
|
setShowAnnouncement(Boolean(!hasSeenAnnouncement && announcement));
|
|
}
|
|
}
|
|
}, [announcement]);
|
|
|
|
// 收藏夹数据
|
|
type FavoriteItem = {
|
|
id: string;
|
|
source: string;
|
|
title: string;
|
|
poster: string;
|
|
episodes: number;
|
|
source_name: string;
|
|
currentEpisode?: number;
|
|
search_title?: string;
|
|
origin?: 'vod' | 'live';
|
|
};
|
|
|
|
const [favoriteItems, setFavoriteItems] = useState<FavoriteItem[]>([]);
|
|
|
|
useEffect(() => {
|
|
const fetchRecommendData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
|
|
// 并行获取热门电影、热门剧集和热门综艺
|
|
const [moviesData, tvShowsData, varietyShowsData] =
|
|
await Promise.all([
|
|
getDoubanCategories({
|
|
kind: 'movie',
|
|
category: '热门',
|
|
type: '全部',
|
|
}),
|
|
getDoubanCategories({ kind: 'tv', category: 'tv', type: 'tv' }),
|
|
getDoubanCategories({ kind: 'tv', category: 'show', type: 'show' }),
|
|
]);
|
|
|
|
if (moviesData.code === 200) {
|
|
setHotMovies(moviesData.list);
|
|
}
|
|
|
|
if (tvShowsData.code === 200) {
|
|
setHotTvShows(tvShowsData.list);
|
|
}
|
|
|
|
if (varietyShowsData.code === 200) {
|
|
setHotVarietyShows(varietyShowsData.list);
|
|
}
|
|
} catch (error) {
|
|
console.error('获取推荐数据失败:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchRecommendData();
|
|
}, []);
|
|
|
|
// 处理收藏数据更新的函数
|
|
const updateFavoriteItems = async (allFavorites: Record<string, any>) => {
|
|
const allPlayRecords = await getAllPlayRecords();
|
|
|
|
// 根据保存时间排序(从近到远)
|
|
const sorted = Object.entries(allFavorites)
|
|
.sort(([, a], [, b]) => b.save_time - a.save_time)
|
|
.map(([key, fav]) => {
|
|
const plusIndex = key.indexOf('+');
|
|
const source = key.slice(0, plusIndex);
|
|
const id = key.slice(plusIndex + 1);
|
|
|
|
// 查找对应的播放记录,获取当前集数
|
|
const playRecord = allPlayRecords[key];
|
|
const currentEpisode = playRecord?.index;
|
|
|
|
return {
|
|
id,
|
|
source,
|
|
title: fav.title,
|
|
year: fav.year,
|
|
poster: fav.cover,
|
|
episodes: fav.total_episodes,
|
|
source_name: fav.source_name,
|
|
currentEpisode,
|
|
search_title: fav?.search_title,
|
|
origin: fav?.origin,
|
|
} as FavoriteItem;
|
|
});
|
|
setFavoriteItems(sorted);
|
|
};
|
|
|
|
// 当切换到收藏夹时加载收藏数据
|
|
useEffect(() => {
|
|
if (activeTab !== 'favorites') return;
|
|
|
|
const loadFavorites = async () => {
|
|
const allFavorites = await getAllFavorites();
|
|
await updateFavoriteItems(allFavorites);
|
|
};
|
|
|
|
loadFavorites();
|
|
|
|
// 监听收藏更新事件
|
|
const unsubscribe = subscribeToDataUpdates(
|
|
'favoritesUpdated',
|
|
(newFavorites: Record<string, any>) => {
|
|
updateFavoriteItems(newFavorites);
|
|
}
|
|
);
|
|
|
|
return unsubscribe;
|
|
}, [activeTab]);
|
|
|
|
const handleCloseAnnouncement = (announcement: string) => {
|
|
setShowAnnouncement(false);
|
|
localStorage.setItem('hasSeenAnnouncement', announcement); // 记录已查看弹窗
|
|
};
|
|
|
|
return (
|
|
<PageLayout>
|
|
<div className='overflow-visible px-4 py-6 sm:px-10 sm:py-10'>
|
|
{/* 顶部 Tab 切换 */}
|
|
<div className='mb-10 flex justify-center'>
|
|
<CapsuleSwitch
|
|
options={[
|
|
{ label: '首页', value: 'home' },
|
|
{ label: '收藏', value: 'favorites' },
|
|
]}
|
|
active={activeTab}
|
|
compact
|
|
onChange={(value) => setActiveTab(value as 'home' | 'favorites')}
|
|
/>
|
|
</div>
|
|
|
|
<div className='mx-auto max-w-[1380px] space-y-10'>
|
|
{announcement && showAnnouncement && (
|
|
<AnnouncementBanner
|
|
announcement={announcement}
|
|
onDismiss={() => handleCloseAnnouncement(announcement)}
|
|
/>
|
|
)}
|
|
|
|
{activeTab === 'favorites' ? (
|
|
// 收藏夹视图
|
|
<Card>
|
|
<Card.Header className='flex-row items-end justify-between gap-4'>
|
|
<div>
|
|
<Card.Description>Saved</Card.Description>
|
|
<Card.Title>我的收藏</Card.Title>
|
|
</div>
|
|
{favoriteItems.length > 0 && (
|
|
<Button
|
|
variant='danger'
|
|
onPress={async () => {
|
|
await clearAllFavorites();
|
|
setFavoriteItems([]);
|
|
}}
|
|
>
|
|
清空
|
|
</Button>
|
|
)}
|
|
</Card.Header>
|
|
<div className='grid grid-cols-[repeat(auto-fill,_minmax(96px,_96px))] justify-start gap-x-3 gap-y-10 px-0 pt-2 pb-5 sm:grid-cols-[repeat(auto-fill,_minmax(180px,_180px))] sm:gap-x-5 sm:gap-y-12 sm:pb-3'>
|
|
{favoriteItems.map((item) => (
|
|
<div key={item.id + item.source} className='w-full'>
|
|
<VideoCard
|
|
query={item.search_title}
|
|
{...item}
|
|
from='favorite'
|
|
type={item.episodes > 1 ? 'tv' : ''}
|
|
/>
|
|
</div>
|
|
))}
|
|
{favoriteItems.length === 0 && (
|
|
<EmptyState className='col-span-full'>
|
|
暂无收藏内容
|
|
</EmptyState>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
) : (
|
|
// 首页视图
|
|
<>
|
|
{/* 继续观看 */}
|
|
<ContinueWatching />
|
|
|
|
{/* 热门电影 */}
|
|
<Card>
|
|
<Card.Header className='flex-row items-end justify-between gap-4'>
|
|
<div>
|
|
<Card.Description>精选推荐</Card.Description>
|
|
<Card.Title>热门电影</Card.Title>
|
|
</div>
|
|
<HeroLink href='/douban?type=movie'>查看更多</HeroLink>
|
|
</Card.Header>
|
|
<ScrollableRow>
|
|
{loading
|
|
? // 加载状态显示灰色占位数据
|
|
Array.from({ length: 8 }).map((_, index) => (
|
|
<div
|
|
key={index}
|
|
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
|
>
|
|
<Skeleton className='aspect-[2/3] w-full' />
|
|
<Skeleton className='mt-3 h-4' />
|
|
</div>
|
|
))
|
|
: // 显示真实数据
|
|
hotMovies.map((movie, index) => (
|
|
<div
|
|
key={index}
|
|
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
|
>
|
|
<VideoCard
|
|
from='douban'
|
|
title={movie.title}
|
|
poster={movie.poster}
|
|
douban_id={Number(movie.id)}
|
|
rate={movie.rate}
|
|
year={movie.year}
|
|
type='movie'
|
|
/>
|
|
</div>
|
|
))}
|
|
</ScrollableRow>
|
|
</Card>
|
|
|
|
{/* 热门剧集 */}
|
|
<Card>
|
|
<Card.Header className='flex-row items-end justify-between gap-4'>
|
|
<div>
|
|
<Card.Description>Series</Card.Description>
|
|
<Card.Title>热门剧集</Card.Title>
|
|
</div>
|
|
<HeroLink href='/douban?type=tv'>查看更多</HeroLink>
|
|
</Card.Header>
|
|
<ScrollableRow>
|
|
{loading
|
|
? // 加载状态显示灰色占位数据
|
|
Array.from({ length: 8 }).map((_, index) => (
|
|
<div
|
|
key={index}
|
|
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
|
>
|
|
<Skeleton className='aspect-[2/3] w-full' />
|
|
<Skeleton className='mt-3 h-4' />
|
|
</div>
|
|
))
|
|
: // 显示真实数据
|
|
hotTvShows.map((show, index) => (
|
|
<div
|
|
key={index}
|
|
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
|
>
|
|
<VideoCard
|
|
from='douban'
|
|
title={show.title}
|
|
poster={show.poster}
|
|
douban_id={Number(show.id)}
|
|
rate={show.rate}
|
|
year={show.year}
|
|
/>
|
|
</div>
|
|
))}
|
|
</ScrollableRow>
|
|
</Card>
|
|
|
|
{/* 热门综艺 */}
|
|
<Card>
|
|
<Card.Header className='flex-row items-end justify-between gap-4'>
|
|
<div>
|
|
<Card.Description>Shows</Card.Description>
|
|
<Card.Title>热门综艺</Card.Title>
|
|
</div>
|
|
<HeroLink href='/douban?type=show'>查看更多</HeroLink>
|
|
</Card.Header>
|
|
<ScrollableRow>
|
|
{loading
|
|
? // 加载状态显示灰色占位数据
|
|
Array.from({ length: 8 }).map((_, index) => (
|
|
<div
|
|
key={index}
|
|
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
|
>
|
|
<Skeleton className='aspect-[2/3] w-full' />
|
|
<Skeleton className='mt-3 h-4' />
|
|
</div>
|
|
))
|
|
: // 显示真实数据
|
|
hotVarietyShows.map((show, index) => (
|
|
<div
|
|
key={index}
|
|
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
|
>
|
|
<VideoCard
|
|
from='douban'
|
|
title={show.title}
|
|
poster={show.poster}
|
|
douban_id={Number(show.id)}
|
|
rate={show.rate}
|
|
year={show.year}
|
|
/>
|
|
</div>
|
|
))}
|
|
</ScrollableRow>
|
|
</Card>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</PageLayout>
|
|
);
|
|
}
|
|
|
|
export default function Home() {
|
|
return (
|
|
<Suspense>
|
|
<HomeClient />
|
|
</Suspense>
|
|
);
|
|
}
|