diff --git a/services/web-ui/public/js/timeline.js b/services/web-ui/public/js/timeline.js index f050674..03f4c6d 100644 --- a/services/web-ui/public/js/timeline.js +++ b/services/web-ui/public/js/timeline.js @@ -28,8 +28,11 @@ playheadFrames: 0, activeTool: 'select', // 'select' | 'razor' | 'hand' selectedId: null, + contextMenu: null, onClipsChanged: null, // callback(clips[]) onPlayheadMoved: null, // callback(frames) + onClipContextMenu: null, // callback({clip, x, y}) + onExternalDrop: null, // callback({track, timelineFrames, dataTransfer}) }; let _uid = 0; @@ -46,6 +49,8 @@ s.scale = options.scale || 100; s.onClipsChanged = options.onClipsChanged || null; s.onPlayheadMoved = options.onPlayheadMoved || null; + s.onClipContextMenu = options.onClipContextMenu || null; + s.onExternalDrop = options.onExternalDrop || null; container.innerHTML = ''; container.style.cssText = [ @@ -96,6 +101,8 @@ area.dataset.trackId = t.id; area.style.cssText = 'flex:1;position:relative;overflow:visible;'; area.addEventListener('click', _onAreaClick); + area.addEventListener('dragover', _onAreaDragOver); + area.addEventListener('drop', _onAreaDrop); row.appendChild(area); s.tracksEl.appendChild(row); @@ -241,7 +248,7 @@ // Clip label var lbl = document.createElement('span'); - lbl.textContent = clip.display_name || 'Clip'; + lbl.textContent = clip.display_name || clip.name || 'Clip'; lbl.style.cssText = [ 'font-size:9px', 'font-weight:500', 'color:var(--text-secondary)', @@ -250,6 +257,14 @@ ].join(';'); el.appendChild(lbl); + el.addEventListener('contextmenu', function (e) { + e.preventDefault(); + e.stopPropagation(); + s.selectedId = clip._id; + _renderClips(); + if (s.onClipContextMenu) s.onClipContextMenu({ clip: Object.assign({}, clip), x: e.clientX, y: e.clientY }); + }); + if (s.activeTool === 'select') { // Trim handles (shown on hover) var lh = _makeHandle('left'); @@ -268,6 +283,7 @@ // Drag to move (body only) el.addEventListener('mousedown', function (e) { + if (e.button !== 0) return; if (e.target === lh || e.target === rh) return; e.preventDefault(); s.selectedId = clip._id; @@ -279,10 +295,12 @@ }); lh.addEventListener('mousedown', function (e) { + if (e.button !== 0) return; e.stopPropagation(); _onTrimStart(e, clip, 'left'); }); rh.addEventListener('mousedown', function (e) { + if (e.button !== 0) return; e.stopPropagation(); _onTrimStart(e, clip, 'right'); }); @@ -411,7 +429,7 @@ if (s.onClipsChanged) s.onClipsChanged(s.clips.slice()); } - // ── Area click (deselect) ─────────────────────────────────────────────────── + // ── Area click/drop ───────────────────────────────────────────────────────── function _onAreaClick(e) { if (e.target !== e.currentTarget) return; // hit a clip, not empty space @@ -421,6 +439,23 @@ } } + function _onAreaDragOver(e) { + if (!s.onExternalDrop) return; + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; + } + + function _onAreaDrop(e) { + if (!s.onExternalDrop) return; + e.preventDefault(); + var area = e.currentTarget; + var rect = area.getBoundingClientRect(); + var x = e.clientX - rect.left + s.container.scrollLeft; + var track = Number(area.dataset.trackId); + var timelineFrames = Math.max(0, pxToFrames(x)); + s.onExternalDrop({ track: track, timelineFrames: timelineFrames, dataTransfer: e.dataTransfer }); + } + // ── Keyboard ──────────────────────────────────────────────────────────────── function _onKeyDown(e) { @@ -515,6 +550,7 @@ s.tracksEl.addEventListener('mousedown', function (e) { if (s.activeTool !== 'hand') return; + if (e.button !== 0) return; dragging = true; startX = e.clientX; startScroll = s.container.scrollLeft; @@ -537,7 +573,7 @@ // ── Add clip at playhead ───────────────────────────────────────────────────── - function addClip(asset, srcIn, srcOut, track) { + function addClip(asset, srcIn, srcOut, track, timelineInFrames) { track = track !== undefined ? track : 0; srcIn = srcIn || 0; srcOut = srcOut || (asset.duration_ms ? asset.duration_ms / 1000 : 10); @@ -546,13 +582,13 @@ // regardless of sequence frame rate (not hardcoded to 59.94). var srcInFr = Math.round(srcIn * s.fps); var srcOutFr = Math.round(srcOut * s.fps); - var tlInFr = s.playheadFrames; + var tlInFr = timelineInFrames !== undefined ? Math.max(0, Math.round(timelineInFrames)) : s.playheadFrames; var tlOutFr = tlInFr + (srcOutFr - srcInFr); var clip = { _id: uid(), asset_id: asset.id || asset.asset_id, - display_name: asset.display_name || asset.filename || 'Clip', + display_name: asset.display_name || asset.filename || asset.name || 'Clip', duration_ms: asset.duration_ms || null, streamUrl: asset.streamUrl || null, track: track,