mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-21 19:53:43 +08:00
174 lines
5.9 KiB
TypeScript
174 lines
5.9 KiB
TypeScript
'use client';
|
|
|
|
import { Suspense, useEffect } from 'react';
|
|
import { useSearchParams, useRouter } from 'next/navigation';
|
|
import { Button } from '@/components/ui/Button';
|
|
import { ThemeSwitcher } from '@/components/ThemeSwitcher';
|
|
import { Icons } from '@/components/ui/Icon';
|
|
import { VideoPlayer } from '@/components/player/VideoPlayer';
|
|
import { VideoMetadata } from '@/components/player/VideoMetadata';
|
|
import { EpisodeList } from '@/components/player/EpisodeList';
|
|
import { PlayerError } from '@/components/player/PlayerError';
|
|
import { useVideoPlayer } from '@/lib/hooks/useVideoPlayer';
|
|
import { useHistoryStore } from '@/lib/store/history-store';
|
|
import { WatchHistorySidebar } from '@/components/history/WatchHistorySidebar';
|
|
import Image from 'next/image';
|
|
|
|
function PlayerContent() {
|
|
const searchParams = useSearchParams();
|
|
const router = useRouter();
|
|
const { addToHistory } = useHistoryStore();
|
|
|
|
const videoId = searchParams.get('id');
|
|
const source = searchParams.get('source');
|
|
const title = searchParams.get('title');
|
|
const episodeParam = searchParams.get('episode');
|
|
|
|
// Redirect if no video ID or source
|
|
if (!videoId || !source) {
|
|
router.push('/');
|
|
return null;
|
|
}
|
|
|
|
const {
|
|
videoData,
|
|
loading,
|
|
videoError,
|
|
currentEpisode,
|
|
playUrl,
|
|
setCurrentEpisode,
|
|
setPlayUrl,
|
|
setVideoError,
|
|
fetchVideoDetails,
|
|
} = useVideoPlayer(videoId, source, episodeParam);
|
|
|
|
// Add initial history entry when video data is loaded
|
|
useEffect(() => {
|
|
if (videoData && playUrl && videoId) {
|
|
// Map episodes to include index
|
|
const mappedEpisodes = videoData.episodes?.map((ep, idx) => ({
|
|
name: ep.name || `第${idx + 1}集`,
|
|
url: ep.url,
|
|
index: idx,
|
|
})) || [];
|
|
|
|
addToHistory(
|
|
videoId,
|
|
videoData.vod_name || title || '未知视频',
|
|
playUrl,
|
|
currentEpisode,
|
|
source,
|
|
0, // Initial playback position
|
|
0, // Will be updated by VideoPlayer
|
|
videoData.vod_pic,
|
|
mappedEpisodes
|
|
);
|
|
}
|
|
}, [videoData, playUrl, videoId, currentEpisode, source, title, addToHistory]);
|
|
|
|
const handleEpisodeClick = (episode: any, index: number) => {
|
|
setCurrentEpisode(index);
|
|
setPlayUrl(episode.url);
|
|
setVideoError('');
|
|
|
|
// Update URL to reflect current episode
|
|
const params = new URLSearchParams(searchParams.toString());
|
|
params.set('episode', index.toString());
|
|
router.replace(`/player?${params.toString()}`, { scroll: false });
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[var(--bg-color)]">
|
|
{/* Glass Navbar */}
|
|
<nav className="sticky top-0 z-50 pt-4 pb-2 px-4" style={{ transform: 'translateZ(0)' }}>
|
|
<div className="max-w-7xl mx-auto bg-[var(--glass-bg)] backdrop-blur-[25px] saturate-[180%] border border-[var(--glass-border)] rounded-[var(--radius-2xl)] shadow-[0_4px_12px_color-mix(in_srgb,var(--shadow-color)_40%,transparent)] px-4 sm:px-6 py-4">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className="flex items-center gap-2 sm:gap-4 min-w-0">
|
|
<button
|
|
onClick={() => router.push('/')}
|
|
className="flex items-center justify-center hover:opacity-80 transition-opacity flex-shrink-0"
|
|
title="返回首页"
|
|
>
|
|
<Image
|
|
src="/icon.png"
|
|
alt="KVideo"
|
|
width={40}
|
|
height={40}
|
|
className="object-contain"
|
|
/>
|
|
</button>
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => router.back()}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<Icons.ChevronLeft size={20} />
|
|
<span className="hidden sm:inline">返回</span>
|
|
</Button>
|
|
</div>
|
|
<div className="flex-shrink-0">
|
|
<ThemeSwitcher />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
|
|
{loading ? (
|
|
<div className="flex flex-col items-center justify-center py-20">
|
|
<div className="animate-spin rounded-full h-16 w-16 border-4 border-[var(--accent-color)] border-t-transparent mb-4"></div>
|
|
<p className="text-[var(--text-color-secondary)]">正在加载视频详情...</p>
|
|
</div>
|
|
) : videoError && !videoData ? (
|
|
<PlayerError
|
|
error={videoError}
|
|
onBack={() => router.back()}
|
|
onRetry={fetchVideoDetails}
|
|
/>
|
|
) : (
|
|
<div className="grid lg:grid-cols-3 gap-6">
|
|
{/* Video Player Section */}
|
|
<div className="lg:col-span-2 space-y-6">
|
|
<VideoPlayer
|
|
playUrl={playUrl}
|
|
videoId={videoId || undefined}
|
|
currentEpisode={currentEpisode}
|
|
onBack={() => router.back()}
|
|
/>
|
|
<VideoMetadata
|
|
videoData={videoData}
|
|
source={source}
|
|
title={title}
|
|
/>
|
|
</div>
|
|
|
|
{/* Episodes Sidebar */}
|
|
<div className="lg:col-span-1">
|
|
<EpisodeList
|
|
episodes={videoData?.episodes || null}
|
|
currentEpisode={currentEpisode}
|
|
onEpisodeClick={handleEpisodeClick}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</main>
|
|
|
|
{/* Watch History Sidebar */}
|
|
<WatchHistorySidebar />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function PlayerPage() {
|
|
return (
|
|
<Suspense fallback={
|
|
<div className="min-h-screen flex items-center justify-center bg-[var(--bg-color)]">
|
|
<div className="animate-spin rounded-full h-16 w-16 border-4 border-[var(--accent-color)] border-t-transparent"></div>
|
|
</div>
|
|
}>
|
|
<PlayerContent />
|
|
</Suspense>
|
|
);
|
|
}
|