"use client"; import { useTimelineStore, type TimelineClip, type TimelineTrack, } from "@/stores/timeline-store"; import { useMediaStore, type MediaItem, getMediaAspectRatio, } from "@/stores/media-store"; 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 { 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"; import { formatTimeCode } from "@/lib/time"; interface ActiveClip { clip: TimelineClip; track: TimelineTrack; mediaItem: MediaItem | null; } export function PreviewPanel() { const { tracks } = useTimelineStore(); const { mediaItems } = useMediaStore(); const { currentTime, muted, toggleMute, volume } = usePlaybackStore(); const { canvasSize, canvasPresets, setCanvasSize } = useEditorStore(); const previewRef = useRef(null); const containerRef = useRef(null); const [previewDimensions, setPreviewDimensions] = useState({ width: 0, height: 0, }); // Calculate optimal preview size that fits in container while maintaining aspect ratio useEffect(() => { const updatePreviewSize = () => { if (!containerRef.current) return; const container = containerRef.current.getBoundingClientRect(); const computedStyle = getComputedStyle(containerRef.current); // Get padding values const paddingTop = parseFloat(computedStyle.paddingTop); const paddingBottom = parseFloat(computedStyle.paddingBottom); const paddingLeft = parseFloat(computedStyle.paddingLeft); const paddingRight = parseFloat(computedStyle.paddingRight); // Get gap value (gap-4 = 1rem = 16px) const gap = parseFloat(computedStyle.gap) || 16; // Get toolbar height if it exists const toolbar = containerRef.current.querySelector("[data-toolbar]"); const toolbarHeight = toolbar ? toolbar.getBoundingClientRect().height : 0; // Calculate available space after accounting for padding, gap, and toolbar const availableWidth = container.width - paddingLeft - paddingRight; const availableHeight = container.height - paddingTop - paddingBottom - toolbarHeight - (toolbarHeight > 0 ? gap : 0); const targetRatio = canvasSize.width / canvasSize.height; const containerRatio = availableWidth / availableHeight; let width, height; if (containerRatio > targetRatio) { // Container is wider - constrain by height height = availableHeight; width = height * targetRatio; } else { // Container is taller - constrain by width width = availableWidth; height = width / targetRatio; } setPreviewDimensions({ width, height }); }; updatePreviewSize(); const resizeObserver = new ResizeObserver(updatePreviewSize); if (containerRef.current) { resizeObserver.observe(containerRef.current); } return () => resizeObserver.disconnect(); }, [canvasSize.width, canvasSize.height]); // Get active clips at current time const getActiveClips = (): ActiveClip[] => { const activeClips: ActiveClip[] = []; tracks.forEach((track) => { track.clips.forEach((clip) => { const clipStart = clip.startTime; const clipEnd = clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); if (currentTime >= clipStart && currentTime < clipEnd) { const mediaItem = clip.mediaId === "test" ? null // Test clips don't have a real media item : mediaItems.find((item) => item.id === clip.mediaId) || null; activeClips.push({ clip, track, mediaItem }); } }); }); return activeClips; }; const activeClips = getActiveClips(); // Check if there are any clips in the timeline at all const hasAnyClips = tracks.some((track) => track.clips.length > 0); // Render a clip const renderClip = (clipData: ActiveClip, index: number) => { const { clip, mediaItem } = clipData; // Test clips if (!mediaItem || clip.mediaId === "test") { return (
🎬

{clip.name}

); } // Video clips if (mediaItem.type === "video") { return (
); } // Image clips if (mediaItem.type === "image") { return (
{mediaItem.name}
); } // Audio clips (visual representation) if (mediaItem.type === "audio") { return (
🎵

{mediaItem.name}

); } return null; }; return (
{hasAnyClips ? (
{activeClips.length === 0 ? (
No clips at current time
) : ( activeClips.map((clipData, index) => renderClip(clipData, index)) )}
) : ( <> {/* Empty div so toolbar stays at the bottom */}
)}
); } function PreviewToolbar({ hasAnyClips }: { hasAnyClips: boolean }) { const { isPlaying, toggle, currentTime } = usePlaybackStore(); const { canvasSize, canvasPresets, setCanvasSize, setCanvasSizeFromAspectRatio, } = useEditorStore(); const { mediaItems } = useMediaStore(); const { tracks, getTotalDuration } = 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 getMediaAspectRatio(mediaItem); } } } 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 (

{formatTimeCode(currentTime, "HH:MM:SS:CS")}/ {formatTimeCode(getTotalDuration(), "HH:MM:SS:CS")}

Original {canvasPresets.map((preset) => ( handlePresetSelect(preset)} className={cn( "text-xs", currentPreset?.name === preset.name && "font-semibold" )} > {preset.name} ))}
); }