From 6edd5b36cf7f09b8b146f57a880eb919c5fdad7f Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Sun, 6 Jul 2025 22:51:11 +0200 Subject: feat: main track and ordering tracks --- apps/web/src/components/editor/preview-panel.tsx | 34 ++++---- apps/web/src/components/editor/timeline-track.tsx | 101 +++++++++++++++------- 2 files changed, 91 insertions(+), 44 deletions(-) (limited to 'apps/web/src/components/editor') diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index 9a02560b..bbd112c1 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -1,10 +1,7 @@ "use client"; -import { - useTimelineStore, - type TimelineTrack, -} from "@/stores/timeline-store"; -import { TimelineElement } from "@/types/timeline"; +import { useTimelineStore } from "@/stores/timeline-store"; +import { TimelineElement, TimelineTrack } from "@/types/timeline"; import { useMediaStore, type MediaItem, @@ -112,16 +109,19 @@ export function PreviewPanel() { track.elements.forEach((element) => { const elementStart = element.startTime; const elementEnd = - element.startTime + (element.duration - element.trimStart - element.trimEnd); + 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; + 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 }); @@ -165,9 +165,9 @@ export function PreviewPanel() { fontWeight: element.fontWeight, fontStyle: element.fontStyle, textDecoration: element.textDecoration, - padding: '4px 8px', - borderRadius: '2px', - whiteSpace: 'pre-wrap', + padding: "4px 8px", + borderRadius: "2px", + whiteSpace: "pre-wrap", }} > {element.content} @@ -262,7 +262,9 @@ export function PreviewPanel() { No elements at current time ) : ( - activeElements.map((elementData, index) => renderElement(elementData, index)) + activeElements.map((elementData, index) => + renderElement(elementData, index) + ) )} ) : ( @@ -305,7 +307,9 @@ function PreviewToolbar({ hasAnyElements }: { hasAnyElements: boolean }) { for (const track of tracks) { for (const element of track.elements) { if (element.type === "media") { - const mediaItem = mediaItems.find((item) => item.id === element.mediaId); + const mediaItem = mediaItems.find( + (item) => item.id === element.mediaId + ); if ( mediaItem && (mediaItem.type === "video" || mediaItem.type === "image") diff --git a/apps/web/src/components/editor/timeline-track.tsx b/apps/web/src/components/editor/timeline-track.tsx index dd4219f9..eb58e1ee 100644 --- a/apps/web/src/components/editor/timeline-track.tsx +++ b/apps/web/src/components/editor/timeline-track.tsx @@ -13,7 +13,12 @@ import { ContextMenuSeparator, ContextMenuTrigger, } from "../ui/context-menu"; -import { TimelineTrack } from "@/stores/timeline-store"; +import { + TimelineTrack, + sortTracksByOrder, + ensureMainTrack, + getMainTrack +} from "@/types/timeline"; import { usePlaybackStore } from "@/stores/playback-store"; import type { TimelineElement as TimelineElementType, @@ -555,15 +560,23 @@ export function TimelineTrackContent({ // Handle position-aware track creation for text if (track.type !== "text" || dropPosition !== "on") { - // Determine where to insert the new text track + // Text tracks should go above the main track + const mainTrack = getMainTrack(tracks); let insertIndex: number; + if (dropPosition === "above") { insertIndex = currentTrackIndex; } else if (dropPosition === "below") { insertIndex = currentTrackIndex + 1; } else { // dropPosition === "on" but track is not text type - insertIndex = currentTrackIndex + 1; + // Insert above main track if main track exists, otherwise at top + if (mainTrack) { + const mainTrackIndex = tracks.findIndex(t => t.id === mainTrack.id); + insertIndex = mainTrackIndex; + } else { + insertIndex = 0; // Top of timeline + } } targetTrackId = insertTrackAt("text", insertIndex); @@ -646,42 +659,72 @@ export function TimelineTrackContent({ const needsNewTrack = !isCompatible || dropPosition !== "on"; if (needsNewTrack) { - // Determine where to insert the new track - let insertIndex: number; - if (dropPosition === "above") { - insertIndex = currentTrackIndex; - } else if (dropPosition === "below") { - insertIndex = currentTrackIndex + 1; - } else { - // dropPosition === "on" but track is incompatible - insertIndex = currentTrackIndex + 1; - } - if (isVideoOrImage) { - // For video/image, check if main media track is empty and at the right position - const mainMediaTrack = tracks.find((t) => t.type === "media"); - if ( - mainMediaTrack && - mainMediaTrack.elements.length === 0 && - dropPosition === "on" - ) { - targetTrackId = mainMediaTrack.id; - targetTrack = mainMediaTrack; + // For video/image, check if we need a main track or additional media track + const mainTrack = getMainTrack(tracks); + + if (!mainTrack) { + // No main track exists, create it + const updatedTracks = ensureMainTrack(tracks); + const newMainTrack = getMainTrack(updatedTracks); + if (newMainTrack && newMainTrack.elements.length === 0) { + targetTrackId = newMainTrack.id; + targetTrack = newMainTrack; + } else { + // Main track was created but somehow has elements, create new media track + const mainTrackIndex = updatedTracks.findIndex(t => t.id === newMainTrack?.id); + targetTrackId = insertTrackAt("media", mainTrackIndex); + const updatedTracksAfterInsert = useTimelineStore.getState().tracks; + const newTargetTrack = updatedTracksAfterInsert.find(t => t.id === targetTrackId); + if (!newTargetTrack) return; + targetTrack = newTargetTrack; + } + } else if (mainTrack.elements.length === 0 && dropPosition === "on") { + // Main track exists and is empty, use it + targetTrackId = mainTrack.id; + targetTrack = mainTrack; } else { + // Create new media track above main track + const mainTrackIndex = tracks.findIndex(t => t.id === mainTrack.id); + let insertIndex: number; + + if (dropPosition === "above") { + insertIndex = currentTrackIndex; + } else if (dropPosition === "below") { + insertIndex = currentTrackIndex + 1; + } else { + // Insert above main track + insertIndex = mainTrackIndex; + } + targetTrackId = insertTrackAt("media", insertIndex); const updatedTracks = useTimelineStore.getState().tracks; - const newTargetTrack = updatedTracks.find( - (t) => t.id === targetTrackId - ); + const newTargetTrack = updatedTracks.find(t => t.id === targetTrackId); if (!newTargetTrack) return; targetTrack = newTargetTrack; } } else if (isAudio) { + // Audio tracks go at the bottom + const mainTrack = getMainTrack(tracks); + let insertIndex: number; + + if (dropPosition === "above") { + insertIndex = currentTrackIndex; + } else if (dropPosition === "below") { + insertIndex = currentTrackIndex + 1; + } else { + // Insert after main track (bottom area) + if (mainTrack) { + const mainTrackIndex = tracks.findIndex(t => t.id === mainTrack.id); + insertIndex = mainTrackIndex + 1; + } else { + insertIndex = tracks.length; // Bottom of timeline + } + } + targetTrackId = insertTrackAt("audio", insertIndex); const updatedTracks = useTimelineStore.getState().tracks; - const newTargetTrack = updatedTracks.find( - (t) => t.id === targetTrackId - ); + const newTargetTrack = updatedTracks.find(t => t.id === targetTrackId); if (!newTargetTrack) return; targetTrack = newTargetTrack; } -- cgit v1.3.1