Files
xiaodoudou-ComfyUI-RigShare/web/rigshare/save-dialog.js
T

158 lines
7.8 KiB
JavaScript

// "Save" asks where: My files, a shared folder, or Common.
//
// ComfyUI's save flow asks the workflow for a file name (promptSave) and saves
// to "<workflow directory>/<name>.json". Returning "<folder>/<name>" makes it
// save straight into the chosen folder, with ComfyUI's own overwrite checks.
import { h, storage } from "./ui.js";
const LAST_FOLDER = "rigshare.lastSaveFolder";
function cleanName(name) {
return (name || "").trim().replace(/\.json$/i, "").replace(/[\\/]+/g, "-").slice(0, 120);
}
export function installSaveDialog(app, api, client) {
const patch = () => {
const wf = app.extensionManager?.workflow?.activeWorkflow;
if (!wf) return false;
let proto = Object.getPrototypeOf(wf);
while (proto && !Object.prototype.hasOwnProperty.call(proto, "promptSave")) proto = Object.getPrototypeOf(proto);
if (!proto) return false;
if (proto.__rigshare) return true;
const original = proto.promptSave;
proto.promptSave = async function (...args) {
if (!client.online || !client.self || !this.path?.startsWith("workflows/")) return original.apply(this, args);
try {
return await chooseLocation(app, api, client, this);
} catch (e) {
console.error("[RigShare] save dialog", e);
return original.apply(this, args);
}
};
proto.__rigshare = true;
return true;
};
const timer = setInterval(() => { if (patch()) clearInterval(timer); }, 1000);
}
export async function chooseLocation(app, api, client, wf) {
let tree = await client.request("GET", "/rigshare/api/workspace");
const directory = wf.directory || "workflows";
const options = () => {
const out = [];
if (tree.private) out.push({ folder: tree.private.path, label: "My files", hint: "Only you", icon: "pi-user" });
for (const f of tree.shared) {
if (f.role === "edit") out.push({ folder: f.path, label: f.name, hint: f.restricted ? "Shared with chosen people" : "Shared with everyone", icon: f.restricted ? "pi-lock" : "pi-folder" });
}
if (tree.common?.role === "edit") out.push({ folder: "", label: "Common", hint: "Top level, visible to everyone", icon: "pi-globe" });
return out;
};
const current = directory === "workflows" ? null : directory.slice("workflows/".length);
const pick = (opts) => {
const remembered = storage.get(LAST_FOLDER);
return opts.find((o) => o.folder === current)?.folder
?? opts.find((o) => o.folder === remembered)?.folder
?? opts[0]?.folder;
};
const result = await new Promise((resolve) => {
let opts = options();
let selected = pick(opts);
const name = h("input", { class: "rs-input", value: cleanName(wf.filename) || "Untitled", maxLength: 120 });
const list = h("div", { class: "rs-save-list" });
const newFolder = h("input", { class: "rs-input", placeholder: "New shared folder name", maxLength: 64 });
const error = h("div", { class: "rs-error" });
const renderList = () => {
list.replaceChildren(...opts.map((o) => h("label", { class: `rs-save-option ${o.folder === selected ? "on" : ""}` },
h("input", { type: "radio", name: "rs-save-folder", checked: o.folder === selected, onchange: () => { selected = o.folder; renderList(); } }),
h("i", { class: `pi ${o.icon}` }),
h("span", { class: "rs-grow rs-min0" },
h("div", { class: "rs-name rs-ellipsis" }, o.label),
h("div", { class: "rs-muted rs-small" }, o.hint)))));
};
const close = (value) => {
backdrop.remove();
document.removeEventListener("keydown", onKey, true);
resolve(value);
};
const submit = () => {
const n = cleanName(name.value);
if (!n) { error.textContent = "Enter a name"; return; }
if (selected === undefined) { error.textContent = "Choose where to save it"; return; }
storage.set(LAST_FOLDER, selected);
close({ name: n, folder: selected });
};
const onKey = (e) => {
if (!backdrop.isConnected) return;
if (e.key === "Escape") { e.stopPropagation(); close(null); }
if (e.key === "Enter" && document.activeElement !== newFolder) { e.preventDefault(); e.stopPropagation(); submit(); }
};
const canCreate = tree.can_create;
const createRow = canCreate ? h("div", { class: "rs-row" }, newFolder,
h("button", {
class: "rs-btn", onclick: async () => {
error.textContent = "";
try {
const res = await client.request("POST", "/rigshare/api/workspace/folders", { name: newFolder.value });
tree = await client.request("GET", "/rigshare/api/workspace");
opts = options();
selected = res.path;
newFolder.value = "";
renderList();
} catch (e) { error.textContent = e.message; }
},
}, h("i", { class: "pi pi-folder-plus" }), "Create")) : null;
const backdrop = h("div", { class: "rs-modal-backdrop rs-panel-vars", onmousedown: (e) => { if (e.target === backdrop) close(null); } },
h("div", { class: "rs-modal", role: "dialog", "aria-label": "Save workflow" },
h("div", { class: "rs-modal-title" }, h("i", { class: "pi pi-save" }), "Save workflow"),
h("label", { class: "rs-label" }, "Name"), name,
h("label", { class: "rs-label" }, "Save in"), list,
createRow, error,
h("div", { class: "rs-actions rs-modal-actions" },
h("button", { class: "rs-btn rs-btn-ghost", onclick: () => close(null) }, "Cancel"),
h("button", { class: "rs-btn rs-btn-primary", onclick: submit }, "Save"))));
backdrop.addEventListener("keydown", (e) => e.stopPropagation());
renderList();
document.body.append(backdrop);
document.addEventListener("keydown", onKey, true);
setTimeout(() => { name.focus(); name.select(); }, 30);
});
if (!result) return null;
const targetDir = result.folder ? `workflows/${result.folder}` : "workflows";
if (targetDir === directory) return result.name;
if (directory === "workflows") return `${result.folder}/${result.name}`;
// "Save As" from a file that lives in another folder: write the copy
// ourselves, open it, and tell ComfyUI there is nothing left to do.
return await saveCopyElsewhere(app, api, client, wf, `${targetDir}/${result.name}.json`);
}
async function saveCopyElsewhere(app, api, client, wf, path) {
const store = app.extensionManager.workflow;
await store.syncWorkflows?.();
if (store.getWorkflowByPath?.(path)) {
const dialog = app.extensionManager?.dialog;
const ok = dialog?.confirm
? await dialog.confirm({ title: "Overwrite?", message: `${path.slice("workflows/".length)} already exists. Replace it?` })
: window.confirm(`${path} already exists. Replace it?`);
if (!ok) return null;
}
const state = wf.activeState ?? app.rootGraph?.serialize?.() ?? app.graph.serialize();
await api.storeUserData(path, JSON.stringify(state), { overwrite: true, full_info: true, throwOnError: true });
await store.syncWorkflows?.();
const copy = store.getWorkflowByPath?.(path);
if (copy) {
if (!copy.isLoaded) await copy.load();
await app.loadGraphData(copy.activeState, true, true, copy);
}
client.emit("toast", { severity: "success", summary: "RigShare", detail: `Saved as ${path.slice("workflows/".length)}` });
return null;
}