Timeline: Rate scene, Upscale and Interpolate in the header after Takes; From bin, Effect and Overlay folded into one + Add menu
This commit is contained in:
+1
-1
@@ -43,7 +43,7 @@ const link = linkPipeline({ project, pipeline, onOpen: (fn) => on((what) => { if
|
||||
const frame = build(document.getElementById("app"));
|
||||
const selection = createSelection({ project });
|
||||
selection.on(() => say("select"));
|
||||
const app = { editorSettings: [], timelineLanes: [], timelineView: {}, monitor: {}, mediaMenu: [], mediaPeek: {}, sceneSections: [], clipTags: [], has: new Set(), actions: [], inputHooks: [], beforeRun: [], promptPrefix: [], lastRun: { typed: undefined, n: 0 }, runner: {}, project, pipeline, pipelineOwned: link.owns, pipelineWhy: link.why, generate, api, on, say, title: frame.setZoneTitle, selection, playhead: createPlayhead(), theme: window.ComposerTheme, dock: createDock(), maintenance: createMaintenance({ api, flush: async () => { await project.flush(); if (project.unsaved) throw new Error("Your latest edits could not be saved, so nothing was changed. Try again in a moment."); } }), keys: createKeys(), dom: createDom(), peek: createPeek(), resetLayout: () => { frame.resetLayout(); app.dock.reset(); } };
|
||||
const app = { editorSettings: [], timelineLanes: [], timelineView: {}, monitor: {}, mediaMenu: [], addMenu: [], mediaPeek: {}, sceneSections: [], clipTags: [], has: new Set(), actions: [], inputHooks: [], beforeRun: [], promptPrefix: [], lastRun: { typed: undefined, n: 0 }, runner: {}, project, pipeline, pipelineOwned: link.owns, pipelineWhy: link.why, generate, api, on, say, title: frame.setZoneTitle, selection, playhead: createPlayhead(), theme: window.ComposerTheme, dock: createDock(), maintenance: createMaintenance({ api, flush: async () => { await project.flush(); if (project.unsaved) throw new Error("Your latest edits could not be saved, so nothing was changed. Try again in a moment."); } }), keys: createKeys(), dom: createDom(), peek: createPeek(), resetLayout: () => { frame.resetLayout(); app.dock.reset(); } };
|
||||
// Which backend modules the server has (loaded, even if this model cannot use them). Unknown (server down) = assume all, so nothing is hidden by a failed fetch.
|
||||
const installed = await Promise.race([api.modules(), new Promise((_, no) => setTimeout(() => no(new Error("slow")), 4000))]) // a stalled server must not leave the page blank
|
||||
.then((m) => (Array.isArray(m.modules) ? new Set([...m.modules, ...(m.incompatible || [])].map((x) => x.id)) : null)).catch(() => null);
|
||||
|
||||
@@ -37,14 +37,11 @@ export default {
|
||||
} })] });
|
||||
}
|
||||
|
||||
const menu = c.button.menu({ label: "+ Effect", tone: "ghost", onClick: () => c.menu.dropdown({ anchor: menu, items: [
|
||||
{ id: "fx", label: "Effect…", disabled: !p.selected }, { id: "tr", label: "Transition…", disabled: !p.selected },
|
||||
{ separator: true }, { id: "off", label: "Remove effects and transition", danger: true, disabled: !p.selected || !tags(p.selected).length }],
|
||||
onPick: (id) => {
|
||||
if (id === "fx") ask("effects"); else if (id === "tr") ask("video_transitions");
|
||||
else p.edit((pr) => { const s = pr.scenes.find((x) => x.id === p.selectedId); if (!s) return false; applyEffect(s, "reset"); return clearTransition(s); });
|
||||
} }) });
|
||||
host.append(menu.node);
|
||||
const entry = { order: 20, items: () => [
|
||||
{ id: "fx", label: "Effect…", disabled: !p.selected, run: () => ask("effects") }, { id: "tr", label: "Transition…", disabled: !p.selected, run: () => ask("video_transitions") },
|
||||
{ id: "fx-off", label: "Remove effects and transition", danger: true, disabled: !p.selected || !tags(p.selected).length,
|
||||
run: () => p.edit((pr) => { const s = pr.scenes.find((x) => x.id === p.selectedId); if (!s) return false; applyEffect(s, "reset"); return clearTransition(s); }) }] };
|
||||
app.addMenu.push(entry); // listed under the timeline's "+ Add"
|
||||
|
||||
app.clipTags.push(tags);
|
||||
const section = { key: (sc) => `${sc.audio_volume}|${sc.audio_separated}`, rows: (sc) => [
|
||||
@@ -54,7 +51,7 @@ export default {
|
||||
app.sceneSections.push(section);
|
||||
return () => {
|
||||
for (const [list, x] of [[app.clipTags, tags], [app.sceneSections, section]]) { const i = list.indexOf(x); if (i >= 0) list.splice(i, 1); }
|
||||
menu.node.remove();
|
||||
const k = app.addMenu.indexOf(entry); if (k >= 0) app.addMenu.splice(k, 1);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -33,9 +33,8 @@ export default {
|
||||
if (seconds == null) c.toast.warn({ text: asset.kind === "video" ? "The file's length could not be read; the clip uses the project's length." : "The file's length could not be read: the lane cannot be trimmed, and plays to the end of the file." });
|
||||
}
|
||||
|
||||
const menu = c.button.menu({ label: "+ From bin", tone: "ghost", onClick: () => c.menu.dropdown({ anchor: menu, items: [{ id: "video", label: "Video clip…", disabled: !p.project }, { id: "audio", label: "Audio track…", disabled: !p.project }],
|
||||
onPick: (id) => choose(id) }) });
|
||||
host.append(menu.node);
|
||||
const entry = { order: 10, items: () => [{ id: "bin-video", label: "Video clip from bin…", disabled: !p.project, run: () => choose("video") }, { id: "bin-audio", label: "Audio track from bin…", disabled: !p.project, run: () => choose("audio") }] };
|
||||
app.addMenu.push(entry); // listed under the timeline's "+ Add"
|
||||
|
||||
// Dropping a bin tile on the timeline: a video makes a clip, a sound a lane, a picture a scene's anchor (on a clip) or a new scene (beside them).
|
||||
const at = (e) => (app.timelineView && app.timelineView.timeAt ? app.timelineView.timeAt(e.clientX) : app.playhead.at);
|
||||
@@ -63,6 +62,6 @@ export default {
|
||||
p.edit((pr) => addAudioTrack(pr, a, at(e), seconds));
|
||||
}),
|
||||
];
|
||||
return () => { offs.forEach((f) => f()); menu.node.remove(); };
|
||||
return () => { offs.forEach((f) => f()); const i = app.addMenu.indexOf(entry); if (i >= 0) app.addMenu.splice(i, 1); };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import { DEFAULTS, RTX, interpolate, modeLabel } from "./job.js";
|
||||
|
||||
export default {
|
||||
id: "interpolate",
|
||||
mount: "timeline.toolbar.end",
|
||||
mount: "timeline.actions",
|
||||
needs: ["project", "api", "selection"],
|
||||
modules: ["interpolate"],
|
||||
setup({ host, app }) {
|
||||
|
||||
@@ -46,18 +46,18 @@ export default {
|
||||
app.timelineLanes.push(entry);
|
||||
app.say("timeline.lanes"); // the stage draws what it has; tell it there is more
|
||||
|
||||
const add = c.button.menu({ label: "+ Overlay", tone: "ghost", disabled: !p.project, onClick: () => c.menu.dropdown({ anchor: add,
|
||||
items: [{ id: "text", label: "Text…" }, { id: "image", label: "Picture…" }, { separator: true }, { id: "lane", label: "New lane on top" },
|
||||
{ id: "unlane", label: "Remove top lane", danger: true, disabled: !ov.lanesOf(p.project).length }],
|
||||
onPick: (id) => {
|
||||
if (id === "text" || id === "image") return open(id);
|
||||
if (id === "lane") return edit((pr) => ov.addLane(pr));
|
||||
const top = ov.lanesOf(p.project).slice(-1)[0];
|
||||
if (!top) return;
|
||||
if (top && !ov.overlaysOf(p.project).some((o) => o.lane_id === top.id)) return edit((pr) => ov.removeLane(pr, top.id));
|
||||
c.modal.dialogue({ title: "Remove lane", message: `Remove “${top.label}” and everything on it?`, tone: "danger", confirmLabel: "Remove" }).result.then((yes) => yes && edit((pr) => ov.removeLane(pr, top.id)));
|
||||
} }) });
|
||||
host.append(add.node);
|
||||
const removeTop = () => {
|
||||
const top = ov.lanesOf(p.project).slice(-1)[0];
|
||||
if (!top) return;
|
||||
if (!ov.overlaysOf(p.project).some((o) => o.lane_id === top.id)) return edit((pr) => ov.removeLane(pr, top.id));
|
||||
c.modal.dialogue({ title: "Remove lane", message: `Remove “${top.label}” and everything on it?`, tone: "danger", confirmLabel: "Remove" }).result.then((yes) => yes && edit((pr) => ov.removeLane(pr, top.id)));
|
||||
};
|
||||
const addEntry = { order: 30, items: () => [
|
||||
{ id: "ov-text", label: "Text overlay…", disabled: !p.project, run: () => open("text") },
|
||||
{ id: "ov-image", label: "Picture overlay…", disabled: !p.project, run: () => open("image") },
|
||||
{ id: "ov-lane", label: "New overlay lane on top", disabled: !p.project, run: () => edit((pr) => ov.addLane(pr)) },
|
||||
{ id: "ov-unlane", label: "Remove top overlay lane", danger: true, disabled: !p.project || !ov.lanesOf(p.project).length, run: removeTop }] };
|
||||
app.addMenu.push(addEntry); // listed under the timeline's "+ Add"
|
||||
const offDrop = onMediaDrop("lane:overlay", async (item, lane, e, where) => { // a picture dropped on an overlay lane becomes an overlay there
|
||||
if (item.kind !== "image" || !p.project) return c.toast.warn({ text: "Only a picture can be laid over the cut." });
|
||||
let name = "Image"; try { name = ((await app.api.media()).media || []).find((m) => m.id === item.id).name; } catch { /* keep the generic label */ }
|
||||
@@ -65,7 +65,6 @@ export default {
|
||||
const sec = app.timelineView && app.timelineView.timeAt ? app.timelineView.timeAt(e.clientX) : app.playhead.at;
|
||||
edit((pr) => ov.addImage(pr, item.id, name, sec, laneId));
|
||||
});
|
||||
const off = app.on(() => { add.setDisabled && add.setDisabled(!p.project); });
|
||||
return () => { off(); offDrop(); const at = app.timelineLanes.indexOf(entry); if (at >= 0) app.timelineLanes.splice(at, 1); add.node.remove(); app.say("timeline.lanes"); };
|
||||
return () => { const k = app.addMenu.indexOf(addEntry); if (k >= 0) app.addMenu.splice(k, 1); offDrop(); const at = app.timelineLanes.indexOf(entry); if (at >= 0) app.timelineLanes.splice(at, 1); app.say("timeline.lanes"); };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ export function notTaught(r, ps) {
|
||||
|
||||
export default {
|
||||
id: "rating",
|
||||
mount: "timeline.toolbar.end",
|
||||
mount: "timeline.actions",
|
||||
needs: ["project", "api", "selection", "pipeline"],
|
||||
setup({ host, app }) {
|
||||
const p = app.project, sel = app.selection;
|
||||
|
||||
@@ -77,10 +77,18 @@ const tools = {
|
||||
const inside = head.at > seg.start && head.at < seg.start + seg.dur;
|
||||
if (!p.edit((pr) => edits.split(pr, seg.id, inside ? framesAt(seg.scene, pr, head.at - seg.start) : undefined, gridFor(app)))) c.toast.warn({ text: "Too close to the clip's edge to cut there." });
|
||||
};
|
||||
// One "+ Add" for everything that can go on the timeline: a scene, a clip or sound from the bin, an effect, an overlay.
|
||||
const addButton = c.button.menu({ label: "+ Add", tone: "ghost", onClick: () => {
|
||||
const groups = [...(app.addMenu || [])].sort((a, b) => a.order - b.order).map((g) => { try { return g.items(); } catch { return []; } }).filter((g) => g.length);
|
||||
const items = [{ id: "scene", label: "Scene", disabled: !p.project, run: () => { const sc = p.edit((pr) => edits.addScene(pr)); if (sc) p.select(sc.id); } },
|
||||
...groups.flatMap((g) => [{ separator: true }, ...g])];
|
||||
c.menu.dropdown({ anchor: addButton, items, onPick: (id) => { const hit = items.find((i) => i.id === id); if (hit && hit.run) hit.run(); } });
|
||||
} });
|
||||
const draw = () => {
|
||||
const open = p.project, ids = open ? sel.ids : [];
|
||||
if (addButton.setDisabled) addButton.setDisabled(!open);
|
||||
put(c.hint.default({ text: `${clock(head.at)} / ${clock(open ? totalSeconds(open) : 0)}` }).node,
|
||||
btn("+ Add", () => { const sc = p.edit((pr) => edits.addScene(pr)); if (sc) p.select(sc.id); }, !open),
|
||||
addButton.node,
|
||||
btn("Split", split, !ids.length, "Cut the selected clip at the playhead (in the middle when the playhead is elsewhere)"),
|
||||
btn("Remove", () => ids.forEach((id) => p.edit((pr) => edits.removeScene(pr, id))), !ids.length),
|
||||
c.chip.neutral({ label: ids.length ? `${ids.length} clip${ids.length > 1 ? "s" : ""} selected` : "no clip selected" }).node);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { RTX, RTX_DEFAULTS, keepsSize, keyOf, qualityLabel, swap, upscale } from
|
||||
|
||||
export default {
|
||||
id: "upscale",
|
||||
mount: "timeline.toolbar.end",
|
||||
mount: "timeline.actions",
|
||||
needs: ["project", "api", "selection"],
|
||||
modules: ["render"],
|
||||
setup({ host, app }) {
|
||||
|
||||
+1
-1
@@ -14,8 +14,8 @@ export default [
|
||||
"./features/menus/index.js",
|
||||
"./features/render/index.js",
|
||||
"./features/render/selected.js",
|
||||
"./features/rating/index.js",
|
||||
"./features/takes/index.js",
|
||||
"./features/rating/index.js",
|
||||
"./features/subjects/index.js",
|
||||
"./features/upscale/index.js",
|
||||
"./features/interpolate/index.js",
|
||||
|
||||
Reference in New Issue
Block a user