forked from 77media/video-flow
864 lines
29 KiB
TypeScript
864 lines
29 KiB
TypeScript
'use client';
|
||
|
||
import React, { useRef, useEffect, useState, SetStateAction } from 'react';
|
||
import { motion, AnimatePresence } from 'framer-motion';
|
||
import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize } from 'lucide-react';
|
||
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
|
||
import { GlassIconButton } from '@/components/ui/glass-icon-button';
|
||
import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
|
||
import { mockScriptData } from '@/components/script-renderer/mock';
|
||
import { Skeleton } from '@/components/ui/skeleton';
|
||
|
||
interface MediaViewerProps {
|
||
scriptData: any;
|
||
currentStep: string;
|
||
currentSketchIndex: number;
|
||
taskSketch: any[];
|
||
taskVideos: any[];
|
||
isVideoPlaying: boolean;
|
||
isPlaying: boolean;
|
||
showControls: boolean;
|
||
isGeneratingSketch: boolean;
|
||
isGeneratingVideo: boolean;
|
||
onControlsChange: (show: boolean) => void;
|
||
onEditModalOpen: (tab: string) => void;
|
||
onToggleVideoPlay: () => void;
|
||
onTogglePlay: () => void;
|
||
final?: any;
|
||
setIsPauseWorkFlow: (isPause: boolean) => void;
|
||
setAnyAttribute: any;
|
||
isPauseWorkFlow: boolean;
|
||
applyScript: any;
|
||
mode: string;
|
||
}
|
||
|
||
export function MediaViewer({
|
||
scriptData,
|
||
currentStep,
|
||
currentSketchIndex,
|
||
taskSketch,
|
||
taskVideos,
|
||
isVideoPlaying,
|
||
isPlaying,
|
||
showControls,
|
||
isGeneratingSketch,
|
||
isGeneratingVideo,
|
||
onControlsChange,
|
||
onEditModalOpen,
|
||
onToggleVideoPlay,
|
||
onTogglePlay,
|
||
final,
|
||
setIsPauseWorkFlow,
|
||
setAnyAttribute,
|
||
isPauseWorkFlow,
|
||
applyScript,
|
||
mode
|
||
}: MediaViewerProps) {
|
||
const mainVideoRef = useRef<HTMLVideoElement>(null);
|
||
const finalVideoRef = useRef<HTMLVideoElement>(null);
|
||
|
||
// 音量控制状态
|
||
const [isMuted, setIsMuted] = useState(false);
|
||
const [volume, setVolume] = useState(0.8);
|
||
|
||
// 最终视频控制状态
|
||
const [isFinalVideoPlaying, setIsFinalVideoPlaying] = useState(true);
|
||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||
const [finalVideoReady, setFinalVideoReady] = useState(false);
|
||
const [userHasInteracted, setUserHasInteracted] = useState(false);
|
||
|
||
// 音量控制函数
|
||
const toggleMute = () => {
|
||
setUserHasInteracted(true);
|
||
setIsMuted(!isMuted);
|
||
if (mainVideoRef.current) {
|
||
mainVideoRef.current.muted = !isMuted;
|
||
}
|
||
if (finalVideoRef.current) {
|
||
finalVideoRef.current.muted = !isMuted;
|
||
}
|
||
};
|
||
|
||
const handleVolumeChange = (newVolume: number) => {
|
||
setUserHasInteracted(true);
|
||
setVolume(newVolume);
|
||
if (mainVideoRef.current) {
|
||
mainVideoRef.current.volume = newVolume;
|
||
}
|
||
if (finalVideoRef.current) {
|
||
finalVideoRef.current.volume = newVolume;
|
||
}
|
||
};
|
||
|
||
// 应用音量设置到视频元素
|
||
const applyVolumeSettings = (videoElement: HTMLVideoElement) => {
|
||
if (videoElement) {
|
||
videoElement.volume = volume;
|
||
videoElement.muted = isMuted;
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (finalVideoRef.current && finalVideoReady) {
|
||
if (isFinalVideoPlaying) {
|
||
finalVideoRef.current.play().catch(error => {
|
||
console.log('最终视频自动播放被阻止:', error);
|
||
// 如果自动播放被阻止,将状态设置为暂停
|
||
setIsFinalVideoPlaying(false);
|
||
});
|
||
} else {
|
||
finalVideoRef.current.pause();
|
||
}
|
||
}
|
||
}, [isFinalVideoPlaying, finalVideoReady]);
|
||
|
||
// 最终视频播放控制
|
||
const toggleFinalVideoPlay = () => {
|
||
setUserHasInteracted(true);
|
||
setIsFinalVideoPlaying(!isFinalVideoPlaying);
|
||
};
|
||
|
||
// 处理最终视频加载完成
|
||
const handleFinalVideoLoaded = () => {
|
||
if (finalVideoRef.current) {
|
||
setFinalVideoReady(true);
|
||
applyVolumeSettings(finalVideoRef.current);
|
||
|
||
// 如果当前状态是应该播放的,尝试播放
|
||
if (isFinalVideoPlaying) {
|
||
finalVideoRef.current.play().catch(error => {
|
||
console.log('最终视频自动播放被阻止:', error);
|
||
setIsFinalVideoPlaying(false);
|
||
});
|
||
}
|
||
}
|
||
};
|
||
|
||
// 处理视频点击 - 首次交互时尝试播放
|
||
const handleVideoClick = () => {
|
||
if (!userHasInteracted && finalVideoRef.current && finalVideoReady) {
|
||
setUserHasInteracted(true);
|
||
if (isFinalVideoPlaying) {
|
||
finalVideoRef.current.play().catch(error => {
|
||
console.log('视频播放失败:', error);
|
||
});
|
||
}
|
||
}
|
||
};
|
||
|
||
// 包装编辑按钮点击事件
|
||
const handleEditClick = (tab: string, from?: string) => {
|
||
if (from === 'final') {
|
||
// 暂停视频播放
|
||
finalVideoRef.current?.pause();
|
||
}
|
||
// TODO 点击没有任何事件效果,页面没变化
|
||
setUserHasInteracted(true);
|
||
onEditModalOpen(tab);
|
||
};
|
||
|
||
// 全屏控制
|
||
const toggleFullscreen = () => {
|
||
setUserHasInteracted(true);
|
||
if (!document.fullscreenElement) {
|
||
// 进入全屏
|
||
if (finalVideoRef.current) {
|
||
finalVideoRef.current.requestFullscreen?.() ||
|
||
(finalVideoRef.current as any).webkitRequestFullscreen?.() ||
|
||
(finalVideoRef.current as any).msRequestFullscreen?.();
|
||
setIsFullscreen(true);
|
||
}
|
||
} else {
|
||
// 退出全屏
|
||
document.exitFullscreen?.() ||
|
||
(document as any).webkitExitFullscreen?.() ||
|
||
(document as any).msExitFullscreen?.();
|
||
setIsFullscreen(false);
|
||
}
|
||
};
|
||
|
||
// 视频播放控制
|
||
useEffect(() => {
|
||
if (mainVideoRef.current) {
|
||
applyVolumeSettings(mainVideoRef.current);
|
||
if (isVideoPlaying) {
|
||
mainVideoRef.current.play().catch(error => {
|
||
console.log('视频播放失败:', error);
|
||
});
|
||
} else {
|
||
mainVideoRef.current.pause();
|
||
}
|
||
}
|
||
}, [isVideoPlaying]);
|
||
|
||
// 当切换视频时重置视频播放
|
||
useEffect(() => {
|
||
if (mainVideoRef.current) {
|
||
applyVolumeSettings(mainVideoRef.current);
|
||
mainVideoRef.current.currentTime = 0;
|
||
if (isVideoPlaying) {
|
||
mainVideoRef.current.play().catch(error => {
|
||
console.log('视频播放失败:', error);
|
||
});
|
||
}
|
||
}
|
||
}, [currentSketchIndex]);
|
||
|
||
// 音量设置变化时应用到所有视频
|
||
useEffect(() => {
|
||
if (mainVideoRef.current) {
|
||
applyVolumeSettings(mainVideoRef.current);
|
||
}
|
||
if (finalVideoRef.current) {
|
||
applyVolumeSettings(finalVideoRef.current);
|
||
}
|
||
}, [volume, isMuted]);
|
||
|
||
// 监听全屏状态变化
|
||
useEffect(() => {
|
||
const handleFullscreenChange = () => {
|
||
setIsFullscreen(!!document.fullscreenElement);
|
||
};
|
||
|
||
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
||
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
|
||
document.addEventListener('msfullscreenchange', handleFullscreenChange);
|
||
|
||
return () => {
|
||
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
||
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);
|
||
document.removeEventListener('msfullscreenchange', handleFullscreenChange);
|
||
};
|
||
}, []);
|
||
|
||
// 组件卸载时清理视频状态
|
||
useEffect(() => {
|
||
return () => {
|
||
// 清理最终视频状态
|
||
setFinalVideoReady(false);
|
||
if (finalVideoRef.current) {
|
||
finalVideoRef.current.pause();
|
||
}
|
||
};
|
||
}, []);
|
||
|
||
// 渲染音量控制组件
|
||
const renderVolumeControls = () => (
|
||
<div className="flex items-center gap-2">
|
||
{/* 静音按钮 */}
|
||
<GlassIconButton
|
||
icon={isMuted ? VolumeX : Volume2}
|
||
tooltip={isMuted ? "取消静音" : "静音"}
|
||
onClick={toggleMute}
|
||
size="sm"
|
||
/>
|
||
|
||
{/* 音量滑块 - 一直显示 */}
|
||
<div className="flex items-center gap-2">
|
||
<div className="relative">
|
||
<input
|
||
type="range"
|
||
min="0"
|
||
max="1"
|
||
step="0.1"
|
||
value={volume}
|
||
onChange={(e) => handleVolumeChange(parseFloat(e.target.value))}
|
||
className="w-16 h-1 bg-white/20 rounded-lg appearance-none cursor-pointer
|
||
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3
|
||
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white
|
||
[&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:shadow-lg
|
||
[&::-moz-range-thumb]:w-3 [&::-moz-range-thumb]:h-3 [&::-moz-range-thumb]:rounded-full
|
||
[&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:cursor-pointer
|
||
[&::-moz-range-thumb]:border-none [&::-moz-range-thumb]:shadow-lg"
|
||
style={{
|
||
background: `linear-gradient(to right, white 0%, white ${volume * 100}%, rgba(255,255,255,0.2) ${volume * 100}%, rgba(255,255,255,0.2) 100%)`
|
||
}}
|
||
/>
|
||
</div>
|
||
<span className="text-xs text-white/70 w-8 text-center">
|
||
{Math.round(volume * 100)}%
|
||
</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
// 渲染最终成片
|
||
const renderFinalVideo = (currentStep: string) => {
|
||
// 使用真实的final数据,如果没有则使用默认值
|
||
const finalVideo = final || {
|
||
url: 'https://cdn.qikongjian.com/videos/1750389908_37d4fffa-8516-43a3-a423-fc0274f40e8a_text_to_video_0.mp4'
|
||
};
|
||
|
||
return (
|
||
<div
|
||
className="relative w-full h-full rounded-lg overflow-hidden"
|
||
key={`render-video-${currentStep}`}
|
||
onMouseEnter={() => onControlsChange(true)}
|
||
onMouseLeave={() => onControlsChange(false)}
|
||
>
|
||
<div className="relative w-full h-full">
|
||
{/* 背景模糊的视频 */}
|
||
<motion.div
|
||
className="absolute inset-0 overflow-hidden"
|
||
initial={{ filter: "blur(0px)", scale: 1, opacity: 1 }}
|
||
animate={{ filter: "blur(20px)", scale: 1.1, opacity: 0.5 }}
|
||
transition={{ duration: 0.8, ease: "easeInOut" }}
|
||
>
|
||
<video
|
||
className="w-full h-full rounded-lg object-cover object-center"
|
||
src={taskVideos[currentSketchIndex]?.url}
|
||
loop
|
||
playsInline
|
||
muted
|
||
/>
|
||
</motion.div>
|
||
|
||
{/* 最终成片视频 */}
|
||
<motion.div
|
||
initial={{ clipPath: "inset(0 50% 0 50%)", filter: "blur(10px)" }}
|
||
animate={{ clipPath: "inset(0 0% 0 0%)", filter: "blur(0px)" }}
|
||
transition={{
|
||
clipPath: { duration: 1.2, ease: [0.43, 0.13, 0.23, 0.96] },
|
||
filter: { duration: 0.6, delay: 0.3 }
|
||
}}
|
||
className="relative z-10 w-full h-full"
|
||
>
|
||
<video
|
||
ref={finalVideoRef}
|
||
className="w-full h-full object-cover rounded-lg"
|
||
src={finalVideo.url}
|
||
autoPlay={isFinalVideoPlaying}
|
||
loop
|
||
playsInline
|
||
preload="metadata"
|
||
poster={`${finalVideo.url}?vframe/jpg/offset/1`}
|
||
onLoadedData={handleFinalVideoLoaded}
|
||
onPlay={() => setIsFinalVideoPlaying(true)}
|
||
onPause={() => setIsFinalVideoPlaying(false)}
|
||
onClick={handleVideoClick}
|
||
/>
|
||
</motion.div>
|
||
|
||
{/* 操作按钮组 */}
|
||
<AnimatePresence>
|
||
{showControls && (
|
||
<motion.div
|
||
className="absolute top-4 right-4 z-10 flex gap-2"
|
||
initial={{ opacity: 0, y: -10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -10 }}
|
||
transition={{ duration: 0.2 }}
|
||
>
|
||
<GlassIconButton
|
||
icon={Edit3}
|
||
tooltip="Edit sketch"
|
||
onClick={() => handleEditClick('4', 'final')}
|
||
/>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
|
||
{/* 视频信息浮层 */}
|
||
<motion.div
|
||
className="absolute bottom-0 left-0 right-0 z-10 p-4 bg-gradient-to-t from-black/80 via-black/40 to-transparent"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: 1, duration: 0.6 }}
|
||
>
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<motion.div
|
||
className="w-2 h-2 rounded-full bg-emerald-500"
|
||
animate={{
|
||
scale: [1, 1.2, 1],
|
||
opacity: [1, 0.6, 1]
|
||
}}
|
||
transition={{
|
||
duration: 2,
|
||
repeat: Infinity,
|
||
ease: "easeInOut"
|
||
}}
|
||
/>
|
||
<span className="text-sm font-medium text-white/90">{currentStep === '6' ? 'Final product' : 'Trailer Video'}</span>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
|
||
{/* 底部控制区域 */}
|
||
<motion.div
|
||
className="absolute bottom-16 left-4 z-10 flex items-center gap-3"
|
||
initial={{ opacity: 0, scale: 0.8 }}
|
||
animate={{ opacity: 1, scale: 1 }}
|
||
transition={{ delay: 1, duration: 0.6 }}
|
||
>
|
||
{/* 播放/暂停按钮 */}
|
||
<motion.div
|
||
whileHover={{ scale: 1.1 }}
|
||
whileTap={{ scale: 0.9 }}
|
||
className="relative"
|
||
>
|
||
{/* 播放时的发光效果 */}
|
||
{isFinalVideoPlaying && (
|
||
<motion.div
|
||
className="absolute inset-0 rounded-full blur-md"
|
||
animate={{
|
||
scale: [1, 1.2, 1],
|
||
opacity: [0.5, 0.8, 0.5]
|
||
}}
|
||
transition={{
|
||
duration: 2,
|
||
repeat: Infinity,
|
||
ease: "easeInOut"
|
||
}}
|
||
/>
|
||
)}
|
||
<GlassIconButton
|
||
icon={isFinalVideoPlaying ? Pause : Play}
|
||
tooltip={isFinalVideoPlaying ? "Pause video" : "Play video"}
|
||
onClick={toggleFinalVideoPlay}
|
||
size="sm"
|
||
/>
|
||
</motion.div>
|
||
|
||
{/* 音量控制 */}
|
||
{renderVolumeControls()}
|
||
|
||
{/* 全屏按钮 */}
|
||
<GlassIconButton
|
||
icon={isFullscreen ? Minimize : Maximize}
|
||
tooltip={isFullscreen ? "退出全屏" : "全屏"}
|
||
onClick={toggleFullscreen}
|
||
size="sm"
|
||
/>
|
||
</motion.div>
|
||
|
||
{/* 完成标记 */}
|
||
<motion.div
|
||
className="absolute top-4 right-4 px-3 py-1.5 rounded-full bg-emerald-500/20 backdrop-blur-sm
|
||
border border-emerald-500/30 text-emerald-400 text-sm font-medium"
|
||
initial={{ opacity: 0, scale: 0.8, x: 20 }}
|
||
animate={{ opacity: 1, scale: 1, x: 0 }}
|
||
transition={{ delay: 1.2, duration: 0.6 }}
|
||
>
|
||
Task completed
|
||
</motion.div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 渲染视频内容
|
||
const renderVideoContent = () => {
|
||
const currentSketch = taskSketch[currentSketchIndex];
|
||
const defaultBgColors = ['RGB(45, 50, 70)', 'RGB(75, 80, 100)', 'RGB(105, 110, 130)'];
|
||
const bgColors = currentSketch?.bg_rgb || defaultBgColors;
|
||
|
||
return (
|
||
<div
|
||
className="relative w-full h-full rounded-lg"
|
||
onMouseEnter={() => onControlsChange(true)}
|
||
onMouseLeave={() => onControlsChange(false)}
|
||
>
|
||
{/* 只在生成过程中或没有视频时使用ProgressiveReveal */}
|
||
{(isGeneratingVideo || !taskVideos[currentSketchIndex]) ? (
|
||
taskVideos[currentSketchIndex] ? (
|
||
<ProgressiveReveal
|
||
key={`generte-video-${currentSketchIndex}`}
|
||
className="w-full h-full rounded-lg"
|
||
revealDuration={0.8}
|
||
blurDuration={0.3}
|
||
initialBlur={10}
|
||
customVariants={{
|
||
hidden: {
|
||
opacity: 0,
|
||
scale: 0.9,
|
||
filter: "blur(30px)",
|
||
clipPath: "inset(0 100% 0 0)"
|
||
},
|
||
visible: {
|
||
opacity: 1,
|
||
scale: 1,
|
||
filter: "blur(0px)",
|
||
clipPath: "inset(0 0% 0 0)",
|
||
transition: {
|
||
duration: 1.5,
|
||
ease: [0.23, 1, 0.32, 1],
|
||
opacity: { duration: 0.8, ease: "easeOut" },
|
||
scale: { duration: 1.2, ease: "easeOut" },
|
||
filter: { duration: 0.8, delay: 0.4, ease: "easeOut" },
|
||
clipPath: { duration: 1, ease: "easeInOut" }
|
||
}
|
||
}
|
||
}}
|
||
loadingBgConfig={{
|
||
fromColor: `from-[${bgColors[0]}]`,
|
||
viaColor: `via-[${bgColors[1]}]`,
|
||
toColor: `to-[${bgColors[2]}]`,
|
||
glowOpacity: 0.8,
|
||
duration: 4,
|
||
}}
|
||
>
|
||
<div className="relative w-full h-full">
|
||
{/* 背景模糊的图片 */}
|
||
<div className="absolute inset-0 overflow-hidden">
|
||
<img
|
||
className="w-full h-full object-cover filter blur-lg scale-110 opacity-50"
|
||
src={taskSketch[currentSketchIndex]?.url}
|
||
alt="background"
|
||
/>
|
||
</div>
|
||
|
||
{/* 视频 */}
|
||
<motion.div
|
||
initial={{ clipPath: "inset(0 100% 0 0)" }}
|
||
animate={{ clipPath: "inset(0 0% 0 0)" }}
|
||
transition={{ duration: 0.8, ease: [0.43, 0.13, 0.23, 0.96] }}
|
||
className="relative z-10 w-full h-full"
|
||
>
|
||
<video
|
||
ref={mainVideoRef}
|
||
key={taskVideos[currentSketchIndex].url}
|
||
className="w-full h-full rounded-lg object-cover object-center relative z-10"
|
||
src={taskVideos[currentSketchIndex].url}
|
||
autoPlay={isVideoPlaying}
|
||
loop={true}
|
||
playsInline
|
||
onLoadedData={() => applyVolumeSettings(mainVideoRef.current!)}
|
||
onEnded={() => {
|
||
if (isVideoPlaying) {
|
||
// 自动切换到下一个视频的逻辑在父组件处理
|
||
}
|
||
}}
|
||
/>
|
||
</motion.div>
|
||
</div>
|
||
</ProgressiveReveal>
|
||
) : (
|
||
<div className="w-full h-full flex items-center justify-center rounded-lg overflow-hidden relative">
|
||
<img
|
||
className="absolute inset-0 w-full h-full object-cover"
|
||
src={taskSketch[currentSketchIndex]?.url}
|
||
alt={`Sketch ${currentSketchIndex + 1}`}
|
||
/>
|
||
</div>
|
||
)
|
||
) : (
|
||
/* 生成完成后直接显示视频,不使用ProgressiveReveal */
|
||
<div className="relative w-full h-full">
|
||
|
||
{/* 视频 修复播放没有声音 */}
|
||
<video
|
||
ref={mainVideoRef}
|
||
key={taskVideos[currentSketchIndex].url}
|
||
className="w-full h-full rounded-lg object-cover object-center relative z-10"
|
||
src={taskVideos[currentSketchIndex].url}
|
||
autoPlay={isVideoPlaying}
|
||
loop={true}
|
||
playsInline
|
||
onLoadedData={() => applyVolumeSettings(mainVideoRef.current!)}
|
||
onEnded={() => {
|
||
if (isVideoPlaying) {
|
||
// 自动切换到下一个视频的逻辑在父组件处理
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* 操作按钮组 */}
|
||
<AnimatePresence>
|
||
{showControls && (
|
||
<motion.div
|
||
className="absolute top-4 right-4 flex gap-2 z-[11]"
|
||
initial={{ opacity: 0, y: -10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -10 }}
|
||
transition={{ duration: 0.2 }}
|
||
>
|
||
<GlassIconButton
|
||
icon={Edit3}
|
||
tooltip="Edit sketch"
|
||
onClick={() => handleEditClick('3')}
|
||
/>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
|
||
{/* 底部控制区域 */}
|
||
{ taskVideos[currentSketchIndex] && (
|
||
<AnimatePresence>
|
||
<motion.div
|
||
className="absolute bottom-4 left-4 z-[11] flex items-center gap-3"
|
||
initial={{ opacity: 0, scale: 0.8 }}
|
||
animate={{ opacity: 1, scale: 1 }}
|
||
exit={{ opacity: 0, scale: 0.8 }}
|
||
transition={{ duration: 0.2 }}
|
||
>
|
||
{/* 播放按钮 */}
|
||
<motion.div
|
||
whileHover={{ scale: 1.1 }}
|
||
whileTap={{ scale: 0.9 }}
|
||
className="relative"
|
||
>
|
||
{/* 播放时的发光效果 */}
|
||
{isVideoPlaying && (
|
||
<motion.div
|
||
className="absolute inset-0 rounded-full blur-md"
|
||
animate={{
|
||
scale: [1, 1.2, 1],
|
||
opacity: [0.5, 0.8, 0.5]
|
||
}}
|
||
transition={{
|
||
duration: 2,
|
||
repeat: Infinity,
|
||
ease: "easeInOut"
|
||
}}
|
||
/>
|
||
)}
|
||
<GlassIconButton
|
||
icon={isVideoPlaying ? Pause : Play}
|
||
tooltip={isVideoPlaying ? "Pause video" : "Play video"}
|
||
onClick={onToggleVideoPlay}
|
||
size="sm"
|
||
/>
|
||
</motion.div>
|
||
|
||
{/* 音量控制 */}
|
||
{renderVolumeControls()}
|
||
</motion.div>
|
||
</AnimatePresence>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 渲染分镜草图
|
||
const renderSketchContent = () => {
|
||
const currentSketch = taskSketch[currentSketchIndex];
|
||
const defaultBgColors = ['RGB(45, 50, 70)', 'RGB(75, 80, 100)', 'RGB(105, 110, 130)'];
|
||
const bgColors = currentSketch?.bg_rgb || defaultBgColors;
|
||
|
||
return (
|
||
<div
|
||
className="relative w-full h-full rounded-lg"
|
||
onMouseEnter={() => onControlsChange(true)}
|
||
onMouseLeave={() => onControlsChange(false)}
|
||
>
|
||
{/* 只在生成过程中或没有分镜图片时使用ProgressiveReveal */}
|
||
{(isGeneratingSketch || !currentSketch) ? (
|
||
currentSketch ? (
|
||
<ProgressiveReveal
|
||
key={`sketch-generating-${currentSketchIndex}`}
|
||
className="w-full h-full rounded-lg"
|
||
revealDuration={0.8}
|
||
blurDuration={0.3}
|
||
initialBlur={10}
|
||
customVariants={{
|
||
hidden: {
|
||
opacity: 0,
|
||
scale: 0.9,
|
||
filter: "blur(30px)"
|
||
},
|
||
visible: {
|
||
opacity: 1,
|
||
scale: 1,
|
||
filter: "blur(0px)",
|
||
transition: {
|
||
duration: 1.5,
|
||
ease: [0.23, 1, 0.32, 1],
|
||
opacity: { duration: 0.8, ease: "easeOut" },
|
||
scale: { duration: 1.2, ease: "easeOut" },
|
||
filter: { duration: 0.8, delay: 0.4, ease: "easeOut" }
|
||
}
|
||
}
|
||
}}
|
||
loadingBgConfig={{
|
||
fromColor: `from-[${bgColors[0]}]`,
|
||
viaColor: `via-[${bgColors[1]}]`,
|
||
toColor: `to-[${bgColors[2]}]`,
|
||
glowOpacity: 0.8,
|
||
duration: 4,
|
||
}}
|
||
>
|
||
<img
|
||
key={currentSketchIndex}
|
||
src={currentSketch.url}
|
||
alt={`Sketch ${currentSketchIndex + 1}`}
|
||
className="w-full h-full rounded-lg object-cover"
|
||
/>
|
||
</ProgressiveReveal>
|
||
) : (
|
||
<div className="w-full h-full flex items-center justify-center rounded-lg overflow-hidden relative">
|
||
{/* 动态渐变背景 */}
|
||
<motion.div
|
||
className={`absolute inset-0 bg-gradient-to-r from-[${bgColors[0]}] via-[${bgColors[1]}] to-[${bgColors[2]}]`}
|
||
animate={{
|
||
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
|
||
}}
|
||
transition={{
|
||
duration: 5,
|
||
repeat: Infinity,
|
||
ease: "linear"
|
||
}}
|
||
style={{
|
||
backgroundSize: "200% 200%",
|
||
}}
|
||
/>
|
||
{/* 动态光效 */}
|
||
<motion.div
|
||
className="absolute inset-0 opacity-50"
|
||
style={{
|
||
background: "radial-gradient(circle at center, rgba(255,255,255,0.8) 0%, transparent 50%)",
|
||
}}
|
||
animate={{
|
||
scale: [1, 1.2, 1],
|
||
}}
|
||
transition={{
|
||
duration: 2,
|
||
repeat: Infinity,
|
||
ease: "easeInOut"
|
||
}}
|
||
/>
|
||
<motion.div
|
||
className="flex flex-col items-center gap-4 relative z-10"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.5 }}
|
||
>
|
||
<div className="relative">
|
||
<motion.div
|
||
className="absolute -inset-4 bg-gradient-to-r from-white via-sky-200 to-cyan-200 rounded-full opacity-60 blur-xl"
|
||
animate={{
|
||
scale: [1, 1.2, 1],
|
||
rotate: [0, 180, 360],
|
||
}}
|
||
transition={{
|
||
duration: 4,
|
||
repeat: Infinity,
|
||
ease: "linear"
|
||
}}
|
||
/>
|
||
</div>
|
||
</motion.div>
|
||
</div>
|
||
)
|
||
|
||
) : (
|
||
/* 生成完成后直接显示图片,不使用ProgressiveReveal */
|
||
<img
|
||
key={currentSketchIndex}
|
||
src={currentSketch.url}
|
||
alt={`NG-Sketch ${currentSketchIndex + 1}`}
|
||
className="w-full h-full rounded-lg object-cover"
|
||
/>
|
||
)}
|
||
|
||
{/* 操作按钮组 */}
|
||
<AnimatePresence>
|
||
{showControls && (
|
||
<motion.div
|
||
className="absolute top-4 right-4 flex gap-2"
|
||
initial={{ opacity: 0, y: -10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -10 }}
|
||
transition={{ duration: 0.2 }}
|
||
>
|
||
<GlassIconButton
|
||
icon={Edit3}
|
||
tooltip="Edit sketch"
|
||
onClick={() => handleEditClick('2')}
|
||
/>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
|
||
{/* 底部播放按钮 */}
|
||
<AnimatePresence>
|
||
<motion.div
|
||
className="absolute bottom-4 left-4"
|
||
initial={{ opacity: 0, scale: 0.8 }}
|
||
animate={{ opacity: 1, scale: 1 }}
|
||
exit={{ opacity: 0, scale: 0.8 }}
|
||
transition={{ duration: 0.2 }}
|
||
>
|
||
<motion.div
|
||
whileHover={{ scale: 1.1 }}
|
||
whileTap={{ scale: 0.9 }}
|
||
className="relative"
|
||
>
|
||
{/* 播放时的发光效果 */}
|
||
{isPlaying && (
|
||
<motion.div
|
||
className="absolute inset-0 rounded-full bg-blue-500/30 blur-md"
|
||
animate={{
|
||
scale: [1, 1.2, 1],
|
||
opacity: [0.5, 0.8, 0.5]
|
||
}}
|
||
transition={{
|
||
duration: 2,
|
||
repeat: Infinity,
|
||
ease: "easeInOut"
|
||
}}
|
||
/>
|
||
)}
|
||
<GlassIconButton
|
||
icon={isPlaying ? Pause : Play}
|
||
tooltip={isPlaying ? "Pause auto play" : "Start auto play"}
|
||
onClick={onTogglePlay}
|
||
size="sm"
|
||
className={isPlaying ? "border-blue-500/50 bg-blue-500/10" : ""}
|
||
/>
|
||
</motion.div>
|
||
</motion.div>
|
||
</AnimatePresence>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 渲染剧本
|
||
const renderScriptContent = () => {
|
||
return (
|
||
<div className="relative w-full h-full bg-white/10 rounded-lg overflow-hidden p-2">
|
||
{
|
||
scriptData ? (
|
||
<ScriptRenderer
|
||
data={scriptData}
|
||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||
setAnyAttribute={setAnyAttribute}
|
||
isPauseWorkFlow={isPauseWorkFlow}
|
||
applyScript={applyScript}
|
||
mode={mode}
|
||
/>
|
||
) : (
|
||
<div className="flex gap-2 w-full h-full">
|
||
<div className="w-[70%] h-full rounded-lg gap-2 flex flex-col">
|
||
<Skeleton className="w-full h-[33%] rounded-lg" />
|
||
<Skeleton className="w-full h-[33%] rounded-lg" />
|
||
<Skeleton className="w-full h-[33%] rounded-lg" />
|
||
</div>
|
||
<div className="w-[30%] h-full rounded-lg">
|
||
<Skeleton className="w-full h-full rounded-lg" />
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 根据当前步骤渲染对应内容
|
||
if (Number(currentStep) === 6 || Number(currentStep) === 5.5) {
|
||
return renderFinalVideo(currentStep);
|
||
}
|
||
|
||
if (Number(currentStep) > 2 && Number(currentStep) < 6) {
|
||
return renderVideoContent();
|
||
}
|
||
|
||
if (Number(currentStep) === 0) {
|
||
return renderScriptContent();
|
||
}
|
||
|
||
return renderSketchContent();
|
||
}
|