diff options
| author | Maze Winther <mazewinther@gmail.com> | 2025-07-01 00:58:21 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2025-07-01 00:58:21 +0200 |
| commit | 9b37ce6610eeba26fc2b1b1d2589a7e9ba5e6c00 (patch) | |
| tree | 93dbd334b9c11a2945e0e033c554a1e405469e0e | |
| parent | d11d835c7cc255b4e7e3e8bac59bea653bd9e1ef (diff) | |
| download | opencut-9b37ce6610eeba26fc2b1b1d2589a7e9ba5e6c00.tar.gz | |
style: give preview panel a fresh look
| -rw-r--r-- | apps/web/src/app/globals.css | 2 | ||||
| -rw-r--r-- | apps/web/src/components/editor/preview-panel.tsx | 157 |
2 files changed, 112 insertions, 47 deletions
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 4f9bb10a..eafdc861 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -40,7 +40,7 @@ } .dark { --background: 0 0% 8%; - --foreground: 0 0% 98%; + --foreground: 0 0% 89%; --card: 0 0% 3.9%; --card-foreground: 0 0% 98%; --popover: 0 0% 14.9%; diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index 0c269483..72d58985 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -10,8 +10,16 @@ import { usePlaybackStore } from "@/stores/playback-store"; import { useEditorStore } from "@/stores/editor-store"; import { VideoPlayer } from "@/components/ui/video-player"; import { Button } from "@/components/ui/button"; -import { Play, Pause, Volume2, VolumeX, Plus } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuSeparator, +} from "@/components/ui/dropdown-menu"; +import { Play, Pause, Volume2, VolumeX, Plus, Square } from "lucide-react"; import { useState, useRef, useEffect } from "react"; +import { cn } from "@/lib/utils"; interface ActiveClip { clip: TimelineClip; @@ -189,51 +197,11 @@ export function PreviewPanel() { return ( <div className="h-full w-full flex flex-col min-h-0 min-w-0"> - {/* Controls */} - <div className="border-b p-2 flex items-center gap-2 text-xs flex-shrink-0"> - <span className="text-muted-foreground">Canvas:</span> - <select - value={`${canvasSize.width}x${canvasSize.height}`} - onChange={(e) => { - const preset = canvasPresets.find( - (p) => `${p.width}x${p.height}` === e.target.value - ); - if (preset) - setCanvasSize({ width: preset.width, height: preset.height }); - }} - className="bg-background border rounded px-2 py-1 text-xs" - > - {canvasPresets.map((preset) => ( - <option - key={preset.name} - value={`${preset.width}x${preset.height}`} - > - {preset.name} ({preset.width}×{preset.height}) - </option> - ))} - </select> - - <Button - variant="outline" - size="sm" - onClick={toggleMute} - className="ml-auto" - > - {muted || volume === 0 ? ( - <VolumeX className="h-3 w-3 mr-1" /> - ) : ( - <Volume2 className="h-3 w-3 mr-1" /> - )} - {muted || volume === 0 ? "Unmute" : "Mute"} - </Button> - </div> - - {/* Preview Area */} <div ref={containerRef} className="flex-1 flex flex-col items-center justify-center p-3 min-h-0 min-w-0 gap-4" > - {hasAnyClips && ( + {hasAnyClips ? ( <div ref={previewRef} className="relative overflow-hidden rounded-sm bg-black border" @@ -250,29 +218,126 @@ export function PreviewPanel() { activeClips.map((clipData, index) => renderClip(clipData, index)) )} </div> + ) : ( + <> + {/* Empty div so toolbar stays at the bottom */} + <div className="w-full h-full"></div> + </> )} - {hasAnyClips && <PreviewToolbar />} + <PreviewToolbar hasAnyClips={hasAnyClips} /> </div> </div> ); } -function PreviewToolbar() { +function PreviewToolbar({ hasAnyClips }: { hasAnyClips: boolean }) { const { isPlaying, toggle } = usePlaybackStore(); + const { + canvasSize, + canvasPresets, + setCanvasSize, + setCanvasSizeFromAspectRatio, + } = useEditorStore(); + const { mediaItems } = useMediaStore(); + const { tracks } = useTimelineStore(); + + // Find the current preset based on canvas size + const currentPreset = canvasPresets.find( + (preset) => + preset.width === canvasSize.width && preset.height === canvasSize.height + ); + + const handlePresetSelect = (preset: { width: number; height: number }) => { + setCanvasSize({ width: preset.width, height: preset.height }); + }; + + // Get the first video/image media item to determine original aspect ratio + const getOriginalAspectRatio = () => { + // Find first video or image in timeline + for (const track of tracks) { + for (const clip of track.clips) { + const mediaItem = mediaItems.find((item) => item.id === clip.mediaId); + if ( + mediaItem && + (mediaItem.type === "video" || mediaItem.type === "image") + ) { + return mediaItem.aspectRatio || 16 / 9; // Default to 16:9 if aspectRatio not available + } + } + } + return 16 / 9; // Default aspect ratio + }; + + const handleOriginalSelect = () => { + const aspectRatio = getOriginalAspectRatio(); + setCanvasSizeFromAspectRatio(aspectRatio); + }; + + // Check if current size is "Original" (not matching any preset) + const isOriginal = !currentPreset; return ( <div data-toolbar - className="flex items-center justify-center gap-2 px-4 pt-2 bg-background-500 w-full" + className="flex items-end justify-between gap-2 p-1 pt-2 bg-background-500 w-full" > - <Button variant="text" size="icon" onClick={toggle}> + <div> + <p + className={cn( + "text-xs text-muted-foreground", + !hasAnyClips && "opacity-50" + )} + > + 00:00:00:00/00:00:00:00 + </p> + </div> + <Button + variant="text" + size="icon" + onClick={toggle} + disabled={!hasAnyClips} + > {isPlaying ? ( <Pause className="h-3 w-3" /> ) : ( <Play className="h-3 w-3" /> )} </Button> + <div> + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button + size="sm" + className="!bg-background text-foreground text-xs h-auto rounded-none border border-foreground px-0.5 py-0 font-light" + disabled={!hasAnyClips} + > + {currentPreset?.name || "Ratio"} + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuItem + onClick={handleOriginalSelect} + className={cn("text-xs", isOriginal && "font-semibold")} + > + Original + </DropdownMenuItem> + <DropdownMenuSeparator /> + {canvasPresets.map((preset) => ( + <DropdownMenuItem + key={preset.name} + onClick={() => handlePresetSelect(preset)} + className={cn( + "text-xs", + currentPreset?.name === preset.name && "font-semibold" + )} + > + {preset.name} + </DropdownMenuItem> + ))} + </DropdownMenuContent> + </DropdownMenu> + </div> </div> ); } |
