Files
digital-garbageandClaude Sonnet 5.5 30d1aa14a9 MiniMax H3-Image saves a PNG picture; Length and FPS are not offered and are set to 1
A pipeline with no length or rate input hides those controls in the Project and Scene panels. A preset can carry project numbers it goes with.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-11 03:27:51 +02:00

58 lines
3.9 KiB
JavaScript

// Project Setup Wizard: look, a name and a model's starting point, then the tour offer. Opens from File ▸ Project Setup Wizard.
import { composer as c } from "../../composer/composer.js";
export default {
id: "wizard",
mount: "menubar.menus",
needs: ["project", "api", "pipeline", "theme"],
setup({ app }) {
let win = null;
const ctx = { name: "Untitled montage", preset: "", tour: true };
let presets = [], step = 0;
const STEPS = [
{ title: "Choose your look", sub: "You can change this any time in Settings ▸ Appearance.", body: () => c.segmented.md({ label: "Colour scheme", value: app.theme.get(), onChange: (v) => app.theme.apply(v),
options: [{ value: "dark", label: "Dark" }, { value: "light", label: "Light" }, { value: "auto", label: "Auto" }] }) },
{ title: "Name it, and pick a model", sub: "The model decides which nodes and which files this project needs.", body: () => c.region.stack({ gap: "sm", children: [
c.field.default({ label: "Project name", control: c.input.md({ label: "Project name", value: ctx.name, onInput: (v) => { ctx.name = v; } }) }),
c.field.default({ label: "Start from", hint: "A model's own pipeline, ready to fill with files. “Keep the current pipeline” leaves Models & Pipeline as it is.",
control: c.select.md({ label: "Start from", value: ctx.preset, onChange: (v) => { ctx.preset = v; },
options: [{ value: "", label: "Keep the current pipeline" }, ...presets.map((p) => ({ value: p.id, label: p.title }))] }) })] }) },
{ title: "Want the guided tour?", sub: "A walk through the screen, one part at a time. It changes nothing; Help ▸ Start tour runs it again.", body: () => c.toggle.default({ label: "Show me around when I finish", checked: ctx.tour, onChange: (v) => { ctx.tour = v; } }) },
];
let busy = false;
async function finish() {
if (busy) return;
busy = true;
const pick = presets.find((p) => p.id === ctx.preset);
try {
await app.project.newProject((ctx.name || "").trim() || "Untitled montage");
if (pick) { const r = await app.pipeline.restore({ slots: pick.slots, removed: [], unwired: {} }); if (r.refused.length) c.toast.warn({ text: `The model's pipeline was not loaded: ${r.refused[0]}` }); else { Object.entries(pick.prefs || {}).forEach(([k, v]) => app.project.setPref(k, v)); Object.entries(pick.project || {}).forEach(([k, v]) => app.project.setField(k, v)); } }
} catch (err) { busy = false; return c.toast.warn({ text: `Could not make the project: ${err.message}` }); }
busy = false;
close();
if (ctx.tour) setTimeout(() => app.say("tour.start"), 300);
}
const close = () => { if (win) { const w = win; win = null; w.close("done"); } };
function draw() {
const s = STEPS[step], last = step === STEPS.length - 1;
body(s);
win.setFooter({ actions: [step ? c.button.sm({ label: "Back", tone: "ghost", onClick: () => { step -= 1; draw(); } }) : null,
c.button.sm({ label: last ? "Create project" : "Continue", tone: "primary", onClick: () => (last ? finish() : (step += 1, draw())) })].filter(Boolean) });
}
const slot = c.region.stack({ gap: "md" });
const body = (s) => (slot.set([c.label.section({ text: `${step + 1} / ${STEPS.length} · ${s.title}` }), c.hint.default({ text: s.sub }), s.body()]), slot);
async function open() {
if (win) return;
try { presets = (await app.api.pipelinePresets()).presets || []; } catch { presets = []; }
step = 0; ctx.name = "Untitled montage"; ctx.preset = ""; ctx.tour = true;
win = c.modal.generic({ title: "Project Setup", size: "md", closeOnOutside: false, body: body(STEPS[0]), onClose: () => { win = null; } });
draw();
}
app.has.add("wizard");
const off = app.on((what) => { if (what === "wizard.open") open(); });
return () => { off(); app.has.delete("wizard"); };
},
};