Files
jaidaken f09734b0ee
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
Add custom nodes, Civitai loras (LFS), and vast.ai setup script
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>
2026-02-09 00:56:42 +00:00

265 lines
7.9 KiB
TypeScript

import {app} from "scripts/app.js";
import {iconGear, iconStarFilled, logoRgthreeAsync} from "rgthree/common/media/svgs.js";
import {$el, empty} from "rgthree/common/utils_dom.js";
import {SERVICE as BOOKMARKS_SERVICE} from "./services/bookmarks_services.js";
import {SERVICE as CONFIG_SERVICE} from "./services/config_service.js";
import {RgthreeConfigDialog} from "./config.js";
import {wait} from "rgthree/common/shared_utils.js";
let rgthreeButtonGroup: RgthreeComfyButtonGroup | null = null;
function addRgthreeTopBarButtons() {
if (!CONFIG_SERVICE.getFeatureValue("comfy_top_bar_menu.enabled")) {
if (rgthreeButtonGroup?.element?.parentElement) {
rgthreeButtonGroup.element.parentElement.removeChild(rgthreeButtonGroup.element);
}
return;
} else if (rgthreeButtonGroup) {
app.menu?.settingsGroup.element.before(rgthreeButtonGroup.element);
return;
}
const buttons = [];
const rgthreeButton = new RgthreeComfyButton({
icon: "<svg></svg>",
tooltip: "rgthree-comfy",
primary: true,
// content: 'rgthree-comfy',
// app,
enabled: true,
classList: "comfyui-button comfyui-menu-mobile-collapse primary",
});
buttons.push(rgthreeButton);
logoRgthreeAsync().then((t) => {
rgthreeButton.setIcon(t);
});
rgthreeButton.withPopup(
new RgthreeComfyPopup(
{target: rgthreeButton.element},
$el("menu.rgthree-menu.rgthree-top-menu", {
children: [
$el("li", {
child: $el("button.rgthree-button-reset", {
html: iconGear + "Settings (rgthree-comfy)",
onclick: () => new RgthreeConfigDialog().show(),
}),
}),
$el("li", {
child: $el("button.rgthree-button-reset", {
html: iconStarFilled + "Star on Github",
onclick: () => window.open("https://github.com/rgthree/rgthree-comfy", "_blank"),
}),
}),
],
}),
),
"click",
);
if (CONFIG_SERVICE.getFeatureValue("comfy_top_bar_menu.button_bookmarks.enabled")) {
const bookmarksListEl = $el("menu.rgthree-menu.rgthree-top-menu");
bookmarksListEl.appendChild(
$el("li.rgthree-message", {
child: $el("span", {text: "No bookmarks in current workflow."}),
}),
);
const bookmarksButton = new RgthreeComfyButton({
icon: "bookmark",
tooltip: "Workflow Bookmarks (rgthree-comfy)",
// app,
});
const bookmarksPopup = new RgthreeComfyPopup(
{target: bookmarksButton.element, modal: false},
bookmarksListEl,
);
bookmarksPopup.onOpen(() => {
const bookmarks = BOOKMARKS_SERVICE.getCurrentBookmarks();
empty(bookmarksListEl);
if (bookmarks.length) {
for (const b of bookmarks) {
bookmarksListEl.appendChild(
$el("li", {
child: $el("button.rgthree-button-reset", {
text: `[${b.shortcutKey}] ${b.title}`,
onclick: () => {
b.canvasToBookmark();
},
}),
}),
);
}
} else {
bookmarksListEl.appendChild(
$el("li.rgthree-message", {
child: $el("span", {text: "No bookmarks in current workflow."}),
}),
);
}
// bookmarksPopup.update();
});
bookmarksButton.withPopup(bookmarksPopup, "hover");
buttons.push(bookmarksButton);
}
rgthreeButtonGroup = new RgthreeComfyButtonGroup(...buttons);
app.menu?.settingsGroup.element.before(rgthreeButtonGroup.element);
}
app.registerExtension({
name: "rgthree.TopMenu",
async setup() {
addRgthreeTopBarButtons();
CONFIG_SERVICE.addEventListener("config-change", ((e: CustomEvent) => {
if (e.detail?.key?.includes("features.comfy_top_bar_menu")) {
addRgthreeTopBarButtons();
}
}) as EventListener);
},
});
// The following are rough hacks since ComfyUI took away their button/buttongroup/popup
// functionality. TODO: Find a better spot to add rgthree controls to the UI, I suppose.
class RgthreeComfyButtonGroup {
element = $el("div.rgthree-comfybar-top-button-group");
buttons: RgthreeComfyButton[];
constructor(...buttons: RgthreeComfyButton[]) {
this.buttons = buttons;
this.update();
}
insert(button: RgthreeComfyButton, index: number) {
this.buttons.splice(index, 0, button);
this.update();
}
append(button: RgthreeComfyButton) {
this.buttons.push(button);
this.update();
}
remove(indexOrButton: RgthreeComfyButton | number) {
if (typeof indexOrButton !== "number") {
indexOrButton = this.buttons.indexOf(indexOrButton);
}
if (indexOrButton > -1) {
const btn = this.buttons.splice(indexOrButton, 1);
this.update();
return btn;
}
return null;
}
update() {
this.element.replaceChildren(...this.buttons.map((b) => b["element"] ?? b));
}
}
interface RgthreeComfyButtonOptions {
icon?: string;
primary?: boolean;
overIcon?: string;
iconSize?: number;
content?: string | HTMLElement;
tooltip?: string;
enabled?: boolean;
action?: (e: Event, btn: RgthreeComfyButton) => void;
classList?: string;
visibilitySetting?: {id: string; showValue: any};
// app?: ComfyApp;
}
class RgthreeComfyButton {
element = $el("button.rgthree-comfybar-top-button.rgthree-button-reset.rgthree-button");
iconElement = $el("span.rgthree-button-icon");
constructor(opts: RgthreeComfyButtonOptions) {
opts.icon && this.setIcon(opts.icon);
opts.tooltip && this.element.setAttribute("title", opts.tooltip);
opts.primary && this.element.classList.add("-primary");
}
setIcon(iconOrMarkup: string) {
const markup = iconOrMarkup.startsWith("<")
? iconOrMarkup
: `<i class="mdi mdi-${iconOrMarkup}"></i>`;
this.iconElement.innerHTML = markup;
if (!this.iconElement.parentElement) {
this.element.appendChild(this.iconElement);
}
}
withPopup(popup: RgthreeComfyPopup, trigger: "click" | "hover") {
if (trigger === "click") {
this.element.addEventListener("click", () => {
popup.open();
});
}
if (trigger === "hover") {
this.element.addEventListener("pointerenter", () => {
popup.open();
});
}
}
}
interface RgthreeComfyPopupOptions {
target: HTMLElement;
classList?: string;
modal?: boolean;
}
class RgthreeComfyPopup {
element: HTMLElement;
target?: HTMLElement;
onOpenFn: (() => Promise<void> | void) | null = null;
opts: RgthreeComfyPopupOptions;
onWindowClickBound = this.onWindowClick.bind(this);
constructor(opts: RgthreeComfyPopupOptions, element: HTMLElement) {
this.element = element;
this.opts = opts;
opts.target && (this.target = opts.target);
opts.modal && this.element.classList.add("-modal");
}
async open() {
if (!this.target) {
throw new Error("No target for RgthreeComfyPopup");
}
if (this.onOpenFn) {
await this.onOpenFn();
}
await wait(16);
const rect = this.target.getBoundingClientRect();
this.element.setAttribute("state", "measuring");
document.body.appendChild(this.element);
this.element.style.position = "fixed";
this.element.style.left = `${rect.left}px`;
this.element.style.top = `${rect.top + rect.height}px`;
this.element.setAttribute("state", "open");
if (this.opts.modal) {
document.body.classList.add("rgthree-modal-menu-open");
}
window.addEventListener("click", this.onWindowClickBound);
}
close() {
this.element.remove();
document.body.classList.remove("rgthree-modal-menu-open");
window.removeEventListener("click", this.onWindowClickBound);
}
onOpen(fn: (() => void) | null) {
this.onOpenFn = fn;
}
onWindowClick() {
this.close();
}
}