Add custom nodes, Civitai loras (LFS), and vast.ai setup script
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
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>
This commit is contained in:
@@ -0,0 +1,400 @@
|
||||
import { app } from "../../../../scripts/app.js";
|
||||
import { ComfyWidgets } from "../../../../scripts/widgets.js";
|
||||
|
||||
const KEY_CODES = { ENTER: 13, ESC: 27, ARROW_DOWN: 40, ARROW_UP: 38 };
|
||||
const WIDGET_GAP = -4;
|
||||
|
||||
function hideInfoWidget(e, node, widget) {
|
||||
let dropdownShouldBeRemoved = false;
|
||||
let selectionIndex = -1;
|
||||
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
displayDropdown(widget);
|
||||
} else {
|
||||
hideWidget(widget, node);
|
||||
}
|
||||
|
||||
function createDropdownElement() {
|
||||
const dropdown = document.createElement('ul');
|
||||
dropdown.id = 'hideinfo-dropdown';
|
||||
dropdown.setAttribute('role', 'listbox');
|
||||
dropdown.classList.add('hideInfo-dropdown');
|
||||
return dropdown;
|
||||
}
|
||||
|
||||
function createDropdownItem(textContent, action) {
|
||||
const listItem = document.createElement('li');
|
||||
listItem.id = `hideInfo-item-${textContent.replace(/ /g, '')}`;
|
||||
listItem.classList.add('hideInfo-item');
|
||||
listItem.setAttribute('role', 'option');
|
||||
listItem.textContent = textContent;
|
||||
listItem.addEventListener('mousedown', (event) => {
|
||||
event.preventDefault();
|
||||
action(widget, node); // perform the action when dropdown item is clicked
|
||||
removeDropdown();
|
||||
dropdownShouldBeRemoved = false;
|
||||
});
|
||||
listItem.dataset.action = textContent.replace(/ /g, ''); // store the action in a data attribute
|
||||
return listItem;
|
||||
}
|
||||
|
||||
function displayDropdown(widget) {
|
||||
removeDropdown();
|
||||
|
||||
const dropdown = createDropdownElement();
|
||||
const listItemHide = createDropdownItem('Hide info Widget', hideWidget);
|
||||
const listItemHideAll = createDropdownItem('Hide for all of this node-type', hideWidgetForNodetype);
|
||||
|
||||
dropdown.appendChild(listItemHide);
|
||||
dropdown.appendChild(listItemHideAll);
|
||||
|
||||
const inputRect = widget.inputEl.getBoundingClientRect();
|
||||
dropdown.style.top = `${inputRect.top + inputRect.height}px`;
|
||||
dropdown.style.left = `${inputRect.left}px`;
|
||||
dropdown.style.width = `${inputRect.width}px`;
|
||||
|
||||
document.body.appendChild(dropdown);
|
||||
dropdownShouldBeRemoved = true;
|
||||
|
||||
widget.inputEl.removeEventListener('keydown', handleKeyDown);
|
||||
widget.inputEl.addEventListener('keydown', handleKeyDown);
|
||||
document.addEventListener('click', handleDocumentClick);
|
||||
}
|
||||
|
||||
function removeDropdown() {
|
||||
const dropdown = document.getElementById('hideinfo-dropdown');
|
||||
if (dropdown) {
|
||||
dropdown.remove();
|
||||
widget.inputEl.removeEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
document.removeEventListener('click', handleDocumentClick);
|
||||
|
||||
}
|
||||
|
||||
function handleKeyDown(event) {
|
||||
const dropdownItems = document.querySelectorAll('.hideInfo-item');
|
||||
|
||||
if (event.keyCode === KEY_CODES.ENTER && dropdownShouldBeRemoved) {
|
||||
event.preventDefault();
|
||||
if (selectionIndex !== -1) {
|
||||
const selectedAction = dropdownItems[selectionIndex].dataset.action;
|
||||
if (selectedAction === 'HideinfoWidget') {
|
||||
hideWidget(widget, node);
|
||||
} else if (selectedAction === 'Hideforall') {
|
||||
hideWidgetForNodetype(widget, node);
|
||||
}
|
||||
removeDropdown();
|
||||
dropdownShouldBeRemoved = false;
|
||||
}
|
||||
} else if (event.keyCode === KEY_CODES.ARROW_DOWN && dropdownShouldBeRemoved) {
|
||||
event.preventDefault();
|
||||
if (selectionIndex !== -1) {
|
||||
dropdownItems[selectionIndex].classList.remove('selected');
|
||||
}
|
||||
selectionIndex = (selectionIndex + 1) % dropdownItems.length;
|
||||
dropdownItems[selectionIndex].classList.add('selected');
|
||||
} else if (event.keyCode === KEY_CODES.ARROW_UP && dropdownShouldBeRemoved) {
|
||||
event.preventDefault();
|
||||
if (selectionIndex !== -1) {
|
||||
dropdownItems[selectionIndex].classList.remove('selected');
|
||||
}
|
||||
selectionIndex = (selectionIndex - 1 + dropdownItems.length) % dropdownItems.length;
|
||||
dropdownItems[selectionIndex].classList.add('selected');
|
||||
} else if (event.keyCode === KEY_CODES.ESC && dropdownShouldBeRemoved) {
|
||||
event.preventDefault();
|
||||
removeDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
function hideWidget(widget, node) {
|
||||
node.properties['infoWidgetHidden'] = true;
|
||||
widget.type = "esayHidden";
|
||||
widget.computeSize = () => [0, WIDGET_GAP];
|
||||
node.setSize([node.size[0], node.size[1]]);
|
||||
}
|
||||
|
||||
function hideWidgetForNodetype(widget, node) {
|
||||
hideWidget(widget, node)
|
||||
const hiddenNodeTypes = JSON.parse(localStorage.getItem('hiddenWidgetNodeTypes') || "[]");
|
||||
if (!hiddenNodeTypes.includes(node.constructor.type)) {
|
||||
hiddenNodeTypes.push(node.constructor.type);
|
||||
}
|
||||
localStorage.setItem('hiddenWidgetNodeTypes', JSON.stringify(hiddenNodeTypes));
|
||||
}
|
||||
|
||||
function handleDocumentClick(event) {
|
||||
const dropdown = document.getElementById('hideinfo-dropdown');
|
||||
|
||||
// If the click was outside the dropdown and the dropdown should be removed, remove it
|
||||
if (dropdown && !dropdown.contains(event.target) && dropdownShouldBeRemoved) {
|
||||
removeDropdown();
|
||||
dropdownShouldBeRemoved = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var styleElement = document.createElement("style");
|
||||
const cssCode = `
|
||||
.easy-info_widget {
|
||||
background-color: var(--comfy-input-bg);
|
||||
color: var(--input-text);
|
||||
overflow: hidden;
|
||||
padding: 2px;
|
||||
resize: none;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
font-size: 10px;
|
||||
border-radius: 7px;
|
||||
text-align: center;
|
||||
text-wrap: balance;
|
||||
}
|
||||
.hideInfo-dropdown {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
background-color: #121212;
|
||||
border-radius: 7px;
|
||||
box-shadow: 0 2px 4px rgba(255, 255, 255, .25);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
z-index: 1000;
|
||||
overflow: auto;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.hideInfo-dropdown li {
|
||||
padding: 4px 10px;
|
||||
cursor: pointer;
|
||||
font-family: system-ui;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.hideInfo-dropdown li:hover,
|
||||
.hideInfo-dropdown li.selected {
|
||||
background-color: #e5e5e5;
|
||||
border-radius: 7px;
|
||||
}
|
||||
`
|
||||
styleElement.innerHTML = cssCode
|
||||
document.head.appendChild(styleElement);
|
||||
|
||||
const InfoSymbol = Symbol();
|
||||
const InfoResizeSymbol = Symbol();
|
||||
|
||||
|
||||
|
||||
|
||||
// WIDGET FUNCTIONS
|
||||
function addInfoWidget(node, name, opts, app) {
|
||||
const INFO_W_SIZE = 50;
|
||||
|
||||
node.addProperty('infoWidgetHidden', false)
|
||||
|
||||
function computeSize(size) {
|
||||
if (node.widgets[0].last_y == null) return;
|
||||
|
||||
let y = node.widgets[0].last_y;
|
||||
|
||||
// Compute the height of all non easyInfo widgets
|
||||
let widgetHeight = 0;
|
||||
const infoWidges = [];
|
||||
for (let i = 0; i < node.widgets.length; i++) {
|
||||
const w = node.widgets[i];
|
||||
if (w.type === "easyInfo") {
|
||||
infoWidges.push(w);
|
||||
} else {
|
||||
if (w.computeSize) {
|
||||
widgetHeight += w.computeSize()[1] + 4;
|
||||
} else {
|
||||
widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let infoWidgetSpace = infoWidges.length * INFO_W_SIZE; // Height for all info widgets
|
||||
|
||||
// Check if there's enough space for all widgets
|
||||
if (size[1] < y + widgetHeight + infoWidgetSpace) {
|
||||
// There isn't enough space for all the widgets, increase the size of the node
|
||||
node.size[1] = y + widgetHeight + infoWidgetSpace;
|
||||
node.graph.setDirtyCanvas(true);
|
||||
}
|
||||
|
||||
// Position each of the widgets
|
||||
for (const w of node.widgets) {
|
||||
w.y = y;
|
||||
if (w.type === "easyInfo") {
|
||||
y += INFO_W_SIZE;
|
||||
} else if (w.computeSize) {
|
||||
y += w.computeSize()[1] + 4;
|
||||
} else {
|
||||
y += LiteGraph.NODE_WIDGET_HEIGHT + 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const widget = {
|
||||
type: "easyInfo",
|
||||
name,
|
||||
get value() {
|
||||
return this.inputEl.value;
|
||||
},
|
||||
set value(x) {
|
||||
this.inputEl.value = x;
|
||||
},
|
||||
draw: function (ctx, _, widgetWidth, y, widgetHeight) {
|
||||
if (!this.parent.inputHeight) {
|
||||
// If we are initially offscreen when created we wont have received a resize event
|
||||
// Calculate it here instead
|
||||
computeSize(node.size);
|
||||
}
|
||||
const visible = app.canvas.ds.scale > 0.5 && this.type === "easyInfo";
|
||||
const margin = 10;
|
||||
const elRect = ctx.canvas.getBoundingClientRect();
|
||||
const transform = new DOMMatrix()
|
||||
.scaleSelf(elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height)
|
||||
.multiplySelf(ctx.getTransform())
|
||||
.translateSelf(margin, margin + y);
|
||||
|
||||
Object.assign(this.inputEl.style, {
|
||||
transformOrigin: "0 0",
|
||||
transform: transform,
|
||||
left: "0px",
|
||||
top: "0px",
|
||||
width: `${widgetWidth - (margin * 2)}px`,
|
||||
height: `${this.parent.inputHeight - (margin * 2)}px`,
|
||||
position: "absolute",
|
||||
background: (!node.color)?'':node.color,
|
||||
color: (!node.color)?'':'white',
|
||||
zIndex: app.graph._nodes.indexOf(node),
|
||||
});
|
||||
this.inputEl.hidden = !visible;
|
||||
},
|
||||
};
|
||||
widget.inputEl = document.createElement("textarea");
|
||||
widget.inputEl.className = "easy-info_widget";
|
||||
widget.inputEl.value = opts.defaultVal;
|
||||
widget.inputEl.placeholder = opts.placeholder || "";
|
||||
widget.inputEl.readOnly = true;
|
||||
widget.parent = node;
|
||||
|
||||
document.body.appendChild(widget.inputEl);
|
||||
|
||||
node.addCustomWidget(widget);
|
||||
|
||||
app.canvas.onDrawBackground = function () {
|
||||
// Draw node isnt fired once the node is off the screen
|
||||
// if it goes off screen quickly, the input may not be removed
|
||||
// this shifts it off screen so it can be moved back if the node is visible.
|
||||
for (let n in app.graph._nodes) {
|
||||
n = app.graph._nodes[n];
|
||||
for (let w in n.widgets) {
|
||||
let wid = n.widgets[w];
|
||||
if (Object.hasOwn(wid, "inputEl")) {
|
||||
wid.inputEl.style.left = -8000 + "px";
|
||||
wid.inputEl.style.position = "absolute";
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
node.onRemoved = function () {
|
||||
// When removing this node we need to remove the input from the DOM
|
||||
for (let y in this.widgets) {
|
||||
if (this.widgets[y].inputEl) {
|
||||
this.widgets[y].inputEl.remove();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
widget.onRemove = () => {
|
||||
widget.inputEl?.remove();
|
||||
|
||||
// Restore original size handler if we are the last
|
||||
if (!--node[InfoSymbol]) {
|
||||
node.onResize = node[InfoResizeSymbol];
|
||||
delete node[InfoSymbol];
|
||||
delete node[InfoResizeSymbol];
|
||||
}
|
||||
};
|
||||
|
||||
if (node[InfoSymbol]) {
|
||||
node[InfoSymbol]++;
|
||||
} else {
|
||||
node[InfoSymbol] = 1;
|
||||
const onResize = (node[InfoResizeSymbol] = node.onResize);
|
||||
|
||||
node.onResize = function (size) {
|
||||
computeSize(size);
|
||||
|
||||
// Call original resizer handler
|
||||
if (onResize) {
|
||||
console.log(this, arguments)
|
||||
onResize.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return { widget };
|
||||
}
|
||||
|
||||
// WIDGETS
|
||||
const easyCustomWidgets = {
|
||||
INFO(node, inputName, inputData, app) {
|
||||
const defaultVal = inputData[1].default || "";
|
||||
return addInfoWidget(node, inputName, { defaultVal, ...inputData[1] }, app);
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
app.registerExtension({
|
||||
name: "comfy.easy.widgets",
|
||||
getCustomWidgets(app) {
|
||||
return easyCustomWidgets;
|
||||
},
|
||||
nodeCreated(node) {
|
||||
if (node.widgets) {
|
||||
// Locate info widgets
|
||||
const widgets = node.widgets.filter((n) => (n.type === "easyInfo"));
|
||||
for (const widget of widgets) {
|
||||
widget.inputEl.addEventListener('contextmenu', function(e) {
|
||||
hideInfoWidget(e, node, widget);
|
||||
});
|
||||
widget.inputEl.addEventListener('click', function(e) {
|
||||
hideInfoWidget(e, node, widget);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
const hiddenNodeTypes = JSON.parse(localStorage.getItem('hiddenWidgetNodeTypes') || "[]");
|
||||
const origOnConfigure = nodeType.prototype.onConfigure;
|
||||
nodeType.prototype.onConfigure = function () {
|
||||
const r = origOnConfigure ? origOnConfigure.apply(this, arguments) : undefined;
|
||||
if (this.properties['infoWidgetHidden']) {
|
||||
for (let i in this.widgets) {
|
||||
if (this.widgets[i].type == "easyInfo") {
|
||||
hideInfoWidget(null, this, this.widgets[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return r;
|
||||
};
|
||||
const origOnAdded = nodeType.prototype.onAdded;
|
||||
nodeType.prototype.onAdded = function () {
|
||||
const r = origOnAdded ? origOnAdded.apply(this, arguments) : undefined;
|
||||
if (hiddenNodeTypes.includes(this.type)) {
|
||||
for (let i in this.widgets) {
|
||||
if (this.widgets[i].type == "easyInfo") {
|
||||
this.properties['infoWidgetHidden'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user