style: give preview panel a fresh look
This commit is contained in:
@ -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%;
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user