fix(ui): maintenance refuses when edits are unsaved and reports a blocked or absent restart; Models/Engine keep typing, show save errors, multiline text; export keeps its project name and picked clips
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
d5efcc8b46
commit
175c1a7643
+1
-1
@@ -28,7 +28,7 @@ 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 = { 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: () => project.flush() }), keys: createKeys(), resetLayout: () => { frame.resetLayout(); app.dock.reset(); } };
|
||||
const app = { 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") });
|
||||
const result = await loadFeatures(paths, { app });
|
||||
settings.addButton();
|
||||
|
||||
@@ -7,7 +7,7 @@ export function clipSpecs(p, only) { // `only`: a Set of scene ids to keep (th
|
||||
const clips = [];
|
||||
let missing = 0;
|
||||
for (const seg of segments(p)) {
|
||||
if (seg.kind !== "scene" || (seg.scene.excluded && !seg.scene.removed_from_plan)) continue; // removed from the plan still plays
|
||||
if (seg.kind !== "scene" || (!only && seg.scene.excluded && !seg.scene.removed_from_plan)) continue; // removed from the plan still plays; a clip picked by hand is wanted even if left out of the full run
|
||||
if (only && !only.has(seg.scene.id)) continue;
|
||||
const sc = seg.scene, r = renders[sc.id], base = { scene_id: sc.id, dur: seg.dur, fps: effFps(sc, p), gap_after: Math.max(0, +sc.gap_after_sec || 0) };
|
||||
if (isVideoClip(sc)) {
|
||||
|
||||
@@ -20,3 +20,7 @@ test("only the picked clips are kept, still in timeline order, and unpicked ones
|
||||
assert.deepEqual(clips.map((c) => c.scene_id), ["a", "v"]);
|
||||
assert.equal(missing, 0);
|
||||
});
|
||||
|
||||
test("a clip picked by hand is exported even when it is excluded from the full run", () => {
|
||||
assert.deepEqual(clipSpecs(p, new Set(["c"])).clips.map((c) => c.scene_id), ["c"]);
|
||||
});
|
||||
|
||||
@@ -31,15 +31,15 @@ export default {
|
||||
try { await work(); } catch (err) { tell(err.message); } finally { busy = false; draw(); }
|
||||
};
|
||||
const exportIt = run(async () => {
|
||||
const media = await join();
|
||||
const name = p.project.name, media = await join(); // named for the project it was started in
|
||||
if (!media) return;
|
||||
const link = Object.assign(document.createElement("a"), { href: viewUrl(media), download: `${p.project.name}.mp4` });
|
||||
const link = Object.assign(document.createElement("a"), { href: viewUrl(media), download: `${name}.mp4` });
|
||||
link.click();
|
||||
});
|
||||
const toBin = run(async () => {
|
||||
const media = await join();
|
||||
const name = p.project.name, media = await join();
|
||||
if (!media) return;
|
||||
const { media: entry } = await call("POST", "/api/import-clip", { clip: media, name: `${p.project.name}.mp4` });
|
||||
const { media: entry } = await call("POST", "/api/import-clip", { clip: media, name: `${name}.mp4` });
|
||||
app.say("media");
|
||||
c.toast.good({ text: `Saved to the media bin as ${entry.name}.` });
|
||||
});
|
||||
|
||||
@@ -27,6 +27,8 @@ export const engine = (app) => function mount() {
|
||||
}
|
||||
draw();
|
||||
ps.ensureLoaded().then(draw);
|
||||
const off = ps.subscribe(draw);
|
||||
const typing = () => page.node.contains(document.activeElement) && /^(input|textarea)$/i.test(document.activeElement.tagName);
|
||||
page.node.addEventListener("focusout", () => setTimeout(() => { if (!typing()) draw(); }));
|
||||
const off = ps.subscribe(() => { if (!typing()) draw(); });
|
||||
return { node: page.node, destroy: () => { off(); page.node.remove(); } };
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ const widgetControl = (w, current, set) => {
|
||||
return c.number.md({ label, value: current ?? w.default ?? 0, min: w.min, max: w.max, step: w.step ?? (w.type === "INT" ? 1 : undefined),
|
||||
precision: w.type === "INT" ? 0 : undefined, onChange: set });
|
||||
}
|
||||
return c.input.md({ label, value: current ?? "", onCommit: set });
|
||||
return w.multiline ? c.textarea.md({ label, value: current ?? "", rows: 4, onCommit: set }) : c.input.md({ label, value: current ?? "", onCommit: set });
|
||||
};
|
||||
|
||||
export const models = (app) => function mount() {
|
||||
@@ -30,7 +30,11 @@ export const models = (app) => function mount() {
|
||||
try { Object.assign(specs, (await app.api.describeNodes(missing)).nodes || {}); } catch (err) { note = err.message; }
|
||||
missing.forEach((n) => { if (!(n in specs)) specs[n] = null; });
|
||||
}
|
||||
const set = (slot, name) => async (value) => { await ps.save({ inputs: { [slot.id]: { [name]: value } } }); note = (ps.refused() || []).join(" "); draw(); };
|
||||
const set = (slot, name) => async (value) => {
|
||||
if (JSON.stringify((slot.inputs || {})[name]) === JSON.stringify(value)) return; // a blur that changed nothing saves nothing
|
||||
await ps.save({ inputs: { [slot.id]: { [name]: value } } });
|
||||
note = [...(ps.refused() || []), ...ps.saveNotes()].join(" "); draw();
|
||||
};
|
||||
const groups = [];
|
||||
slots.forEach((s) => { const g = s.group || "Other"; (groups.find((x) => x[0] === g) || (groups.push([g, []]), groups[groups.length - 1]))[1].push(s); });
|
||||
page.set([
|
||||
@@ -45,8 +49,11 @@ export const models = (app) => function mount() {
|
||||
})]),
|
||||
].filter(Boolean));
|
||||
}
|
||||
// A redraw while a box has focus would eat what is being typed: it waits for the focus to leave.
|
||||
const typing = () => page.node.contains(document.activeElement) && /^(input|textarea)$/i.test(document.activeElement.tagName);
|
||||
page.node.addEventListener("focusout", () => setTimeout(() => { if (!typing()) draw(); }));
|
||||
ps.ensureLoaded().then(draw);
|
||||
off = ps.subscribe(() => draw());
|
||||
off = ps.subscribe(() => { if (!typing()) draw(); });
|
||||
const handle = { node: page.node, destroy: () => { off && off(); page.node.remove(); } };
|
||||
return handle;
|
||||
};
|
||||
|
||||
@@ -26,8 +26,9 @@ export const modules = (app) => function mount() {
|
||||
return c.toggle.default({ label: name(m), hint: ps.isOff(m.id) ? "Off for this project" : undefined, checked: !ps.isOff(m.id),
|
||||
onChange: (on) => ps.setOff(m.id, !on).then(draw) });
|
||||
};
|
||||
page.set([c.hint.default({ text: "A module you turn off here is off for this project only, and disappears from Engine settings. One that failed while generating is off for every project until its code is repaired or you turn it back on." }),
|
||||
...CATEGORIES.flatMap(([key, label]) => { const list = mine.filter((m) => (m.category || "") === key); return list.length ? [c.label.section({ text: label }), ...list.map(row)] : []; })]);
|
||||
const notes = ps.saveNotes();
|
||||
page.set([notes.length ? c.banner.warn({ text: notes.join(" ") }) : null, c.hint.default({ text: "A module you turn off here is off for this project only, and disappears from Engine settings. One that failed while generating is off for every project until its code is repaired or you turn it back on." }),
|
||||
...CATEGORIES.flatMap(([key, label]) => { const list = mine.filter((m) => (m.category || "") === key); return list.length ? [c.label.section({ text: label }), ...list.map(row)] : []; })].filter(Boolean));
|
||||
}
|
||||
draw();
|
||||
ps.ensureLoaded().then(() => ps.refreshControl()).then(draw);
|
||||
|
||||
@@ -27,12 +27,18 @@ export function createMaintenance({ api, flush }) {
|
||||
}
|
||||
}
|
||||
async function run(message, action, said) {
|
||||
await flush();
|
||||
try { await flush(); } catch (err) { return tell(err.message); }
|
||||
const text = c.text.md({ text: message });
|
||||
const card = c.modal.generic({ title: "Working", size: "sm", closeOnOutside: false, body: c.region.stack({ gap: "md", children: [c.progress.indeterminate({ label: "Working" }), text] }) });
|
||||
card.setText = (t) => text.setText ? text.setText(t) : (text.node.textContent = t);
|
||||
try {
|
||||
const res = await action();
|
||||
if (res && res.restarting === false) { // nothing restarted: say what is true, and stay on the page
|
||||
card.close();
|
||||
return c.modal.dialogue({ title: "Not restarted", message: `${res.blocked || (said ? said(res) : "Nothing changed.")} ${res.blocked ? "Restart ComfyUI yourself when the generation has finished." : ""}`.trim(), confirmLabel: "OK", cancelLabel: "Close" }).result;
|
||||
}
|
||||
const bad = res && res.requirements && res.requirements.ran && !res.requirements.ok;
|
||||
if (bad) { await c.modal.dialogue({ title: "Dependencies failed", message: `${res.requirements.detail || ""}\n\nFunPack was updated, but installing its dependencies failed: it may not load until they are installed by hand. ComfyUI will restart now.`, confirmLabel: "Restart", cancelLabel: "Restart" }).result; }
|
||||
card.setText(`${said ? said(res) : message}\nRestarting ComfyUI…`);
|
||||
} catch (err) {
|
||||
if (action.dropsConnection) { /* the server went down as it restarted: expected */ } else { card.close(); return tell(err.message); }
|
||||
@@ -47,7 +53,7 @@ export function createMaintenance({ api, flush }) {
|
||||
let gs; try { gs = await api.gitFull(); } catch { gs = null; }
|
||||
if (!gs || !gs.ok) return { ok: false, update: "Git unavailable for this install", branch: "Git unavailable for this install", rollback: "Git unavailable for this install" };
|
||||
return { ok: true,
|
||||
update: gs.dirty ? "Local changes — commit first" : gs.behind > 0 ? `${gs.behind} commit(s) behind origin/${gs.branch}` : `origin/${gs.branch} up to date`,
|
||||
update: gs.fetch_ok === false ? "Could not reach origin: update status unknown" : gs.dirty ? "Local changes — commit first" : gs.behind > 0 ? `${gs.behind} commit(s) behind origin/${gs.branch}` : `origin/${gs.branch} up to date`,
|
||||
branch: `On ${gs.branch}${gs.dirty ? " · local changes — commit first" : " · pick another"}`,
|
||||
rollbackOk: Boolean(gs.rollback_target),
|
||||
rollback: gs.rollback_target ? `Undo last update — back to ${String(gs.rollback_target.commit).slice(0, 8)}` : "Nothing to undo yet" };
|
||||
|
||||
@@ -169,6 +169,7 @@ export function createProject({ onChange, onError, onOpen } = {}) {
|
||||
|
||||
return {
|
||||
get project() { return project; },
|
||||
get unsaved() { return dirty; },
|
||||
get scenes() { return project ? project.scenes || [] : []; },
|
||||
get selectedId() { return selected; },
|
||||
get selected() { return sceneAt(selected); },
|
||||
|
||||
Reference in New Issue
Block a user