From 247ab84e34bb8f47632209d24deecd2751f67665 Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sat, 20 Jun 2026 11:12:05 +0300 Subject: [PATCH] opencut_ui: inline clip reorder controls on the timeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reordering was Edit-menu-only (Move Clip Left/Right). Added ◀ ▶ buttons to each clip's head bar (next to Remove) that call the same moveTimelineClip, disabled at the ends. Works for both generative and video clips. (Add was already on the timeline toolbar via + Add → Clip.) Co-Authored-By: Claude Opus 4.8 --- movie_editor/frontend/styles.css | 3 +++ movie_editor/frontend/timeline.js | 25 +++++++++++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/movie_editor/frontend/styles.css b/movie_editor/frontend/styles.css index 0343fe8d..c2b0d930 100644 --- a/movie_editor/frontend/styles.css +++ b/movie_editor/frontend/styles.css @@ -938,6 +938,9 @@ body.tl-spacing { cursor: grabbing; user-select: none; } .clip-src { font-size: 12px; color: var(--faint); flex-shrink: 0; } .clip-dur { font-family: var(--font-mono); font-size: 10px; color: var(--faint); white-space: nowrap; } .clip-rm.btn.tiny { padding: 1px 6px; font-size: 10px; line-height: 1.25; flex-shrink: 0; } +.clip-move.btn.tiny { padding: 1px 5px; font-size: 9px; line-height: 1.25; flex-shrink: 0; color: var(--muted); } +.clip-move.btn.tiny:hover { color: var(--amber-2); } +.clip-move.disabled { opacity: .3; cursor: default; pointer-events: none; } .clip-text { font-size: 12px; line-height: 1.35; color: var(--text); overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } .clip-text.empty { color: var(--faint); font-style: italic; } diff --git a/movie_editor/frontend/timeline.js b/movie_editor/frontend/timeline.js index e61c7c31..698135fe 100644 --- a/movie_editor/frontend/timeline.js +++ b/movie_editor/frontend/timeline.js @@ -47,9 +47,21 @@ head.append(el("span", "clip-src", SRC_ICON[srcType] || "▦")); } - function appendClipHeadBar(head, durSec, fps, onRemove, removeTitle) { + // `reorder` (optional): { canLeft, canRight, onLeft, onRight } adds inline move + // controls so clips can be reordered directly on the timeline (not just the Edit menu). + function appendClipHeadBar(head, durSec, fps, onRemove, removeTitle, reorder) { const bar = el("div", "clip-head-bar"); bar.append(el("span", "clip-dur", timecode(durSec, fps))); + if (reorder) { + const mk = (glyph, can, fn, title) => { + const b = el("button", "clip-move btn ghost tiny" + (can ? "" : " disabled"), glyph); + b.type = "button"; b.title = title; b.disabled = !can; + if (can) b.onclick = (e) => { e.stopPropagation(); fn(); }; + return b; + }; + bar.append(mk("◀", reorder.canLeft, reorder.onLeft, "Move clip left")); + bar.append(mk("▶", reorder.canRight, reorder.onRight, "Move clip right")); + } const rm = el("button", "clip-rm btn ghost tiny danger", "Remove"); rm.type = "button"; rm.title = removeTitle || "Remove clip (Delete / Backspace)"; @@ -530,7 +542,7 @@ } // ── clip ─────────────────────────────────────────────────────────────────────── - function clipEl(st, p, scene, index, leftPx, widthPx) { + function clipEl(st, p, scene, index, total, leftPx, widthPx) { const unitCuts = (p.scenes || []).filter((s) => (s.gen_unit_id || s.id) === (scene.gen_unit_id || scene.id)).length; const subclip = (scene.cut_offset_frames || 0) > 0; const src = sceneSourceForClip(scene, p); @@ -645,7 +657,12 @@ const head = el("div", "clip-head"); head.append(el("span", "clip-no", S.isVideoClip(scene) ? "V" : p2(index + 1))); appendSrcBadge(head, srcType); - appendClipHeadBar(head, sDur(scene, p), sFps(scene, p), () => S.removeScene(scene.id)); + appendClipHeadBar(head, sDur(scene, p), sFps(scene, p), () => S.removeScene(scene.id), null, { + canLeft: index > 0, + canRight: total != null && index < total - 1, + onLeft: () => S.moveTimelineClip(scene.id, -1), + onRight: () => S.moveTimelineClip(scene.id, 1), + }); clip.append(head); const root = unitCuts > 1 @@ -1897,7 +1914,7 @@ lay.forEach(({ seg, o, d }) => { if (seg.kind === "gap") return; if (seg.kind === "ghost") track.append(ghostClipEl(st, p, seg.ghost, o * pxPerSec, d * pxPerSec)); - else track.append(clipEl(st, p, seg.scene, scenes.indexOf(seg.scene), o * pxPerSec, d * pxPerSec)); + else track.append(clipEl(st, p, seg.scene, scenes.indexOf(seg.scene), scenes.length, o * pxPerSec, d * pxPerSec)); }); for (let i = 0; i < scenes.length - 1; i++) { const prevSeg = segs.find((s) => s.kind === "scene" && s.scene.id === scenes[i].id);