"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 total = images.length; const exact = ratio * total; const targetIdx = Math.min(total - 1, Math.floor(exact)); const remainder = exact - targetIdx; imageCarouselState.idxRef.current = targetIdx; imageCarouselState.setIdx(targetIdx); imageCarouselState.segStartRef.current = performance.now() - remainder * SEGMENT_MS; playerState.setProgress((targetIdx + remainder) / total); const el = scrollerRef.current; if (el) el.scrollTo({ left: targetIdx * el.clientWidth, behavior: "smooth" }); }; 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(); const el = scrollerRef.current; if (el) el.scrollTo({ left: next * el.clientWidth, behavior: "smooth" }); }; 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(); const el = scrollerRef.current; if (el) el.scrollTo({ left: next * el.clientWidth, behavior: "smooth" }); }; 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} comments={data.comments} mounted={commentState.mounted} />
); }