Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Has been cancelled
Execution Tests / test (macos-latest) (push) Has been cancelled
Execution Tests / test (ubuntu-latest) (push) Has been cancelled
Execution Tests / test (windows-latest) (push) Has been cancelled
Test server launches without errors / test (push) Has been cancelled
Unit Tests / test (macos-latest) (push) Has been cancelled
Unit Tests / test (ubuntu-latest) (push) Has been cancelled
Unit Tests / test (windows-2022) (push) Has been cancelled
Includes 30 custom nodes committed directly, 7 Civitai-exclusive loras stored via Git LFS, and a setup script that installs all dependencies and downloads HuggingFace-hosted models on vast.ai. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
277 lines
11 KiB
JavaScript
277 lines
11 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
||
import { BaseAnyInputConnectedNode } from "./base_any_input_connected_node.js";
|
||
import { NodeTypesString } from "./constants.js";
|
||
import { addMenuItem, changeModeOfNodes } from "./utils.js";
|
||
import { rgthree } from "./rgthree.js";
|
||
const MODE_ALWAYS = 0;
|
||
const MODE_MUTE = 2;
|
||
const MODE_BYPASS = 4;
|
||
class FastActionsButton extends BaseAnyInputConnectedNode {
|
||
constructor(title) {
|
||
super(title);
|
||
this.comfyClass = NodeTypesString.FAST_ACTIONS_BUTTON;
|
||
this.logger = rgthree.newLogSession("[FastActionsButton]");
|
||
this.isVirtualNode = true;
|
||
this.serialize_widgets = true;
|
||
this.widgetToData = new Map();
|
||
this.nodeIdtoFunctionCache = new Map();
|
||
this.executingFromShortcut = false;
|
||
this.properties["buttonText"] = "🎬 Action!";
|
||
this.properties["shortcutModifier"] = "alt";
|
||
this.properties["shortcutKey"] = "";
|
||
this.buttonWidget = this.addWidget("button", this.properties["buttonText"], "", () => {
|
||
this.executeConnectedNodes();
|
||
}, { serialize: false });
|
||
this.keypressBound = this.onKeypress.bind(this);
|
||
this.keyupBound = this.onKeyup.bind(this);
|
||
this.onConstructed();
|
||
}
|
||
configure(info) {
|
||
super.configure(info);
|
||
setTimeout(() => {
|
||
if (info.widgets_values) {
|
||
for (let [index, value] of info.widgets_values.entries()) {
|
||
if (index > 0) {
|
||
if (typeof value === "string" && value.startsWith("comfy_action:")) {
|
||
value = value.replace("comfy_action:", "");
|
||
this.addComfyActionWidget(index, value);
|
||
}
|
||
if (this.widgets[index]) {
|
||
this.widgets[index].value = value;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}, 100);
|
||
}
|
||
clone() {
|
||
const cloned = super.clone();
|
||
cloned.properties["buttonText"] = "🎬 Action!";
|
||
cloned.properties["shortcutKey"] = "";
|
||
return cloned;
|
||
}
|
||
onAdded(graph) {
|
||
window.addEventListener("keydown", this.keypressBound);
|
||
window.addEventListener("keyup", this.keyupBound);
|
||
}
|
||
onRemoved() {
|
||
window.removeEventListener("keydown", this.keypressBound);
|
||
window.removeEventListener("keyup", this.keyupBound);
|
||
}
|
||
async onKeypress(event) {
|
||
const target = event.target;
|
||
if (this.executingFromShortcut ||
|
||
target.localName == "input" ||
|
||
target.localName == "textarea") {
|
||
return;
|
||
}
|
||
if (this.properties["shortcutKey"].trim() &&
|
||
this.properties["shortcutKey"].toLowerCase() === event.key.toLowerCase()) {
|
||
const shortcutModifier = this.properties["shortcutModifier"];
|
||
let good = shortcutModifier === "ctrl" && event.ctrlKey;
|
||
good = good || (shortcutModifier === "alt" && event.altKey);
|
||
good = good || (shortcutModifier === "shift" && event.shiftKey);
|
||
good = good || (shortcutModifier === "meta" && event.metaKey);
|
||
if (good) {
|
||
setTimeout(() => {
|
||
this.executeConnectedNodes();
|
||
}, 20);
|
||
this.executingFromShortcut = true;
|
||
event.preventDefault();
|
||
event.stopImmediatePropagation();
|
||
app.canvas.dirty_canvas = true;
|
||
return false;
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
onKeyup(event) {
|
||
const target = event.target;
|
||
if (target.localName == "input" || target.localName == "textarea") {
|
||
return;
|
||
}
|
||
this.executingFromShortcut = false;
|
||
}
|
||
onPropertyChanged(property, value, prevValue) {
|
||
var _a, _b;
|
||
if (property == "buttonText" && typeof value === "string") {
|
||
this.buttonWidget.name = value;
|
||
}
|
||
if (property == "shortcutKey" && typeof value === "string") {
|
||
this.properties["shortcutKey"] = (_b = (_a = value.trim()[0]) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : "";
|
||
}
|
||
return true;
|
||
}
|
||
handleLinkedNodesStabilization(linkedNodes) {
|
||
var _a, _b, _c, _d, _e, _f, _g, _h;
|
||
let changed = false;
|
||
for (const [widget, data] of this.widgetToData.entries()) {
|
||
if (!data.node) {
|
||
continue;
|
||
}
|
||
if (!linkedNodes.includes(data.node)) {
|
||
const index = this.widgets.indexOf(widget);
|
||
if (index > -1) {
|
||
this.widgetToData.delete(widget);
|
||
this.removeWidget(widget);
|
||
changed = true;
|
||
}
|
||
else {
|
||
const [m, a] = this.logger.debugParts("Connected widget is not in widgets... weird.");
|
||
(_a = console[m]) === null || _a === void 0 ? void 0 : _a.call(console, ...a);
|
||
}
|
||
}
|
||
}
|
||
const badNodes = [];
|
||
let indexOffset = 1;
|
||
for (const [index, node] of linkedNodes.entries()) {
|
||
if (!node) {
|
||
const [m, a] = this.logger.debugParts("linkedNode provided that does not exist. ");
|
||
(_b = console[m]) === null || _b === void 0 ? void 0 : _b.call(console, ...a);
|
||
badNodes.push(node);
|
||
continue;
|
||
}
|
||
let widgetAtSlot = this.widgets[index + indexOffset];
|
||
if (widgetAtSlot && ((_c = this.widgetToData.get(widgetAtSlot)) === null || _c === void 0 ? void 0 : _c.comfy)) {
|
||
indexOffset++;
|
||
widgetAtSlot = this.widgets[index + indexOffset];
|
||
}
|
||
if (!widgetAtSlot || ((_e = (_d = this.widgetToData.get(widgetAtSlot)) === null || _d === void 0 ? void 0 : _d.node) === null || _e === void 0 ? void 0 : _e.id) !== node.id) {
|
||
let widget = null;
|
||
for (let i = index + indexOffset; i < this.widgets.length; i++) {
|
||
if (((_g = (_f = this.widgetToData.get(this.widgets[i])) === null || _f === void 0 ? void 0 : _f.node) === null || _g === void 0 ? void 0 : _g.id) === node.id) {
|
||
widget = this.widgets.splice(i, 1)[0];
|
||
this.widgets.splice(index + indexOffset, 0, widget);
|
||
changed = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!widget) {
|
||
const exposedActions = node.constructor.exposedActions || [];
|
||
widget = this.addWidget("combo", node.title, "None", "", {
|
||
values: ["None", "Mute", "Bypass", "Enable", ...exposedActions],
|
||
});
|
||
widget.serializeValue = async (_node, _index) => {
|
||
return widget === null || widget === void 0 ? void 0 : widget.value;
|
||
};
|
||
this.widgetToData.set(widget, { node });
|
||
changed = true;
|
||
}
|
||
}
|
||
}
|
||
for (let i = this.widgets.length - 1; i > linkedNodes.length + indexOffset - 1; i--) {
|
||
const widgetAtSlot = this.widgets[i];
|
||
if (widgetAtSlot && ((_h = this.widgetToData.get(widgetAtSlot)) === null || _h === void 0 ? void 0 : _h.comfy)) {
|
||
continue;
|
||
}
|
||
this.removeWidget(widgetAtSlot);
|
||
changed = true;
|
||
}
|
||
return changed;
|
||
}
|
||
removeWidget(widget) {
|
||
widget = typeof widget === "number" ? this.widgets[widget] : widget;
|
||
if (widget && this.widgetToData.has(widget)) {
|
||
this.widgetToData.delete(widget);
|
||
}
|
||
super.removeWidget(widget);
|
||
}
|
||
async executeConnectedNodes() {
|
||
var _a, _b;
|
||
for (const widget of this.widgets) {
|
||
if (widget == this.buttonWidget) {
|
||
continue;
|
||
}
|
||
const action = widget.value;
|
||
const { comfy, node } = (_a = this.widgetToData.get(widget)) !== null && _a !== void 0 ? _a : {};
|
||
if (comfy) {
|
||
if (action === "Queue Prompt") {
|
||
await comfy.queuePrompt(0);
|
||
}
|
||
continue;
|
||
}
|
||
if (node) {
|
||
if (action === "Mute") {
|
||
changeModeOfNodes(node, MODE_MUTE);
|
||
}
|
||
else if (action === "Bypass") {
|
||
changeModeOfNodes(node, MODE_BYPASS);
|
||
}
|
||
else if (action === "Enable") {
|
||
changeModeOfNodes(node, MODE_ALWAYS);
|
||
}
|
||
if (node.handleAction) {
|
||
if (typeof action !== "string") {
|
||
throw new Error("Fast Actions Button action should be a string: " + action);
|
||
}
|
||
await node.handleAction(action);
|
||
}
|
||
(_b = this.graph) === null || _b === void 0 ? void 0 : _b.change();
|
||
continue;
|
||
}
|
||
console.warn("Fast Actions Button has a widget without correct data.");
|
||
}
|
||
}
|
||
addComfyActionWidget(slot, value) {
|
||
let widget = this.addWidget("combo", "Comfy Action", "None", () => {
|
||
if (String(widget.value).startsWith("MOVE ")) {
|
||
this.widgets.push(this.widgets.splice(this.widgets.indexOf(widget), 1)[0]);
|
||
widget.value = String(widget.rgthree_lastValue);
|
||
}
|
||
else if (String(widget.value).startsWith("REMOVE ")) {
|
||
this.removeWidget(widget);
|
||
}
|
||
widget.rgthree_lastValue = widget.value;
|
||
}, {
|
||
values: ["None", "Queue Prompt", "REMOVE Comfy Action", "MOVE to end"],
|
||
});
|
||
widget.rgthree_lastValue = value;
|
||
widget.serializeValue = async (_node, _index) => {
|
||
return `comfy_app:${widget === null || widget === void 0 ? void 0 : widget.value}`;
|
||
};
|
||
this.widgetToData.set(widget, { comfy: app });
|
||
if (slot != null) {
|
||
this.widgets.splice(slot, 0, this.widgets.splice(this.widgets.indexOf(widget), 1)[0]);
|
||
}
|
||
return widget;
|
||
}
|
||
onSerialize(serialised) {
|
||
var _a, _b;
|
||
(_a = super.onSerialize) === null || _a === void 0 ? void 0 : _a.call(this, serialised);
|
||
for (let [index, value] of (serialised.widgets_values || []).entries()) {
|
||
if (((_b = this.widgets[index]) === null || _b === void 0 ? void 0 : _b.name) === "Comfy Action") {
|
||
serialised.widgets_values[index] = `comfy_action:${value}`;
|
||
}
|
||
}
|
||
}
|
||
static setUp() {
|
||
super.setUp();
|
||
addMenuItem(this, app, {
|
||
name: "➕ Append a Comfy Action",
|
||
callback: (nodeArg) => {
|
||
nodeArg.addComfyActionWidget();
|
||
},
|
||
});
|
||
}
|
||
}
|
||
FastActionsButton.type = NodeTypesString.FAST_ACTIONS_BUTTON;
|
||
FastActionsButton.title = NodeTypesString.FAST_ACTIONS_BUTTON;
|
||
FastActionsButton["@buttonText"] = { type: "string" };
|
||
FastActionsButton["@shortcutModifier"] = {
|
||
type: "combo",
|
||
values: ["ctrl", "alt", "shift"],
|
||
};
|
||
FastActionsButton["@shortcutKey"] = { type: "string" };
|
||
FastActionsButton.collapsible = false;
|
||
app.registerExtension({
|
||
name: "rgthree.FastActionsButton",
|
||
registerCustomNodes() {
|
||
FastActionsButton.setUp();
|
||
},
|
||
loadedGraphNode(node) {
|
||
if (node.type == FastActionsButton.title) {
|
||
node._tempWidth = node.size[0];
|
||
}
|
||
},
|
||
});
|