style: give preview panel a fresh look

This commit is contained in:
Maze Winther
2025-07-01 00:58:21 +02:00
parent d11d835c7c
commit 9b37ce6610
2 changed files with 112 additions and 47 deletions

View File

@ -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%;

View File

@ -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>
);
}