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:
@@ -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); }
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user