Simplify timeline clip controls to timestamp plus Remove only.

Drop per-clip split and reorder buttons; split stays on toolbar and S key. Layout keeps duration left of Remove with no overlap.
This commit is contained in:
digital-garbage
2026-06-11 16:26:57 +03:00
parent 4001370453
commit 5b1479d794
2 changed files with 27 additions and 35 deletions
+8 -8
View File
@@ -687,13 +687,15 @@ body.tl-reordering { cursor: grabbing; user-select: none; }
.clip-src.carry-only { font-size: 12px; color: var(--teal-soft); opacity: .85; }
.pm-segment-badge.mixed-mode { color: #c8fff4; border-color: color-mix(in srgb, var(--teal-soft) 45%, var(--line)); }
/* a clip that has a rendered video available (in temp) */
.clip.rendered::after { content: "●"; position: absolute; top: 4px; right: 6px; font-size: 7px;
color: #57c98e; text-shadow: 0 0 5px rgba(87,201,142,.7); pointer-events: none; z-index: 4; }
.clip.rendered::after { content: "●"; position: absolute; bottom: 5px; right: 10px; top: auto; font-size: 7px;
color: #57c98e; text-shadow: 0 0 5px rgba(87,201,142,.7); pointer-events: none; z-index: 1; }
.clip.trimming { border-color: var(--amber); z-index: 5; }
.clip-head { display: flex; align-items: center; gap: 6px; }
.clip-no { font-family: var(--font-mono); font-size: 11px; color: var(--amber); font-weight: 600; }
.clip-src { font-size: 12px; color: var(--faint); }
.clip-dur { margin-left: auto; font-family: var(--font-mono); font-size: 10px; color: var(--faint); }
.clip-head { display: flex; align-items: center; gap: 6px; min-width: 0; flex-shrink: 0; position: relative; z-index: 2; }
.clip-head-bar { margin-left: auto; display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.clip-no { font-family: var(--font-mono); font-size: 11px; color: var(--amber); font-weight: 600; flex-shrink: 0; }
.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-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; }
@@ -722,8 +724,6 @@ body.tl-reordering { cursor: grabbing; user-select: none; }
.clip.stale-render.rendered::after { color: var(--amber-2); text-shadow: 0 0 5px rgba(224, 162, 58, .7); }
.clip-gen-anchor .clip-gen-prompt-label { color: var(--teal-soft); }
.clip-gen-anchor .clip-gen-prompt-text { color: #c8fff4; }
.clip-actions { position: absolute; top: 5px; right: 8px; display: flex; gap: 2px; opacity: 0; transition: opacity .12s; }
.clip:hover .clip-actions { opacity: 1; }
.ic-btn { width: 18px; height: 18px; padding: 0; font-size: 11px; line-height: 1; border: 1px solid var(--line);
background: var(--ink-1); color: var(--muted); border-radius: 4px; cursor: pointer; }
.ic-btn:hover { color: var(--text); border-color: var(--muted); }
+19 -27
View File
@@ -46,6 +46,18 @@
}
head.append(el("span", "clip-src", SRC_ICON[srcType] || "▦"));
}
function appendClipHeadBar(head, durSec, fps, onRemove, removeTitle) {
const bar = el("div", "clip-head-bar");
bar.append(el("span", "clip-dur", timecode(durSec, fps)));
const rm = el("button", "clip-rm btn ghost tiny danger", "Remove");
rm.type = "button";
rm.title = removeTitle || "Remove clip (Delete / Backspace)";
rm.onclick = (e) => { e.stopPropagation(); onRemove(); };
bar.append(rm);
head.append(bar);
}
const ZOOM_KEY = "funpack_tl_zoom";
const GUTTER_W = 56; // sticky lane labels (Video / Audio) — shared time origin in tl-content
const AUDIO_LANE_H = 44;
@@ -368,16 +380,11 @@
head.append(el("span", "clip-src", "👻"));
const fps = (ghost.fps_mode !== "project" && ghost.fps != null) ? ghost.fps : p.frame_rate;
const frames = (ghost.frames_mode !== "project" && ghost.frames != null) ? ghost.frames : p.num_frames_per_scene;
head.append(el("span", "clip-dur", timecode((frames || 1) / (fps || 25), fps || 25)));
appendClipHeadBar(head, (frames || 1) / (fps || 25), fps || 25,
() => S.dismissGhost(ghost.id), "Remove ghost from preview");
clip.append(head);
const label = ghost.pendingGen ? "removed · generating…" : (ghost.text || "removed scene");
clip.append(el("div", "clip-text ghost-label", label));
const actions = el("div", "clip-actions");
const dismiss = el("button", "ic-btn", "✕");
dismiss.title = "Dismiss ghost from preview";
dismiss.onclick = (e) => { e.stopPropagation(); S.dismissGhost(ghost.id); };
actions.append(dismiss);
clip.append(actions);
return clip;
}
@@ -401,7 +408,7 @@
// drag the clip body left/right to reorder it on the timeline (a small threshold keeps
// plain clicks = select; trim handle / action buttons opt out).
clip.addEventListener("mousedown", (e) => {
if (e.button !== 0 || e.target.closest(".clip-actions, .clip-trim, .clip-vt-tail, .seam-cut, button")) return;
if (e.button !== 0 || e.target.closest(".clip-head-bar, .clip-rm, .clip-trim, .clip-vt-tail, .seam-cut, button")) return;
const startX = e.clientX;
let dragging = false, drop = null;
const track = clip.parentNode;
@@ -476,7 +483,7 @@
const head = el("div", "clip-head");
head.append(el("span", "clip-no", S.isVideoClip(scene) ? "V" : p2(index + 1)));
appendSrcBadge(head, srcType);
head.append(el("span", "clip-dur", timecode(sDur(scene, p), sFps(scene, p))));
appendClipHeadBar(head, sDur(scene, p), sFps(scene, p), () => S.removeScene(scene.id));
clip.append(head);
const root = unitCuts > 1
@@ -539,21 +546,6 @@
}
}
const actions = el("div", "clip-actions");
const mk = (label, title, cls, fn) => { const b = el("button", "ic-btn" + (cls ? " " + cls : ""), label); b.title = title; b.onclick = (e) => { e.stopPropagation(); fn(); }; return b; };
actions.append(mk("⟈", "Split clip at playhead", "", () => {
const offsetSec = leftPx / pxPerSec, durSec = sDur(scene, p), fps = sFps(scene, p);
const ph = window.Player?.getPlayhead() ?? 0;
let at = null;
if (ph > offsetSec + 0.05 && ph < offsetSec + durSec - 0.05)
at = Math.round((ph - offsetSec) * fps);
S.splitScene(scene.id, at);
}));
actions.append(mk("‹", "Move left", "", () => S.moveScene(scene.id, -1)));
actions.append(mk("›", "Move right", "", () => S.moveScene(scene.id, 1)));
actions.append(mk("✕", "Delete clip", "danger", () => S.removeScene(scene.id)));
clip.append(actions);
// right-edge trim → new duration → frames recomputed (duration × fps).
const locked = scene.frames_mode === "custom";
const leftHandle = el("div", "clip-trim clip-trim-left" + (locked ? " locked" : ""));
@@ -1055,11 +1047,11 @@
// Clip actions on the selected clip (also bound to S / Delete).
const selIds = selectedIds(st);
const hasSel = selIds.length > 0;
const split = el("button", "btn ghost tiny", "⧅ Split");
const split = el("button", "btn ghost tiny", "Split");
split.dataset.needsSel = "1";
split.title = "Split the selected clip at the playhead (S)"; split.disabled = !hasSel;
split.onclick = () => splitSelectedAtPlayhead();
const del = el("button", "btn ghost tiny danger", "✕ Remove");
const del = el("button", "btn ghost tiny danger", "Remove");
del.dataset.needsSel = "1";
del.title = "Remove selected clip(s) (Delete / Backspace)"; del.disabled = !hasSel;
del.onclick = () => S.removeSelectedScenes();
@@ -1194,7 +1186,7 @@
track.addEventListener("click", (e) => {
const clip = e.target.closest(".clip:not(.ghost)[data-scene-id]");
if (!clip) return;
if (e.target.closest(".clip-actions, .clip-trim, .clip-vt-tail, .seam-cut, button")) return;
if (e.target.closest(".clip-head-bar, .clip-rm, .clip-trim, .clip-vt-tail, .seam-cut, button")) return;
onClipSelect(e, clip.dataset.sceneId);
});
lay.forEach(({ seg, o, d }) => {