From 40c7fbb4f86130c83fbd9029e83c5e7550e3219d Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Sun, 6 Jul 2025 20:45:29 +0200 Subject: refactor: move to a typed-tracks system and add support for text --- apps/web/src/hooks/use-drag-clip.ts | 89 +++++++++++++++------------- apps/web/src/hooks/use-playback-controls.ts | 91 ++++++++++++++--------------- 2 files changed, 95 insertions(+), 85 deletions(-) (limited to 'apps/web/src/hooks') diff --git a/apps/web/src/hooks/use-drag-clip.ts b/apps/web/src/hooks/use-drag-clip.ts index eb4d7435..1d38343b 100644 --- a/apps/web/src/hooks/use-drag-clip.ts +++ b/apps/web/src/hooks/use-drag-clip.ts @@ -3,23 +3,24 @@ import { useTimelineStore } from "@/stores/timeline-store"; interface DragState { isDragging: boolean; - clipId: string | null; + elementId: string | null; trackId: string | null; startMouseX: number; - startClipTime: number; + startElementTime: number; clickOffsetTime: number; currentTime: number; } export function useDragClip(zoomLevel: number) { - const { tracks, updateClipStartTime, moveClipToTrack } = useTimelineStore(); + const { tracks, updateElementStartTime, moveElementToTrack } = + useTimelineStore(); const [dragState, setDragState] = useState({ isDragging: false, - clipId: null, + elementId: null, trackId: null, startMouseX: 0, - startClipTime: 0, + startElementTime: 0, clickOffsetTime: 0, currentTime: 0, }); @@ -33,9 +34,9 @@ export function useDragClip(zoomLevel: number) { const startDrag = useCallback( ( e: React.MouseEvent, - clipId: string, + elementId: string, trackId: string, - clipStartTime: number, + elementStartTime: number, clickOffsetTime: number ) => { e.preventDefault(); @@ -43,12 +44,12 @@ export function useDragClip(zoomLevel: number) { setDragState({ isDragging: true, - clipId, + elementId, trackId, startMouseX: e.clientX, - startClipTime: clipStartTime, + startElementTime: elementStartTime, clickOffsetTime, - currentTime: clipStartTime, + currentTime: elementStartTime, }); }, [] @@ -76,7 +77,7 @@ export function useDragClip(zoomLevel: number) { const endDrag = useCallback( (targetTrackId?: string) => { - if (!dragState.isDragging || !dragState.clipId || !dragState.trackId) + if (!dragState.isDragging || !dragState.elementId || !dragState.trackId) return; const finalTrackId = targetTrackId || dragState.trackId; @@ -85,71 +86,81 @@ export function useDragClip(zoomLevel: number) { // Check for overlaps const sourceTrack = tracks.find((t) => t.id === dragState.trackId); const targetTrack = tracks.find((t) => t.id === finalTrackId); - const movingClip = sourceTrack?.clips.find( - (c) => c.id === dragState.clipId + const movingElement = sourceTrack?.elements.find( + (e) => e.id === dragState.elementId ); - if (!movingClip || !targetTrack) { + if (!movingElement || !targetTrack) { setDragState((prev) => ({ ...prev, isDragging: false })); return; } - const movingClipDuration = - movingClip.duration - movingClip.trimStart - movingClip.trimEnd; - const movingClipEnd = finalTime + movingClipDuration; + const movingElementDuration = + movingElement.duration - + movingElement.trimStart - + movingElement.trimEnd; + const movingElementEnd = finalTime + movingElementDuration; - const hasOverlap = targetTrack.clips.some((existingClip) => { - // Skip the clip being moved if it's on the same track + const hasOverlap = targetTrack.elements.some((existingElement) => { + // Skip the element being moved if it's on the same track if ( dragState.trackId === finalTrackId && - existingClip.id === dragState.clipId + existingElement.id === dragState.elementId ) { return false; } - const existingStart = existingClip.startTime; + const existingStart = existingElement.startTime; const existingEnd = - existingClip.startTime + - (existingClip.duration - - existingClip.trimStart - - existingClip.trimEnd); + existingElement.startTime + + (existingElement.duration - + existingElement.trimStart - + existingElement.trimEnd); - return finalTime < existingEnd && movingClipEnd > existingStart; + return finalTime < existingEnd && movingElementEnd > existingStart; }); if (!hasOverlap) { if (dragState.trackId === finalTrackId) { // Moving within same track - updateClipStartTime(finalTrackId, dragState.clipId!, finalTime); + updateElementStartTime(finalTrackId, dragState.elementId!, finalTime); } else { // Moving to different track - moveClipToTrack(dragState.trackId!, finalTrackId, dragState.clipId!); + moveElementToTrack( + dragState.trackId!, + finalTrackId, + dragState.elementId! + ); requestAnimationFrame(() => { - updateClipStartTime(finalTrackId, dragState.clipId!, finalTime); + updateElementStartTime( + finalTrackId, + dragState.elementId!, + finalTime + ); }); } } setDragState({ isDragging: false, - clipId: null, + elementId: null, trackId: null, startMouseX: 0, - startClipTime: 0, + startElementTime: 0, clickOffsetTime: 0, currentTime: 0, }); }, - [dragState, tracks, updateClipStartTime, moveClipToTrack] + [dragState, tracks, updateElementStartTime, moveElementToTrack] ); const cancelDrag = useCallback(() => { setDragState({ isDragging: false, - clipId: null, + elementId: null, trackId: null, startMouseX: 0, - startClipTime: 0, + startElementTime: 0, clickOffsetTime: 0, currentTime: 0, }); @@ -176,12 +187,12 @@ export function useDragClip(zoomLevel: number) { }; }, [dragState.isDragging, updateDrag, endDrag, cancelDrag]); - const getDraggedClipPosition = useCallback( - (clipId: string) => { + const getDraggedElementPosition = useCallback( + (elementId: string) => { // Use ref to get current state, not stale closure const currentDragState = dragStateRef.current; const isMatch = - currentDragState.isDragging && currentDragState.clipId === clipId; + currentDragState.isDragging && currentDragState.elementId === elementId; if (isMatch) { return currentDragState.currentTime; @@ -209,7 +220,7 @@ export function useDragClip(zoomLevel: number) { return { // State isDragging: dragState.isDragging, - draggedClipId: dragState.clipId, + draggedElementId: dragState.elementId, currentDragTime: dragState.currentTime, clickOffsetTime: dragState.clickOffsetTime, @@ -217,7 +228,7 @@ export function useDragClip(zoomLevel: number) { startDrag, endDrag, cancelDrag, - getDraggedClipPosition, + getDraggedElementPosition, isValidDropTarget, // Refs diff --git a/apps/web/src/hooks/use-playback-controls.ts b/apps/web/src/hooks/use-playback-controls.ts index 2c689470..1191d4b4 100644 --- a/apps/web/src/hooks/use-playback-controls.ts +++ b/apps/web/src/hooks/use-playback-controls.ts @@ -7,106 +7,105 @@ export const usePlaybackControls = () => { const { isPlaying, currentTime, play, pause, seek } = usePlaybackStore(); const { - selectedClips, + selectedElements, tracks, - splitClip, + splitElement, splitAndKeepLeft, splitAndKeepRight, separateAudio, } = useTimelineStore(); - const handleSplitSelectedClip = useCallback(() => { - if (selectedClips.length !== 1) { - toast.error("Select exactly one clip to split"); + const handleSplitSelectedElement = useCallback(() => { + if (selectedElements.length !== 1) { + toast.error("Select exactly one element to split"); return; } - const { trackId, clipId } = selectedClips[0]; + const { trackId, elementId } = selectedElements[0]; const track = tracks.find((t) => t.id === trackId); - const clip = track?.clips.find((c) => c.id === clipId); + const element = track?.elements.find((e) => e.id === elementId); - if (!clip) return; + if (!element) return; - const effectiveStart = clip.startTime; + const effectiveStart = element.startTime; const effectiveEnd = - clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + element.startTime + + (element.duration - element.trimStart - element.trimEnd); if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { - toast.error("Playhead must be within selected clip"); + toast.error("Playhead must be within selected element"); return; } - splitClip(trackId, clipId, currentTime); - toast.success("Clip split at playhead"); - }, [selectedClips, tracks, currentTime, splitClip]); + splitElement(trackId, elementId, currentTime); + }, [selectedElements, tracks, currentTime, splitElement]); const handleSplitAndKeepLeftCallback = useCallback(() => { - if (selectedClips.length !== 1) { - toast.error("Select exactly one clip"); + if (selectedElements.length !== 1) { + toast.error("Select exactly one element"); return; } - const { trackId, clipId } = selectedClips[0]; + const { trackId, elementId } = selectedElements[0]; const track = tracks.find((t) => t.id === trackId); - const clip = track?.clips.find((c) => c.id === clipId); + const element = track?.elements.find((e) => e.id === elementId); - if (!clip) return; + if (!element) return; - const effectiveStart = clip.startTime; + const effectiveStart = element.startTime; const effectiveEnd = - clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + element.startTime + + (element.duration - element.trimStart - element.trimEnd); if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { - toast.error("Playhead must be within selected clip"); + toast.error("Playhead must be within selected element"); return; } - splitAndKeepLeft(trackId, clipId, currentTime); - toast.success("Split and kept left portion"); - }, [selectedClips, tracks, currentTime, splitAndKeepLeft]); + splitAndKeepLeft(trackId, elementId, currentTime); + }, [selectedElements, tracks, currentTime, splitAndKeepLeft]); const handleSplitAndKeepRightCallback = useCallback(() => { - if (selectedClips.length !== 1) { - toast.error("Select exactly one clip"); + if (selectedElements.length !== 1) { + toast.error("Select exactly one element"); return; } - const { trackId, clipId } = selectedClips[0]; + const { trackId, elementId } = selectedElements[0]; const track = tracks.find((t) => t.id === trackId); - const clip = track?.clips.find((c) => c.id === clipId); + const element = track?.elements.find((e) => e.id === elementId); - if (!clip) return; + if (!element) return; - const effectiveStart = clip.startTime; + const effectiveStart = element.startTime; const effectiveEnd = - clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + element.startTime + + (element.duration - element.trimStart - element.trimEnd); if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { - toast.error("Playhead must be within selected clip"); + toast.error("Playhead must be within selected element"); return; } - splitAndKeepRight(trackId, clipId, currentTime); - toast.success("Split and kept right portion"); - }, [selectedClips, tracks, currentTime, splitAndKeepRight]); + splitAndKeepRight(trackId, elementId, currentTime); + }, [selectedElements, tracks, currentTime, splitAndKeepRight]); const handleSeparateAudioCallback = useCallback(() => { - if (selectedClips.length !== 1) { - toast.error("Select exactly one video clip to separate audio"); + if (selectedElements.length !== 1) { + toast.error("Select exactly one media element to separate audio"); return; } - const { trackId, clipId } = selectedClips[0]; + const { trackId, elementId } = selectedElements[0]; const track = tracks.find((t) => t.id === trackId); - if (!track || track.type !== "video") { - toast.error("Select a video clip to separate audio"); + if (!track || track.type !== "media") { + toast.error("Select a media element to separate audio"); return; } - separateAudio(trackId, clipId); - toast.success("Audio separated to audio track"); - }, [selectedClips, tracks, separateAudio]); + separateAudio(trackId, elementId); + }, [selectedElements, tracks, separateAudio]); const handleKeyPress = useCallback( (e: KeyboardEvent) => { @@ -130,7 +129,7 @@ export const usePlaybackControls = () => { case "s": if (e.ctrlKey || e.metaKey) { e.preventDefault(); - handleSplitSelectedClip(); + handleSplitSelectedElement(); } break; @@ -160,7 +159,7 @@ export const usePlaybackControls = () => { isPlaying, play, pause, - handleSplitSelectedClip, + handleSplitSelectedElement, handleSplitAndKeepLeftCallback, handleSplitAndKeepRightCallback, handleSeparateAudioCallback, -- cgit v1.3.1