summaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
Diffstat (limited to 'apps')
-rw-r--r--apps/web/src/app/globals.css2
-rw-r--r--apps/web/src/components/editor/preview-panel.tsx157
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>
);
}