diff options
| author | Maze Winther <mazewinther@gmail.com> | 2025-07-15 16:28:08 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2025-07-15 16:28:08 +0200 |
| commit | fdeddbe3edf761ad41a5cd6bf9e1fb7f18624453 (patch) | |
| tree | 24d9e6a21f5811779e8675d6e0e97e89953c613e /apps/web/src/lib | |
| parent | b1ccf3695e3c955de0ffe11ff2cf2dfec37a049d (diff) | |
| parent | 03b3c952c75b7642b537fa2499d081edd1a0a1e4 (diff) | |
| download | opencut-fdeddbe3edf761ad41a5cd6bf9e1fb7f18624453.tar.gz | |
Merge branch 'feature/plus-button-timeline' into staging
Diffstat (limited to 'apps/web/src/lib')
| -rw-r--r-- | apps/web/src/lib/timeline-utils.ts | 117 |
1 files changed, 117 insertions, 0 deletions
diff --git a/apps/web/src/lib/timeline-utils.ts b/apps/web/src/lib/timeline-utils.ts new file mode 100644 index 00000000..994cb314 --- /dev/null +++ b/apps/web/src/lib/timeline-utils.ts @@ -0,0 +1,117 @@ +import { useTimelineStore } from "@/stores/timeline-store"; +import { type MediaItem } from "@/stores/media-store"; +import { toast } from "sonner"; +import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; +import { DragData, TextElement } from "@/types/timeline"; + + +const findOrCreateTrack = (trackType: "media" | "audio" | "text") => { + const timelineStore = useTimelineStore.getState(); + + // Always create new text track to allow multiple text elements + if (trackType === "text") { + return timelineStore.addTrack(trackType); + } + + const existingTrack = timelineStore.tracks.find(track => track.type === trackType); + return existingTrack ? existingTrack.id : timelineStore.addTrack(trackType); +}; + + +const checkOverlap = (trackId: string, startTime: number, duration: number, excludeElementId?: string) => { + const timelineStore = useTimelineStore.getState(); + const targetTrack = timelineStore.tracks.find(track => track.id === trackId); + + if (!targetTrack) { + return true; + } + + const elementEnd = startTime + duration; + + return targetTrack.elements.some((existingElement) => { + if (excludeElementId && existingElement.id === excludeElementId) { + return false; + } + + const existingStart = existingElement.startTime; + const existingEnd = existingElement.startTime + + (existingElement.duration - existingElement.trimStart - existingElement.trimEnd); + + return startTime < existingEnd && elementEnd > existingStart; + }); +}; + +const addMediaElement = (trackId: string, item: MediaItem, startTime: number) => { + const timelineStore = useTimelineStore.getState(); + + timelineStore.addElementToTrack(trackId, { + type: "media", + mediaId: item.id, + name: item.name, + duration: item.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION, + startTime, + trimStart: 0, + trimEnd: 0, + }); +}; + +const addTextElement = (trackId: string, item: TextElement | DragData, startTime: number) => { + const timelineStore = useTimelineStore.getState(); + + timelineStore.addElementToTrack(trackId, { + type: "text", + name: item.name, + content: ("content" in item ? item.content : "Default Text"), + duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION, + startTime, + trimStart: ("trimStart" in item ? item.trimStart : 0), + trimEnd: ("trimEnd" in item ? item.trimEnd : 0), + fontSize: ("fontSize" in item ? item.fontSize : 48), + fontFamily: ("fontFamily" in item ? item.fontFamily : "Arial"), + color: ("color" in item ? item.color : "#ffffff"), + backgroundColor: ("backgroundColor" in item ? item.backgroundColor : "transparent"), + textAlign: ("textAlign" in item ? item.textAlign : "center"), + fontWeight: ("fontWeight" in item ? item.fontWeight : "normal"), + fontStyle: ("fontStyle" in item ? item.fontStyle : "normal"), + textDecoration: ("textDecoration" in item ? item.textDecoration : "none"), + x: ("x" in item ? item.x : 0), + y: ("y" in item ? item.y : 0), + rotation: ("rotation" in item ? item.rotation : 0), + opacity: ("opacity" in item && item.opacity !== undefined ? item.opacity : 1), + }); +}; + + +// Adds a media item to the timeline at the specified time +export const addMediaToTimeline = (item: MediaItem, currentTime: number = 0) => { + const trackType = item.type === "audio" ? "audio" : "media"; + const targetTrackId = findOrCreateTrack(trackType); + + const duration = item.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION; + + if (checkOverlap(targetTrackId, currentTime, duration)) { + toast.error("Cannot place element here - it would overlap with existing elements"); + return; + } + + addMediaElement(targetTrackId, item, currentTime); +}; + +// Adds a text item to the timeline at the specified time +export const addTextToTimeline = (item: TextElement, currentTime: number = 0) => { + const targetTrackId = findOrCreateTrack("text"); + addTextElement(targetTrackId, item, currentTime); +}; + +// Adds a media item to timeline +export const addMediaToNewTrack = (item: MediaItem) => { + const trackType = item.type === "audio" ? "audio" : "media"; + const targetTrackId = findOrCreateTrack(trackType); + addMediaElement(targetTrackId, item, 0); +}; + +// Adds a text item to timeline +export const addTextToNewTrack = (item: TextElement | DragData) => { + const targetTrackId = findOrCreateTrack("text"); + addTextElement(targetTrackId, item, 0); +}; |
