"use client"; import { useTimelineStore } from "@/stores/timeline-store"; import { TimelineElement, TimelineTrack } from "@/types/timeline"; import { useMediaStore, type MediaItem } from "@/stores/media-store"; import { usePlaybackStore } from "@/stores/playback-store"; import { useEditorStore } from "@/stores/editor-store"; import { useAspectRatio } from "@/hooks/use-aspect-ratio"; import { VideoPlayer } from "@/components/ui/video-player"; import { AudioPlayer } from "@/components/ui/audio-player"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator, } from "@/components/ui/dropdown-menu"; import { Play, Pause, Expand } from "lucide-react"; import { useState, useRef, useEffect, useCallback } from "react"; import { cn } from "@/lib/utils"; import { formatTimeCode } from "@/lib/time"; import { FONT_CLASS_MAP } from "@/lib/font-config"; import { BackgroundSettings } from "../background-settings"; import { useProjectStore } from "@/stores/project-store"; interface ActiveElement { element: TimelineElement; track: TimelineTrack; mediaItem: MediaItem | null; } export function PreviewPanel() { const { tracks } = useTimelineStore(); const { mediaItems } = useMediaStore(); const { currentTime } = usePlaybackStore(); const { canvasSize } = useEditorStore(); const previewRef = useRef(null); const containerRef = useRef(null); const [previewDimensions, setPreviewDimensions] = useState({ width: 0, height: 0, }); const [isExpanded, setIsExpanded] = useState(false); const { activeProject } = useProjectStore(); // Calculate optimal preview size that fits in container while maintaining aspect ratio useEffect(() => { const updatePreviewSize = () => { if (!containerRef.current) return; let container; let availableWidth, availableHeight; if (isExpanded) { // Use full viewport when expanded availableWidth = window.innerWidth; availableHeight = window.innerHeight; } else { 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 availableWidth = container.width - paddingLeft - paddingRight; 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 * (isExpanded ? 0.9 : 1); // Leave some margin when expanded width = height * targetRatio; } else { // Container is taller - constrain by width width = availableWidth * (isExpanded ? 0.9 : 1); // Leave some margin when expanded height = width / targetRatio; } setPreviewDimensions({ width, height }); }; updatePreviewSize(); const resizeObserver = new ResizeObserver(updatePreviewSize); if (containerRef.current) { resizeObserver.observe(containerRef.current); } // Listen for window resize when expanded if (isExpanded) { window.addEventListener("resize", updatePreviewSize); } return () => { resizeObserver.disconnect(); if (isExpanded) { window.removeEventListener("resize", updatePreviewSize); } }; }, [canvasSize.width, canvasSize.height, isExpanded]); // Handle escape key to exit expanded mode useEffect(() => { const handleEscapeKey = (event: KeyboardEvent) => { if (event.key === "Escape" && isExpanded) { setIsExpanded(false); } }; if (isExpanded) { document.addEventListener("keydown", handleEscapeKey); // Prevent body scroll when expanded document.body.style.overflow = "hidden"; } else { document.body.style.overflow = ""; } return () => { document.removeEventListener("keydown", handleEscapeKey); document.body.style.overflow = ""; }; }, [isExpanded]); const toggleExpanded = useCallback(() => { setIsExpanded((prev) => !prev); }, []); // Get active elements at current time const getActiveElements = (): ActiveElement[] => { const activeElements: ActiveElement[] = []; tracks.forEach((track) => { track.elements.forEach((element) => { const elementStart = element.startTime; const elementEnd = element.startTime + (element.duration - element.trimStart - element.trimEnd); if (currentTime >= elementStart && currentTime < elementEnd) { let mediaItem = null; // Only get media item for media elements if (element.type === "media") { mediaItem = element.mediaId === "test" ? null // Test elements don't have a real media item : mediaItems.find((item) => item.id === element.mediaId) || null; } activeElements.push({ element, track, mediaItem }); } }); }); return activeElements; }; const activeElements = getActiveElements(); // Check if there are any elements in the timeline at all const hasAnyElements = tracks.some((track) => track.elements.length > 0); // Get media elements for blur background (video/image only) const getBlurBackgroundElements = (): ActiveElement[] => { return activeElements.filter( ({ element, mediaItem }) => element.type === "media" && mediaItem && (mediaItem.type === "video" || mediaItem.type === "image") && element.mediaId !== "test", // Exclude test elements ); }; const blurBackgroundElements = getBlurBackgroundElements(); // Render blur background layer const renderBlurBackground = () => { if ( !activeProject?.backgroundType || activeProject.backgroundType !== "blur" || blurBackgroundElements.length === 0 ) { return null; } // Use the first media element for background (could be enhanced to use primary/focused element) const backgroundElement = blurBackgroundElements[0]; const { element, mediaItem } = backgroundElement; if (!mediaItem) return null; const blurIntensity = activeProject.blurIntensity || 8; if (mediaItem.type === "video") { return (
); } if (mediaItem.type === "image") { return (
{mediaItem.name}
); } return null; }; // Render an element const renderElement = (elementData: ActiveElement, index: number) => { const { element, mediaItem } = elementData; // Text elements if (element.type === "text") { const fontClassName = FONT_CLASS_MAP[element.fontFamily as keyof typeof FONT_CLASS_MAP] || ""; const scaleRatio = previewDimensions.width / canvasSize.width; return (
{element.content}
); } // Media elements if (element.type === "media") { // Test elements if (!mediaItem || element.mediaId === "test") { return (
🎬

{element.name}

); } // Video elements if (mediaItem.type === "video") { return (
); } // Image elements if (mediaItem.type === "image") { return (
{mediaItem.name}
); } // Audio elements (no visual representation) if (mediaItem.type === "audio") { return (
); } } return null; }; return ( <>
{hasAnyElements ? (
{renderBlurBackground()} {activeElements.length === 0 ? (
No elements at current time
) : ( activeElements.map((elementData, index) => renderElement(elementData, index), ) )} {/* Show message when blur is selected but no media available */} {activeProject?.backgroundType === "blur" && blurBackgroundElements.length === 0 && activeElements.length > 0 && (
Add a video or image to use blur background
)}
) : null}
{/* Expanded/Fullscreen Mode */} {isExpanded && (
{renderBlurBackground()} {activeElements.length === 0 ? (
No elements at current time
) : ( activeElements.map((elementData, index) => renderElement(elementData, index), ) )} {/* Show message when blur is selected but no media available */} {activeProject?.backgroundType === "blur" && blurBackgroundElements.length === 0 && activeElements.length > 0 && (
Add a video or image to use blur background
)}
)} ); } function PreviewToolbar({ hasAnyElements, onToggleExpanded, isExpanded, }: { hasAnyElements: boolean; onToggleExpanded: () => void; isExpanded: boolean; }) { const { isPlaying, toggle, currentTime } = usePlaybackStore(); const { setCanvasSize, setCanvasSizeToOriginal } = useEditorStore(); const { getTotalDuration } = useTimelineStore(); const { activeProject } = useProjectStore(); const { currentPreset, isOriginal, getOriginalAspectRatio, getDisplayName, canvasPresets, } = useAspectRatio(); const handlePresetSelect = (preset: { width: number; height: number }) => { setCanvasSize({ width: preset.width, height: preset.height }); }; const handleOriginalSelect = () => { const aspectRatio = getOriginalAspectRatio(); setCanvasSizeToOriginal(aspectRatio); }; return (

{formatTimeCode( currentTime, "HH:MM:SS:FF", activeProject?.fps || 30, )} / {formatTimeCode( getTotalDuration(), "HH:MM:SS:FF", activeProject?.fps || 30, )}

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