Files

165 lines
7.0 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { t } from "./i18n.js";
const NODE_NAME = "PromptWarehouseSaveImageWithDelete";
const DELETE_PATH = "/prompt-warehouse/delete-output-images";
const SESSION_PATH = "/prompt-warehouse/session";
const TOKEN_HEADER = "X-Prompt-Warehouse-Token";
// The server refuses a delete that does not carry a session token in a custom
// header. A page from another site cannot set that header — browsers require a
// pre-flight for custom headers and ComfyUI does not allow it — and cannot read
// the token either, so the delete stays an action only this UI can trigger.
// The token is regenerated every time ComfyUI starts, hence the single
// refetch-and-retry in `sendDelete`.
let sessionToken = null;
async function deleteToken(refresh = false) {
if (sessionToken && !refresh) return sessionToken;
const response = await api.fetchApi(SESSION_PATH);
const payload = response.ok ? await response.json() : null;
sessionToken = String(payload?.token || "");
if (!sessionToken) throw new Error(t("deleteFailed"));
return sessionToken;
}
async function sendDelete(images) {
const send = (token) => api.fetchApi(DELETE_PATH, {
method: "POST",
headers: { "Content-Type": "application/json", [TOKEN_HEADER]: token },
body: JSON.stringify({ images }),
});
const read = async (response) => {
const payload = await response.json().catch(() => null);
return { response, payload };
};
const first = await read(await send(await deleteToken()));
if (first.response.status !== 403 || first.payload?.code !== "token") return first;
// Stale token (ComfyUI restarted under us): fetch a fresh one, retry once.
sessionToken = null;
return read(await send(await deleteToken(true)));
}
function withCount(label, count) {
return count > 1 ? `${label} (${count})` : label;
}
function removeWidget(node, widget) {
if (!widget) return;
const index = node.widgets?.indexOf(widget) ?? -1;
if (index >= 0) node.widgets.splice(index, 1);
}
function addDeleteButton(node) {
removeWidget(node, node._pwDeleteButton);
const count = node._pwSavedImages?.length || 0;
const button = node.addWidget("button", withCount(t("deleteRecent"), count), null, () => {
showDeleteConfirm(node);
});
button.options = { ...(button.options || {}), serialize: false };
node._pwDeleteButton = button;
node.setSize([node.size[0], Math.max(node.size[1], node.computeSize()[1])]);
}
function previewMatchesOutput(node, images) {
const filenames = new Set(images.map((image) => String(image.filename || "")));
return Boolean(node.imgs?.some((image) => {
let source = String(image?.src || "");
try { source = decodeURIComponent(source); } catch (_) { /* Keep the raw URL. */ }
return [...filenames].some((filename) => filename && source.includes(filename));
}));
}
function addDeleteButtonAfterPreview(node, images, outputVersion, attempt = 0) {
if (node._pwOutputVersion !== outputVersion || !images.length) return;
if (previewMatchesOutput(node, images) || attempt >= 300) {
// Wait one additional frame after the matching image appears so ComfyUI has
// committed its preview layout before this widget is appended.
requestAnimationFrame(() => {
if (node._pwOutputVersion !== outputVersion) return;
addDeleteButton(node);
node.setDirtyCanvas(true, true);
});
return;
}
requestAnimationFrame(() => addDeleteButtonAfterPreview(node, images, outputVersion, attempt + 1));
}
function showDeleteConfirm(node) {
const count = node._pwSavedImages?.length || 0;
if (!count) return;
if (!document.getElementById("pw-image-delete-style")) {
const style = document.createElement("style");
style.id = "pw-image-delete-style";
style.textContent = `
.pw-image-delete-backdrop{position:fixed;inset:0;z-index:10020;display:grid;place-items:center;background:#05070a99;font-family:Inter,system-ui,sans-serif}
.pw-image-delete-dialog{width:330px;padding:19px;background:#202329;color:#edf0f5;border:1px solid #555d69;border-radius:11px;box-shadow:0 20px 60px #000a}
.pw-image-delete-dialog strong{display:block;margin-bottom:8px;font-size:16px}.pw-image-delete-dialog p{margin:0;color:#adb6c3;font-size:13px;line-height:1.55}
.pw-image-delete-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:17px}.pw-image-delete-actions button{padding:7px 14px;color:#e7ebf1;background:#353a43;border:1px solid #59616e;border-radius:6px;cursor:pointer}
.pw-image-delete-actions button[data-confirm]{color:#fff;background:#a84545;border-color:#c25c5c}
`;
document.head.append(style);
}
const targetText = count > 1 ? t("deleteManyTarget", { count }) : t("deleteOneTarget");
const root = document.createElement("div");
root.className = "pw-image-delete-backdrop";
root.innerHTML = `<div class="pw-image-delete-dialog" role="dialog" aria-modal="true">
<strong>${t("deleteTitle")}</strong>
<p>${t("deleteMessage", { target: targetText })}</p>
<div class="pw-image-delete-actions"><button data-cancel>${t("cancel")}</button><button data-confirm>${t("delete")}</button></div>
</div>`;
document.body.append(root);
const close = () => root.remove();
root.querySelector("[data-cancel]").onclick = close;
root.querySelector("[data-confirm]").onclick = () => {
close();
deleteLastOutput(node, node._pwDeleteButton);
};
root.onclick = (event) => { if (event.target === root) close(); };
root.onkeydown = (event) => { if (event.key === "Escape") close(); };
}
async function deleteLastOutput(node, button) {
const images = node._pwSavedImages || [];
if (!images.length) return;
button.name = t("deleting");
node.setDirtyCanvas(true, true);
try {
const { response, payload } = await sendDelete(images);
if (!response.ok) throw new Error(payload?.error || t("deleteFailed"));
node._pwSavedImages = [];
node.imgs = [];
removeWidget(node, button);
node._pwDeleteButton = null;
} catch (error) {
button.name = `${t("deleteFailed")}: ${error.message}`;
}
node.setDirtyCanvas(true, true);
}
app.registerExtension({
name: "D33MO.SaveImageWithDelete",
beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== NODE_NAME) return;
const created = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = created?.apply(this, arguments);
this._pwDeleteButton = null;
return result;
};
const executed = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
const result = executed?.apply(this, arguments);
removeWidget(this, this._pwDeleteButton);
this._pwDeleteButton = null;
this._pwSavedImages = (message?.images || []).filter((image) => image?.type === "output");
const outputVersion = this._pwOutputVersion = (this._pwOutputVersion || 0) + 1;
addDeleteButtonAfterPreview(this, this._pwSavedImages, outputVersion);
this.setDirtyCanvas(true, true);
return result;
};
},
});