diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 94d542a..ed9128d 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -73,101 +73,61 @@ export function Timeline() { setIsDragOver(false); dragCounterRef.current = 0; - // Check if this is a timeline clip drop - now we'll handle it! - const timelineClipData = e.dataTransfer.getData( - "application/x-timeline-clip" - ); - if (timelineClipData) { - // Timeline clips dropped on the main timeline area (not on a specific track) - // For now, we'll just ignore these - clips should be dropped on specific tracks - return; - } + const timelineClipData = e.dataTransfer.getData("application/x-timeline-clip"); + if (timelineClipData) return; - // Check if this is an internal media item drop const mediaItemData = e.dataTransfer.getData("application/x-media-item"); if (mediaItemData) { try { - const { id, type, name } = JSON.parse(mediaItemData); - - // Find the full media item from the store + const { id, type } = JSON.parse(mediaItemData); const mediaItem = mediaItems.find((item) => item.id === id); + if (!mediaItem) { toast.error("Media item not found"); return; } - // Determine track type based on media type - let trackType: "video" | "audio" | "effects"; - if (type === "video") { - trackType = "video"; - } else if (type === "audio") { - trackType = "audio"; - } else { - // For images, we'll put them on video tracks - trackType = "video"; - } - - // Create a new track and get its ID + const trackType = type === "audio" ? "audio" : "video"; const newTrackId = addTrack(trackType); - // Add the clip to the new track addClipToTrack(newTrackId, { mediaId: mediaItem.id, name: mediaItem.name, duration: mediaItem.duration || 5, + startTime: 0, trimStart: 0, trimEnd: 0, }); - - } catch (error) { console.error("Error parsing media item data:", error); toast.error("Failed to add media to timeline"); } - } else if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { - // Handle external file drops + } else if (e.dataTransfer.files?.length > 0) { setIsProcessing(true); try { const processedItems = await processMediaFiles(e.dataTransfer.files); for (const processedItem of processedItems) { - // Add to media store first addMediaItem(processedItem); - // The media item now has an ID, let's get it from the latest state - // Since addMediaItem is synchronous, we can get the latest item const currentMediaItems = useMediaStore.getState().mediaItems; const addedItem = currentMediaItems.find( - (item) => - item.name === processedItem.name && item.url === processedItem.url + (item) => item.name === processedItem.name && item.url === processedItem.url ); if (addedItem) { - // Determine track type based on media type - let trackType: "video" | "audio" | "effects"; - if (processedItem.type === "video") { - trackType = "video"; - } else if (processedItem.type === "audio") { - trackType = "audio"; - } else { - // For images, we'll put them on video tracks - trackType = "video"; - } - - // Create a new track and get its ID + const trackType = processedItem.type === "audio" ? "audio" : "video"; const newTrackId = addTrack(trackType); - // Add the clip to the new track addClipToTrack(newTrackId, { mediaId: addedItem.id, name: addedItem.name, duration: addedItem.duration || 5, + startTime: 0, trimStart: 0, trimEnd: 0, }); - - } } } catch (error) { @@ -359,7 +319,7 @@ export function Timeline() { function TimelineTrackComponent({ track, zoomLevel }: { track: TimelineTrack, zoomLevel: number }) { const { mediaItems } = useMediaStore(); - const { moveClipToTrack, reorderClipInTrack, updateClipTrim } = useTimelineStore(); + const { moveClipToTrack, updateClipTrim, updateClipStartTime } = useTimelineStore(); const [isDropping, setIsDropping] = useState(false); const [resizing, setResizing] = useState<{ clipId: string; @@ -417,7 +377,6 @@ function TimelineTrackComponent({ track, zoomLevel }: { track: TimelineTrack, zo setResizing(null); }; - // Global mouse events for better resize experience useEffect(() => { if (!resizing) return; @@ -439,67 +398,35 @@ function TimelineTrackComponent({ track, zoomLevel }: { track: TimelineTrack, zo }, [resizing, track.id, zoomLevel, updateClipTrim]); const handleClipDragStart = (e: React.DragEvent, clip: any) => { - // Mark this as an timeline clip drag to differentiate from media items - const dragData = { - clipId: clip.id, - trackId: track.id, - name: clip.name, - }; + const dragData = { clipId: clip.id, trackId: track.id, name: clip.name }; - e.dataTransfer.setData( - "application/x-timeline-clip", - JSON.stringify(dragData) - ); + e.dataTransfer.setData("application/x-timeline-clip", JSON.stringify(dragData)); e.dataTransfer.effectAllowed = "move"; - // Use the entire clip container as the drag image instead of just the content const target = e.currentTarget as HTMLElement; - e.dataTransfer.setDragImage( - target, - target.offsetWidth / 2, - target.offsetHeight / 2 - ); + e.dataTransfer.setDragImage(target, target.offsetWidth / 2, target.offsetHeight / 2); }; const handleTrackDragOver = (e: React.DragEvent) => { e.preventDefault(); - - // Only handle timeline clip drags - if (!e.dataTransfer.types.includes("application/x-timeline-clip")) { - return; - } - + if (!e.dataTransfer.types.includes("application/x-timeline-clip")) return; e.dataTransfer.dropEffect = "move"; }; const handleTrackDragEnter = (e: React.DragEvent) => { e.preventDefault(); - - // Only handle timeline clip drags - if (!e.dataTransfer.types.includes("application/x-timeline-clip")) { - return; - } - + if (!e.dataTransfer.types.includes("application/x-timeline-clip")) return; setIsDropping(true); }; const handleTrackDragLeave = (e: React.DragEvent) => { e.preventDefault(); + if (!e.dataTransfer.types.includes("application/x-timeline-clip")) return; - // Only handle timeline clip drags - if (!e.dataTransfer.types.includes("application/x-timeline-clip")) { - return; - } - - // Check if we're actually leaving the track area const rect = e.currentTarget.getBoundingClientRect(); - const x = e.clientX; - const y = e.clientY; + const { clientX: x, clientY: y } = e; - const isActuallyLeaving = - x < rect.left || x > rect.right || y < rect.top || y > rect.bottom; - - if (isActuallyLeaving) { + if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) { setIsDropping(false); } }; @@ -508,67 +435,25 @@ function TimelineTrackComponent({ track, zoomLevel }: { track: TimelineTrack, zo e.preventDefault(); setIsDropping(false); - // Only handle timeline clip drags - if (!e.dataTransfer.types.includes("application/x-timeline-clip")) { - return; - } + if (!e.dataTransfer.types.includes("application/x-timeline-clip")) return; - const timelineClipData = e.dataTransfer.getData( - "application/x-timeline-clip" - ); - - if (!timelineClipData) { - return; - } + const timelineClipData = e.dataTransfer.getData("application/x-timeline-clip"); + if (!timelineClipData) return; try { - const parsedData = JSON.parse(timelineClipData); - const { clipId, trackId: fromTrackId } = parsedData; + const { clipId, trackId: fromTrackId } = JSON.parse(timelineClipData); + const trackContainer = e.currentTarget.querySelector(".track-clips-container") as HTMLElement; - // Calculate where to insert the clip based on mouse position - const trackContainer = e.currentTarget.querySelector( - ".track-clips-container" - ) as HTMLElement; - - if (!trackContainer) { - return; - } + if (!trackContainer) return; const rect = trackContainer.getBoundingClientRect(); - const mouseX = e.clientX - rect.left; - - // Calculate insertion index based on position - let insertIndex = 0; - const clipElements = trackContainer.querySelectorAll(".timeline-clip"); - - for (let i = 0; i < clipElements.length; i++) { - const clipRect = clipElements[i].getBoundingClientRect(); - const clipCenterX = clipRect.left + clipRect.width / 2 - rect.left; - - if (mouseX > clipCenterX) { - insertIndex = i + 1; - } else { - break; - } - } + const newStartTime = Math.max(0, (e.clientX - rect.left) / (50 * zoomLevel)); if (fromTrackId === track.id) { - // Moving within the same track - reorder - const currentIndex = track.clips.findIndex( - (clip) => clip.id === clipId - ); - - if (currentIndex !== -1 && currentIndex !== insertIndex) { - // Adjust index if we're moving to a position after the current one - const adjustedIndex = - insertIndex > currentIndex ? insertIndex - 1 : insertIndex; - - reorderClipInTrack(track.id, clipId, adjustedIndex); - } + updateClipStartTime(track.id, clipId, newStartTime); } else { - // Moving between different tracks - moveClipToTrack(fromTrackId, track.id, clipId, insertIndex); - + moveClipToTrack(fromTrackId, track.id, clipId); + setTimeout(() => updateClipStartTime(track.id, clipId, newStartTime), 0); } } catch (error) { console.error("Error moving clip:", error); @@ -650,29 +535,28 @@ function TimelineTrackComponent({ track, zoomLevel }: { track: TimelineTrack, zo onMouseUp={handleResizeEnd} onMouseLeave={handleResizeEnd} > -
+
{track.clips.length === 0 ? (
Drop media here
) : ( - track.clips.map((clip, index) => { + track.clips.map((clip) => { const effectiveDuration = clip.duration - clip.trimStart - clip.trimEnd; const clipWidth = Math.max(80, effectiveDuration * 50 * zoomLevel); + const clipLeft = clip.startTime * 50 * zoomLevel; return (
- {/* Left resize handle */}
handleResizeStart(e, clip.id, 'left')} /> - {/* Clip content */}
- {/* Right resize handle */}
handleResizeStart(e, clip.id, 'right')} diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index ac4a45d..4b56e6b 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -5,6 +5,7 @@ export interface TimelineClip { mediaId: string; name: string; duration: number; + startTime: number; trimStart: number; trimEnd: number; } @@ -27,13 +28,7 @@ interface TimelineStore { moveClipToTrack: ( fromTrackId: string, toTrackId: string, - clipId: string, - insertIndex?: number - ) => void; - reorderClipInTrack: ( - trackId: string, - clipId: string, - newIndex: number + clipId: string ) => void; updateClipTrim: ( trackId: string, @@ -41,6 +36,11 @@ interface TimelineStore { trimStart: number, trimEnd: number ) => void; + updateClipStartTime: ( + trackId: string, + clipId: string, + startTime: number + ) => void; // Computed values getTotalDuration: () => number; @@ -72,6 +72,7 @@ export const useTimelineStore = create((set, get) => ({ const newClip: TimelineClip = { ...clipData, id: crypto.randomUUID(), + startTime: clipData.startTime || 0, trimStart: 0, trimEnd: 0, }; @@ -98,9 +99,8 @@ export const useTimelineStore = create((set, get) => ({ })); }, - moveClipToTrack: (fromTrackId, toTrackId, clipId, insertIndex) => { + moveClipToTrack: (fromTrackId, toTrackId, clipId) => { set((state) => { - // Find the clip to move const fromTrack = state.tracks.find((track) => track.id === fromTrackId); const clipToMove = fromTrack?.clips.find((clip) => clip.id === clipId); @@ -109,20 +109,14 @@ export const useTimelineStore = create((set, get) => ({ return { tracks: state.tracks.map((track) => { if (track.id === fromTrackId) { - // Remove clip from source track return { ...track, clips: track.clips.filter((clip) => clip.id !== clipId), }; } else if (track.id === toTrackId) { - // Add clip to destination track - const newClips = [...track.clips]; - const index = - insertIndex !== undefined ? insertIndex : newClips.length; - newClips.splice(index, 0, clipToMove); return { ...track, - clips: newClips, + clips: [...track.clips, clipToMove], }; } return track; @@ -131,22 +125,7 @@ export const useTimelineStore = create((set, get) => ({ }); }, - reorderClipInTrack: (trackId, clipId, newIndex) => { - set((state) => ({ - tracks: state.tracks.map((track) => { - if (track.id !== trackId) return track; - const clipIndex = track.clips.findIndex((clip) => clip.id === clipId); - if (clipIndex === -1) return track; - - const newClips = [...track.clips]; - const [movedClip] = newClips.splice(clipIndex, 1); - newClips.splice(newIndex, 0, movedClip); - - return { ...track, clips: newClips }; - }), - })); - }, updateClipTrim: (trackId, clipId, trimStart, trimEnd) => { set((state) => ({ @@ -165,16 +144,34 @@ export const useTimelineStore = create((set, get) => ({ })); }, + updateClipStartTime: (trackId, clipId, startTime) => { + set((state) => ({ + tracks: state.tracks.map((track) => + track.id === trackId + ? { + ...track, + clips: track.clips.map((clip) => + clip.id === clipId + ? { ...clip, startTime } + : clip + ), + } + : track + ), + })); + }, + getTotalDuration: () => { const { tracks } = get(); if (tracks.length === 0) return 0; - // Calculate the duration of each track (sum of all clips in that track) - const trackDurations = tracks.map((track) => - track.clips.reduce((total, clip) => total + clip.duration, 0) + const trackEndTimes = tracks.map((track) => + track.clips.reduce((maxEnd, clip) => { + const clipEnd = clip.startTime + clip.duration - clip.trimStart - clip.trimEnd; + return Math.max(maxEnd, clipEnd); + }, 0) ); - // Return the maximum track duration (longest track determines project duration) - return Math.max(...trackDurations, 0); + return Math.max(...trackEndTimes, 0); }, }));