video-flow-b/components/workflow/generate-shots-step.tsx
2025-06-19 17:15:03 +08:00

897 lines
39 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useRef } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Slider } from '@/components/ui/slider';
import { ArrowLeft, ArrowRight, Play, Trash2, Replace, Scissors, Volume2, Edit, Upload, Image, Sparkles, ChevronLeft, ChevronRight, Layers, Pause, File, Ruler, UnfoldHorizontal, RefreshCcw, RotateCcw } from 'lucide-react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
interface GenerateShotsStepProps {
onNext: () => void;
onPrevious: () => void;
}
const mockChapters = [
{
id: 1,
title: 'Chapter 1',
shots: [
{
id: '1-1',
type: 'talking-head',
duration: 8,
shotVideo: 'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4',
generatedVideos:[
'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4'
],
description: 'Opening welcome shot with character',
transition: 'Selected Automatically by Preset',
volume: 55,
mediaNumber: 1,
},
{
id: '1-2',
type: 'b-roll',
duration: 9,
shotVideo: 'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4',
generatedVideos:[
'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4'
],
description: 'Technology overview montage',
transition: 'Selected Automatically by Preset',
volume: 55,
mediaNumber: 2,
}
],
},
{
id: 2,
title: 'Chapter 2',
shots: [
{
id: '2-1',
type: 'talking-head',
duration: 8,
shotVideo: 'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4',
generatedVideos:[
'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4'
],
description: 'Opening welcome shot with character',
transition: 'Selected Automatically by Preset',
volume: 55,
mediaNumber: 1,
},
{
id: '2-2',
type: 'b-roll',
duration: 9,
shotVideo: 'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4',
generatedVideos:[
'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4'
],
description: 'Technology overview montage',
transition: 'Selected Automatically by Preset',
volume: 55,
mediaNumber: 2,
},
{
id: '2-3',
type: 'animation',
duration: 10,
shotVideo: 'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4',
generatedVideos:[
'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4'
],
description: 'Animation sequence',
transition: 'Selected Automatically by Preset',
volume: 55,
mediaNumber: 3,
},
{
id: '2-4',
type: 'talking-head',
duration: 8,
shotVideo: 'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4',
generatedVideos:[
'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4',
'https://cdn.qikongjian.com/videos/1750162598_db834807-3f1c-4c52-8f50-b25396bd73ef_text_to_video_0.mp4'
],
description: 'Character dialogue',
transition: 'Selected Automatically by Preset',
volume: 55,
mediaNumber: 4,
}
]
}
];
const transitionTypes = [
{ value: 'fade', label: 'Fade' },
{ value: 'slide', label: 'Slide' },
{ value: 'zoom', label: 'Zoom' },
{ value: 'cut', label: 'Cut' },
];
const replaceMediaTabs = [
{ value: 'uploaded', label: 'Uploaded media' },
{ value: 'stock', label: 'Stock media' },
{ value: 'generative', label: 'Generative media' },
];
const mediaPropertyTabs = [
{ value: 'media', label: 'Media' },
{ value: 'audio', label: 'Audio & SFX' },
];
export function GenerateShotsStep({ onNext, onPrevious }: GenerateShotsStepProps) {
const [selectedChapter, setSelectedChapter] = useState(1);
const [selectedShot, setSelectedShot] = useState('1-1');
const [chapters, setChapters] = useState(mockChapters);
const videoRef = useRef<HTMLVideoElement>(null);
const [isPlaying, setIsPlaying] = useState(false);
const currentChapter = chapters.find(ch => ch.id === selectedChapter);
const currentShot = currentChapter?.shots.find(shot => shot.id === selectedShot);
const [isCheckVideoOpen, setIsCheckVideoOpen] = useState(false);
const [isReplaceMediaOpen, setIsReplaceMediaOpen] = useState(false);
const [isMediaPropertyOpen, setIsMediaPropertyOpen] = useState(false);
const [activeTabReplaceMedia, setActiveTabReplaceMedia] = useState('uploaded');
const [activeTabMediaProperty, setActiveTabMediaProperty] = useState('media');
const handlePlayPause = () => {
if (videoRef.current) {
if (videoRef.current.paused) {
videoRef.current.play();
setIsPlaying(true);
} else {
videoRef.current.pause();
setIsPlaying(false);
}
}
};
const handleTransitionChange = (shotId: string, transition: string) => {
setChapters(chapters.map(ch =>
ch.id === selectedChapter
? {
...ch,
shots: ch.shots.map(shot =>
shot.id === shotId ? { ...shot, transition } : shot
)
}
: ch
));
};
const handleVolumeChange = (shotId: string, volume: number[]) => {
setChapters(chapters.map(ch =>
ch.id === selectedChapter
? {
...ch,
shots: ch.shots.map(shot =>
shot.id === shotId ? { ...shot, volume: volume[0] } : shot
)
}
: ch
));
};
const handleDeleteShot = (shotId: string) => {
setChapters(chapters.map(ch =>
ch.id === selectedChapter
? {
...ch,
shots: ch.shots.filter(shot => shot.id !== shotId)
}
: ch
));
};
const handleOpenReplaceMedia = (tab: string) => {
setActiveTabReplaceMedia(tab);
setIsReplaceMediaOpen(true);
};
const handleOpenMediaProperty = (tab: string) => {
setActiveTabMediaProperty(tab);
setIsMediaPropertyOpen(true);
};
return (
<div className="min-h-screen text-white">
{/* 分镜视频列表 弹窗 */}
<Dialog open={isCheckVideoOpen} onOpenChange={setIsCheckVideoOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Media history</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4">
{currentShot?.generatedVideos.map((video, index) => (
<div key={index} className={`flex items-center justify-between ${currentShot?.shotVideo === video ? 'border-2 border-blue-500' : 'border-gray-600'}`}>
<video src={video} className="w-full h-full object-cover" autoPlay={false} muted={false} loop={false} controls />
</div>
))}
</div>
{/* Apply 按钮 Cancel 按钮 */}
<div className="flex justify-end gap-4">
<Button variant="outline">Cancel</Button>
<Button>Apply</Button>
</div>
</DialogContent>
</Dialog>
{/* 替换媒体 弹窗;点击弹窗以外 不触发关闭弹窗 */}
<Dialog open={isReplaceMediaOpen} onOpenChange={setIsReplaceMediaOpen}>
<DialogContent className="max-w-4xl w-full h-full overflow-y-auto p-0">
<DialogHeader className="fixed top-0 left-5 right-10 z-10 h-10 flex justify-center">
<DialogTitle>Replace media</DialogTitle>
</DialogHeader>
{/* 占剩余高度 溢出滚动 */}
<div className="flex flex-col gap-4 h-[calc(100vh-7rem)] overflow-y-auto mt-5 hide-scrollbar p-5">
{/* 章节列表 */}
<div>
{chapters.map((chapter, index) => (
<div key={chapter.id} className="flex items-center justify-between">
{/* Chapter: index + 1 文字竖着显示 */}
<div className="text-sm text-gray-300" style={{
transform: 'rotate(180deg)',
whiteSpace: 'nowrap',
height: 'fit-content',
writingMode: 'vertical-lr',
marginRight: '5px'
}}>
<div>Chapter {chapter.id}</div>
</div>
{/* flex: 1 */}
<div className="flex flex-1 space-x-2 overflow-x-auto pb-2">
{chapter.shots.map((shot, index) => (
<div
key={shot.id}
className={`relative flex-shrink-0 cursor-pointer rounded-lg overflow-hidden border-2 ${
selectedShot === shot.id ? 'border-blue-500' : 'border-gray-600'
}`}
onClick={() => setSelectedShot(shot.id)}
>
<div className="w-32 h-20 relative">
<video
src={shot.shotVideo}
className="w-full h-full object-cover"
autoPlay={false}
muted={false}
loop={false}
/>
<div className="absolute bottom-1 right-1 bg-black/70 text-white text-xs px-1 rounded">
00:{shot.duration.toString().padStart(2, '0')}
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
{/* 标签页 */}
<Tabs defaultValue={activeTabReplaceMedia} className="w-full">
<TabsList>
{replaceMediaTabs.map((tab) => (
<TabsTrigger key={tab.value} value={tab.value} className="w-full">{tab.label}</TabsTrigger>
))}
</TabsList>
<TabsContent value="uploaded">
<div className="flex flex-col gap-4">
{/* 上传媒体 */}
{/* 上传按钮;筛选下拉框 视频 图片;一行展示 两边对齐 */}
<div className="flex flex-row gap-4">
<Button
variant="outline"
className="h-12 border-gray-600 hover:bg-gray-700 text-white"
>
<Upload className="mr-2 h-4 w-4" />
Uploaded media
</Button>
<Select defaultValue="all">
<SelectTrigger>
<SelectValue placeholder="Select a type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All</SelectItem>
<SelectItem value="video">Video</SelectItem>
<SelectItem value="image">Image</SelectItem>
</SelectContent>
</Select>
</div>
{/* 媒体库 */}
<div className="grid grid-cols-3 gap-4">
{/* 媒体库 网格展示 */}
</div>
{/* 没有数据 提示 请上传媒体 */}
<div className="flex flex-col items-center justify-center h-full w-full">
<p className="text-gray-300">No media uploaded</p>
</div>
</div>
</TabsContent>
<TabsContent value="stock">
<div className="flex flex-col gap-4">
{/* 素材库 */}
</div>
</TabsContent>
<TabsContent value="generative">
<div className="flex flex-col gap-4">
{/* 生成媒体 */}
</div>
</TabsContent>
</Tabs>
</div>
{/* Apply 按钮 Cancel 按钮 */}
<div className="flex justify-end gap-4 fixed bottom-0 left-0 right-0 z-10 p-5">
<Button variant="outline">Cancel</Button>
<Button>Apply</Button>
</div>
</DialogContent>
</Dialog>
{/* 媒体属性 弹窗 高度全屏 */}
<Dialog open={isMediaPropertyOpen} onOpenChange={setIsMediaPropertyOpen}>
<DialogContent className="max-w-5xl w-full h-full overflow-hidden p-0">
<DialogHeader className="fixed top-0 left-5 right-10 z-10 h-10 flex justify-center">
<DialogTitle>Media properties</DialogTitle>
</DialogHeader>
<div className="flex mt-5 hide-scrollbar p-5">
{/* 左侧内容区域 */}
<div className="flex-1 pr-4">
{/* 标签页 */}
<Tabs value={activeTabMediaProperty} onValueChange={setActiveTabMediaProperty} className="h-[calc(100vh-7rem)] overflow-hidden">
<TabsList className="grid w-full grid-cols-2">
{mediaPropertyTabs.map((tab) => (
<TabsTrigger key={tab.value} value={tab.value} className="w-full">{tab.label}</TabsTrigger>
))}
</TabsList>
<TabsContent value="media" className="space-y-6 mt-6 h-[calc(100%-5rem)] overflow-y-auto hide-scrollbar">
{/* Duration */}
<div className="space-y-2">
<Label className="text-sm font-medium">Duration</Label>
<div className="text-sm text-gray-300">00m : 10s : 500ms / 00m : 17s : 320ms</div>
</div>
{/* Trim */}
<div className="space-y-4">
<Label className="text-sm font-medium">Trim</Label>
<div className="flex items-center space-x-2">
<Checkbox id="trim-auto" />
<Label htmlFor="trim-auto" className="text-sm">Trim automatically</Label>
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<Label className="text-sm">from</Label>
<Input
type="text"
placeholder="0.00s"
className="w-20 h-8 text-sm"
/>
</div>
<div className="flex items-center space-x-2">
<Label className="text-sm">to</Label>
<Input
type="text"
placeholder="s"
className="w-20 h-8 text-sm"
/>
</div>
</div>
</div>
{/* Center point */}
<div className="space-y-4">
<Label className="text-sm font-medium">Center point</Label>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<Label className="text-sm w-4">X</Label>
<Input
type="text"
value="0.5"
className="w-16 h-8 text-sm"
/>
</div>
<div className="flex items-center space-x-2">
<Label className="text-sm w-4">Y</Label>
<Input
type="text"
value="0.5"
className="w-16 h-8 text-sm"
/>
</div>
</div>
</div>
{/* Zoom & Rotation */}
<div className="space-y-4">
<Label className="text-sm font-medium">Zoom & Rotation</Label>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<Label className="text-sm w-4">
<Ruler className="h-4 w-4" />
</Label>
<Input
type="text"
value="0.5"
className="w-16 h-8 text-sm"
/>
</div>
<div className="flex items-center space-x-2">
<Label className="text-sm w-4">
<RotateCcw className="h-4 w-4" />
</Label>
<Input
type="text"
value="0.5"
className="w-16 h-8 text-sm"
/>
</div>
</div>
</div>
{/* Transition */}
<div className="space-y-2">
<Label className="text-sm font-medium">Transition</Label>
<Select defaultValue="auto">
<SelectTrigger className="w-32 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">Auto</SelectItem>
<SelectItem value="fade">Fade</SelectItem>
<SelectItem value="slide">Slide</SelectItem>
<SelectItem value="zoom">Zoom</SelectItem>
</SelectContent>
</Select>
</div>
{/* Script */}
<div className="space-y-2">
<Label className="text-sm font-medium">Script</Label>
<div className="text-sm text-gray-300">This part of the script is 21.00 seconds long.</div>
<div className="text-sm text-gray-400 mt-2">There are 2 media attached to this part of the script:</div>
{/* 章节列表 */}
<div>
{chapters.map((chapter, index) => (
<div key={chapter.id} className="flex items-center justify-between">
{/* Chapter: index + 1 文字竖着显示 */}
<div className="text-sm text-gray-300" style={{
transform: 'rotate(180deg)',
whiteSpace: 'nowrap',
height: 'fit-content',
writingMode: 'vertical-lr',
marginRight: '5px'
}}>
<div>Chapter {chapter.id}</div>
</div>
{/* flex: 1 */}
<div className="flex flex-1 space-x-2 overflow-x-auto pb-2">
{chapter.shots.map((shot, index) => (
<div
key={shot.id}
className={`relative flex-shrink-0 cursor-pointer rounded-lg overflow-hidden border-2 ${
selectedShot === shot.id ? 'border-blue-500' : 'border-gray-600'
}`}
onClick={() => setSelectedShot(shot.id)}
>
<div className="w-32 h-20 relative">
<video
src={shot.shotVideo}
className="w-full h-full object-cover"
autoPlay={false}
muted={false}
loop={false}
/>
<div className="absolute bottom-1 right-1 bg-black/70 text-white text-xs px-1 rounded">
00:{shot.duration.toString().padStart(2, '0')}
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
</TabsContent>
<TabsContent value="audio" className="space-y-6 mt-6">
{/* SFX name */}
<div className="space-y-2">
<Label className="text-sm font-medium">SFX name</Label>
<div className="text-sm text-gray-300">Airplane Rocket Fire Close</div>
</div>
{/* SFX volume */}
<div className="space-y-2">
<Label className="text-sm font-medium flex items-center">
SFX volume
<span className="ml-auto text-gray-300">30%</span>
</Label>
<Slider
value={[30]}
max={100}
step={1}
className="w-full"
/>
</div>
{/* Replace audio */}
<div className="space-y-4">
<Label className="text-sm font-medium">Replace audio</Label>
<div className="flex space-x-2">
<Button
variant="outline"
size="sm"
className="text-white"
>
Upload audio
</Button>
<Button
variant="outline"
size="sm"
className="text-white"
>
Stock SFX
</Button>
<Button
variant="outline"
size="sm"
className="text-white"
>
Generate SFX
</Button>
</div>
</div>
</TabsContent>
</Tabs>
</div>
{/* 右侧预览区域 */}
<div className="w-80 rounded-lg p-4">
{currentShot && (
<div className="space-y-4">
{/* 视频预览 */}
<div className="aspect-video bg-black rounded overflow-hidden border border-yellow-500">
<video
src={currentShot.shotVideo}
className="w-full h-full object-cover"
controls
muted
/>
</div>
<div className="text-sm text-gray-300">
Chapter 1 / media 1 / People gathered in a city square to watch a fireworks display
</div>
{/* 音频波形 */}
<div className="bg-gray-700 rounded p-2">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-2">
<Button variant="ghost" size="sm" className="h-6 w-6 p-0">
<Play className="h-3 w-3" />
</Button>
<span className="text-xs text-gray-300">0:00</span>
</div>
<span className="text-xs text-gray-300">0:12</span>
</div>
{/* 简单的音频波形显示 */}
<div className="h-8 bg-gray-600 rounded flex items-end justify-center space-x-px overflow-hidden">
{Array.from({ length: 40 }).map((_, i) => (
<div
key={i}
className="bg-gray-300 w-1"
style={{
height: `${Math.random() * 100}%`,
minHeight: '10%'
}}
/>
))}
</div>
<div className="text-xs text-gray-400 mt-2">
Chapter 1 / Audio & SFX / Airplane Rocket Fire Close
</div>
</div>
</div>
)}
</div>
</div>
{/* 底部按钮 */}
<div className="flex justify-end gap-4 pt-4 fixed bottom-0 left-0 right-10 z-10 p-5">
<Button variant="outline" className="text-white">
Reset
</Button>
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
Apply
</Button>
</div>
</DialogContent>
</Dialog>
{/* Timeline Header */}
<div className=" p-4">
{/* 章节列表 */}
<div>
{chapters.map((chapter, index) => (
<div key={chapter.id} className="flex items-center justify-between">
{/* Chapter: index + 1 文字竖着显示 */}
<div className="text-sm text-gray-300" style={{
transform: 'rotate(180deg)',
whiteSpace: 'nowrap',
height: 'fit-content',
writingMode: 'vertical-lr',
marginRight: '5px'
}}>
<div>Chapter {chapter.id}</div>
</div>
{/* flex: 1 */}
<div className="flex flex-1 space-x-2 overflow-x-auto pb-2">
{chapter.shots.map((shot, index) => (
<div
key={shot.id}
className={`relative flex-shrink-0 cursor-pointer rounded-lg overflow-hidden border-2 ${
selectedShot === shot.id ? 'border-blue-500' : 'border-gray-600'
}`}
onClick={() => setSelectedShot(shot.id)}
>
<div className="w-32 h-20 relative">
<video
src={shot.shotVideo}
className="w-full h-full object-cover"
autoPlay={false}
muted={false}
loop={false}
/>
<div className="absolute top-1 left-1">
{/* 图层icon 点击打开弹窗 视频列表 generatedVideos 选择替换 */}
<Button size="sm" variant="ghost" className="h-6 w-6 p-0 bg-black/50 hover:bg-black/70" onClick={() => {
setIsCheckVideoOpen(true);
}}>
<Layers className="h-4 w-4" />
</Button>
</div>
<div className="absolute bottom-1 right-1 bg-black/70 text-white text-xs px-1 rounded">
00:{shot.duration.toString().padStart(2, '0')}
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
{/* Chinese Text 不换行 溢出可左右滚动 隐藏滚动条 */}
<div className="text-sm text-gray-300 leading-relaxed overflow-x-auto whitespace-nowrap scrollbar-hide">
......
</div>
</div>
{/* Main Content */}
<div className="flex">
{/* Left Panel */}
<div className="w-full p-6 space-y-6">
{/* Replace Media Section */}
{currentShot && (
<div className="space-y-4">
<h2 className="text-lg font-medium">Replace media {currentShot.mediaNumber} with:</h2>
<div className="grid grid-cols-3 gap-4">
<Button
variant="outline"
className="h-12 border-gray-600 hover:bg-gray-700 text-white"
onClick={() => handleOpenReplaceMedia('uploaded')}
>
<Upload className="mr-2 h-4 w-4" />
Uploaded media
</Button>
<Button
variant="outline"
className="h-12 border-gray-600 hover:bg-gray-700 text-white"
onClick={() => handleOpenReplaceMedia('stock')}
>
<Image className="mr-2 h-4 w-4" />
Stock media
</Button>
<Button
variant="outline"
className="h-12 border-gray-600 hover:bg-gray-700 text-white"
onClick={() => handleOpenReplaceMedia('generative')}
>
<Sparkles className="mr-2 h-4 w-4" />
Generative Media
</Button>
</div>
</div>
)}
{/* Media Info */}
{currentShot && (
<div className="flex">
<div className="space-y-4 w-2/3">
<h3 className="text-lg font-medium">Media info:</h3>
<div className="space-y-3">
{/* Chapter/Media Info */}
<div className="flex items-center space-x-3">
{/* 文档图标 */}
<File className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-300">
Chapter 1 / media {currentShot.mediaNumber} / Generated media
</span>
<Popover>
<PopoverTrigger>
<Button size="sm" variant="ghost" className="h-6 w-6 p-0">
<Trash2 className="h-3 w-3" />
</Button>
</PopoverTrigger>
<PopoverContent align="start" sideOffset={-40} className="w-100 ml-8 p-0">
<div className="flex flex-col">
<div className="flex pt-2 pb-1 pl-2 pr-2 items-center font-bold border-b border-gray-600">Delete media</div>
<div className="flex flex-col gap-2">
<div className="p-2 cursor-pointer hover:bg-gray-700">Delete and add blank media</div>
</div>
</div>
</PopoverContent>
</Popover>
</div>
{/* Duration */}
<div className="flex items-center space-x-3">
{/* 标尺图标 */}
<Ruler className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-300">
00m : 08s : 070ms / 00m : 08s : 070ms
</span>
<Popover>
<PopoverTrigger>
<Button size="sm" variant="ghost" className="h-6 w-6 p-0">
<Edit className="h-3 w-3" />
</Button>
</PopoverTrigger>
<PopoverContent align="start" sideOffset={-40} className="w-100 ml-8 p-0">
<div className="flex flex-col">
<div className="flex pt-2 pb-1 pl-2 pr-2 items-center font-bold border-b border-gray-600">Trim</div>
<div className="flex flex-col gap-2">
{/* checkbox */}
<div className="flex items-center space-x-2 p-2">
<Checkbox id="trim" />
<Label htmlFor="trim">Trim automatically</Label>
</div>
<div className="flex items-center space-x-2 p-2">
<Label htmlFor="trim">From</Label>
<Input type="text" placeholder="00:00" />
<Label htmlFor="trim">To</Label>
<Input type="text" placeholder="00:00" />
</div>
{/* 按钮 */}
<div className="flex justify-end p-2">
<Button variant="outline" className="mr-2">Cancel</Button>
<Button>Apply</Button>
</div>
</div>
</div>
</PopoverContent>
</Popover>
</div>
{/* Transition */}
<div className="flex items-center space-x-3">
{/* 图标 */}
<UnfoldHorizontal className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-300">
Transition: {currentShot.transition}
</span>
<Popover>
<PopoverTrigger>
<Button size="sm" variant="ghost" className="h-6 w-6 p-0">
<Edit className="h-3 w-3" />
</Button>
</PopoverTrigger>
<PopoverContent align="start" sideOffset={-40} className="w-100 ml-8 p-0">
<div className="flex flex-col">
<div className="flex pt-2 pb-1 pl-2 pr-2 items-center font-bold border-b border-gray-600">Transition</div>
<div className="flex flex-col gap-2">
{/* select */}
<div className="flex items-center space-x-2 p-2">
<Select>
<SelectTrigger>
<SelectValue placeholder="Select a transition" />
</SelectTrigger>
<SelectContent>
<SelectItem value="fade">Fade</SelectItem>
<SelectItem value="slide">Slide</SelectItem>
<SelectItem value="zoom">Zoom</SelectItem>
</SelectContent>
</Select>
</div>
{/* 按钮 */}
<div className="flex justify-end p-2">
<Button variant="outline" className="mr-2">Cancel</Button>
<Button>Apply</Button>
</div>
</div>
</div>
</PopoverContent>
</Popover>
</div>
{/* Audio Volume */}
<div className="flex items-center space-x-3">
<Volume2 className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-300">
Audio volume: {currentShot.volume}% volume
</span>
<Button size="sm" variant="ghost" className="h-6 w-6 p-0" onClick={() => handleOpenMediaProperty('audio')}>
<Edit className="h-3 w-3" />
</Button>
</div>
</div>
</div>
{/* Right Panel - Preview */}
<div className="w-1/3 p-6">
{currentShot && (
<div className="space-y-4">
<div className="aspect-video bg-black rounded-lg overflow-hidden relative">
<video
ref={videoRef}
src={currentShot.shotVideo}
className="w-full h-full object-cover"
controls
autoPlay={false}
muted={false}
loop={false}
/>
</div>
{/* 刷新图标 */}
<Button size="sm" variant="ghost" className="h-8 w-8 p-0 border border-gray-600 rounded w-full">
<RefreshCcw className="h-4 w-4" />
</Button>
</div>
)}
</div>
</div>
)}
{/* Media Properties */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-medium underline cursor-pointer" onClick={() => handleOpenMediaProperty('media')}>Media properties</h3>
</div>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex justify-between p-6 ">
<Button variant="outline" onClick={onPrevious} className="bg-gray-700 border-gray-600 text-white hover:bg-gray-600">
<ArrowLeft className="mr-2 h-4 w-4" />
</Button>
<Button onClick={onNext} className="bg-blue-600 hover:bg-blue-700 text-white">
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</div>
);
}