108 lines
4.2 KiB
JavaScript
108 lines
4.2 KiB
JavaScript
// Viewer "Edit": quick airbrush touch-up over the image before a follow-up
|
|
// inpaint or upscale. The modal paints on a layer over the untouched source;
|
|
// Save composites them in the browser and POSTs the PNG to /promptchain/
|
|
// save-edited-image, which re-attaches the parent's embedded prompt/workflow
|
|
// chunks (so the result still anchors inpaint/upscale grafts) and records it
|
|
// with parent lineage.
|
|
|
|
import { app } from "../../../scripts/app.js";
|
|
import { api } from "../../../scripts/api.js";
|
|
import { sourceSavePrefixInfo } from "./upscale-from-image.js";
|
|
import { refreshEntryWorkflows } from "./history.js";
|
|
|
|
function toast(severity, detail) {
|
|
app.extensionManager?.toast?.add({ severity, summary: "Edit", detail, life: 7000 });
|
|
}
|
|
|
|
function freshWorkflowId() {
|
|
if (globalThis.crypto?.randomUUID) return crypto.randomUUID();
|
|
return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2, 10)}`;
|
|
}
|
|
|
|
export async function prepareEdit(entry) {
|
|
const params = entry._browsePath != null
|
|
? `scope=${encodeURIComponent(entry._browseScope || "output")}&path=${encodeURIComponent(entry._browsePath)}`
|
|
: `hash=${encodeURIComponent(entry.hash)}`;
|
|
let data = null;
|
|
try {
|
|
const res = await api.fetchApi(`/promptchain/image-workflow?${params}`);
|
|
if (res.ok) data = await res.json();
|
|
} catch (e) {
|
|
console.error("[PromptChain] image-workflow fetch failed", e);
|
|
}
|
|
if (!data?.input_ref) {
|
|
toast("error", "Couldn't read the image on the server.");
|
|
return null;
|
|
}
|
|
const { prefix } = sourceSavePrefixInfo(data.workflow);
|
|
// No workflow-derived prefix (image dropped into a sidebar folder, no
|
|
// embedded SaveImage): default to the image's own folder instead of the
|
|
// generic edit/ bucket — but only when that folder is in the OUTPUT tree,
|
|
// since save-edited-image always writes output-side. A workflows/input
|
|
// folder name would mint a phantom output subfolder.
|
|
const sourceFolder = entry._browsePath != null
|
|
? ((entry._browseScope || "output") === "output"
|
|
? entry._browsePath.split("/").slice(0, -1).join("/")
|
|
: "")
|
|
: (entry.subfolder || "");
|
|
const defaultSavePrefix = prefix
|
|
? (prefix.endsWith("_edit") ? prefix : `${prefix}_edit`)
|
|
: sourceFolder
|
|
? `${sourceFolder}/%date:yyyy-MM-dd_hhmmss%`
|
|
: "edit/edit";
|
|
return { data, caps: { defaultSavePrefix } };
|
|
}
|
|
|
|
// Layer-stack sidecar (the "don't-flatten = keep your PSD" persistence). Keyed
|
|
// by the SAVED image's content hash; written AFTER the flat PNG save so it never
|
|
// blocks the gallery record. planes = [{ name:"L3.png", blob }]. Best-effort —
|
|
// the flat PNG is always the source of truth, a failed doc write is non-fatal.
|
|
export async function persistEditDoc(hash, manifest, planes) {
|
|
if (!hash || !manifest) return;
|
|
try {
|
|
const form = new FormData();
|
|
form.append("manifest", JSON.stringify(manifest));
|
|
for (const { name, blob } of planes) form.append(name, new File([blob], name, { type: "image/png" }));
|
|
await api.fetchApi(`/promptchain/edit-doc/${hash}`, { method: "POST", body: form });
|
|
} catch (e) {
|
|
console.warn("[PromptChain] edit-doc persist failed", e);
|
|
}
|
|
}
|
|
|
|
export async function fetchEditDoc(hash) {
|
|
if (!hash) return null;
|
|
try {
|
|
const res = await api.fetchApi(`/promptchain/edit-doc/${hash}`);
|
|
if (!res.ok) return null;
|
|
return await res.json();
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function editDocPlaneUrl(hash, file) {
|
|
return `/promptchain/edit-doc/${encodeURIComponent(hash)}/${encodeURIComponent(file)}`;
|
|
}
|
|
|
|
export async function saveEditedImage(prepared, blob, prefix) {
|
|
try {
|
|
const form = new FormData();
|
|
form.append("image", new File([blob], "edit.png", { type: "image/png" }));
|
|
form.append("prefix", prefix || "");
|
|
form.append("parent_filename", prepared?.data?.input_ref || "");
|
|
form.append("workflow_id", freshWorkflowId());
|
|
const res = await api.fetchApi("/promptchain/save-edited-image", { method: "POST", body: form });
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => null);
|
|
toast("error", `Save failed: ${err?.error || res.status}`);
|
|
return null;
|
|
}
|
|
const entry = await res.json();
|
|
refreshEntryWorkflows(entry);
|
|
return entry;
|
|
} catch (e) {
|
|
toast("error", `Save failed: ${e.message}`);
|
|
return null;
|
|
}
|
|
}
|