feat: main track and ordering tracks
This commit is contained in:
@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useTimelineStore, type TimelineTrack } from "@/stores/timeline-store";
|
||||
import { useTimelineStore } from "@/stores/timeline-store";
|
||||
import { useMediaStore, type MediaItem } from "@/stores/media-store";
|
||||
import { TimelineTrack } from "@/types/timeline";
|
||||
import { usePlaybackStore } from "@/stores/playback-store";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useState } from "react";
|
||||
|
@ -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
|
||||
</div>
|
||||
) : (
|
||||
activeElements.map((elementData, index) => renderElement(elementData, index))
|
||||
activeElements.map((elementData, index) =>
|
||||
renderElement(elementData, index)
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
@ -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")
|
||||
|
@ -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;
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,4 @@
|
||||
import { MediaType } from "@/stores/media-store";
|
||||
import { TimelineTrack } from "@/stores/timeline-store";
|
||||
|
||||
export type TrackType = "media" | "text" | "audio";
|
||||
|
||||
@ -77,3 +76,50 @@ export interface TextItemDragData {
|
||||
}
|
||||
|
||||
export type DragData = MediaItemDragData | TextItemDragData;
|
||||
|
||||
export interface TimelineTrack {
|
||||
id: string;
|
||||
name: string;
|
||||
type: TrackType;
|
||||
elements: TimelineElement[];
|
||||
muted?: boolean;
|
||||
isMain?: boolean;
|
||||
}
|
||||
|
||||
export function sortTracksByOrder(tracks: TimelineTrack[]): TimelineTrack[] {
|
||||
return [...tracks].sort((a, b) => {
|
||||
// Audio tracks always go to bottom
|
||||
if (a.type === "audio" && b.type !== "audio") return 1;
|
||||
if (b.type === "audio" && a.type !== "audio") return -1;
|
||||
|
||||
// Main track goes above audio but below other tracks
|
||||
if (a.isMain && !b.isMain && b.type !== "audio") return 1;
|
||||
if (b.isMain && !a.isMain && a.type !== "audio") return -1;
|
||||
|
||||
// Within same category, maintain creation order
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
export function getMainTrack(tracks: TimelineTrack[]): TimelineTrack | null {
|
||||
return tracks.find((track) => track.isMain) || null;
|
||||
}
|
||||
|
||||
export function ensureMainTrack(tracks: TimelineTrack[]): TimelineTrack[] {
|
||||
const hasMainTrack = tracks.some((track) => track.isMain);
|
||||
|
||||
if (!hasMainTrack) {
|
||||
// Create main track if it doesn't exist
|
||||
const mainTrack: TimelineTrack = {
|
||||
id: crypto.randomUUID(),
|
||||
name: "Main Track",
|
||||
type: "media",
|
||||
elements: [],
|
||||
muted: false,
|
||||
isMain: true,
|
||||
};
|
||||
return [mainTrack, ...tracks];
|
||||
}
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
Reference in New Issue
Block a user