From 293fc369a00d439ca7abf5cc0ed64d5a814fca93 Mon Sep 17 00:00:00 2001 From: robinroy03 Date: Thu, 31 Jul 2025 18:51:39 +0530 Subject: [PATCH 01/11] fix: onDrop @ scrubber and transitions --- app/components/timeline/TimelineTracks.tsx | 50 +++++++++++----------- app/utils/api.ts | 2 +- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/app/components/timeline/TimelineTracks.tsx b/app/components/timeline/TimelineTracks.tsx index fe171fd..30d45fd 100644 --- a/app/components/timeline/TimelineTracks.tsx +++ b/app/components/timeline/TimelineTracks.tsx @@ -127,9 +127,8 @@ export const TimelineTracks: React.FC = ({ {/* Scrollable Tracks Area */}
0 ? onScroll : undefined} > {timeline.tracks.length === 0 ? ( @@ -168,22 +167,23 @@ export const TimelineTracks: React.FC = ({ }} onDrop={(e) => { e.preventDefault(); - + const jsonString = e.dataTransfer.getData("application/json"); if (!jsonString) return; const data = JSON.parse(jsonString); - const timelineBounds = e.currentTarget.getBoundingClientRect(); - const tracksScrollContainer = e.currentTarget.parentElement; - if (!timelineBounds || !tracksScrollContainer) return; + // Use containerRef for consistent coordinate calculation like the ruler does + const scrollContainer = containerRef.current; + if (!scrollContainer) return; + + const containerBounds = scrollContainer.getBoundingClientRect(); + const scrollLeft = scrollContainer.scrollLeft || 0; + const scrollTop = scrollContainer.scrollTop || 0; - const scrollLeft = tracksScrollContainer.scrollLeft || 0; - const scrollTop = tracksScrollContainer.scrollTop || 0; - const dropXInTimeline = - e.clientX - timelineBounds.left + scrollLeft; - const dropYInTimeline = - e.clientY - timelineBounds.top + scrollTop; + // Calculate drop position relative to the scroll container, accounting for scroll + const dropXInTimeline = e.clientX - containerBounds.left + scrollLeft; + const dropYInTimeline = e.clientY - containerBounds.top + scrollTop; let trackIndex = Math.floor( dropYInTimeline / DEFAULT_TRACK_HEIGHT @@ -194,7 +194,7 @@ export const TimelineTracks: React.FC = ({ ); const trackId = timeline.tracks[trackIndex]?.id; - + if (!trackId) { console.warn("No tracks to drop on, or track detection failed."); return; @@ -218,11 +218,10 @@ export const TimelineTracks: React.FC = ({ > {/* Track background */}
= ({ top: `0px`, width: "1px", height: `${DEFAULT_TRACK_HEIGHT}px`, - backgroundColor: `rgb(var(--border) / ${ - gridIndex % 5 === 0 ? 0.5 : 0.25 - })`, + backgroundColor: `rgb(var(--border) / ${gridIndex % 5 === 0 ? 0.5 : 0.25 + })`, }} /> ))} @@ -268,7 +266,7 @@ export const TimelineTracks: React.FC = ({ {/* Scrubbers */} {getAllScrubbers().map((scrubber) => { // Get all transitions for the track containing this scrubber - const scrubberTrack = timeline.tracks.find(track => + const scrubberTrack = timeline.tracks.find(track => track.scrubbers.some(s => s.id === scrubber.id) ); const trackTransitions = scrubberTrack?.transitions || []; @@ -300,11 +298,11 @@ export const TimelineTracks: React.FC = ({ const transitionComponents = []; for (const track of timeline.tracks) { for (const transition of track.transitions) { - const leftScrubber = transition.leftScrubberId ? + const leftScrubber = transition.leftScrubberId ? track.scrubbers.find(s => s.id === transition.leftScrubberId) || null : null; - const rightScrubber = transition.rightScrubberId ? + const rightScrubber = transition.rightScrubberId ? track.scrubbers.find(s => s.id === transition.rightScrubberId) || null : null; - + transitionComponents.push( { if (!isProduction) { From fa292335cdc97b6f9607a5028e6011caeeb2e015 Mon Sep 17 00:00:00 2001 From: robinroy03 Date: Thu, 31 Jul 2025 19:01:05 +0530 Subject: [PATCH 02/11] chore: cleanup unused functions --- app/components/timeline/Scrubber.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/components/timeline/Scrubber.tsx b/app/components/timeline/Scrubber.tsx index 067fd7e..27749bf 100644 --- a/app/components/timeline/Scrubber.tsx +++ b/app/components/timeline/Scrubber.tsx @@ -145,15 +145,6 @@ export const Scrubber: React.FC = ({ [containerRef] ); - const checkCollision = useCallback( - (scrubber1: ScrubberState, scrubber2: ScrubberState) => { - const bounds1 = getScrubberBounds(scrubber1); - const bounds2 = getScrubberBounds(scrubber2); - return !(bounds1.right <= bounds2.left || bounds1.left >= bounds2.right); - }, - [getScrubberBounds] - ); - const handleMouseDown = useCallback( (e: React.MouseEvent, mode: "drag" | "resize-left" | "resize-right") => { e.preventDefault(); From e4b694e88d1fce7915ae488c5f1545413bb74190 Mon Sep 17 00:00:00 2001 From: robinroy03 Date: Thu, 31 Jul 2025 22:27:08 +0530 Subject: [PATCH 03/11] chore: cleanup unused functions --- app/components/timeline/Scrubber.tsx | 57 +++++++++++----------------- 1 file changed, 22 insertions(+), 35 deletions(-) diff --git a/app/components/timeline/Scrubber.tsx b/app/components/timeline/Scrubber.tsx index 27749bf..2d13b0c 100644 --- a/app/components/timeline/Scrubber.tsx +++ b/app/components/timeline/Scrubber.tsx @@ -122,7 +122,7 @@ export const Scrubber: React.FC = ({ const newEnd = newScrubber.left + newScrubber.width; const hasOverlap = !(newEnd <= otherStart || newStart >= otherEnd); - + // If there's overlap, check if there's a transition that allows it if (hasOverlap && hasTransitionBetween(newScrubber.id, other.id)) { return false; // Allow overlap due to transition @@ -134,17 +134,6 @@ export const Scrubber: React.FC = ({ [otherScrubbers, hasTransitionBetween] ); - const getScrubberBounds = useCallback( - (scrubber: ScrubberState) => { - const scrollLeft = containerRef.current?.scrollLeft || 0; - return { - left: scrubber.left + scrollLeft, - right: scrubber.left + scrubber.width + scrollLeft, - }; - }, - [containerRef] - ); - const handleMouseDown = useCallback( (e: React.MouseEvent, mode: "drag" | "resize-left" | "resize-right") => { e.preventDefault(); @@ -197,7 +186,7 @@ export const Scrubber: React.FC = ({ // Smart collision handling with push-through logic const rawScrubber = { ...scrubber, left: rawNewLeft, y: newTrack }; - + // Find colliding scrubbers on the same track const collidingScrubbers = otherScrubbers.filter(other => { if (other.y !== newTrack) return false; @@ -212,7 +201,7 @@ export const Scrubber: React.FC = ({ // No collision - try snapping to nearby edges const snappedLeft = findSnapPoint(rawNewLeft, scrubber.id); const snappedScrubber = { ...scrubber, left: snappedLeft, y: newTrack }; - + // Double-check snapped position doesn't cause collision if (!checkCollisionWithTrack(snappedScrubber, scrubber.id)) { onUpdate(snappedScrubber); @@ -224,14 +213,14 @@ export const Scrubber: React.FC = ({ const collidingScrubber = collidingScrubbers[0]; // Handle first collision const collidingStart = collidingScrubber.left; const collidingEnd = collidingScrubber.left + collidingScrubber.width; - + // Determine which side of the colliding scrubber the mouse is closest to const mouseCenter = rawNewLeft + scrubber.width / 2; const collidingCenter = collidingStart + collidingScrubber.width / 2; - + let snapToLeft: number; let snapToRight: number; - + if (mouseCenter < collidingCenter) { // Mouse is on the left side - try snapping to left edge first snapToLeft = collidingStart - scrubber.width; @@ -241,12 +230,12 @@ export const Scrubber: React.FC = ({ snapToRight = collidingEnd; snapToLeft = collidingStart - scrubber.width; } - + // Try the preferred side first - const preferredScrubber = mouseCenter < collidingCenter + const preferredScrubber = mouseCenter < collidingCenter ? { ...scrubber, left: Math.max(0, snapToLeft), y: newTrack } : { ...scrubber, left: Math.min(snapToRight, timelineWidth - scrubber.width), y: newTrack }; - + if (!checkCollisionWithTrack(preferredScrubber, scrubber.id)) { onUpdate(preferredScrubber); } else { @@ -254,7 +243,7 @@ export const Scrubber: React.FC = ({ const alternateScrubber = mouseCenter < collidingCenter ? { ...scrubber, left: Math.min(snapToRight, timelineWidth - scrubber.width), y: newTrack } : { ...scrubber, left: Math.max(0, snapToLeft), y: newTrack }; - + if (!checkCollisionWithTrack(alternateScrubber, scrubber.id)) { onUpdate(alternateScrubber); } @@ -431,12 +420,12 @@ export const Scrubber: React.FC = ({ const handleContextMenu = useCallback((e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); - + // Select the scrubber when right-clicked if (onSelect) { onSelect(scrubber.id); } - + // Get the position relative to the viewport setContextMenu({ visible: true, @@ -456,11 +445,11 @@ export const Scrubber: React.FC = ({ const handleContextMenuDelete = useCallback((e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); - + if (onDelete) { onDelete(scrubber.id); } - + // Close context menu setContextMenu({ visible: false, x: 0, y: 0 }); }, [onDelete, scrubber.id]); @@ -470,7 +459,7 @@ export const Scrubber: React.FC = ({ if (contextMenu.visible) { document.addEventListener("click", handleClickOutside); document.addEventListener("contextmenu", handleClickOutside); - + return () => { document.removeEventListener("click", handleClickOutside); document.removeEventListener("contextmenu", handleClickOutside); @@ -532,9 +521,8 @@ export const Scrubber: React.FC = ({ {/* Name and position tooltip when dragging - positioned above or below based on track */} {isDragging && (
{scrubber.name} • {(scrubber.left / pixelsPerSecond).toFixed(2)}s -{" "} {((scrubber.left + scrubber.width) / pixelsPerSecond).toFixed(2)}s @@ -544,16 +532,15 @@ export const Scrubber: React.FC = ({ {/* Resize tooltips when resizing - showing precise timestamps with dynamic positioning */} {isResizing && (
{resizeMode === "left" ? `Start: ${(scrubber.left / pixelsPerSecond).toFixed(2)}s` : `End: ${( - (scrubber.left + scrubber.width) / - pixelsPerSecond - ).toFixed(2)}s`} + (scrubber.left + scrubber.width) / + pixelsPerSecond + ).toFixed(2)}s`}
)}
From dfd77fd4eb15ebeb67993a18fcf35c4976cf0c96 Mon Sep 17 00:00:00 2001 From: robinroy03 Date: Fri, 1 Aug 2025 11:19:31 +0530 Subject: [PATCH 04/11] dragging of clips with transition, fixed --- app/components/timeline/Scrubber.tsx | 120 ++---------------- app/components/timeline/TimelineTracks.tsx | 2 - app/hooks/useTimeline.ts | 135 +++++++++++++++++++-- 3 files changed, 131 insertions(+), 126 deletions(-) diff --git a/app/components/timeline/Scrubber.tsx b/app/components/timeline/Scrubber.tsx index 2d13b0c..3082204 100644 --- a/app/components/timeline/Scrubber.tsx +++ b/app/components/timeline/Scrubber.tsx @@ -21,8 +21,6 @@ export interface ScrubberProps { pixelsPerSecond: number; isSelected?: boolean; onSelect?: (scrubberId: string) => void; - // Add transitions prop to check for overlapping allowance - transitions?: Transition[]; } export const Scrubber: React.FC = ({ @@ -38,7 +36,6 @@ export const Scrubber: React.FC = ({ pixelsPerSecond, isSelected = false, onSelect, - transitions = [], }) => { const [isDragging, setIsDragging] = useState(false); const [isResizing, setIsResizing] = useState(false); @@ -98,41 +95,7 @@ export const Scrubber: React.FC = ({ [snapConfig, getSnapPoints] ); - // Check if there's a transition between two scrubbers that allows overlap - const hasTransitionBetween = useCallback( - (scrubber1Id: string, scrubber2Id: string) => { - return transitions.some( - (transition) => - (transition.leftScrubberId === scrubber1Id && transition.rightScrubberId === scrubber2Id) || - (transition.leftScrubberId === scrubber2Id && transition.rightScrubberId === scrubber1Id) - ); - }, - [transitions] - ); - - // Check collision with track awareness - allow overlap if transition exists - const checkCollisionWithTrack = useCallback( - (newScrubber: ScrubberState, excludeId?: string) => { - return otherScrubbers.some((other) => { - if (other.id === excludeId || other.y !== newScrubber.y) return false; - - const otherStart = other.left; - const otherEnd = other.left + other.width; - const newStart = newScrubber.left; - const newEnd = newScrubber.left + newScrubber.width; - - const hasOverlap = !(newEnd <= otherStart || newStart >= otherEnd); - // If there's overlap, check if there's a transition that allows it - if (hasOverlap && hasTransitionBetween(newScrubber.id, other.id)) { - return false; // Allow overlap due to transition - } - - return hasOverlap; - }); - }, - [otherScrubbers, hasTransitionBetween] - ); const handleMouseDown = useCallback( (e: React.MouseEvent, mode: "drag" | "resize-left" | "resize-right") => { @@ -166,7 +129,6 @@ export const Scrubber: React.FC = ({ const handleMouseMove = useCallback( (e: MouseEvent) => { if (!isDragging && !isResizing) return; - // Remove throttling and requestAnimationFrame for responsive dragging if (isDragging) { let rawNewLeft = e.clientX - dragStateRef.current.offsetX; @@ -184,72 +146,12 @@ export const Scrubber: React.FC = ({ newTrack = Math.max(0, Math.min(trackCount - 1, trackIndex)); } - // Smart collision handling with push-through logic - const rawScrubber = { ...scrubber, left: rawNewLeft, y: newTrack }; - - // Find colliding scrubbers on the same track - const collidingScrubbers = otherScrubbers.filter(other => { - if (other.y !== newTrack) return false; - const otherStart = other.left; - const otherEnd = other.left + other.width; - const newStart = rawNewLeft; - const newEnd = rawNewLeft + scrubber.width; - return !(newEnd <= otherStart || newStart >= otherEnd); - }); - - if (collidingScrubbers.length === 0) { - // No collision - try snapping to nearby edges - const snappedLeft = findSnapPoint(rawNewLeft, scrubber.id); - const snappedScrubber = { ...scrubber, left: snappedLeft, y: newTrack }; - - // Double-check snapped position doesn't cause collision - if (!checkCollisionWithTrack(snappedScrubber, scrubber.id)) { - onUpdate(snappedScrubber); - } else { - onUpdate(rawScrubber); - } - } else { - // Collision detected - try smart positioning - const collidingScrubber = collidingScrubbers[0]; // Handle first collision - const collidingStart = collidingScrubber.left; - const collidingEnd = collidingScrubber.left + collidingScrubber.width; - - // Determine which side of the colliding scrubber the mouse is closest to - const mouseCenter = rawNewLeft + scrubber.width / 2; - const collidingCenter = collidingStart + collidingScrubber.width / 2; - - let snapToLeft: number; - let snapToRight: number; - - if (mouseCenter < collidingCenter) { - // Mouse is on the left side - try snapping to left edge first - snapToLeft = collidingStart - scrubber.width; - snapToRight = collidingEnd; - } else { - // Mouse is on the right side - try snapping to right edge first - snapToRight = collidingEnd; - snapToLeft = collidingStart - scrubber.width; - } + // Apply snapping to the position + const snappedLeft = findSnapPoint(rawNewLeft, scrubber.id); + const updatedScrubber = { ...scrubber, left: snappedLeft, y: newTrack }; - // Try the preferred side first - const preferredScrubber = mouseCenter < collidingCenter - ? { ...scrubber, left: Math.max(0, snapToLeft), y: newTrack } - : { ...scrubber, left: Math.min(snapToRight, timelineWidth - scrubber.width), y: newTrack }; - - if (!checkCollisionWithTrack(preferredScrubber, scrubber.id)) { - onUpdate(preferredScrubber); - } else { - // Try the other side - const alternateScrubber = mouseCenter < collidingCenter - ? { ...scrubber, left: Math.min(snapToRight, timelineWidth - scrubber.width), y: newTrack } - : { ...scrubber, left: Math.max(0, snapToLeft), y: newTrack }; - - if (!checkCollisionWithTrack(alternateScrubber, scrubber.id)) { - onUpdate(alternateScrubber); - } - // If both sides are blocked, don't update (scrubber stops) - } - } + // Let the timeline handle collision detection and connected scrubber logic + onUpdate(updatedScrubber); // Auto-scroll when dragging near edges if (containerRef.current) { @@ -297,10 +199,7 @@ export const Scrubber: React.FC = ({ } const newScrubber = { ...scrubber, left: newLeft, width: newWidth }; - - if (!checkCollisionWithTrack(newScrubber, scrubber.id)) { - onUpdate(newScrubber); - } + onUpdate(newScrubber); } else if (resizeMode === "right") { let newWidth = dragStateRef.current.startWidth + deltaX; @@ -326,10 +225,7 @@ export const Scrubber: React.FC = ({ } const newScrubber = { ...scrubber, width: newWidth }; - - if (!checkCollisionWithTrack(newScrubber, scrubber.id)) { - onUpdate(newScrubber); - } + onUpdate(newScrubber); } } }, @@ -339,13 +235,11 @@ export const Scrubber: React.FC = ({ resizeMode, scrubber, timelineWidth, - checkCollisionWithTrack, onUpdate, expandTimeline, containerRef, findSnapPoint, trackCount, - otherScrubbers, ] ); diff --git a/app/components/timeline/TimelineTracks.tsx b/app/components/timeline/TimelineTracks.tsx index 30d45fd..00c475e 100644 --- a/app/components/timeline/TimelineTracks.tsx +++ b/app/components/timeline/TimelineTracks.tsx @@ -269,7 +269,6 @@ export const TimelineTracks: React.FC = ({ const scrubberTrack = timeline.tracks.find(track => track.scrubbers.some(s => s.id === scrubber.id) ); - const trackTransitions = scrubberTrack?.transitions || []; return ( = ({ snapConfig={{ enabled: true, distance: 10 }} trackCount={timeline.tracks.length} pixelsPerSecond={pixelsPerSecond} - transitions={trackTransitions} /> ); })} diff --git a/app/hooks/useTimeline.ts b/app/hooks/useTimeline.ts index b46da20..0afd30a 100644 --- a/app/hooks/useTimeline.ts +++ b/app/hooks/useTimeline.ts @@ -512,7 +512,7 @@ export const useTimeline = () => { const getConnectedElements = useCallback((elementId: string): string[] => { const connected = new Set(); const toProcess = [elementId]; - + while (toProcess.length > 0) { const currentId = toProcess.pop()!; if (connected.has(currentId)) continue; @@ -651,10 +651,10 @@ export const useTimeline = () => { // Move the right scrubber to create overlap // The right scrubber should start at (leftScrubber.end - transitionWidth) const newLeft = leftScrubber ? (leftScrubber.left + leftScrubber.width - transitionWidthPx) : scrubber.left; - return { - ...scrubber, + return { + ...scrubber, left: newLeft, - left_transition_id: updatedTransition.id + left_transition_id: updatedTransition.id }; } return scrubber; @@ -680,10 +680,10 @@ export const useTimeline = () => { // Find the right scrubber and calculate its movement const rightScrubber = track.scrubbers.find(s => s.id === transitionToDelete.rightScrubberId); const leftScrubber = track.scrubbers.find(s => s.id === transitionToDelete.leftScrubberId); - + let movementDistance = 0; let rightScrubberNewLeft = 0; - + if (rightScrubber && leftScrubber) { // Calculate where the right scrubber should be positioned (no overlap) rightScrubberNewLeft = leftScrubber.left + leftScrubber.width; @@ -733,14 +733,122 @@ export const useTimeline = () => { toast.success("Transition deleted"); }, [getPixelsPerSecond]); + // Check if there's a transition between two scrubbers that allows overlap + const hasTransitionBetween = useCallback( + (scrubber1Id: string, scrubber2Id: string) => { + const allTransitions = timeline.tracks.flatMap(track => track.transitions); + return allTransitions.some( + (transition) => + (transition.leftScrubberId === scrubber1Id && transition.rightScrubberId === scrubber2Id) || + (transition.leftScrubberId === scrubber2Id && transition.rightScrubberId === scrubber1Id) + ); + }, + [timeline] + ); + + // Check collision with track awareness - allow overlap if transition exists + const checkCollisionWithTrack = useCallback( + (newScrubber: ScrubberState, excludeId?: string) => { + const allScrubbers = getAllScrubbers(); + return allScrubbers.some((other) => { + if (other.id === excludeId || other.y !== newScrubber.y) return false; + + const otherStart = other.left; + const otherEnd = other.left + other.width; + const newStart = newScrubber.left; + const newEnd = newScrubber.left + newScrubber.width; + + const hasOverlap = !(newEnd <= otherStart || newStart >= otherEnd); + + // If there's overlap, check if there's a transition that allows it + if (hasOverlap && hasTransitionBetween(newScrubber.id, other.id)) { + return false; // Allow overlap due to transition + } + + return hasOverlap; + }); + }, + [getAllScrubbers, hasTransitionBetween] + ); + + // Handle collision detection and smart positioning + const handleCollisionDetection = useCallback((updatedScrubber: ScrubberState, originalScrubber: ScrubberState, timelineWidth: number) => { + const allScrubbers = getAllScrubbers(); + const otherScrubbers = allScrubbers.filter(s => s.id !== updatedScrubber.id); + + // Find colliding scrubbers on the same track + const collidingScrubbers = otherScrubbers.filter(other => { + if (other.y !== updatedScrubber.y) return false; + const otherStart = other.left; + const otherEnd = other.left + other.width; + const newStart = updatedScrubber.left; + const newEnd = updatedScrubber.left + updatedScrubber.width; + return !(newEnd <= otherStart || newStart >= otherEnd); + }); + + if (collidingScrubbers.length === 0) { + // No collision - use the updated position + if (!checkCollisionWithTrack(updatedScrubber, updatedScrubber.id)) { + return updatedScrubber; + } + } else { + // Collision detected - try smart positioning + const collidingScrubber = collidingScrubbers[0]; // Handle first collision + const collidingStart = collidingScrubber.left; + const collidingEnd = collidingScrubber.left + collidingScrubber.width; + + // Determine which side of the colliding scrubber the mouse is closest to + const mouseCenter = updatedScrubber.left + updatedScrubber.width / 2; + const collidingCenter = collidingStart + collidingScrubber.width / 2; + + let snapToLeft: number; + let snapToRight: number; + + if (mouseCenter < collidingCenter) { + // Mouse is on the left side - try snapping to left edge first + snapToLeft = collidingStart - updatedScrubber.width; + snapToRight = collidingEnd; + } else { + // Mouse is on the right side - try snapping to right edge first + snapToRight = collidingEnd; + snapToLeft = collidingStart - updatedScrubber.width; + } + + // Try the preferred side first + const preferredScrubber = mouseCenter < collidingCenter + ? { ...updatedScrubber, left: Math.max(0, snapToLeft) } + : { ...updatedScrubber, left: Math.min(snapToRight, timelineWidth - updatedScrubber.width) }; + + if (!checkCollisionWithTrack(preferredScrubber, updatedScrubber.id)) { + return preferredScrubber; + } else { + // Try the other side + const alternateScrubber = mouseCenter < collidingCenter + ? { ...updatedScrubber, left: Math.min(snapToRight, timelineWidth - updatedScrubber.width) } + : { ...updatedScrubber, left: Math.max(0, snapToLeft) }; + + if (!checkCollisionWithTrack(alternateScrubber, updatedScrubber.id)) { + return alternateScrubber; + } + } + // If both sides are blocked, return original position (scrubber stops) + return originalScrubber; + } + + return updatedScrubber; + }, [getAllScrubbers, checkCollisionWithTrack]); + const handleUpdateScrubberWithLocking = useCallback((updatedScrubber: ScrubberState) => { const connectedElements = getConnectedElements(updatedScrubber.id); - const scrubberConnected = connectedElements.filter(id => + const scrubberConnected = connectedElements.filter(id => timeline.tracks.some(track => track.scrubbers.some(s => s.id === id)) ); - if (scrubberConnected.length > 1) { - // Calculate offset + // Check if this scrubber is connected to transitions (more than just itself) + const isConnectedToTransitions = scrubberConnected.length > 1; + + if (isConnectedToTransitions) { + // Skip collision detection for connected scrubbers - move them all together const originalScrubber = getAllScrubbers().find(s => s.id === updatedScrubber.id); if (!originalScrubber) return; @@ -765,9 +873,14 @@ export const useTimeline = () => { })) })); } else { - handleUpdateScrubber(updatedScrubber); + // Run collision detection for standalone scrubbers + const originalScrubber = getAllScrubbers().find(s => s.id === updatedScrubber.id); + if (!originalScrubber) return; + + const finalScrubber = handleCollisionDetection(updatedScrubber, originalScrubber, timelineWidth); + handleUpdateScrubber(finalScrubber); } - }, [getConnectedElements, timeline, getAllScrubbers, handleUpdateScrubber]); + }, [getConnectedElements, timeline, getAllScrubbers, handleUpdateScrubber, handleCollisionDetection, timelineWidth]); return { timeline, From e4ec7f5f3aa4d03ca4fab63e2817ec5f15e29f3a Mon Sep 17 00:00:00 2001 From: robinroy03 Date: Fri, 1 Aug 2025 11:56:26 +0530 Subject: [PATCH 05/11] fix: transition bug if there is a gap (to left or right, it used to auto snap. fixed --- app/components/timeline/TransitionOverlay.tsx | 23 +++- app/hooks/useTimeline.ts | 129 ++++++++++++++---- 2 files changed, 119 insertions(+), 33 deletions(-) diff --git a/app/components/timeline/TransitionOverlay.tsx b/app/components/timeline/TransitionOverlay.tsx index aefa2c2..648a35e 100644 --- a/app/components/timeline/TransitionOverlay.tsx +++ b/app/components/timeline/TransitionOverlay.tsx @@ -27,13 +27,26 @@ export const TransitionOverlay: React.FC = ({ let left = 0; const width = (transition.durationInFrames / 30) * pixelsPerSecond; // Convert frames to pixels let top = 0; + + // Define snap distance threshold (same as in useTimeline.ts) + const SNAP_DISTANCE = 10; if (leftScrubber && rightScrubber) { - // Position transition on the overlap area - // The overlap starts at the rightScrubber.left and has width equal to transition duration - const overlapStart = rightScrubber.left; - left = overlapStart; - top = leftScrubber.y * DEFAULT_TRACK_HEIGHT; + // Check if there's an overlap or a gap between scrubbers + const leftScrubberEnd = leftScrubber.left + leftScrubber.width; + const gap = rightScrubber.left - leftScrubberEnd; + + if (gap <= SNAP_DISTANCE) { + // Scrubbers are close enough - position transition on the overlap area + // The overlap starts at the rightScrubber.left and has width equal to transition duration + const overlapStart = rightScrubber.left; + left = overlapStart; + top = leftScrubber.y * DEFAULT_TRACK_HEIGHT; + } else { + // There's a gap - position the transition as an outro from the left scrubber + left = leftScrubber.left + leftScrubber.width - width; + top = leftScrubber.y * DEFAULT_TRACK_HEIGHT; + } } else if (leftScrubber) { // Transition after a scrubber (outro) - position at the end of left scrubber left = leftScrubber.left + leftScrubber.width - width; diff --git a/app/hooks/useTimeline.ts b/app/hooks/useTimeline.ts index 0afd30a..ee7b5b2 100644 --- a/app/hooks/useTimeline.ts +++ b/app/hooks/useTimeline.ts @@ -577,12 +577,44 @@ export const useTimeline = () => { // Dropped on a scrubber if (dropPosition <= scrubber.left + scrubber.width / 2) { // Closer to left edge - leftScrubber = scrubbers[i - 1] || null; - rightScrubber = scrubber; + const prevScrubber = scrubbers[i - 1] || null; + if (prevScrubber) { + // Check gap to previous scrubber + const gap = scrubber.left - (prevScrubber.left + prevScrubber.width); + if (gap <= 10) { // Within snap distance + // Create transition between previous and current scrubber + leftScrubber = prevScrubber; + rightScrubber = scrubber; + } else { + // Too far - create intro transition for current scrubber + leftScrubber = null; + rightScrubber = scrubber; + } + } else { + // No previous scrubber - create intro transition + leftScrubber = null; + rightScrubber = scrubber; + } } else { // Closer to right edge - leftScrubber = scrubber; - rightScrubber = scrubbers[i + 1] || null; + const nextScrubber = scrubbers[i + 1] || null; + if (nextScrubber) { + // Check gap to next scrubber + const gap = nextScrubber.left - (scrubber.left + scrubber.width); + if (gap <= 10) { // Within snap distance + // Create transition between current and next scrubber + leftScrubber = scrubber; + rightScrubber = nextScrubber; + } else { + // Too far - create outro transition for current scrubber + leftScrubber = scrubber; + rightScrubber = null; + } + } else { + // No next scrubber - create outro transition + leftScrubber = scrubber; + rightScrubber = null; + } } break; } else if (i === 0 && dropPosition < scrubber.left) { @@ -633,6 +665,20 @@ export const useTimeline = () => { // Calculate the overlap distance needed for the transition const pixelsPerSecond = getPixelsPerSecond(); const transitionWidthPx = (updatedTransition.durationInFrames / 30) * pixelsPerSecond; + + // Define snap distance threshold (same as in TimelineTracks.tsx) + const SNAP_DISTANCE = 10; + + // Calculate the gap between scrubbers to determine if they should be moved together + const shouldMoveScrubbersTogetherForOverlap = () => { + if (!leftScrubber || !rightScrubber) return false; + + const leftScrubberEnd = leftScrubber.left + leftScrubber.width; + const gap = rightScrubber.left - leftScrubberEnd; + + // Only move scrubbers together if the gap is within snap distance + return gap <= SNAP_DISTANCE; + }; // Add transition to track and update scrubber references with overlap positioning setTimeline(prev => ({ @@ -648,14 +694,23 @@ export const useTimeline = () => { return { ...scrubber, right_transition_id: updatedTransition.id }; } if (scrubber.id === rightScrubber?.id) { - // Move the right scrubber to create overlap - // The right scrubber should start at (leftScrubber.end - transitionWidth) - const newLeft = leftScrubber ? (leftScrubber.left + leftScrubber.width - transitionWidthPx) : scrubber.left; - return { - ...scrubber, - left: newLeft, - left_transition_id: updatedTransition.id - }; + // Only move the right scrubber to create overlap if scrubbers are close enough + if (shouldMoveScrubbersTogetherForOverlap()) { + // Move the right scrubber to create overlap + // The right scrubber should start at (leftScrubber.end - transitionWidth) + const newLeft = leftScrubber ? (leftScrubber.left + leftScrubber.width - transitionWidthPx) : scrubber.left; + return { + ...scrubber, + left: newLeft, + left_transition_id: updatedTransition.id + }; + } else { + // If there's a large gap, don't move the scrubber - treat it as a transition for the scrubber alone + return { + ...scrubber, + left_transition_id: updatedTransition.id + }; + } } return scrubber; }) @@ -676,6 +731,9 @@ export const useTimeline = () => { // Calculate the overlap distance to restore const pixelsPerSecond = getPixelsPerSecond(); const transitionWidthPx = (transitionToDelete.durationInFrames / FPS) * pixelsPerSecond; + + // Define snap distance threshold (same as in creation logic) + const SNAP_DISTANCE = 10; // Find the right scrubber and calculate its movement const rightScrubber = track.scrubbers.find(s => s.id === transitionToDelete.rightScrubberId); @@ -683,12 +741,21 @@ export const useTimeline = () => { let movementDistance = 0; let rightScrubberNewLeft = 0; + let shouldRestorePosition = false; if (rightScrubber && leftScrubber) { - // Calculate where the right scrubber should be positioned (no overlap) - rightScrubberNewLeft = leftScrubber.left + leftScrubber.width; - // Calculate how much we're moving the right scrubber - movementDistance = rightScrubberNewLeft - rightScrubber.left; + // Check if the scrubbers were originally close enough to be moved together + // We need to calculate what the original gap would have been before the transition was created + const currentGap = rightScrubber.left - (leftScrubber.left + leftScrubber.width); + const originalGap = currentGap + transitionWidthPx; + + if (originalGap <= SNAP_DISTANCE) { + // Scrubbers were moved together during creation, so restore them + shouldRestorePosition = true; + rightScrubberNewLeft = leftScrubber.left + leftScrubber.width + originalGap; + movementDistance = rightScrubberNewLeft - rightScrubber.left; + } + // If originalGap > SNAP_DISTANCE, the right scrubber was never moved, so don't move it back } return { @@ -702,19 +769,22 @@ export const useTimeline = () => { right_transition_id: scrubber.right_transition_id === transitionId ? null : scrubber.right_transition_id, }; - // If this scrubber was the right scrubber in the deleted transition, move it back - if (scrubber.id === transitionToDelete.rightScrubberId) { - return { ...baseScrubber, left: rightScrubberNewLeft }; - } + // Only move scrubbers if they were originally moved during transition creation + if (shouldRestorePosition) { + // If this scrubber was the right scrubber in the deleted transition, move it back + if (scrubber.id === transitionToDelete.rightScrubberId) { + return { ...baseScrubber, left: rightScrubberNewLeft }; + } - // If this scrubber comes after where the right scrubber originally ended, move it by the same distance - if (rightScrubber && scrubber.id !== rightScrubber.id && movementDistance > 0) { - // Check if this scrubber is on the same track as the transition - if (scrubber.y === rightScrubber.y) { - // Check if this scrubber starts at or after where the right scrubber originally ended - const rightScrubberOriginalEnd = rightScrubber.left + rightScrubber.width; - if (scrubber.left >= rightScrubberOriginalEnd) { - return { ...baseScrubber, left: scrubber.left + movementDistance }; + // If this scrubber comes after where the right scrubber originally ended, move it by the same distance + if (rightScrubber && scrubber.id !== rightScrubber.id && movementDistance > 0) { + // Check if this scrubber is on the same track as the transition + if (scrubber.y === rightScrubber.y) { + // Check if this scrubber starts at or after where the right scrubber originally ended + const rightScrubberOriginalEnd = rightScrubber.left + rightScrubber.width; + if (scrubber.left >= rightScrubberOriginalEnd) { + return { ...baseScrubber, left: scrubber.left + movementDistance }; + } } } } @@ -848,6 +918,9 @@ export const useTimeline = () => { const isConnectedToTransitions = scrubberConnected.length > 1; if (isConnectedToTransitions) { + // IMPORTANT: THIS IS A BUG. WE STILL NEED COLLISION DETECTION FOR CONNECTED SCRUBBERS. + // I'm ignoring it right now because I think eventually we'll remove this block of code entirely to improve performance. + // Skip collision detection for connected scrubbers - move them all together const originalScrubber = getAllScrubbers().find(s => s.id === updatedScrubber.id); if (!originalScrubber) return; From 35390158dcd3f19acce6cdff40decfebb8527b1f Mon Sep 17 00:00:00 2001 From: robinroy03 Date: Fri, 1 Aug 2025 18:21:48 +0530 Subject: [PATCH 06/11] ui: chat is minimized by default (for now) --- app/hooks/useTimeline.ts | 18 +++++++++--------- app/routes/home.tsx | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/hooks/useTimeline.ts b/app/hooks/useTimeline.ts index ee7b5b2..6c02fca 100644 --- a/app/hooks/useTimeline.ts +++ b/app/hooks/useTimeline.ts @@ -665,17 +665,17 @@ export const useTimeline = () => { // Calculate the overlap distance needed for the transition const pixelsPerSecond = getPixelsPerSecond(); const transitionWidthPx = (updatedTransition.durationInFrames / 30) * pixelsPerSecond; - + // Define snap distance threshold (same as in TimelineTracks.tsx) const SNAP_DISTANCE = 10; - + // Calculate the gap between scrubbers to determine if they should be moved together const shouldMoveScrubbersTogetherForOverlap = () => { if (!leftScrubber || !rightScrubber) return false; - + const leftScrubberEnd = leftScrubber.left + leftScrubber.width; const gap = rightScrubber.left - leftScrubberEnd; - + // Only move scrubbers together if the gap is within snap distance return gap <= SNAP_DISTANCE; }; @@ -731,7 +731,7 @@ export const useTimeline = () => { // Calculate the overlap distance to restore const pixelsPerSecond = getPixelsPerSecond(); const transitionWidthPx = (transitionToDelete.durationInFrames / FPS) * pixelsPerSecond; - + // Define snap distance threshold (same as in creation logic) const SNAP_DISTANCE = 10; @@ -748,7 +748,7 @@ export const useTimeline = () => { // We need to calculate what the original gap would have been before the transition was created const currentGap = rightScrubber.left - (leftScrubber.left + leftScrubber.width); const originalGap = currentGap + transitionWidthPx; - + if (originalGap <= SNAP_DISTANCE) { // Scrubbers were moved together during creation, so restore them shouldRestorePosition = true; @@ -845,7 +845,7 @@ export const useTimeline = () => { const handleCollisionDetection = useCallback((updatedScrubber: ScrubberState, originalScrubber: ScrubberState, timelineWidth: number) => { const allScrubbers = getAllScrubbers(); const otherScrubbers = allScrubbers.filter(s => s.id !== updatedScrubber.id); - + // Find colliding scrubbers on the same track const collidingScrubbers = otherScrubbers.filter(other => { if (other.y !== updatedScrubber.y) return false; @@ -904,7 +904,7 @@ export const useTimeline = () => { // If both sides are blocked, return original position (scrubber stops) return originalScrubber; } - + return updatedScrubber; }, [getAllScrubbers, checkCollisionWithTrack]); @@ -920,7 +920,7 @@ export const useTimeline = () => { if (isConnectedToTransitions) { // IMPORTANT: THIS IS A BUG. WE STILL NEED COLLISION DETECTION FOR CONNECTED SCRUBBERS. // I'm ignoring it right now because I think eventually we'll remove this block of code entirely to improve performance. - + // Skip collision detection for connected scrubbers - move them all together const originalScrubber = getAllScrubbers().find(s => s.id === updatedScrubber.id); if (!originalScrubber) return; diff --git a/app/routes/home.tsx b/app/routes/home.tsx index 0f5d275..c50c661 100644 --- a/app/routes/home.tsx +++ b/app/routes/home.tsx @@ -90,7 +90,7 @@ export default function TimelineEditor() { const [width, setWidth] = useState(1920); const [height, setHeight] = useState(1080); const [isAutoSize, setIsAutoSize] = useState(false); - const [isChatMinimized, setIsChatMinimized] = useState(false); + const [isChatMinimized, setIsChatMinimized] = useState(true); const [chatMessages, setChatMessages] = useState([]); const [starCount, setStarCount] = useState(null); From a3fccf711499adb6b0f97907e3de5584a4ebcbed Mon Sep 17 00:00:00 2001 From: robinroy03 Date: Fri, 1 Aug 2025 22:55:06 +0530 Subject: [PATCH 07/11] all big bugs squashed --- app/hooks/useTimeline.ts | 75 ++++++++++++++++++-------- app/utils/api.ts | 2 +- app/video-compositions/VideoPlayer.tsx | 6 +-- 3 files changed, 58 insertions(+), 25 deletions(-) diff --git a/app/hooks/useTimeline.ts b/app/hooks/useTimeline.ts index 6c02fca..405a3d9 100644 --- a/app/hooks/useTimeline.ts +++ b/app/hooks/useTimeline.ts @@ -918,33 +918,66 @@ export const useTimeline = () => { const isConnectedToTransitions = scrubberConnected.length > 1; if (isConnectedToTransitions) { - // IMPORTANT: THIS IS A BUG. WE STILL NEED COLLISION DETECTION FOR CONNECTED SCRUBBERS. - // I'm ignoring it right now because I think eventually we'll remove this block of code entirely to improve performance. - - // Skip collision detection for connected scrubbers - move them all together + // Handle collision detection for connected scrubbers as a group const originalScrubber = getAllScrubbers().find(s => s.id === updatedScrubber.id); if (!originalScrubber) return; const offsetX = updatedScrubber.left - originalScrubber.left; const offsetY = updatedScrubber.y - originalScrubber.y; - // Update all connected scrubbers with the same offset - setTimeline(prev => ({ - ...prev, - tracks: prev.tracks.map(track => ({ - ...track, - scrubbers: track.scrubbers.map(scrubber => { - if (scrubberConnected.includes(scrubber.id)) { - return { - ...scrubber, - left: scrubber.left + offsetX, - y: scrubber.y + offsetY, - }; - } - return scrubber; - }) - })) + // Calculate new positions for all connected scrubbers + const allScrubbers = getAllScrubbers(); + const connectedScrubbers = allScrubbers.filter(s => scrubberConnected.includes(s.id)); + const updatedConnectedScrubbers = connectedScrubbers.map(scrubber => ({ + ...scrubber, + left: scrubber.left + offsetX, + y: scrubber.y + offsetY, })); + + // Check if any of the connected scrubbers would collide with non-connected scrubbers + const hasCollision = updatedConnectedScrubbers.some(updatedConnectedScrubber => { + return allScrubbers.some(other => { + // Skip if other scrubber is also in the connected group + if (scrubberConnected.includes(other.id)) return false; + // Skip if on different tracks + if (other.y !== updatedConnectedScrubber.y) return false; + + const otherStart = other.left; + const otherEnd = other.left + other.width; + const newStart = updatedConnectedScrubber.left; + const newEnd = updatedConnectedScrubber.left + updatedConnectedScrubber.width; + + const hasOverlap = !(newEnd <= otherStart || newStart >= otherEnd); + + // If there's overlap, check if there's a transition that allows it + if (hasOverlap && hasTransitionBetween(updatedConnectedScrubber.id, other.id)) { + return false; // Allow overlap due to transition + } + + return hasOverlap; + }); + }); + + // Only update if there's no collision + if (!hasCollision) { + setTimeline(prev => ({ + ...prev, + tracks: prev.tracks.map(track => ({ + ...track, + scrubbers: track.scrubbers.map(scrubber => { + if (scrubberConnected.includes(scrubber.id)) { + return { + ...scrubber, + left: scrubber.left + offsetX, + y: scrubber.y + offsetY, + }; + } + return scrubber; + }) + })) + })); + } + // If there is a collision, don't move the connected scrubbers (they stay in place) } else { // Run collision detection for standalone scrubbers const originalScrubber = getAllScrubbers().find(s => s.id === updatedScrubber.id); @@ -953,7 +986,7 @@ export const useTimeline = () => { const finalScrubber = handleCollisionDetection(updatedScrubber, originalScrubber, timelineWidth); handleUpdateScrubber(finalScrubber); } - }, [getConnectedElements, timeline, getAllScrubbers, handleUpdateScrubber, handleCollisionDetection, timelineWidth]); + }, [getConnectedElements, timeline, getAllScrubbers, handleUpdateScrubber, handleCollisionDetection, timelineWidth, hasTransitionBetween]); return { timeline, diff --git a/app/utils/api.ts b/app/utils/api.ts index 97fbfdb..a07129e 100644 --- a/app/utils/api.ts +++ b/app/utils/api.ts @@ -1,5 +1,5 @@ // Set to true for production, false for development -const isProduction = false; +const isProduction = true; export const getApiBaseUrl = (fastapi: boolean = false): string => { if (!isProduction) { diff --git a/app/video-compositions/VideoPlayer.tsx b/app/video-compositions/VideoPlayer.tsx index 3de2e6f..c63b1c1 100644 --- a/app/video-compositions/VideoPlayer.tsx +++ b/app/video-compositions/VideoPlayer.tsx @@ -223,7 +223,7 @@ export function TimelineComposition({ transitionSeriesElements.push( @@ -249,7 +249,7 @@ export function TimelineComposition({ transitionSeriesElements.push( {mediaContent} @@ -280,7 +280,7 @@ export function TimelineComposition({ transitionSeriesElements.push( From 08cce8be44d6e572163c7899b54adec7f08ffe8b Mon Sep 17 00:00:00 2001 From: robinroy03 Date: Sat, 2 Aug 2025 15:50:25 +0530 Subject: [PATCH 08/11] disable single scrubber transition for now --- app/hooks/useTimeline.ts | 6 ++-- app/utils/api.ts | 2 +- app/video-compositions/VideoPlayer.tsx | 48 +++++++++++++------------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/app/hooks/useTimeline.ts b/app/hooks/useTimeline.ts index 405a3d9..2e7a974 100644 --- a/app/hooks/useTimeline.ts +++ b/app/hooks/useTimeline.ts @@ -501,9 +501,9 @@ export const useTimeline = () => { return { valid: false, error: "Cannot place transitions next to each other" }; } - // Rule 3: Must have at least one sequence before or after - if (!leftScrubber && !rightScrubber) { - return { valid: false, error: "Must have at least one sequence before or after a transition" }; + // Rule 3: Must have both left and right scrubbers for a transition + if (!leftScrubber || !rightScrubber) { + return { valid: false, error: "You need 2 scrubbers for a transition" }; } return { valid: true }; diff --git a/app/utils/api.ts b/app/utils/api.ts index a07129e..97fbfdb 100644 --- a/app/utils/api.ts +++ b/app/utils/api.ts @@ -1,5 +1,5 @@ // Set to true for production, false for development -const isProduction = true; +const isProduction = false; export const getApiBaseUrl = (fastapi: boolean = false): string => { if (!isProduction) { diff --git a/app/video-compositions/VideoPlayer.tsx b/app/video-compositions/VideoPlayer.tsx index c63b1c1..bc667f4 100644 --- a/app/video-compositions/VideoPlayer.tsx +++ b/app/video-compositions/VideoPlayer.tsx @@ -230,18 +230,18 @@ export function TimelineComposition({ ); } - // Add left transition if exists (only for first scrubber) - if (isFirstScrubber && scrubber.left_transition_id && allTransitions[scrubber.left_transition_id]) { - const transition = allTransitions[scrubber.left_transition_id]; - transitionSeriesElements.push( - - ); - } + // // Add left transition if exists (only for first scrubber) + // if (isFirstScrubber && scrubber.left_transition_id && allTransitions[scrubber.left_transition_id]) { + // const transition = allTransitions[scrubber.left_transition_id]; + // transitionSeriesElements.push( + // + // ); + // } // Add the scrubber content const mediaContent = createMediaContent(scrubber); @@ -256,18 +256,18 @@ export function TimelineComposition({ ); } - // Add right transition if exists - if (scrubber.right_transition_id && allTransitions[scrubber.right_transition_id]) { - const transition = allTransitions[scrubber.right_transition_id]; - transitionSeriesElements.push( - - ); - } + // // Add right transition if exists + // if (scrubber.right_transition_id && allTransitions[scrubber.right_transition_id]) { + // const transition = allTransitions[scrubber.right_transition_id]; + // transitionSeriesElements.push( + // + // ); + // } // Add gap between scrubbers if there's a gap if (!isLastScrubber) { From 315ee7de2e60955f72b780d642144051d0183d0e Mon Sep 17 00:00:00 2001 From: robinroy03 Date: Sat, 2 Aug 2025 16:05:39 +0530 Subject: [PATCH 09/11] deleting scrubber will delete the connecting transitions --- app/hooks/useTimeline.ts | 90 ++++++++++++++++++++++++-- app/video-compositions/VideoPlayer.tsx | 48 +++++++------- 2 files changed, 110 insertions(+), 28 deletions(-) diff --git a/app/hooks/useTimeline.ts b/app/hooks/useTimeline.ts index 2e7a974..d6114db 100644 --- a/app/hooks/useTimeline.ts +++ b/app/hooks/useTimeline.ts @@ -271,28 +271,110 @@ export const useTimeline = () => { }, []); const handleDeleteScrubber = useCallback((scrubberId: string) => { + // Find all transitions connected to the scrubber being deleted + const connectedTransitionIds: string[] = []; + + timeline.tracks.forEach(track => { + track.transitions.forEach(transition => { + if (transition.leftScrubberId === scrubberId || transition.rightScrubberId === scrubberId) { + connectedTransitionIds.push(transition.id); + } + }); + }); + setTimeline((prev) => ({ ...prev, tracks: prev.tracks.map((track) => ({ ...track, + // Remove the scrubber scrubbers: track.scrubbers.filter( (scrubber) => scrubber.id !== scrubberId ), - })), + // Remove connected transitions + transitions: track.transitions.filter( + (transition) => !connectedTransitionIds.includes(transition.id) + ) + })).map(track => ({ + ...track, + // Clean up transition references in remaining scrubbers + scrubbers: track.scrubbers.map(scrubber => ({ + ...scrubber, + left_transition_id: connectedTransitionIds.includes(scrubber.left_transition_id || '') + ? null + : scrubber.left_transition_id, + right_transition_id: connectedTransitionIds.includes(scrubber.right_transition_id || '') + ? null + : scrubber.right_transition_id, + })) + })) })); - }, []); + + // Show feedback message + if (connectedTransitionIds.length > 0) { + toast.success(`Scrubber and ${connectedTransitionIds.length} connected transition${connectedTransitionIds.length > 1 ? 's' : ''} deleted`); + } else { + toast.success("Scrubber deleted"); + } + }, [timeline]); const handleDeleteScrubbersByMediaBinId = useCallback((mediaBinId: string) => { + // Find all scrubbers that will be deleted + const scrubbersToDelete: string[] = []; + timeline.tracks.forEach(track => { + track.scrubbers.forEach(scrubber => { + if (scrubber.sourceMediaBinId === mediaBinId) { + scrubbersToDelete.push(scrubber.id); + } + }); + }); + + // Find all transitions connected to any of the scrubbers being deleted + const connectedTransitionIds: string[] = []; + timeline.tracks.forEach(track => { + track.transitions.forEach(transition => { + if (scrubbersToDelete.includes(transition.leftScrubberId || '') || + scrubbersToDelete.includes(transition.rightScrubberId || '')) { + connectedTransitionIds.push(transition.id); + } + }); + }); + setTimeline((prev) => ({ ...prev, tracks: prev.tracks.map((track) => ({ ...track, + // Remove scrubbers with matching media bin ID scrubbers: track.scrubbers.filter( (scrubber) => scrubber.sourceMediaBinId !== mediaBinId ), - })), + // Remove connected transitions + transitions: track.transitions.filter( + (transition) => !connectedTransitionIds.includes(transition.id) + ) + })).map(track => ({ + ...track, + // Clean up transition references in remaining scrubbers + scrubbers: track.scrubbers.map(scrubber => ({ + ...scrubber, + left_transition_id: connectedTransitionIds.includes(scrubber.left_transition_id || '') + ? null + : scrubber.left_transition_id, + right_transition_id: connectedTransitionIds.includes(scrubber.right_transition_id || '') + ? null + : scrubber.right_transition_id, + })) + })) })); - }, []); + + // Show feedback message + if (scrubbersToDelete.length > 0) { + if (connectedTransitionIds.length > 0) { + toast.success(`${scrubbersToDelete.length} scrubber${scrubbersToDelete.length > 1 ? 's' : ''} and ${connectedTransitionIds.length} connected transition${connectedTransitionIds.length > 1 ? 's' : ''} deleted`); + } else { + toast.success(`${scrubbersToDelete.length} scrubber${scrubbersToDelete.length > 1 ? 's' : ''} deleted`); + } + } + }, [timeline]); const handleAddScrubberToTrack = useCallback( (trackId: string, newScrubber: ScrubberState) => { diff --git a/app/video-compositions/VideoPlayer.tsx b/app/video-compositions/VideoPlayer.tsx index bc667f4..c63b1c1 100644 --- a/app/video-compositions/VideoPlayer.tsx +++ b/app/video-compositions/VideoPlayer.tsx @@ -230,18 +230,18 @@ export function TimelineComposition({ ); } - // // Add left transition if exists (only for first scrubber) - // if (isFirstScrubber && scrubber.left_transition_id && allTransitions[scrubber.left_transition_id]) { - // const transition = allTransitions[scrubber.left_transition_id]; - // transitionSeriesElements.push( - // - // ); - // } + // Add left transition if exists (only for first scrubber) + if (isFirstScrubber && scrubber.left_transition_id && allTransitions[scrubber.left_transition_id]) { + const transition = allTransitions[scrubber.left_transition_id]; + transitionSeriesElements.push( + + ); + } // Add the scrubber content const mediaContent = createMediaContent(scrubber); @@ -256,18 +256,18 @@ export function TimelineComposition({ ); } - // // Add right transition if exists - // if (scrubber.right_transition_id && allTransitions[scrubber.right_transition_id]) { - // const transition = allTransitions[scrubber.right_transition_id]; - // transitionSeriesElements.push( - // - // ); - // } + // Add right transition if exists + if (scrubber.right_transition_id && allTransitions[scrubber.right_transition_id]) { + const transition = allTransitions[scrubber.right_transition_id]; + transitionSeriesElements.push( + + ); + } // Add gap between scrubbers if there's a gap if (!isLastScrubber) { From 7f019f66fe2dbed227a234e4ecb55c39587243bc Mon Sep 17 00:00:00 2001 From: robinroy03 Date: Sat, 2 Aug 2025 16:15:29 +0530 Subject: [PATCH 10/11] reduced length of transition --- app/components/media/Transitions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/media/Transitions.tsx b/app/components/media/Transitions.tsx index f91108d..2bfd57f 100644 --- a/app/components/media/Transitions.tsx +++ b/app/components/media/Transitions.tsx @@ -53,7 +53,7 @@ const TransitionThumbnail = ({ transition, isSelected, onClick }: { type: "transition", presentation: transition.type, timing: "linear", - durationInFrames: 4 * FPS, + durationInFrames: 1 * FPS, leftScrubberId: null, rightScrubberId: null, }; From 3dc44d7e5ec0acdb564d371826b0e2d8edf4908f Mon Sep 17 00:00:00 2001 From: robinroy03 Date: Sat, 2 Aug 2025 16:17:27 +0530 Subject: [PATCH 11/11] chore: api.ts im too lazy to move it to a .env one day i'll --- app/utils/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/utils/api.ts b/app/utils/api.ts index 97fbfdb..a07129e 100644 --- a/app/utils/api.ts +++ b/app/utils/api.ts @@ -1,5 +1,5 @@ // Set to true for production, false for development -const isProduction = false; +const isProduction = true; export const getApiBaseUrl = (fastapi: boolean = false): string => { if (!isProduction) {