feat(ui): picker wheel (middle mouse) over the actions features offer
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
14210f8614
commit
3503081cbc
+2
-1
@@ -29,8 +29,9 @@ linkPipeline({ project, pipeline, onOpen: (fn) => on((what) => { if (what === "o
|
||||
const frame = build(document.getElementById("app"));
|
||||
const selection = createSelection({ project });
|
||||
selection.on(() => say("select"));
|
||||
const app = { editorSettings: [], inputHooks: [], lastRun: { typed: undefined, n: 0 }, project, pipeline, generate, api, on, say, title: frame.setZoneTitle, selection, playhead: createPlayhead(), theme: window.ComposerTheme, mode: createMode(), panel: frame.togglePanel, closePanels: frame.closePanels, 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(), resetLayout: () => { frame.resetLayout(); app.dock.reset(); } };
|
||||
const app = { editorSettings: [], actions: [], inputHooks: [], lastRun: { typed: undefined, n: 0 }, project, pipeline, generate, api, on, say, title: frame.setZoneTitle, selection, playhead: createPlayhead(), theme: window.ComposerTheme, mode: createMode(), panel: frame.togglePanel, closePanels: frame.closePanels, 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(), resetLayout: () => { frame.resetLayout(); app.dock.reset(); } };
|
||||
const settings = createSettings({ menubar: hostFor("menubar.menus") });
|
||||
app.openSettings = (id) => settings.open(id);
|
||||
const result = await loadFeatures(paths, { app });
|
||||
settings.addButton();
|
||||
app.project.start().catch((err) => console.warn("could not open a project:", err.message));
|
||||
|
||||
@@ -4,6 +4,7 @@ import { isGenerative, isVideoClip, genUnitId, unitRoot, effFrames, effFps } fro
|
||||
import { QUEUED, RUNNING } from "../../shell/run.js";
|
||||
import { inPlace } from "../../shell/place.js";
|
||||
import { buildInputs } from "./inputs.js";
|
||||
import { offer } from "../../shell/actions.js";
|
||||
|
||||
const expand = (body) => fetch("/funpack/api/prompt/expand", { method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ...body, seed: Math.floor(Math.random() * 2 ** 31) || 1 }) }).then((r) => (r.ok ? r.json() : null));
|
||||
@@ -129,6 +130,8 @@ export default {
|
||||
draw();
|
||||
const offRun = g.subscribe(draw);
|
||||
const offApp = app.on((what) => { if (what === "generate.selected" && !busy && p.selected) runUnits(unitsOf([p.selected])); else draw(); });
|
||||
return () => { offRun(); offApp(); };
|
||||
const offers = [offer(app, { id: "generate-all", label: "Generate", icon: "▶", run: () => { if (!busy && p.project) runUnits(unitsOf(p.scenes)); } }),
|
||||
offer(app, { id: "generate-selected", label: "Generate selected", icon: "▶", run: () => { if (!busy && p.selected) runUnits(unitsOf([p.selected])); } })];
|
||||
return () => { offRun(); offApp(); offers.forEach((f) => f()); };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Auto Montage: the button in the timeline header and its window.
|
||||
import { offer } from "../../shell/actions.js";
|
||||
import { composer as c } from "../../composer/composer.js";
|
||||
import { playable, build } from "./build.js";
|
||||
|
||||
@@ -41,6 +42,8 @@ export default {
|
||||
}
|
||||
const draw = () => button.setDisabled(!p.project);
|
||||
draw();
|
||||
return app.on(draw);
|
||||
const offered = offer(app, { id: "auto-montage", label: "Auto Montage", icon: "⚡", run: () => { if (!button.node.disabled) ask(); } });
|
||||
const off = app.on(draw);
|
||||
return () => { off(); offered(); };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Render: stitch the cut into one file and keep it in the media bin.
|
||||
import { offer } from "../../shell/actions.js";
|
||||
import { composer as c } from "../../composer/composer.js";
|
||||
import { call } from "../../shell/api.js";
|
||||
import { clipSpecs } from "./clips.js";
|
||||
@@ -33,6 +34,8 @@ export default {
|
||||
node = next;
|
||||
}
|
||||
draw();
|
||||
return app.on(() => { if (Boolean(p.project) === node.disabled) draw(); });
|
||||
const offered = offer(app, { id: "render-final", label: "Render", icon: "⧉", run: () => { if (!busy && p.project) render(); } });
|
||||
const off = app.on(() => { if (Boolean(p.project) === node.disabled) draw(); });
|
||||
return () => { off(); offered(); };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// "⏱ Sampler": steps, scheduler and sampler one click away, in a thin window over the timeline. Same settings as Engine ▸ Sampling,
|
||||
// same values tree, so the two cannot disagree. Absent when no installed module has a sampling setting.
|
||||
import { offer } from "../../shell/actions.js";
|
||||
import { composer as c } from "../../composer/composer.js";
|
||||
import { settingRow, whenSatisfied } from "../settings/field.js";
|
||||
|
||||
@@ -34,6 +35,7 @@ export default {
|
||||
draw();
|
||||
pop = c.popover.anchored({ anchor: button, body, side: "top", align: "end", onClose: () => { pop = null; } });
|
||||
}
|
||||
return () => { off(); offShow(); if (pop) pop.close(); button.node.remove(); };
|
||||
const offered = offer(app, { id: "sampler", label: "Sampler", icon: "⏱", run: () => { if (!button.node.hidden) (pop ? pop.close() : open()); } });
|
||||
return () => { off(); offShow(); offered(); if (pop) pop.close(); button.node.remove(); };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// The picker wheel: press the middle mouse button anywhere and every offered action fans out under the pointer; release on one to run it.
|
||||
import { composer as c } from "../../composer/composer.js";
|
||||
import { offer } from "../../shell/actions.js";
|
||||
|
||||
export default {
|
||||
id: "wheel",
|
||||
mount: "menubar.menus",
|
||||
needs: ["project"],
|
||||
setup({ app }) {
|
||||
let wheel = null;
|
||||
const offs = [
|
||||
offer(app, { id: "undo", label: "Undo", icon: "↶", run: () => app.project.undo() }),
|
||||
offer(app, { id: "redo", label: "Redo", icon: "↷", run: () => app.project.redo() }),
|
||||
offer(app, { id: "models", label: "Models & Pipeline", icon: "▤", run: () => app.openSettings && app.openSettings("models") }),
|
||||
offer(app, { id: "engine", label: "Engine settings", icon: "⚙", run: () => app.openSettings && app.openSettings("engine") }),
|
||||
];
|
||||
const onDown = (e) => {
|
||||
if (e.button !== 1 || wheel || document.querySelector('[role="dialog"]')) return; // nothing behind a dialog
|
||||
e.preventDefault();
|
||||
const items = (app.actions || []).map((a) => ({ id: a.id, label: a.label, icon: a.icon }));
|
||||
wheel = c.wheel.picker({ items, x: e.clientX, y: e.clientY, onClose: () => { wheel = null; },
|
||||
onPick: (id) => { const a = (app.actions || []).find((x) => x.id === id); if (a) Promise.resolve().then(() => a.run()).catch((err) => c.toast.warn({ text: err.message })); } });
|
||||
};
|
||||
document.addEventListener("mousedown", onDown, true);
|
||||
return () => { document.removeEventListener("mousedown", onDown, true); if (wheel) wheel.close && wheel.close(); offs.forEach((f) => f()); };
|
||||
},
|
||||
};
|
||||
@@ -21,6 +21,7 @@ export default [
|
||||
"./features/ideas/index.js",
|
||||
"./features/wizard/index.js",
|
||||
"./features/tour/index.js",
|
||||
"./features/wheel/index.js",
|
||||
"./features/status/index.js",
|
||||
"./features/audio/index.js",
|
||||
"./features/montage/index.js",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// Things the app can be asked to do, by name: whoever builds a control offers it here, whoever shows a list (the wheel) reads what is there.
|
||||
// A feature that failed to load offers nothing and is simply not among them.
|
||||
export function offer(app, action) {
|
||||
const list = app.actions || (app.actions = []);
|
||||
list.push(action);
|
||||
return () => { const i = list.indexOf(action); if (i >= 0) list.splice(i, 1); };
|
||||
}
|
||||
Reference in New Issue
Block a user