style: give preview panel a fresh look
This commit is contained in:
@ -40,7 +40,7 @@
|
|||||||
}
|
}
|
||||||
.dark {
|
.dark {
|
||||||
--background: 0 0% 8%;
|
--background: 0 0% 8%;
|
||||||
--foreground: 0 0% 98%;
|
--foreground: 0 0% 89%;
|
||||||
--card: 0 0% 3.9%;
|
--card: 0 0% 3.9%;
|
||||||
--card-foreground: 0 0% 98%;
|
--card-foreground: 0 0% 98%;
|
||||||
--popover: 0 0% 14.9%;
|
--popover: 0 0% 14.9%;
|
||||||
|
@ -10,8 +10,16 @@ import { usePlaybackStore } from "@/stores/playback-store";
|
|||||||
import { useEditorStore } from "@/stores/editor-store";
|
import { useEditorStore } from "@/stores/editor-store";
|
||||||
import { VideoPlayer } from "@/components/ui/video-player";
|
import { VideoPlayer } from "@/components/ui/video-player";
|
||||||
import { Button } from "@/components/ui/button";
|
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 { useState, useRef, useEffect } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface ActiveClip {
|
interface ActiveClip {
|
||||||
clip: TimelineClip;
|
clip: TimelineClip;
|
||||||
@ -189,51 +197,11 @@ export function PreviewPanel() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full flex flex-col min-h-0 min-w-0">
|
<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
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="flex-1 flex flex-col items-center justify-center p-3 min-h-0 min-w-0 gap-4"
|
className="flex-1 flex flex-col items-center justify-center p-3 min-h-0 min-w-0 gap-4"
|
||||||
>
|
>
|
||||||
{hasAnyClips && (
|
{hasAnyClips ? (
|
||||||
<div
|
<div
|
||||||
ref={previewRef}
|
ref={previewRef}
|
||||||
className="relative overflow-hidden rounded-sm bg-black border"
|
className="relative overflow-hidden rounded-sm bg-black border"
|
||||||
@ -250,29 +218,126 @@ export function PreviewPanel() {
|
|||||||
activeClips.map((clipData, index) => renderClip(clipData, index))
|
activeClips.map((clipData, index) => renderClip(clipData, index))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Empty div so toolbar stays at the bottom */}
|
||||||
|
<div className="w-full h-full"></div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasAnyClips && <PreviewToolbar />}
|
<PreviewToolbar hasAnyClips={hasAnyClips} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PreviewToolbar() {
|
function PreviewToolbar({ hasAnyClips }: { hasAnyClips: boolean }) {
|
||||||
const { isPlaying, toggle } = usePlaybackStore();
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
data-toolbar
|
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"
|
||||||
|
>
|
||||||
|
<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}
|
||||||
>
|
>
|
||||||
<Button variant="text" size="icon" onClick={toggle}>
|
|
||||||
{isPlaying ? (
|
{isPlaying ? (
|
||||||
<Pause className="h-3 w-3" />
|
<Pause className="h-3 w-3" />
|
||||||
) : (
|
) : (
|
||||||
<Play className="h-3 w-3" />
|
<Play className="h-3 w-3" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user