Files
ausboss-ComfyUI-AusBoss/js/shared/index.mjs
T
2026-10-01 11:53:03 -05:00

125 lines
5.0 KiB
JavaScript

// Shared helpers for AusBoss frontend extensions.
//
// .mjs files under js/ are NOT auto-loaded by ComfyUI (only .js files are),
// which makes this the right home for import-only utilities. Per-node
// entry points live in js/<node>/index.js and import from here.
import { app } from "/scripts/app.js";
// Must match `version` in pyproject.toml — scripts/release_preflight.py
// enforces it. The backend serves its copy at /ausboss/pack_version; a
// mismatch means the browser cached JavaScript from an older install.
export const AUSBOSS_JS_VERSION = "2.5.1";
export const BRAND = "#00b4aa"; // AusBoss teal — keep node accents consistent.
export const BRAND_DARK = "#007f78";
export const BRAND_BODY = "#081413";
// Wrap a LiteGraph prototype callback without clobbering whoever hooked it
// first. Other node packs patch the same prototypes, so never assign
// `proto[name] = fn` directly — always chain.
export function chainCallback(proto, name, fn) {
const prior = proto[name];
proto[name] = function (...args) {
const result = prior?.apply(this, args);
fn.apply(this, args);
return result;
};
}
// chainCallback for handlers whose truthy return tells LiteGraph the event
// was consumed (onMouseDown skips the node drag): the chain reports consumed
// when either handler did. chainCallback would drop this handler's verdict.
export function chainHandler(proto, name, fn) {
const prior = proto[name];
proto[name] = function (...args) {
const result = prior?.apply(this, args);
const handled = fn.apply(this, args);
return result || handled;
};
}
// Every public AusBoss node, plus the pre-convention LaMa alias that saved
// workflows still load.
export function isAusbossNode(node) {
const comfyClass = node?.comfyClass || "";
return comfyClass.startsWith("AUSBOSS_NODES_") || comfyClass === "SimpleWatermarkRemover";
}
// One toast for every AusBoss message. The frontend's toast store queues a
// PrimeVue toast; a frontend without one gets the console instead, and this
// never throws - a message must not break the action that raised it.
export function showToast({ detail, severity = "info", summary = "AusBoss", life = 5000 } = {}) {
try {
const toast = app?.extensionManager?.toast;
if (typeof toast?.add === "function") {
toast.add({ severity, summary, detail, life });
return;
}
} catch (_error) {
// Toast store missing or incompatible: fall through to the console.
}
const line = `[AusBoss] ${summary === "AusBoss" ? "" : `${summary}: `}${detail}`;
if (severity === "error" || severity === "warn") console.warn(line);
else console.log(line);
}
// ComfyUI's undo/dirty tracker snapshots the graph on canvas mouse-up, but
// our DOM panels commit widget values and node.properties from click/change/
// pointerup handlers that run one phase later. Without a nudge those edits
// never mark the workflow modified and never enter undo history. Call this
// once per user gesture, AFTER the write has landed — and never from a
// workflow-load path, which must stay clean.
//
// The trailing timer batches a gesture's writes into a single snapshot and
// guarantees the capture runs after the handler that invoked us returned.
// A capture that finds nothing changed is a no-op inside the tracker.
let ausbossChangeTimer = null;
export function notifyAusbossChange() {
if (ausbossChangeTimer) clearTimeout(ausbossChangeTimer);
ausbossChangeTimer = setTimeout(() => {
ausbossChangeTimer = null;
try {
const workflow =
app?.extensionManager?.workflow?.activeWorkflow ??
app?.workflowManager?.activeWorkflow;
const tracker = workflow?.changeTracker;
if (!tracker) return;
if (typeof tracker.captureCanvasState === "function") tracker.captureCanvasState();
else tracker.checkState?.();
} catch (_error) {
// Best effort: the widget write itself already succeeded, and a
// frontend rename must never break the control that called us.
}
}, 120);
}
// The frontend sizes a DOM widget's wrapper as `widget.width ?? node.width`
// (GraphView, useDomWidget's updateWidgets). LiteGraph's layout plants
// widget.width during draws, and once planted it OUTRANKS the node width
// forever: resize the node narrower and the wrapper keeps the old width,
// parking the panel's right edge outside the node. The overflow clip cannot
// help - it clips at the wrapper's edge, which is the thing that is too
// wide. Discarding every write keeps the lookup on its node-width fallback,
// so the wrapper tracks the node in both directions for the life of the
// widget. Call it on every DOM widget right after addDOMWidget.
export function keepDomWidgetWidthAuto(widget) {
if (!widget) return widget;
try {
Object.defineProperty(widget, "width", {
get() {
return undefined;
},
set(_value) {
/* a planted layout width must never outrank the node */
},
configurable: true,
});
} catch (_error) {
// A frozen widget object keeps its old behavior; nothing to break.
}
return widget;
}