diff options
Diffstat (limited to 'apps/web/src/components')
| -rw-r--r-- | apps/web/src/components/editor/media-panel.tsx | 28 | ||||
| -rw-r--r-- | apps/web/src/components/editor/timeline-toolbar.tsx | 219 |
2 files changed, 241 insertions, 6 deletions
diff --git a/apps/web/src/components/editor/media-panel.tsx b/apps/web/src/components/editor/media-panel.tsx index dfe08365..f717234d 100644 --- a/apps/web/src/components/editor/media-panel.tsx +++ b/apps/web/src/components/editor/media-panel.tsx @@ -1,14 +1,15 @@ "use client"; -import { Button } from "../ui/button"; -import { AspectRatio } from "../ui/aspect-ratio"; -import { DragOverlay } from "../ui/drag-overlay"; -import { useMediaStore, type MediaItem } from "@/stores/media-store"; -import { processMediaFiles } from "@/lib/media-processing"; -import { Plus, Image, Video, Music, Trash2, Upload } from "lucide-react"; import { useDragDrop } from "@/hooks/use-drag-drop"; +import { processMediaFiles } from "@/lib/media-processing"; +import { useMediaStore, type MediaItem } from "@/stores/media-store"; +import { useTimelineStore } from "@/stores/timeline-store"; +import { Image, Music, Plus, Trash2, Upload, Video } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; +import { AspectRatio } from "../ui/aspect-ratio"; +import { Button } from "../ui/button"; +import { DragOverlay } from "../ui/drag-overlay"; // MediaPanel lets users add, view, and drag media (images, videos, audio) into the project. // You can upload files or drag them from your computer. Dragging from here to the timeline adds them to your video project. @@ -58,6 +59,21 @@ export function MediaPanel() { const handleRemove = (e: React.MouseEvent, id: string) => { // Remove a media item from the store e.stopPropagation(); + + + // Remove tracks automatically when delete media + const { tracks, removeTrack } = useTimelineStore.getState(); + tracks.forEach((track) => { + const clipsToRemove = track.clips.filter((clip) => clip.mediaId === id); + clipsToRemove.forEach((clip) => { + useTimelineStore.getState().removeClipFromTrack(track.id, clip.id); + }); + // Only remove track if it becomes empty and has no other clips + const updatedTrack = useTimelineStore.getState().tracks.find(t => t.id === track.id); + if (updatedTrack && updatedTrack.clips.length === 0) { + removeTrack(track.id); + } + }); removeMediaItem(id); }; diff --git a/apps/web/src/components/editor/timeline-toolbar.tsx b/apps/web/src/components/editor/timeline-toolbar.tsx new file mode 100644 index 00000000..51464794 --- /dev/null +++ b/apps/web/src/components/editor/timeline-toolbar.tsx @@ -0,0 +1,219 @@ +"use client";
+
+import type { TrackType } from "@/types/timeline";
+import {
+ ArrowLeftToLine,
+ ArrowRightToLine,
+ Copy,
+ Pause,
+ Play,
+ Scissors,
+ Snowflake,
+ SplitSquareHorizontal,
+ Trash2,
+} from "lucide-react";
+import { Button } from "../ui/button";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "../ui/select";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "../ui/tooltip";
+
+interface TimelineToolbarProps {
+ isPlaying: boolean;
+ currentTime: number;
+ duration: number;
+ speed: number;
+ tracks: any[];
+ toggle: () => void;
+ setSpeed: (speed: number) => void;
+ addTrack: (type: TrackType) => string;
+ addClipToTrack: (trackId: string, clip: any) => void;
+ handleSplitSelected: () => void;
+ handleDuplicateSelected: () => void;
+ handleFreezeSelected: () => void;
+ handleDeleteSelected: () => void;
+}
+
+export function TimelineToolbar({
+ isPlaying,
+ currentTime,
+ duration,
+ speed,
+ tracks,
+ toggle,
+ setSpeed,
+ addTrack,
+ addClipToTrack,
+ handleSplitSelected,
+ handleDuplicateSelected,
+ handleFreezeSelected,
+ handleDeleteSelected,
+}: TimelineToolbarProps) {
+ return (
+ <div className="border-b flex items-center px-2 py-1 gap-1">
+ <TooltipProvider delayDuration={500}>
+ {/* Play/Pause Button */}
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button
+ variant="text"
+ size="icon"
+ onClick={toggle}
+ className="mr-2"
+ >
+ {isPlaying ? (
+ <Pause className="h-4 w-4" />
+ ) : (
+ <Play className="h-4 w-4" />
+ )}
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>
+ {isPlaying ? "Pause (Space)" : "Play (Space)"}
+ </TooltipContent>
+ </Tooltip>
+
+ <div className="w-px h-6 bg-border mx-1" />
+
+ {/* Time Display */}
+ <div
+ className="text-xs text-muted-foreground font-mono px-2"
+ style={{ minWidth: "18ch", textAlign: "center" }}
+ >
+ {currentTime.toFixed(1)}s / {duration.toFixed(1)}s
+ </div>
+
+ {/* Test Clip Button - for debugging */}
+ {tracks.length === 0 && (
+ <>
+ <div className="w-px h-6 bg-border mx-1" />
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => {
+ const trackId = addTrack("video");
+ addClipToTrack(trackId, {
+ mediaId: "test",
+ name: "Test Clip",
+ duration: 5,
+ startTime: 0,
+ trimStart: 0,
+ trimEnd: 0,
+ });
+ }}
+ className="text-xs"
+ >
+ Add Test Clip
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>Add a test clip to try playback</TooltipContent>
+ </Tooltip>
+ </>
+ )}
+
+ <div className="w-px h-6 bg-border mx-1" />
+
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button variant="text" size="icon" onClick={handleSplitSelected}>
+ <Scissors className="h-4 w-4" />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>Split clip (S)</TooltipContent>
+ </Tooltip>
+
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button variant="text" size="icon">
+ <ArrowLeftToLine className="h-4 w-4" />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>Split and keep left (A)</TooltipContent>
+ </Tooltip>
+
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button variant="text" size="icon">
+ <ArrowRightToLine className="h-4 w-4" />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>Split and keep right (D)</TooltipContent>
+ </Tooltip>
+
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button variant="text" size="icon">
+ <SplitSquareHorizontal className="h-4 w-4" />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>Separate audio (E)</TooltipContent>
+ </Tooltip>
+
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button
+ variant="text"
+ size="icon"
+ onClick={handleDuplicateSelected}
+ >
+ <Copy className="h-4 w-4" />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>Duplicate clip (Ctrl+D)</TooltipContent>
+ </Tooltip>
+
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button variant="text" size="icon" onClick={handleFreezeSelected}>
+ <Snowflake className="h-4 w-4" />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>Freeze frame (F)</TooltipContent>
+ </Tooltip>
+
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Button variant="text" size="icon" onClick={handleDeleteSelected}>
+ <Trash2 className="h-4 w-4" />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent>Delete clip (Delete)</TooltipContent>
+ </Tooltip>
+
+ <div className="w-px h-6 bg-border mx-1" />
+
+ {/* Speed Control */}
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <Select
+ value={speed.toFixed(1)}
+ onValueChange={(value) => setSpeed(parseFloat(value))}
+ >
+ <SelectTrigger className="w-[90px] h-8">
+ <SelectValue placeholder="1.0x" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="0.5">0.5x</SelectItem>
+ <SelectItem value="1.0">1.0x</SelectItem>
+ <SelectItem value="1.5">1.5x</SelectItem>
+ <SelectItem value="2.0">2.0x</SelectItem>
+ </SelectContent>
+ </Select>
+ </TooltipTrigger>
+ <TooltipContent>Playback Speed</TooltipContent>
+ </Tooltip>
+ </TooltipProvider>
+ </div>
+ );
+}
|
