From dc0e4783fbacad5c7eb3cf0d1141b0166960f809 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 15 Jul 2025 16:55:04 +0200 Subject: [PATCH] cleanup --- .../editor/media-panel/views/media.tsx | 8 +- .../editor/media-panel/views/text.tsx | 6 +- apps/web/src/components/editor/timeline.tsx | 19 +- apps/web/src/lib/timeline-utils.ts | 117 ------------- apps/web/src/stores/timeline-store.ts | 164 +++++++++++++++++- 5 files changed, 183 insertions(+), 131 deletions(-) delete mode 100644 apps/web/src/lib/timeline-utils.ts diff --git a/apps/web/src/components/editor/media-panel/views/media.tsx b/apps/web/src/components/editor/media-panel/views/media.tsx index 17893156..393d72db 100644 --- a/apps/web/src/components/editor/media-panel/views/media.tsx +++ b/apps/web/src/components/editor/media-panel/views/media.tsx @@ -24,7 +24,7 @@ import { } from "@/components/ui/select"; import { DraggableMediaItem } from "@/components/ui/draggable-item"; import { useProjectStore } from "@/stores/project-store"; -import { addMediaToTimeline } from "@/lib/timeline-utils"; +import { useTimelineStore } from "@/stores/timeline-store"; export function MediaView() { const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore(); @@ -289,7 +289,11 @@ export function MediaView() { name: item.name, }} showPlusOnDrag={false} - onAddToTimeline={(currentTime) => addMediaToTimeline(item, currentTime)} + onAddToTimeline={(currentTime) => + useTimelineStore + .getState() + .addMediaAtTime(item, currentTime) + } rounded={false} /> diff --git a/apps/web/src/components/editor/media-panel/views/text.tsx b/apps/web/src/components/editor/media-panel/views/text.tsx index fad79343..6d0d310b 100644 --- a/apps/web/src/components/editor/media-panel/views/text.tsx +++ b/apps/web/src/components/editor/media-panel/views/text.tsx @@ -1,6 +1,6 @@ import { DraggableMediaItem } from "@/components/ui/draggable-item"; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; -import { addTextToTimeline } from "@/lib/timeline-utils"; +import { useTimelineStore } from "@/stores/timeline-store"; import { type TextElement } from "@/types/timeline"; let textData: TextElement = { @@ -43,7 +43,9 @@ export function TextView() { content: textData.content, }} aspectRatio={1} - onAddToTimeline={(currentTime) => addTextToTimeline(textData, currentTime)} + onAddToTimeline={(currentTime) => + useTimelineStore.getState().addTextAtTime(textData, currentTime) + } showLabel={false} /> diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index ff9e0694..c1603542 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -48,7 +48,6 @@ import { useSelectionBox } from "@/hooks/use-selection-box"; import { SnapIndicator } from "./snap-indicator"; import { SnapPoint } from "@/hooks/use-timeline-snapping"; import type { DragData, TimelineTrack } from "@/types/timeline"; -import { addTextToNewTrack, addMediaToNewTrack } from "@/lib/timeline-utils"; import { getTrackHeight, getCumulativeHeightBefore, @@ -229,7 +228,7 @@ export function Timeline() { Math.min( duration, (mouseX + scrollLeft) / - (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel) + (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel) ) ); @@ -377,7 +376,7 @@ export function Timeline() { if (dragData.type === "text") { // Always create new text track to avoid overlaps - addTextToNewTrack(dragData); + useTimelineStore.getState().addTextToNewTrack(dragData); } else { // Handle media items const mediaItem = mediaItems.find((item) => item.id === dragData.id); @@ -386,7 +385,7 @@ export function Timeline() { return; } - addMediaToNewTrack(mediaItem); + useTimelineStore.getState().addMediaToNewTrack(mediaItem); } } catch (error) { console.error("Error parsing dropped item data:", error); @@ -414,7 +413,7 @@ export function Timeline() { item.name === processedItem.name && item.url === processedItem.url ); if (addedItem) { - addMediaToNewTrack(addedItem); + useTimelineStore.getState().addMediaToNewTrack(addedItem); } } } catch (error) { @@ -902,19 +901,21 @@ export function Timeline() { return (
{(() => { const formatTime = (seconds: number) => { diff --git a/apps/web/src/lib/timeline-utils.ts b/apps/web/src/lib/timeline-utils.ts deleted file mode 100644 index 994cb314..00000000 --- a/apps/web/src/lib/timeline-utils.ts +++ /dev/null @@ -1,117 +0,0 @@ -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); -}; diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index f6b9bfd8..9a2c7361 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -5,15 +5,22 @@ import { CreateTimelineElement, TimelineTrack, TextElement, + DragData, sortTracksByOrder, ensureMainTrack, validateElementTrackCompatibility, } from "@/types/timeline"; import { useEditorStore } from "./editor-store"; -import { useMediaStore, getMediaAspectRatio } from "./media-store"; +import { + useMediaStore, + getMediaAspectRatio, + type MediaItem, +} from "./media-store"; import { storageService } from "@/lib/storage/storage-service"; import { useProjectStore } from "./project-store"; import { generateUUID } from "@/lib/utils"; +import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; +import { toast } from "sonner"; // Helper function to manage element naming with suffixes const getElementNameWithSuffix = ( @@ -166,6 +173,17 @@ interface TimelineStore { > > ) => void; + checkElementOverlap: ( + trackId: string, + startTime: number, + duration: number, + excludeElementId?: string + ) => boolean; + findOrCreateTrack: (trackType: TrackType) => string; + addMediaAtTime: (item: MediaItem, currentTime?: number) => boolean; + addTextAtTime: (item: TextElement, currentTime?: number) => boolean; + addMediaToNewTrack: (item: MediaItem) => boolean; + addTextToNewTrack: (item: TextElement | DragData) => boolean; } export const useTimelineStore = create((set, get) => { @@ -963,5 +981,149 @@ export const useTimelineStore = create((set, get) => { toggleSnapping: () => { set((state) => ({ snappingEnabled: !state.snappingEnabled })); }, + + checkElementOverlap: (trackId, startTime, duration, excludeElementId) => { + const track = get()._tracks.find((t) => t.id === trackId); + if (!track) return false; + + const overlap = track.elements.some((element) => { + const elementEnd = + element.startTime + + element.duration - + element.trimStart - + element.trimEnd; + + if (element.id === excludeElementId) { + return false; + } + + return ( + (startTime >= element.startTime && startTime < elementEnd) || + (startTime + duration > element.startTime && + startTime + duration <= elementEnd) || + (startTime < element.startTime && startTime + duration > elementEnd) + ); + }); + return overlap; + }, + + findOrCreateTrack: (trackType) => { + // Always create new text track to allow multiple text elements + if (trackType === "text") { + return get().addTrack(trackType); + } + + const existingTrack = get()._tracks.find((t) => t.type === trackType); + if (existingTrack) { + return existingTrack.id; + } + + return get().addTrack(trackType); + }, + + addMediaAtTime: (item, currentTime = 0) => { + const trackType = item.type === "audio" ? "audio" : "media"; + const targetTrackId = get().findOrCreateTrack(trackType); + + const duration = + item.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION; + + if (get().checkElementOverlap(targetTrackId, currentTime, duration)) { + toast.error( + "Cannot place element here - it would overlap with existing elements" + ); + return false; + } + + get().addElementToTrack(targetTrackId, { + type: "media", + mediaId: item.id, + name: item.name, + duration, + startTime: currentTime, + trimStart: 0, + trimEnd: 0, + }); + return true; + }, + + addTextAtTime: (item, currentTime = 0) => { + const targetTrackId = get().addTrack("text"); // Always create new text track to allow multiple text elements + + get().addElementToTrack(targetTrackId, { + type: "text", + name: item.name || "Text", + content: item.content || "Default Text", + duration: item.duration || TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION, + startTime: currentTime, + trimStart: 0, + trimEnd: 0, + fontSize: item.fontSize || 48, + fontFamily: item.fontFamily || "Arial", + color: item.color || "#ffffff", + backgroundColor: item.backgroundColor || "transparent", + textAlign: item.textAlign || "center", + fontWeight: item.fontWeight || "normal", + fontStyle: item.fontStyle || "normal", + textDecoration: item.textDecoration || "none", + x: item.x || 0, + y: item.y || 0, + rotation: item.rotation || 0, + opacity: item.opacity !== undefined ? item.opacity : 1, + }); + return true; + }, + + addMediaToNewTrack: (item) => { + const trackType = item.type === "audio" ? "audio" : "media"; + const targetTrackId = get().findOrCreateTrack(trackType); + + get().addElementToTrack(targetTrackId, { + type: "media", + mediaId: item.id, + name: item.name, + duration: item.duration || TIMELINE_CONSTANTS.DEFAULT_IMAGE_DURATION, + startTime: 0, + trimStart: 0, + trimEnd: 0, + }); + return true; + }, + + addTextToNewTrack: (item) => { + const targetTrackId = get().addTrack("text"); // Always create new text track to allow multiple text elements + + get().addElementToTrack(targetTrackId, { + type: "text", + name: item.name || "Text", + content: + ("content" in item ? item.content : "Default Text") || "Default Text", + duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION, + startTime: 0, + trimStart: 0, + trimEnd: 0, + fontSize: ("fontSize" in item ? item.fontSize : 48) || 48, + fontFamily: + ("fontFamily" in item ? item.fontFamily : "Arial") || "Arial", + color: ("color" in item ? item.color : "#ffffff") || "#ffffff", + backgroundColor: + ("backgroundColor" in item ? item.backgroundColor : "transparent") || + "transparent", + textAlign: + ("textAlign" in item ? item.textAlign : "center") || "center", + fontWeight: + ("fontWeight" in item ? item.fontWeight : "normal") || "normal", + fontStyle: + ("fontStyle" in item ? item.fontStyle : "normal") || "normal", + textDecoration: + ("textDecoration" in item ? item.textDecoration : "none") || "none", + x: ("x" in item ? item.x : 0) || 0, + y: ("y" in item ? item.y : 0) || 0, + rotation: ("rotation" in item ? item.rotation : 0) || 0, + opacity: + "opacity" in item && item.opacity !== undefined ? item.opacity : 1, + }); + return true; + }, }; });