Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 98 additions & 23 deletions apps/web/src/components/editor/timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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();
Expand All @@ -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;
Expand All @@ -713,6 +728,55 @@ export function Timeline() {
}
}, [playheadPosition, duration, zoomLevel]);

const getContextMenuPosition = (x: number, y: number) => {
const menuWidth = 160;
const menuHeight = 200;
const margin = 4;
const verticalOffset = 80;

const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;

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
if (adjustedY + menuHeight > viewportHeight - margin) {
adjustedY = y - menuHeight + verticalOffset;

if (adjustedY < margin) {
adjustedY = viewportHeight - menuHeight - margin;
}
}

if (adjustedY < margin) {
adjustedY = margin;
}

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 (
<div
className={`h-full flex flex-col transition-colors duration-200 relative ${isDragOver ? "bg-accent/30 border-accent" : ""}`}
Expand Down Expand Up @@ -798,7 +862,11 @@ export function Timeline() {

<Tooltip>
<TooltipTrigger asChild>
<Button variant="text" size="icon" onClick={handleSplitAndKeepLeft}>
<Button
variant="text"
size="icon"
onClick={handleSplitAndKeepLeft}
>
<ArrowLeftToLine className="h-4 w-4" />
</Button>
</TooltipTrigger>
Expand All @@ -807,7 +875,11 @@ export function Timeline() {

<Tooltip>
<TooltipTrigger asChild>
<Button variant="text" size="icon" onClick={handleSplitAndKeepRight}>
<Button
variant="text"
size="icon"
onClick={handleSplitAndKeepRight}
>
<ArrowRightToLine className="h-4 w-4" />
</Button>
</TooltipTrigger>
Expand Down Expand Up @@ -1124,7 +1196,10 @@ export function Timeline() {
{contextMenu && (
<div
className="fixed z-50 min-w-[160px] bg-popover border border-border rounded-md shadow-md py-1 text-sm"
style={{ left: contextMenu.x, top: contextMenu.y }}
style={{
left: getContextMenuPosition(contextMenu.x, contextMenu.y).x,
top: getContextMenuPosition(contextMenu.x, contextMenu.y).y,
}}
onContextMenu={(e) => e.preventDefault()}
>
{contextMenu.type === "track" ? (
Expand Down