diff options
| author | Maze Winther <mazewinther@gmail.com> | 2025-07-06 22:51:11 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2025-07-06 22:51:11 +0200 |
| commit | 6edd5b36cf7f09b8b146f57a880eb919c5fdad7f (patch) | |
| tree | f41f166b71715010684b7e9769e557f6902aed02 /apps/web/src/stores | |
| parent | 40c7fbb4f86130c83fbd9029e83c5e7550e3219d (diff) | |
| download | opencut-6edd5b36cf7f09b8b146f57a880eb919c5fdad7f.tar.gz | |
feat: main track and ordering tracks
Diffstat (limited to 'apps/web/src/stores')
| -rw-r--r-- | apps/web/src/stores/timeline-store.ts | 1022 |
1 files changed, 520 insertions, 502 deletions
diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index ef08fffd..c4e4e0e3 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -1,8 +1,11 @@ import { create } from "zustand"; -import type { +import { TrackType, TimelineElement, CreateTimelineElement, + TimelineTrack, + sortTracksByOrder, + ensureMainTrack, } from "@/types/timeline"; import { useEditorStore } from "./editor-store"; import { useMediaStore, getMediaAspectRatio } from "./media-store"; @@ -22,19 +25,18 @@ const getElementNameWithSuffix = ( return `${baseName} (${suffix})`; }; -export interface TimelineTrack { - id: string; - name: string; - type: TrackType; - elements: TimelineElement[]; - muted?: boolean; -} - interface TimelineStore { - tracks: TimelineTrack[]; + // Private track storage + _tracks: TimelineTrack[]; history: TimelineTrack[][]; redoStack: TimelineTrack[][]; + // Always returns properly ordered tracks with main track ensured + tracks: TimelineTrack[]; + + // Manual method if you need to force recomputation + getSortedTracks: () => TimelineTrack[]; + // Multi-selection selectedElements: { trackId: string; elementId: string }[]; selectElement: (trackId: string, elementId: string, multi?: boolean) => void; @@ -116,577 +118,593 @@ interface TimelineStore { pushHistory: () => void; } -export const useTimelineStore = create<TimelineStore>((set, get) => ({ - tracks: [], - history: [], - redoStack: [], - selectedElements: [], - - pushHistory: () => { - const { tracks, history } = get(); +export const useTimelineStore = create<TimelineStore>((set, get) => { + // Helper to update tracks and maintain ordering + const updateTracks = (newTracks: TimelineTrack[]) => { + const tracksWithMain = ensureMainTrack(newTracks); + const sortedTracks = sortTracksByOrder(tracksWithMain); set({ - history: [...history, JSON.parse(JSON.stringify(tracks))], - redoStack: [], + _tracks: tracksWithMain, + tracks: sortedTracks, }); - }, + }; - undo: () => { - const { history, redoStack, tracks } = get(); - if (history.length === 0) return; - const prev = history[history.length - 1]; - set({ - tracks: prev, - history: history.slice(0, -1), - redoStack: [...redoStack, JSON.parse(JSON.stringify(tracks))], - }); - }, + // Initialize with proper track ordering + const initialTracks = ensureMainTrack([]); + const sortedInitialTracks = sortTracksByOrder(initialTracks); - selectElement: (trackId, elementId, multi = false) => { - set((state) => { - const exists = state.selectedElements.some( - (c) => c.trackId === trackId && c.elementId === elementId - ); - if (multi) { - return exists - ? { - selectedElements: state.selectedElements.filter( - (c) => !(c.trackId === trackId && c.elementId === elementId) - ), - } - : { - selectedElements: [ - ...state.selectedElements, - { trackId, elementId }, - ], - }; - } else { - return { selectedElements: [{ trackId, elementId }] }; - } - }); - }, + return { + _tracks: initialTracks, + tracks: sortedInitialTracks, + history: [], + redoStack: [], + selectedElements: [], + + getSortedTracks: () => { + const { _tracks } = get(); + const tracksWithMain = ensureMainTrack(_tracks); + return sortTracksByOrder(tracksWithMain); + }, + + pushHistory: () => { + const { _tracks, history } = get(); + set({ + history: [...history, JSON.parse(JSON.stringify(_tracks))], + redoStack: [], + }); + }, + + undo: () => { + const { history, redoStack, _tracks } = get(); + if (history.length === 0) return; + const prev = history[history.length - 1]; + updateTracks(prev); + set({ + history: history.slice(0, -1), + redoStack: [...redoStack, JSON.parse(JSON.stringify(_tracks))], + }); + }, - deselectElement: (trackId, elementId) => { - set((state) => ({ - selectedElements: state.selectedElements.filter( - (c) => !(c.trackId === trackId && c.elementId === elementId) - ), - })); - }, + selectElement: (trackId, elementId, multi = false) => { + set((state) => { + const exists = state.selectedElements.some( + (c) => c.trackId === trackId && c.elementId === elementId + ); + if (multi) { + return exists + ? { + selectedElements: state.selectedElements.filter( + (c) => !(c.trackId === trackId && c.elementId === elementId) + ), + } + : { + selectedElements: [ + ...state.selectedElements, + { trackId, elementId }, + ], + }; + } else { + return { selectedElements: [{ trackId, elementId }] }; + } + }); + }, - clearSelectedElements: () => { - set({ selectedElements: [] }); - }, + deselectElement: (trackId, elementId) => { + set((state) => ({ + selectedElements: state.selectedElements.filter( + (c) => !(c.trackId === trackId && c.elementId === elementId) + ), + })); + }, - setSelectedElements: (elements) => set({ selectedElements: elements }), + clearSelectedElements: () => { + set({ selectedElements: [] }); + }, - addTrack: (type) => { - get().pushHistory(); + setSelectedElements: (elements) => set({ selectedElements: elements }), - // Generate proper track name based on type - const trackName = - type === "media" - ? "Media Track" - : type === "text" - ? "Text Track" - : type === "audio" - ? "Audio Track" - : "Track"; + addTrack: (type) => { + get().pushHistory(); - const newTrack: TimelineTrack = { - id: crypto.randomUUID(), - name: trackName, - type, - elements: [], - muted: false, - }; - set((state) => ({ - tracks: [...state.tracks, newTrack], - })); - return newTrack.id; - }, + // Generate proper track name based on type + const trackName = + type === "media" + ? "Media Track" + : type === "text" + ? "Text Track" + : type === "audio" + ? "Audio Track" + : "Track"; - insertTrackAt: (type, index) => { - get().pushHistory(); + const newTrack: TimelineTrack = { + id: crypto.randomUUID(), + name: trackName, + type, + elements: [], + muted: false, + }; - // Generate proper track name based on type - const trackName = - type === "media" - ? "Media Track" - : type === "text" - ? "Text Track" - : type === "audio" - ? "Audio Track" - : "Track"; + updateTracks([...get()._tracks, newTrack]); + return newTrack.id; + }, - const newTrack: TimelineTrack = { - id: crypto.randomUUID(), - name: trackName, - type, - elements: [], - muted: false, - }; + insertTrackAt: (type, index) => { + get().pushHistory(); - set((state) => { - const newTracks = [...state.tracks]; - newTracks.splice(index, 0, newTrack); - return { tracks: newTracks }; - }); - return newTrack.id; - }, + // Generate proper track name based on type + const trackName = + type === "media" + ? "Media Track" + : type === "text" + ? "Text Track" + : type === "audio" + ? "Audio Track" + : "Track"; - removeTrack: (trackId) => { - get().pushHistory(); - set((state) => ({ - tracks: state.tracks.filter((track) => track.id !== trackId), - })); - }, + const newTrack: TimelineTrack = { + id: crypto.randomUUID(), + name: trackName, + type, + elements: [], + muted: false, + }; - addElementToTrack: (trackId, elementData) => { - get().pushHistory(); + const newTracks = [...get()._tracks]; + newTracks.splice(index, 0, newTrack); + updateTracks(newTracks); + return newTrack.id; + }, - // Validate element type matches track type - const track = get().tracks.find((t) => t.id === trackId); - if (!track) { - console.error("Track not found:", trackId); - return; - } + removeTrack: (trackId) => { + get().pushHistory(); + updateTracks(get()._tracks.filter((track) => track.id !== trackId)); + }, - // Validate element can be added to this track type - if (track.type === "media" && elementData.type !== "media") { - console.error("Media track only accepts media elements"); - return; - } - if (track.type === "text" && elementData.type !== "text") { - console.error("Text track only accepts text elements"); - return; - } - if (track.type === "audio" && elementData.type !== "media") { - console.error("Audio track only accepts media elements"); - return; - } + addElementToTrack: (trackId, elementData) => { + get().pushHistory(); - // For media elements, validate mediaId exists - if (elementData.type === "media" && !elementData.mediaId) { - console.error("Media element must have mediaId"); - return; - } + // Validate element type matches track type + const track = get()._tracks.find((t) => t.id === trackId); + if (!track) { + console.error("Track not found:", trackId); + return; + } - // For text elements, validate required text properties - if (elementData.type === "text" && !elementData.content) { - console.error("Text element must have content"); - return; - } + // Validate element can be added to this track type + if (track.type === "media" && elementData.type !== "media") { + console.error("Media track only accepts media elements"); + return; + } + if (track.type === "text" && elementData.type !== "text") { + console.error("Text track only accepts text elements"); + return; + } + if (track.type === "audio" && elementData.type !== "media") { + console.error("Audio track only accepts media elements"); + return; + } - // Check if this is the first element being added to the timeline - const currentState = get(); - const totalElementsInTimeline = currentState.tracks.reduce( - (total, track) => total + track.elements.length, - 0 - ); - const isFirstElement = totalElementsInTimeline === 0; + // For media elements, validate mediaId exists + if (elementData.type === "media" && !elementData.mediaId) { + console.error("Media element must have mediaId"); + return; + } - const newElement: TimelineElement = { - ...elementData, - id: crypto.randomUUID(), - startTime: elementData.startTime || 0, - trimStart: 0, - trimEnd: 0, - } as TimelineElement; // Type assertion since we trust the caller passes valid data + // For text elements, validate required text properties + if (elementData.type === "text" && !elementData.content) { + console.error("Text element must have content"); + return; + } - // If this is the first element and it's a media element, automatically set the project canvas size - // to match the media's aspect ratio - if (isFirstElement && newElement.type === "media") { - const mediaStore = useMediaStore.getState(); - const mediaItem = mediaStore.mediaItems.find( - (item) => item.id === newElement.mediaId + // Check if this is the first element being added to the timeline + const currentState = get(); + const totalElementsInTimeline = currentState._tracks.reduce( + (total, track) => total + track.elements.length, + 0 ); + const isFirstElement = totalElementsInTimeline === 0; - if ( - mediaItem && - (mediaItem.type === "image" || mediaItem.type === "video") - ) { - const editorStore = useEditorStore.getState(); - editorStore.setCanvasSizeFromAspectRatio( - getMediaAspectRatio(mediaItem) + const newElement: TimelineElement = { + ...elementData, + id: crypto.randomUUID(), + startTime: elementData.startTime || 0, + trimStart: 0, + trimEnd: 0, + } as TimelineElement; // Type assertion since we trust the caller passes valid data + + // If this is the first element and it's a media element, automatically set the project canvas size + // to match the media's aspect ratio + if (isFirstElement && newElement.type === "media") { + const mediaStore = useMediaStore.getState(); + const mediaItem = mediaStore.mediaItems.find( + (item) => item.id === newElement.mediaId ); - } - } - set((state) => ({ - tracks: state.tracks.map((track) => - track.id === trackId - ? { ...track, elements: [...track.elements, newElement] } - : track - ), - })); - }, + if ( + mediaItem && + (mediaItem.type === "image" || mediaItem.type === "video") + ) { + const editorStore = useEditorStore.getState(); + editorStore.setCanvasSizeFromAspectRatio( + getMediaAspectRatio(mediaItem) + ); + } + } - removeElementFromTrack: (trackId, elementId) => { - get().pushHistory(); - set((state) => ({ - tracks: state.tracks - .map((track) => + updateTracks( + get()._tracks.map((track) => track.id === trackId - ? { - ...track, - elements: track.elements.filter( - (element) => element.id !== elementId - ), - } + ? { ...track, elements: [...track.elements, newElement] } : track ) - .filter((track) => track.elements.length > 0), - })); - }, + ); + }, - moveElementToTrack: (fromTrackId, toTrackId, elementId) => { - get().pushHistory(); - set((state) => { - const fromTrack = state.tracks.find((track) => track.id === fromTrackId); + removeElementFromTrack: (trackId, elementId) => { + get().pushHistory(); + updateTracks( + get() + ._tracks.map((track) => + track.id === trackId + ? { + ...track, + elements: track.elements.filter( + (element) => element.id !== elementId + ), + } + : track + ) + .filter((track) => track.elements.length > 0) + ); + }, + + moveElementToTrack: (fromTrackId, toTrackId, elementId) => { + get().pushHistory(); + + const fromTrack = get()._tracks.find((track) => track.id === fromTrackId); const elementToMove = fromTrack?.elements.find( (element) => element.id === elementId ); - if (!elementToMove) return state; - - return { - tracks: state.tracks - .map((track) => { - if (track.id === fromTrackId) { - return { - ...track, - elements: track.elements.filter( - (element) => element.id !== elementId - ), - }; - } else if (track.id === toTrackId) { - return { - ...track, - elements: [...track.elements, elementToMove], - }; - } - return track; - }) - .filter((track) => track.elements.length > 0), - }; - }); - }, + if (!elementToMove) return; - updateElementTrim: (trackId, elementId, trimStart, trimEnd) => { - get().pushHistory(); - set((state) => ({ - tracks: state.tracks.map((track) => - track.id === trackId - ? { + const newTracks = get() + ._tracks.map((track) => { + if (track.id === fromTrackId) { + return { ...track, - elements: track.elements.map((element) => - element.id === elementId - ? { ...element, trimStart, trimEnd } - : element + elements: track.elements.filter( + (element) => element.id !== elementId ), - } - : track - ), - })); - }, - - updateElementStartTime: (trackId, elementId, startTime) => { - get().pushHistory(); - set((state) => ({ - tracks: state.tracks.map((track) => - track.id === trackId - ? { + }; + } else if (track.id === toTrackId) { + return { ...track, - elements: track.elements.map((element) => - element.id === elementId ? { ...element, startTime } : element - ), - } - : track - ), - })); - }, - - toggleTrackMute: (trackId) => { - get().pushHistory(); - set((state) => ({ - tracks: state.tracks.map((track) => - track.id === trackId ? { ...track, muted: !track.muted } : track - ), - })); - }, - - splitElement: (trackId, elementId, splitTime) => { - const { tracks } = get(); - const track = tracks.find((t) => t.id === trackId); - const element = track?.elements.find((c) => c.id === elementId); + elements: [...track.elements, elementToMove], + }; + } + return track; + }) + .filter((track) => track.elements.length > 0); - if (!element) return null; + updateTracks(newTracks); + }, - const effectiveStart = element.startTime; - const effectiveEnd = - element.startTime + - (element.duration - element.trimStart - element.trimEnd); + updateElementTrim: (trackId, elementId, trimStart, trimEnd) => { + get().pushHistory(); + updateTracks( + get()._tracks.map((track) => + track.id === trackId + ? { + ...track, + elements: track.elements.map((element) => + element.id === elementId + ? { ...element, trimStart, trimEnd } + : element + ), + } + : track + ) + ); + }, - if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return null; + updateElementStartTime: (trackId, elementId, startTime) => { + get().pushHistory(); + updateTracks( + get()._tracks.map((track) => + track.id === trackId + ? { + ...track, + elements: track.elements.map((element) => + element.id === elementId ? { ...element, startTime } : element + ), + } + : track + ) + ); + }, - get().pushHistory(); + toggleTrackMute: (trackId) => { + get().pushHistory(); + updateTracks( + get()._tracks.map((track) => + track.id === trackId ? { ...track, muted: !track.muted } : track + ) + ); + }, - const relativeTime = splitTime - element.startTime; - const firstDuration = relativeTime; - const secondDuration = - element.duration - element.trimStart - element.trimEnd - relativeTime; + splitElement: (trackId, elementId, splitTime) => { + const { _tracks } = get(); + const track = _tracks.find((t) => t.id === trackId); + const element = track?.elements.find((c) => c.id === elementId); - const secondElementId = crypto.randomUUID(); + if (!element) return null; - set((state) => ({ - tracks: state.tracks.map((track) => - track.id === trackId - ? { - ...track, - elements: track.elements.flatMap((c) => - c.id === elementId - ? [ - { - ...c, - trimEnd: c.trimEnd + secondDuration, - name: getElementNameWithSuffix(c.name, "left"), - }, - { - ...c, - id: secondElementId, - startTime: splitTime, - trimStart: c.trimStart + firstDuration, - name: getElementNameWithSuffix(c.name, "right"), - }, - ] - : [c] - ), - } - : track - ), - })); + const effectiveStart = element.startTime; + const effectiveEnd = + element.startTime + + (element.duration - element.trimStart - element.trimEnd); - return secondElementId; - }, + if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return null; - // Split element and keep only the left portion - splitAndKeepLeft: (trackId, elementId, splitTime) => { - const { tracks } = get(); - const track = tracks.find((t) => t.id === trackId); - const element = track?.elements.find((c) => c.id === elementId); + get().pushHistory(); - if (!element) return; + const relativeTime = splitTime - element.startTime; + const firstDuration = relativeTime; + const secondDuration = + element.duration - element.trimStart - element.trimEnd - relativeTime; - const effectiveStart = element.startTime; - const effectiveEnd = - element.startTime + - (element.duration - element.trimStart - element.trimEnd); + const secondElementId = crypto.randomUUID(); - if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return; + updateTracks( + get()._tracks.map((track) => + track.id === trackId + ? { + ...track, + elements: track.elements.flatMap((c) => + c.id === elementId + ? [ + { + ...c, + trimEnd: c.trimEnd + secondDuration, + name: getElementNameWithSuffix(c.name, "left"), + }, + { + ...c, + id: secondElementId, + startTime: splitTime, + trimStart: c.trimStart + firstDuration, + name: getElementNameWithSuffix(c.name, "right"), + }, + ] + : [c] + ), + } + : track + ) + ); - get().pushHistory(); + return secondElementId; + }, - const relativeTime = splitTime - element.startTime; - const durationToRemove = - element.duration - element.trimStart - element.trimEnd - relativeTime; + // Split element and keep only the left portion + splitAndKeepLeft: (trackId, elementId, splitTime) => { + const { _tracks } = get(); + const track = _tracks.find((t) => t.id === trackId); + const element = track?.elements.find((c) => c.id === elementId); - set((state) => ({ - tracks: state.tracks.map((track) => - track.id === trackId - ? { - ...track, - elements: track.elements.map((c) => - c.id === elementId - ? { - ...c, - trimEnd: c.trimEnd + durationToRemove, - name: getElementNameWithSuffix(c.name, "left"), - } - : c - ), - } - : track - ), - })); - }, + if (!element) return; - // Split element and keep only the right portion - splitAndKeepRight: (trackId, elementId, splitTime) => { - const { tracks } = get(); - const track = tracks.find((t) => t.id === trackId); - const element = track?.elements.find((c) => c.id === elementId); + const effectiveStart = element.startTime; + const effectiveEnd = + element.startTime + + (element.duration - element.trimStart - element.trimEnd); - if (!element) return; + if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return; - const effectiveStart = element.startTime; - const effectiveEnd = - element.startTime + - (element.duration - element.trimStart - element.trimEnd); + get().pushHistory(); - if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return; + const relativeTime = splitTime - element.startTime; + const durationToRemove = + element.duration - element.trimStart - element.trimEnd - relativeTime; - get().pushHistory(); + updateTracks( + get()._tracks.map((track) => + track.id === trackId + ? { + ...track, + elements: track.elements.map((c) => + c.id === elementId + ? { + ...c, + trimEnd: c.trimEnd + durationToRemove, + name: getElementNameWithSuffix(c.name, "left"), + } + : c + ), + } + : track + ) + ); + }, - const relativeTime = splitTime - element.startTime; + // Split element and keep only the right portion + splitAndKeepRight: (trackId, elementId, splitTime) => { + const { _tracks } = get(); + const track = _tracks.find((t) => t.id === trackId); + const element = track?.elements.find((c) => c.id === elementId); - set((state) => ({ - tracks: state.tracks.map((track) => - track.id === trackId - ? { - ...track, - elements: track.elements.map((c) => - c.id === elementId - ? { - ...c, - startTime: splitTime, - trimStart: c.trimStart + relativeTime, - name: getElementNameWithSuffix(c.name, "right"), - } - : c - ), - } - : track - ), - })); - }, + if (!element) return; - // Extract audio from video element to an audio track - separateAudio: (trackId, elementId) => { - const { tracks } = get(); - const track = tracks.find((t) => t.id === trackId); - const element = track?.elements.find((c) => c.id === elementId); + const effectiveStart = element.startTime; + const effectiveEnd = + element.startTime + + (element.duration - element.trimStart - element.trimEnd); - if (!element || track?.type !== "media") return null; + if (splitTime <= effectiveStart || splitTime >= effectiveEnd) return; - get().pushHistory(); + get().pushHistory(); - // Find existing audio track or prepare to create one - const existingAudioTrack = tracks.find((t) => t.type === "audio"); - const audioElementId = crypto.randomUUID(); + const relativeTime = splitTime - element.startTime; - if (existingAudioTrack) { - // Add audio element to existing audio track - set((state) => ({ - tracks: state.tracks.map((track) => - track.id === existingAudioTrack.id + updateTracks( + get()._tracks.map((track) => + track.id === trackId ? { ...track, - elements: [ - ...track.elements, - { - ...element, - id: audioElementId, - name: getElementNameWithSuffix(element.name, "audio"), - }, - ], + elements: track.elements.map((c) => + c.id === elementId + ? { + ...c, + startTime: splitTime, + trimStart: c.trimStart + relativeTime, + name: getElementNameWithSuffix(c.name, "right"), + } + : c + ), } : track - ), - })); - } else { - // Create new audio track with the audio element in a single atomic update - const newAudioTrack: TimelineTrack = { - id: crypto.randomUUID(), - name: "Audio Track", - type: "audio", - elements: [ - { - ...element, - id: audioElementId, - name: getElementNameWithSuffix(element.name, "audio"), - }, - ], - muted: false, - }; + ) + ); + }, - set((state) => ({ - tracks: [...state.tracks, newAudioTrack], - })); - } + // Extract audio from video element to an audio track + separateAudio: (trackId, elementId) => { + const { _tracks } = get(); + const track = _tracks.find((t) => t.id === trackId); + const element = track?.elements.find((c) => c.id === elementId); - return audioElementId; - }, + if (!element || track?.type !== "media") return null; - getTotalDuration: () => { - const { tracks } = get(); - if (tracks.length === 0) return 0; + get().pushHistory(); - const trackEndTimes = tracks.map((track) => - track.elements.reduce((maxEnd, element) => { - const elementEnd = - element.startTime + - element.duration - - element.trimStart - - element.trimEnd; - return Math.max(maxEnd, elementEnd); - }, 0) - ); + // Find existing audio track or prepare to create one + const existingAudioTrack = _tracks.find((t) => t.type === "audio"); + const audioElementId = crypto.randomUUID(); - return Math.max(...trackEndTimes, 0); - }, + if (existingAudioTrack) { + // Add audio element to existing audio track + updateTracks( + get()._tracks.map((track) => + track.id === existingAudioTrack.id + ? { + ...track, + elements: [ + ...track.elements, + { + ...element, + id: audioElementId, + name: getElementNameWithSuffix(element.name, "audio"), + }, + ], + } + : track + ) + ); + } else { + // Create new audio track with the audio element in a single atomic update + const newAudioTrack: TimelineTrack = { + id: crypto.randomUUID(), + name: "Audio Track", + type: "audio", + elements: [ + { + ...element, + id: audioElementId, + name: getElementNameWithSuffix(element.name, "audio"), + }, + ], + muted: false, + }; - redo: () => { - const { redoStack } = get(); - if (redoStack.length === 0) return; - const next = redoStack[redoStack.length - 1]; - set({ tracks: next, redoStack: redoStack.slice(0, -1) }); - }, + updateTracks([...get()._tracks, newAudioTrack]); + } - dragState: { - isDragging: false, - elementId: null, - trackId: null, - startMouseX: 0, - startElementTime: 0, - clickOffsetTime: 0, - currentTime: 0, - }, + return audioElementId; + }, - setDragState: (dragState) => - set((state) => ({ - dragState: { ...state.dragState, ...dragState }, - })), + getTotalDuration: () => { + const { _tracks } = get(); + if (_tracks.length === 0) return 0; - startDrag: ( - elementId, - trackId, - startMouseX, - startElementTime, - clickOffsetTime - ) => { - set({ - dragState: { - isDragging: true, - elementId, - trackId, - startMouseX, - startElementTime, - clickOffsetTime, - currentTime: startElementTime, - }, - }); - }, + const trackEndTimes = _tracks.map((track) => + track.elements.reduce((maxEnd, element) => { + const elementEnd = + element.startTime + + element.duration - + element.trimStart - + element.trimEnd; + return Math.max(maxEnd, elementEnd); + }, 0) + ); - updateDragTime: (currentTime) => { - set((state) => ({ - dragState: { - ...state.dragState, - currentTime, - }, - })); - }, + return Math.max(...trackEndTimes, 0); + }, - endDrag: () => { - set({ - dragState: { - isDragging: false, - elementId: null, - trackId: null, - startMouseX: 0, - startElementTime: 0, - clickOffsetTime: 0, - currentTime: 0, - }, - }); - }, -})); + redo: () => { + const { redoStack } = get(); + if (redoStack.length === 0) return; + const next = redoStack[redoStack.length - 1]; + updateTracks(next); + set({ redoStack: redoStack.slice(0, -1) }); + }, + + dragState: { + isDragging: false, + elementId: null, + trackId: null, + startMouseX: 0, + startElementTime: 0, + clickOffsetTime: 0, + currentTime: 0, + }, + + setDragState: (dragState) => + set((state) => ({ + dragState: { ...state.dragState, ...dragState }, + })), + + startDrag: ( + elementId, + trackId, + startMouseX, + startElementTime, + clickOffsetTime + ) => { + set({ + dragState: { + isDragging: true, + elementId, + trackId, + startMouseX, + startElementTime, + clickOffsetTime, + currentTime: startElementTime, + }, + }); + }, + + updateDragTime: (currentTime) => { + set((state) => ({ + dragState: { + ...state.dragState, + currentTime, + }, + })); + }, + + endDrag: () => { + set({ + dragState: { + isDragging: false, + elementId: null, + trackId: null, + startMouseX: 0, + startElementTime: 0, + clickOffsetTime: 0, + currentTime: 0, + }, + }); + }, + }; +}); |
