From 40c7fbb4f86130c83fbd9029e83c5e7550e3219d Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Sun, 6 Jul 2025 20:45:29 +0200 Subject: refactor: move to a typed-tracks system and add support for text --- apps/web/src/components/editor/preview-panel.tsx | 226 ++++++++++++++--------- 1 file changed, 135 insertions(+), 91 deletions(-) (limited to 'apps/web/src/components/editor/preview-panel.tsx') diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index eba8ac04..9a02560b 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -2,9 +2,9 @@ import { useTimelineStore, - type TimelineClip, type TimelineTrack, } from "@/stores/timeline-store"; +import { TimelineElement } from "@/types/timeline"; import { useMediaStore, type MediaItem, @@ -21,13 +21,13 @@ import { DropdownMenuTrigger, DropdownMenuSeparator, } from "@/components/ui/dropdown-menu"; -import { Play, Pause, Volume2, VolumeX, Plus, Square } from "lucide-react"; +import { Play, Pause } from "lucide-react"; import { useState, useRef, useEffect } from "react"; import { cn } from "@/lib/utils"; import { formatTimeCode } from "@/lib/time"; -interface ActiveClip { - clip: TimelineClip; +interface ActiveElement { + element: TimelineElement; track: TimelineTrack; mediaItem: MediaItem | null; } @@ -35,8 +35,8 @@ interface ActiveClip { export function PreviewPanel() { const { tracks } = useTimelineStore(); const { mediaItems } = useMediaStore(); - const { currentTime, muted, toggleMute, volume } = usePlaybackStore(); - const { canvasSize, canvasPresets, setCanvasSize } = useEditorStore(); + const { currentTime } = usePlaybackStore(); + const { canvasSize } = useEditorStore(); const previewRef = useRef(null); const containerRef = useRef(null); const [previewDimensions, setPreviewDimensions] = useState({ @@ -104,97 +104,139 @@ export function PreviewPanel() { return () => resizeObserver.disconnect(); }, [canvasSize.width, canvasSize.height]); - // Get active clips at current time - const getActiveClips = (): ActiveClip[] => { - const activeClips: ActiveClip[] = []; + // Get active elements at current time + const getActiveElements = (): ActiveElement[] => { + const activeElements: ActiveElement[] = []; 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 }); + 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 activeClips; + return activeElements; }; - const activeClips = getActiveClips(); + const activeElements = getActiveElements(); - // Check if there are any clips in the timeline at all - const hasAnyClips = tracks.some((track) => track.clips.length > 0); + // Check if there are any elements in the timeline at all + const hasAnyElements = tracks.some((track) => track.elements.length > 0); - // Render a clip - const renderClip = (clipData: ActiveClip, index: number) => { - const { clip, mediaItem } = clipData; + // Render an element + const renderElement = (elementData: ActiveElement, index: number) => { + const { element, mediaItem } = elementData; - // Test clips - if (!mediaItem || clip.mediaId === "test") { + // Text elements + if (element.type === "text") { return (
-
-
🎬
-

{clip.name}

+
+ {element.content}
); } - // Video clips - if (mediaItem.type === "video") { - return ( -
- -
- ); - } + // Media elements + if (element.type === "media") { + // Test elements + if (!mediaItem || element.mediaId === "test") { + return ( +
+
+
🎬
+

{element.name}

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

{mediaItem.name}

+ // Image elements + if (mediaItem.type === "image") { + return ( +
+ {mediaItem.name}
-
- ); + ); + } + + // Audio elements (visual representation) + if (mediaItem.type === "audio") { + return ( +
+
+
🎵
+

{mediaItem.name}

+
+
+ ); + } } return null; @@ -206,7 +248,7 @@ export function PreviewPanel() { ref={containerRef} className="flex-1 flex flex-col items-center justify-center p-3 min-h-0 min-w-0 gap-4" > - {hasAnyClips ? ( + {hasAnyElements ? (
- {activeClips.length === 0 ? ( + {activeElements.length === 0 ? (
- No clips at current time + No elements at current time
) : ( - activeClips.map((clipData, index) => renderClip(clipData, index)) + activeElements.map((elementData, index) => renderElement(elementData, index)) )}
) : ( @@ -230,13 +272,13 @@ export function PreviewPanel() { )} - +
); } -function PreviewToolbar({ hasAnyClips }: { hasAnyClips: boolean }) { +function PreviewToolbar({ hasAnyElements }: { hasAnyElements: boolean }) { const { isPlaying, toggle, currentTime } = usePlaybackStore(); const { canvasSize, @@ -261,13 +303,15 @@ function PreviewToolbar({ hasAnyClips }: { hasAnyClips: boolean }) { 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); + for (const element of track.elements) { + if (element.type === "media") { + const mediaItem = mediaItems.find((item) => item.id === element.mediaId); + if ( + mediaItem && + (mediaItem.type === "video" || mediaItem.type === "image") + ) { + return getMediaAspectRatio(mediaItem); + } } } } @@ -291,7 +335,7 @@ function PreviewToolbar({ hasAnyClips }: { hasAnyClips: boolean }) {

{formatTimeCode(currentTime, "HH:MM:SS:CS")}/ @@ -302,7 +346,7 @@ function PreviewToolbar({ hasAnyClips }: { hasAnyClips: boolean }) { variant="text" size="icon" onClick={toggle} - disabled={!hasAnyClips} + disabled={!hasAnyElements} > {isPlaying ? ( @@ -316,7 +360,7 @@ function PreviewToolbar({ hasAnyClips }: { hasAnyClips: boolean }) { -- cgit v1.3.1