video-flow-b/components/ui/video-tab-content.tsx
2025-07-03 05:51:09 +08:00

434 lines
16 KiB
TypeScript

'use client';
import React, { useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Trash2, RefreshCw, Play, Pause, Volume2, VolumeX, Upload, Library, Wand2 } from 'lucide-react';
import { GlassIconButton } from './glass-icon-button';
import { cn } from '@/public/lib/utils';
import { ReplaceVideoModal } from './replace-video-modal';
import { MediaPropertiesModal } from './media-properties-modal';
interface VideoTabContentProps {
taskSketch: any[];
currentSketchIndex: number;
onSketchSelect: (index: number) => void;
isPlaying?: boolean;
}
export function VideoTabContent({
taskSketch = [],
currentSketchIndex = 0,
onSketchSelect,
isPlaying: externalIsPlaying = true
}: VideoTabContentProps) {
const thumbnailsRef = useRef<HTMLDivElement>(null);
const videosRef = useRef<HTMLDivElement>(null);
const videoPlayerRef = useRef<HTMLVideoElement>(null);
const [isPlaying, setIsPlaying] = React.useState(externalIsPlaying);
const [isMuted, setIsMuted] = React.useState(false);
const [progress, setProgress] = React.useState(0);
const [isReplaceModalOpen, setIsReplaceModalOpen] = React.useState(false);
const [activeReplaceMethod, setActiveReplaceMethod] = React.useState<'upload' | 'library' | 'generate'>('upload');
const [isMediaPropertiesModalOpen, setIsMediaPropertiesModalOpen] = React.useState(false);
// 监听外部播放状态变化
useEffect(() => {
setIsPlaying(externalIsPlaying);
}, [externalIsPlaying]);
// 确保 taskSketch 是数组
const sketches = Array.isArray(taskSketch) ? taskSketch : [];
// 自动滚动到选中项
useEffect(() => {
if (thumbnailsRef.current && videosRef.current) {
const thumbnailContainer = thumbnailsRef.current;
const videoContainer = videosRef.current;
const thumbnailWidth = thumbnailContainer.children[0]?.clientWidth ?? 0;
const thumbnailGap = 16; // gap-4 = 16px
const thumbnailScrollPosition = (thumbnailWidth + thumbnailGap) * currentSketchIndex;
const videoElement = videoContainer.children[currentSketchIndex] as HTMLElement;
const videoScrollPosition = videoElement?.offsetLeft ?? 0;
thumbnailContainer.scrollTo({
left: thumbnailScrollPosition - thumbnailContainer.clientWidth / 2 + thumbnailWidth / 2,
behavior: 'smooth'
});
videoContainer.scrollTo({
left: videoScrollPosition - videoContainer.clientWidth / 2 + videoElement?.clientWidth / 2,
behavior: 'smooth'
});
}
}, [currentSketchIndex]);
// 视频播放控制
useEffect(() => {
if (videoPlayerRef.current) {
if (isPlaying) {
videoPlayerRef.current.play().catch(() => {
// 处理自动播放策略限制
setIsPlaying(false);
});
} else {
// videoPlayerRef.current.pause();
}
}
}, [isPlaying, currentSketchIndex]);
// 更新进度条
const handleTimeUpdate = () => {
if (videoPlayerRef.current) {
const progress = (videoPlayerRef.current.currentTime / videoPlayerRef.current.duration) * 100;
setProgress(progress);
}
};
// 如果没有数据,显示空状态
if (sketches.length === 0) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
<p>No sketch data</p>
</div>
);
}
return (
<div className="flex flex-col gap-6">
{/* 上部分 */}
<motion.div
className="space-y-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
{/* 分镜缩略图行 */}
<div className="relative">
<div
ref={thumbnailsRef}
className="flex gap-4 overflow-x-auto pb-2 pt-2 hide-scrollbar"
>
{sketches.map((sketch, index) => (
<motion.div
key={sketch.id || index}
className={cn(
'relative flex-shrink-0 w-32 aspect-video rounded-lg overflow-hidden cursor-pointer',
currentSketchIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
)}
onClick={() => onSketchSelect(index)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<video
src={sketch.url}
className="w-full h-full object-cover"
muted
loop
playsInline
onMouseEnter={(e) => e.currentTarget.play()}
onMouseLeave={(e) => e.currentTarget.pause()}
/>
<div className="absolute bottom-0 left-0 right-0 p-1 bg-gradient-to-t from-black/60 to-transparent">
<span className="text-xs text-white/90">Scene {index + 1}</span>
</div>
</motion.div>
))}
</div>
</div>
{/* 视频描述行 - 单行滚动 */}
<div className="relative group">
<div
ref={videosRef}
className="flex overflow-x-auto hide-scrollbar py-2 gap-1"
>
{sketches.map((video, index) => {
const isActive = currentSketchIndex === index;
return (
<motion.div
key={video.id || index}
className={cn(
'flex-shrink-0 cursor-pointer transition-all duration-300',
isActive ? 'text-white' : 'text-white/50 hover:text-white/80'
)}
onClick={() => onSketchSelect(index)}
initial={false}
animate={{
scale: isActive ? 1.02 : 1,
}}
>
<div className="flex items-center gap-2">
<span className="text-sm whitespace-nowrap">
{video.script}
</span>
{index < sketches.length - 1 && (
<span className="text-white/20">|</span>
)}
</div>
</motion.div>
);
})}
</div>
{/* 渐变遮罩 */}
<div className="absolute left-0 top-0 bottom-0 w-8 bg-gradient-to-r from-[#1a1b1e] to-transparent pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="absolute right-0 top-0 bottom-0 w-8 bg-gradient-to-l from-[#1a1b1e] to-transparent pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
</motion.div>
{/* 中间部分 替换视频 可上传、素材库选择、生成视频 */}
<motion.div
className="p-4 rounded-lg bg-white/5"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<h3 className="text-sm font-medium mb-2">Replace video</h3>
<div className="flex gap-4">
<motion.button
className={cn(
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
activeReplaceMethod === 'upload' && isReplaceModalOpen
? 'border-blue-500 bg-blue-500/10'
: 'border-white/10 hover:border-white/20'
)}
onClick={() => {
setActiveReplaceMethod('upload');
setIsReplaceModalOpen(true);
}}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Upload className="w-6 h-6" />
<span>Upload video</span>
</motion.button>
<motion.button
className={cn(
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
activeReplaceMethod === 'library' && isReplaceModalOpen
? 'border-blue-500 bg-blue-500/10'
: 'border-white/10 hover:border-white/20'
)}
onClick={() => {
setActiveReplaceMethod('library');
setIsReplaceModalOpen(true);
}}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Library className="w-6 h-6" />
<span>Library</span>
</motion.button>
<motion.button
className={cn(
'flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors',
activeReplaceMethod === 'generate' && isReplaceModalOpen
? 'border-blue-500 bg-blue-500/10'
: 'border-white/10 hover:border-white/20'
)}
onClick={() => {
setActiveReplaceMethod('generate');
setIsReplaceModalOpen(true);
}}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Wand2 className="w-6 h-6" />
<span>Generate video</span>
</motion.button>
</div>
</motion.div>
{/* 下部分 */}
<motion.div
className="grid grid-cols-2 gap-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
{/* 左列:编辑项 截取视频、设置转场、调节音量 */}
<div className="space-y-4">
{/* 视频截取 */}
<div className="p-4 rounded-lg bg-white/5">
<h3 className="text-sm font-medium mb-2">Video clip</h3>
<div className="flex items-center gap-4">
<input
type="text"
placeholder="00:00"
className="w-20 px-3 py-1 bg-white/5 border border-white/10 rounded-lg
text-center focus:outline-none focus:border-blue-500"
/>
<span className="text-white/50">To</span>
<input
type="text"
placeholder="00:00"
className="w-20 px-3 py-1 bg-white/5 border border-white/10 rounded-lg
text-center focus:outline-none focus:border-blue-500"
/>
</div>
</div>
{/* 转场设置 */}
<div className="p-4 rounded-lg bg-white/5">
<h3 className="text-sm font-medium mb-2">Transition</h3>
<div className="grid grid-cols-3 gap-2">
{['Fade', 'Slide', 'Zoom'].map((transition) => (
<motion.button
key={transition}
className="px-3 py-1 bg-white/5 hover:bg-white/10 rounded-lg text-sm"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{transition}
</motion.button>
))}
</div>
</div>
{/* 音量调节 */}
<div className="p-4 rounded-lg bg-white/5">
<h3 className="text-sm font-medium mb-2">Volume</h3>
<input
type="range"
min="0"
max="100"
className="w-full"
onChange={(e) => console.log('Volume:', e.target.value)}
/>
</div>
{/* 更多设置 点击打开 Media properties 弹窗 */}
<motion.button
className='bg-[transparent] m-4 p-0 border-none rounded-lg transition-colors'
style={{textDecorationLine: 'underline'}}
onClick={() => setIsMediaPropertiesModalOpen(true)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<div className='text-sm font-medium mb-2'>Media properties</div>
</motion.button>
</div>
{/* 右列:视频预览和操作 */}
<div className="space-y-4">
{/* 选中的视频预览 */}
<motion.div
className="aspect-video rounded-lg overflow-hidden relative group"
layoutId={`video-preview-${currentSketchIndex}`}
>
<video
ref={videoPlayerRef}
src={sketches[currentSketchIndex]?.url}
className="w-full h-full object-cover"
loop
autoPlay
playsInline
muted={isMuted}
onTimeUpdate={handleTimeUpdate}
/>
{/* 视频控制层 */}
<div className="absolute inset-0 bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity">
<div className="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/60 to-transparent">
{/* 进度条 */}
<div className="w-full h-1 bg-white/20 rounded-full mb-4 cursor-pointer"
onClick={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const percentage = (x / rect.width) * 100;
if (videoPlayerRef.current) {
videoPlayerRef.current.currentTime = (percentage / 100) * videoPlayerRef.current.duration;
}
}}
>
<motion.div
className="h-full bg-blue-500 rounded-full"
style={{ width: `${progress}%` }}
/>
</div>
{/* 控制按钮 */}
<div className="flex items-center gap-4">
<motion.button
className="p-2 rounded-full hover:bg-white/10"
onClick={() => setIsPlaying(!isPlaying)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
{isPlaying ? (
<Pause className="w-5 h-5" />
) : (
<Play className="w-5 h-5" />
)}
</motion.button>
<motion.button
className="p-2 rounded-full hover:bg-white/10"
onClick={() => setIsMuted(!isMuted)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
{isMuted ? (
<VolumeX className="w-5 h-5" />
) : (
<Volume2 className="w-5 h-5" />
)}
</motion.button>
</div>
</div>
</div>
</motion.div>
{/* 操作按钮 */}
<div className="grid grid-cols-2 gap-2">
<motion.button
onClick={() => console.log('Delete sketch')}
className="flex items-center justify-center gap-2 px-4 py-3 bg-red-500/10 hover:bg-red-500/20
text-red-500 rounded-lg transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Trash2 className="w-4 h-4" />
<span>Delete sketch</span>
</motion.button>
<motion.button
onClick={() => console.log('Regenerate')}
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20
text-blue-500 rounded-lg transition-colors"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<RefreshCw className="w-4 h-4" />
<span>Regenerate</span>
</motion.button>
</div>
</div>
</motion.div>
{/* 替换视频弹窗 */}
<ReplaceVideoModal
isOpen={isReplaceModalOpen}
activeReplaceMethod={activeReplaceMethod}
onClose={() => setIsReplaceModalOpen(false)}
onVideoSelect={(video) => {
console.log('Selected video:', video);
setIsReplaceModalOpen(false);
}}
/>
{/* Media Properties 弹窗 */}
<MediaPropertiesModal
isOpen={isMediaPropertiesModalOpen}
onClose={() => setIsMediaPropertiesModalOpen(false)}
taskSketch={taskSketch}
currentSketchIndex={currentSketchIndex}
onSketchSelect={onSketchSelect}
/>
</div>
);
}