"use client"; import { useRouter } from "next/navigation"; import { useMemo, useRef } from "react"; import { Pause, Play } from "lucide-react"; import type { AwemeData, ImageData, Neighbors, VideoData } from "./types"; import { BackgroundCanvas } from "./components/BackgroundCanvas"; import { CommentPanel } from "./components/CommentPanel"; import { ImageCarousel } from "./components/ImageCarousel"; import { ImageNavigationButtons } from "./components/ImageNavigationButtons"; import { MediaControls } from "./components/MediaControls"; import { NavigationButtons } from "./components/NavigationButtons"; import { VideoPlayer } from "./components/VideoPlayer"; import { useBackgroundCanvas } from "./hooks/useBackgroundCanvas"; import { useCommentState } from "./hooks/useCommentState"; import { useImageCarousel } from "./hooks/useImageCarousel"; import { useNavigation } from "./hooks/useNavigation"; import { usePlayerState } from "./hooks/usePlayerState"; import { useVideoPlayer } from "./hooks/useVideoPlayer"; const SEGMENT_MS = 4000; interface AwemeDetailClientProps { data: AwemeData; neighbors: Neighbors; } export default function AwemeDetailClient({ data, neighbors }: AwemeDetailClientProps) { const router = useRouter(); const isVideo = data.type === "video"; // 状态管理 const playerState = usePlayerState(); const commentState = useCommentState(); // 引用 const mediaContainerRef = useRef(null); const videoRef = useRef(null); const audioRef = useRef(null); const scrollerRef = useRef(null); const backgroundCanvasRef = useRef(null); // 图文轮播状态 const images = isVideo ? [] : (data as ImageData).images; const imageCarouselState = useImageCarousel({ images, isPlaying: playerState.isPlaying, loopMode: playerState.loopMode, neighbors, volume: playerState.volume, audioRef, scrollerRef, setProgress: playerState.setProgress, segmentMs: SEGMENT_MS, }); // 视频播放器 hooks if (isVideo) { useVideoPlayer({ awemeId: data.aweme_id, videoRef, volume: playerState.volume, rate: playerState.rate, loopMode: playerState.loopMode, neighbors, progressRestored: playerState.progressRestored, setIsPlaying: playerState.setIsPlaying, setProgress: playerState.setProgress, setProgressRestored: playerState.setProgressRestored, }); } // 媒体操作函数 const seekTo = (ratio: number) => { ratio = Math.min(1, Math.max(0, ratio)); if (isVideo) { const v = videoRef.current; if (!v || !v.duration) return; v.currentTime = v.duration * ratio; return; } if (!images?.length) return; // 计算每张图片的时长 const durations = images.map(img => img.duration ?? SEGMENT_MS); const totalDuration = durations.reduce((sum, d) => sum + d, 0); const targetTime = ratio * totalDuration; // 找到目标时间对应的图片索引和进度 let accumulatedTime = 0; let targetIdx = 0; let remainder = 0; for (let i = 0; i < images.length; i++) { if (accumulatedTime + durations[i] > targetTime) { targetIdx = i; remainder = (targetTime - accumulatedTime) / durations[i]; break; } accumulatedTime += durations[i]; if (i === images.length - 1) { targetIdx = i; remainder = 1; } } imageCarouselState.idxRef.current = targetIdx; imageCarouselState.setIdx(targetIdx); imageCarouselState.segStartRef.current = performance.now() - remainder * durations[targetIdx]; // 重新计算总进度 let totalProgress = 0; for (let i = 0; i < targetIdx; i++) { totalProgress += 1; } totalProgress += remainder; playerState.setProgress(totalProgress / images.length); // 虚拟滚动不需要实际滚动 DOM }; const togglePlay = async () => { if (isVideo) { const v = videoRef.current; if (!v) return; if (v.paused) await v.play().catch(() => {}); else v.pause(); return; } const el = audioRef.current; if (!playerState.isPlaying) { playerState.setIsPlaying(true); try { await el?.play().catch(() => {}); } catch {} } else { playerState.setIsPlaying(false); el?.pause(); } }; const toggleFullscreen = () => { if (!document.fullscreenElement) { if (document.body.requestFullscreen) { document.body.requestFullscreen().catch(() => {}); return; } const vRef = videoRef.current; if (vRef && vRef.requestFullscreen) { vRef.requestFullscreen().catch(() => {}); return; } // @ts-ignore if (vRef && vRef.webkitEnterFullscreen) { // @ts-ignore vRef.webkitEnterFullscreen(); } } else { document.exitFullscreen().catch(() => {}); } }; const prevImg = () => { if (!images?.length) return; const next = Math.max(0, imageCarouselState.idxRef.current - 1); imageCarouselState.idxRef.current = next; imageCarouselState.setIdx(next); imageCarouselState.segStartRef.current = performance.now(); // 虚拟滚动不需要实际滚动 DOM }; const nextImg = () => { if (!images?.length) return; const next = Math.min(images.length - 1, imageCarouselState.idxRef.current + 1); imageCarouselState.idxRef.current = next; imageCarouselState.setIdx(next); imageCarouselState.segStartRef.current = performance.now(); // 虚拟滚动不需要实际滚动 DOM }; const handleDownload = () => { if (isVideo) { const videoUrl = (data as VideoData).video_url; const link = document.createElement("a"); link.href = videoUrl; link.download = `video_${data.aweme_id}.mp4`; document.body.appendChild(link); link.click(); document.body.removeChild(link); } else { if (!images?.length) return; const currentImage = images[imageCarouselState.idx]; const link = document.createElement("a"); link.href = currentImage.url; link.download = `image_${data.aweme_id}_${imageCarouselState.idx + 1}.jpg`; document.body.appendChild(link); link.click(); document.body.removeChild(link); } }; // 导航相关 useNavigation({ neighbors, isVideo, mediaContainerRef, videoRef, prevImg, nextImg, togglePlay, }); // 背景画布 useBackgroundCanvas({ isVideo, idx: imageCarouselState.idx, videoRef, scrollerRef, backgroundCanvasRef, }); return (
{/* 主媒体区域 */}
{isVideo ? ( ) : ( )} {/* 暂停图标 */} {!playerState.isPlaying && (
)} {/* 图文切换按钮 */} {!isVideo && images.length > 1 && ( )} {/* 媒体控制栏 */}
{/* 导航按钮 */} neighbors.prev && router.push(`/aweme/${neighbors.prev.aweme_id}`)} onNavigateNext={() => neighbors.next && router.push(`/aweme/${neighbors.next.aweme_id}`)} onToggleComments={() => commentState.setOpen((v) => !v)} />
{/* 评论面板 */} commentState.setOpen(false)} author={data.author} createdAt={data.created_at} awemeId={data.aweme_id} mounted={commentState.mounted} />
); }