feat: add clip management with I/O marking, timeline markers, and clip list

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-21 11:05:45 -04:00
parent 36701feb9e
commit c13755e5d1
6 changed files with 513 additions and 6 deletions

View File

@@ -1,10 +1,18 @@
<script lang="ts">
import { session } from '$lib/stores/videoSession.svelte';
import {
clipStore,
selectClip,
updateClip,
} from '$lib/stores/clips.svelte';
import { drawTimeline, type TimelineState } from '$lib/timeline/renderer';
import {
computeZoom,
handleClick as computeClickTime,
} from '$lib/timeline/interactions';
import { hitTestClip } from '$lib/timeline/clipRenderer';
type ClipDragTarget = { clipId: string; edge: 'start' | 'end' };
let canvas = $state<HTMLCanvasElement | null>(null);
let containerEl = $state<HTMLDivElement | null>(null);
@@ -19,6 +27,7 @@
});
let isDragging = $state(false);
let dragTarget = $state<ClipDragTarget | null>(null);
$effect(() => {
if (session.duration > 0) {
@@ -53,7 +62,15 @@
if (!ctx || !canvas) return;
ctx.save();
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
drawTimeline(ctx, timelineState, session.currentTime, session.duration);
drawTimeline(
ctx,
timelineState,
session.currentTime,
session.duration,
clipStore.clips,
clipStore.selectedClipId,
clipStore.pendingInPoint
);
ctx.restore();
animFrameId = requestAnimationFrame(draw);
}
@@ -68,6 +85,12 @@
return e.clientX - rect.left;
}
function getCanvasY(e: MouseEvent): number {
if (!canvas) return 0;
const rect = canvas.getBoundingClientRect();
return e.clientY - rect.top;
}
function seekTo(time: number) {
session.currentTime = time;
const videoEl = document.querySelector('video');
@@ -77,8 +100,22 @@
}
function handleMouseDown(e: MouseEvent) {
isDragging = true;
const x = getCanvasX(e);
const y = getCanvasY(e);
const hit = hitTestClip(x, y, timelineState, clipStore.clips);
if (hit) {
if (hit.edge === 'body') {
selectClip(hit.clipId);
} else if (hit.edge === 'start' || hit.edge === 'end') {
dragTarget = { clipId: hit.clipId, edge: hit.edge };
selectClip(hit.clipId);
}
isDragging = true;
return;
}
isDragging = true;
const targetTime = computeClickTime(x, timelineState, session.duration);
seekTo(targetTime);
}
@@ -86,12 +123,31 @@
function handleMouseMove(e: MouseEvent) {
if (!isDragging) return;
const x = getCanvasX(e);
if (dragTarget) {
const time = computeClickTime(x, timelineState, session.duration);
switch (dragTarget.edge) {
case 'start':
updateClip(dragTarget.clipId, { startTime: time });
break;
case 'end':
updateClip(dragTarget.clipId, { endTime: time });
break;
default: {
const _exhaustive: never = dragTarget.edge;
void _exhaustive;
}
}
return;
}
const targetTime = computeClickTime(x, timelineState, session.duration);
seekTo(targetTime);
}
function handleMouseUp() {
isDragging = false;
dragTarget = null;
}
function handleWheel(e: WheelEvent) {