From 90eaa40bc6f4283018479d0edf0635da45770fd0 Mon Sep 17 00:00:00 2001 From: pratiyankkumar Date: Sat, 28 Jun 2025 21:39:03 +0530 Subject: [PATCH 1/2] fix: Context Menu Placement on edges --- apps/web/src/components/editor/timeline.tsx | 126 ++++++++++++++++---- 1 file changed, 103 insertions(+), 23 deletions(-) diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index ee0a256..6d1a19a 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -555,22 +555,23 @@ export function Timeline() { toast.error("No clips selected"); return; } - + let splitCount = 0; selectedClips.forEach(({ trackId, clipId }) => { const track = tracks.find((t) => t.id === trackId); const clip = track?.clips.find((c) => c.id === clipId); if (clip && track) { const effectiveStart = clip.startTime; - const effectiveEnd = clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); - + const effectiveEnd = + clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + if (currentTime > effectiveStart && currentTime < effectiveEnd) { splitAndKeepLeft(trackId, clipId, currentTime); splitCount++; } } }); - + if (splitCount > 0) { toast.success(`Split and kept left portion of ${splitCount} clip(s)`); } else { @@ -583,22 +584,23 @@ export function Timeline() { toast.error("No clips selected"); return; } - + let splitCount = 0; selectedClips.forEach(({ trackId, clipId }) => { const track = tracks.find((t) => t.id === trackId); const clip = track?.clips.find((c) => c.id === clipId); if (clip && track) { const effectiveStart = clip.startTime; - const effectiveEnd = clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); - + const effectiveEnd = + clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + if (currentTime > effectiveStart && currentTime < effectiveEnd) { splitAndKeepRight(trackId, clipId, currentTime); splitCount++; } } }); - + if (splitCount > 0) { toast.success(`Split and kept right portion of ${splitCount} clip(s)`); } else { @@ -611,19 +613,24 @@ export function Timeline() { toast.error("No clips selected"); return; } - + let separatedCount = 0; selectedClips.forEach(({ trackId, clipId }) => { const track = tracks.find((t) => t.id === trackId); const clip = track?.clips.find((c) => c.id === clipId); const mediaItem = mediaItems.find((item) => item.id === clip?.mediaId); - - if (clip && track && mediaItem?.type === "video" && track.type === "video") { + + if ( + clip && + track && + mediaItem?.type === "video" && + track.type === "video" + ) { const audioClipId = separateAudio(trackId, clipId); if (audioClipId) separatedCount++; } }); - + if (separatedCount > 0) { toast.success(`Separated audio from ${separatedCount} video clip(s)`); } else { @@ -664,8 +671,12 @@ export function Timeline() { // --- Scroll synchronization effect --- useEffect(() => { - const rulerViewport = rulerScrollRef.current?.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement; - const tracksViewport = tracksScrollRef.current?.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement; + const rulerViewport = rulerScrollRef.current?.querySelector( + "[data-radix-scroll-area-viewport]" + ) as HTMLElement; + const tracksViewport = tracksScrollRef.current?.querySelector( + "[data-radix-scroll-area-viewport]" + ) as HTMLElement; if (!rulerViewport || !tracksViewport) return; const handleRulerScroll = () => { const now = Date.now(); @@ -683,18 +694,22 @@ export function Timeline() { rulerViewport.scrollLeft = tracksViewport.scrollLeft; isUpdatingRef.current = false; }; - rulerViewport.addEventListener('scroll', handleRulerScroll); - tracksViewport.addEventListener('scroll', handleTracksScroll); + rulerViewport.addEventListener("scroll", handleRulerScroll); + tracksViewport.addEventListener("scroll", handleTracksScroll); return () => { - rulerViewport.removeEventListener('scroll', handleRulerScroll); - tracksViewport.removeEventListener('scroll', handleTracksScroll); + rulerViewport.removeEventListener("scroll", handleRulerScroll); + tracksViewport.removeEventListener("scroll", handleTracksScroll); }; }, []); // --- Playhead auto-scroll effect --- useEffect(() => { - const rulerViewport = rulerScrollRef.current?.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement; - const tracksViewport = tracksScrollRef.current?.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement; + const rulerViewport = rulerScrollRef.current?.querySelector( + "[data-radix-scroll-area-viewport]" + ) as HTMLElement; + const tracksViewport = tracksScrollRef.current?.querySelector( + "[data-radix-scroll-area-viewport]" + ) as HTMLElement; if (!rulerViewport || !tracksViewport) return; const playheadPx = playheadPosition * 50 * zoomLevel; const viewportWidth = rulerViewport.clientWidth; @@ -713,6 +728,60 @@ export function Timeline() { } }, [playheadPosition, duration, zoomLevel]); + const getContextMenuPosition = (x: number, y: number) => { + const menuWidth = 160; + const menuHeight = 200; + const margin = 4; + + // ADJUSTABLE VALUE: Change this to move menu up/down when it appears above cursor + const verticalOffset = 80; // Reduce this to bring menu closer to cursor when above + + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + // Start with Windows-style default position + let adjustedX = x + 2; + let adjustedY = y + 2; + + // Horizontal positioning + if (adjustedX + menuWidth > viewportWidth - margin) { + adjustedX = x - menuWidth - 2; + if (adjustedX < margin) { + adjustedX = viewportWidth - menuWidth - margin; + } + } + + if (adjustedX < margin) { + adjustedX = margin; + } + + // Vertical positioning with adjustable offset + if (adjustedY + menuHeight > viewportHeight - margin) { + // Instead of y - menuHeight - 2, use adjustable offset + adjustedY = y - menuHeight + verticalOffset; + + if (adjustedY < margin) { + adjustedY = viewportHeight - menuHeight - margin; + } + } + + if (adjustedY < margin) { + adjustedY = margin; + } + + // Final safety clamp + adjustedX = Math.max( + margin, + Math.min(adjustedX, viewportWidth - menuWidth - margin) + ); + adjustedY = Math.max( + margin, + Math.min(adjustedY, viewportHeight - menuHeight - margin) + ); + + return { x: adjustedX, y: adjustedY }; + }; + return (
- @@ -807,7 +880,11 @@ export function Timeline() { - @@ -1124,7 +1201,10 @@ export function Timeline() { {contextMenu && (
e.preventDefault()} > {contextMenu.type === "track" ? ( From 394d9f684c8643cba9fe02991894203713ce99d7 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 1 Jul 2025 19:16:37 +0200 Subject: [PATCH 2/2] refactor: clean up comments --- apps/web/src/components/editor/timeline.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 6d1a19a..d43ae58 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -732,14 +732,11 @@ export function Timeline() { const menuWidth = 160; const menuHeight = 200; const margin = 4; - - // ADJUSTABLE VALUE: Change this to move menu up/down when it appears above cursor - const verticalOffset = 80; // Reduce this to bring menu closer to cursor when above + const verticalOffset = 80; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; - // Start with Windows-style default position let adjustedX = x + 2; let adjustedY = y + 2; @@ -755,9 +752,8 @@ export function Timeline() { adjustedX = margin; } - // Vertical positioning with adjustable offset + // Vertical positioning if (adjustedY + menuHeight > viewportHeight - margin) { - // Instead of y - menuHeight - 2, use adjustable offset adjustedY = y - menuHeight + verticalOffset; if (adjustedY < margin) { @@ -769,7 +765,6 @@ export function Timeline() { adjustedY = margin; } - // Final safety clamp adjustedX = Math.max( margin, Math.min(adjustedX, viewportWidth - menuWidth - margin)