video-flow-b/components/workflow/generate-shots-step.tsx
2025-06-23 20:47:46 +08:00

836 lines
32 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;
}
interface Shot {
id: string;
type: string;
duration: number;
shotVideo: string;
generatedVideos: string[];
description: string;
transition: string;
volume: number;
mediaNumber: number;
}
interface Chapter {
id: number;
title: string;
shots: Shot[];
}
// 时间轴组件
const TimelineView = ({
chapters,
selectedShot,
onShotSelect,
onVideoCheck
}: {
chapters: Chapter[];
selectedShot: string;
onShotSelect: (shotId: string) => void;
onVideoCheck?: () => void;
}) => (
<div>
{chapters.map((chapter) => (
<div key={chapter.id} className="flex items-center justify-between">
<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>
<div className="flex flex-1 space-x-2 overflow-x-auto pb-2">
{chapter.shots.map((shot) => (
<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={() => onShotSelect(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}
/>
{onVideoCheck && (
<div className="absolute top-1 left-1">
<Button size="sm" variant="ghost" className="h-6 w-6 p-0 bg-black/50 hover:bg-black/70" onClick={onVideoCheck}>
<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>
);
// 媒体信息项组件
const MediaInfoItem = ({
icon,
text,
popoverContent
}: {
icon: React.ReactNode;
text: string;
popoverContent?: React.ReactNode;
}) => (
<div className="flex items-center space-x-3">
{icon}
<span className="text-sm text-gray-300">{text}</span>
{popoverContent && (
<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">
{popoverContent}
</PopoverContent>
</Popover>
)}
</div>
);
// 查看视频弹窗
const CheckVideoDialog = ({
isOpen,
onClose,
currentShot
}: {
isOpen: boolean;
onClose: (open: boolean) => void;
currentShot?: Shot;
}) => (
<Dialog open={isOpen} onOpenChange={onClose}>
<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>
<div className="flex justify-end gap-4">
<Button variant="outline">Cancel</Button>
<Button>Apply</Button>
</div>
</DialogContent>
</Dialog>
);
// 替换媒体弹窗
const ReplaceMediaDialog = ({
isOpen,
onClose,
chapters,
selectedShot,
onShotSelect,
activeTab,
setActiveTab
}: {
isOpen: boolean;
onClose: (open: boolean) => void;
chapters: Chapter[];
selectedShot: string;
onShotSelect: (shotId: string) => void;
activeTab: string;
setActiveTab: (tab: string) => void;
}) => {
const replaceMediaTabs = [
{ value: 'uploaded', label: 'Uploaded media' },
{ value: 'stock', label: 'Stock media' },
{ value: 'generative', label: 'Generative media' },
];
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<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">
<TimelineView
chapters={chapters}
selectedShot={selectedShot}
onShotSelect={onShotSelect}
/>
<Tabs defaultValue={activeTab} 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>
<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>
);
};
// 媒体属性弹窗
const MediaPropertyDialog = ({
isOpen,
onClose,
chapters,
selectedShot,
onShotSelect,
currentShot,
activeTab,
setActiveTab
}: {
isOpen: boolean;
onClose: (open: boolean) => void;
chapters: Chapter[];
selectedShot: string;
onShotSelect: (shotId: string) => void;
currentShot?: Shot;
activeTab: string;
setActiveTab: (tab: string) => void;
}) => {
const mediaPropertyTabs = [
{ value: 'media', label: 'Media' },
{ value: 'audio', label: 'Audio & SFX' },
];
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<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={activeTab} onValueChange={setActiveTab} 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">
<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>
<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>
<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>
<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>
<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>
<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>
<TimelineView
chapters={chapters}
selectedShot={selectedShot}
onShotSelect={onShotSelect}
/>
</div>
</TabsContent>
<TabsContent value="audio" className="space-y-6 mt-6">
<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>
<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>
<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>
);
};
const mockChapters: Chapter[] = [
{
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,
}
]
}
];
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 [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 currentChapter = chapters.find(ch => ch.id === selectedChapter);
const currentShot = currentChapter?.shots.find(shot => shot.id === selectedShot);
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">
{/* 弹窗组件 */}
<CheckVideoDialog
isOpen={isCheckVideoOpen}
onClose={setIsCheckVideoOpen}
currentShot={currentShot}
/>
<ReplaceMediaDialog
isOpen={isReplaceMediaOpen}
onClose={setIsReplaceMediaOpen}
chapters={chapters}
selectedShot={selectedShot}
onShotSelect={setSelectedShot}
activeTab={activeTabReplaceMedia}
setActiveTab={setActiveTabReplaceMedia}
/>
<MediaPropertyDialog
isOpen={isMediaPropertyOpen}
onClose={setIsMediaPropertyOpen}
chapters={chapters}
selectedShot={selectedShot}
onShotSelect={setSelectedShot}
currentShot={currentShot}
activeTab={activeTabMediaProperty}
setActiveTab={setActiveTabMediaProperty}
/>
{/* Timeline Header */}
<div className="p-4">
<TimelineView
chapters={chapters}
selectedShot={selectedShot}
onShotSelect={setSelectedShot}
onVideoCheck={() => setIsCheckVideoOpen(true)}
/>
<div className="text-sm text-gray-300 leading-relaxed overflow-x-auto whitespace-nowrap scrollbar-hide">
......
</div>
</div>
{/* Main Content */}
<div className="flex">
<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">
<MediaInfoItem
icon={<File className="h-4 w-4 text-gray-400" />}
text={`Chapter 1 / media ${currentShot.mediaNumber} / Generated media`}
popoverContent={
<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>
}
/>
<MediaInfoItem
icon={<Ruler className="h-4 w-4 text-gray-400" />}
text="00m : 08s : 070ms / 00m : 08s : 070ms"
popoverContent={
<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">
<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>
}
/>
<MediaInfoItem
icon={<UnfoldHorizontal className="h-4 w-4 text-gray-400" />}
text={`Transition: ${currentShot.transition}`}
popoverContent={
<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">
<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>
}
/>
<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>
{/* 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>
<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>
)}
</div>
</div>
{/* Action Buttons */}
<div className="flex justify-between">
<Button variant="outline" onClick={onPrevious}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Chapters
</Button>
<Button onClick={onNext}>
Add Background Music
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</div>
);
}