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:
5
custom_nodes/ComfyUI-Crystools/web/comfy/index.d.ts
vendored
Normal file
5
custom_nodes/ComfyUI-Crystools/web/comfy/index.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export type * from './scripts.js';
|
||||
export * from './scripts.js';
|
||||
export type { ComfyApp } from './typings/comfy.js';
|
||||
export * from './liteGraph.js';
|
||||
export type * from './liteGraph.js';
|
||||
2
custom_nodes/ComfyUI-Crystools/web/comfy/index.js
Normal file
2
custom_nodes/ComfyUI-Crystools/web/comfy/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './scripts.js';
|
||||
export * from './liteGraph.js';
|
||||
5
custom_nodes/ComfyUI-Crystools/web/comfy/index.ts
Normal file
5
custom_nodes/ComfyUI-Crystools/web/comfy/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type * from './scripts.js';
|
||||
export * from './scripts.js';
|
||||
export type { ComfyApp } from './typings/comfy.js';
|
||||
export * from './liteGraph.js';
|
||||
export type * from './liteGraph.js';
|
||||
19
custom_nodes/ComfyUI-Crystools/web/comfy/liteGraph.d.ts
vendored
Normal file
19
custom_nodes/ComfyUI-Crystools/web/comfy/liteGraph.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
export type * from './liteGraph.types.js';
|
||||
import type { IWidget as IWidgetOld, LGraphNode as TypeGraphNode, TypeLiteGraph } from './liteGraph.types.js';
|
||||
declare const LGraphNode: typeof TypeGraphNode;
|
||||
export interface IWidget extends IWidgetOld {
|
||||
onRemove?: () => void;
|
||||
serializeValue?: () => Promise<void>;
|
||||
}
|
||||
export declare class TLGraphNode extends LGraphNode {
|
||||
static category: string;
|
||||
static shape: number;
|
||||
static color: string;
|
||||
static bgcolor: string;
|
||||
static collapsable: boolean;
|
||||
isVirtualNode?: boolean;
|
||||
widgets_values?: any[];
|
||||
name?: string;
|
||||
prototype: TLGraphNode;
|
||||
}
|
||||
export declare const LiteGraph: TypeLiteGraph;
|
||||
30
custom_nodes/ComfyUI-Crystools/web/comfy/liteGraph.js
Normal file
30
custom_nodes/ComfyUI-Crystools/web/comfy/liteGraph.js
Normal file
@@ -0,0 +1,30 @@
|
||||
export class TLGraphNode extends LGraphNode {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
Object.defineProperty(this, "isVirtualNode", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "widgets_values", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "name", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "prototype", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
}
|
||||
}
|
||||
export const LiteGraph = window.LiteGraph;
|
||||
32
custom_nodes/ComfyUI-Crystools/web/comfy/liteGraph.ts
Normal file
32
custom_nodes/ComfyUI-Crystools/web/comfy/liteGraph.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// / <reference path="/types/litegraph.d.ts" />
|
||||
// A LOTS OF PATCHES FOR LITEGRAPH TYPES ¯\_(ツ)_/¯
|
||||
export type * from './liteGraph.types.js';
|
||||
|
||||
import type { IWidget as IWidgetOld, LGraphNode as TypeGraphNode, TypeLiteGraph } from './liteGraph.types.js';
|
||||
|
||||
declare const LGraphNode: typeof TypeGraphNode; // just for get the type
|
||||
|
||||
export interface IWidget extends IWidgetOld {
|
||||
onRemove?: () => void;
|
||||
serializeValue?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export class TLGraphNode extends LGraphNode {
|
||||
// on discovery...
|
||||
static category: string;
|
||||
static shape: number;
|
||||
static color: string;
|
||||
static bgcolor: string;
|
||||
static collapsable: boolean;
|
||||
|
||||
// widgets?: IWidget[];
|
||||
isVirtualNode?: boolean;
|
||||
// override onResize?: (size: [number, number]) => void;
|
||||
widgets_values?: any[];
|
||||
name?: string;
|
||||
|
||||
prototype: TLGraphNode; // yes itself
|
||||
}
|
||||
|
||||
// from globals
|
||||
export const LiteGraph: TypeLiteGraph = (window as any).LiteGraph;
|
||||
19
custom_nodes/ComfyUI-Crystools/web/comfy/liteGraph.types.d.ts
vendored
Normal file
19
custom_nodes/ComfyUI-Crystools/web/comfy/liteGraph.types.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
// A LOTS OF PATCHES FOR LITEGRAPH TYPES ¯\_(ツ)_/¯
|
||||
import { LGraph } from './typings/litegraph.js';
|
||||
|
||||
export type * from './typings/litegraph.js';
|
||||
|
||||
export declare type TypeLiteGraph = typeof LiteGraph & {
|
||||
graph: LGraph;
|
||||
};
|
||||
|
||||
// export declare type ComfyApi = typeof api;
|
||||
export declare type ComfyNode = any;
|
||||
|
||||
// I prefer not use global, but if I change of opinion:
|
||||
// / <reference path="./types.ts" />
|
||||
// import { LGraphNode as TLGraphNode, LiteGraph as TLiteGraph } from '/types/litegraph';
|
||||
// export declare const LGraphNode: typeof TLGraphNode;
|
||||
// declare global {
|
||||
// const LGraphNode: LGraphNode;
|
||||
// }
|
||||
5
custom_nodes/ComfyUI-Crystools/web/comfy/scripts.d.ts
vendored
Normal file
5
custom_nodes/ComfyUI-Crystools/web/comfy/scripts.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export { ComfyWidgets } from '../../../scripts/widgets.js';
|
||||
export { app } from '../../../scripts/app.js';
|
||||
export { api } from '../../../scripts/api.js';
|
||||
export * as utils from '../../../scripts/utils.js';
|
||||
export { ComfyButtonGroup } from '../../../scripts/ui/components/buttonGroup.js';
|
||||
5
custom_nodes/ComfyUI-Crystools/web/comfy/scripts.js
Normal file
5
custom_nodes/ComfyUI-Crystools/web/comfy/scripts.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export { ComfyWidgets } from '../../../scripts/widgets.js';
|
||||
export { app } from '../../../scripts/app.js';
|
||||
export { api } from '../../../scripts/api.js';
|
||||
export * as utils from '../../../scripts/utils.js';
|
||||
export { ComfyButtonGroup } from '../../../scripts/ui/components/buttonGroup.js';
|
||||
10
custom_nodes/ComfyUI-Crystools/web/comfy/scripts.ts
Normal file
10
custom_nodes/ComfyUI-Crystools/web/comfy/scripts.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// @ts-expect-error I could not find a way to make this work
|
||||
export { ComfyWidgets } from '../../../scripts/widgets.js';
|
||||
// @ts-expect-error I could not find a way to make this work
|
||||
export { app } from '../../../scripts/app.js';
|
||||
// @ts-expect-error I could not find a way to make this work
|
||||
export { api } from '../../../scripts/api.js';
|
||||
// @ts-expect-error I could not find a way to make this work
|
||||
export * as utils from '../../../scripts/utils.js';
|
||||
// @ts-expect-error I could not find a way to make this work
|
||||
export { ComfyButtonGroup } from '../../../scripts/ui/components/buttonGroup.js';
|
||||
@@ -0,0 +1,5 @@
|
||||
COPIED FROM rgthree
|
||||
|
||||
The typings in node_modules or in ComfyUI's web/ directory were not that well covered. These typings are hacked together with some of the inconsistencies I found.
|
||||
|
||||
To be honest, I have no idea why I needed a bizarre workaround for litegraph's types. Usually the '/// <reference>' comment should have picked up the types, but it wasn't having it. ¯\_(ツ)_/¯
|
||||
227
custom_nodes/ComfyUI-Crystools/web/comfy/typings/comfy.d.ts
vendored
Normal file
227
custom_nodes/ComfyUI-Crystools/web/comfy/typings/comfy.d.ts
vendored
Normal file
@@ -0,0 +1,227 @@
|
||||
import type { LGraphGroup as TLGraphGroup, LGraphNode as TLGraphNode, IWidget, SerializedLGraphNode, LGraph as TLGraph, LGraphCanvas as TLGraphCanvas, LiteGraph as TLiteGraph } from "./litegraph.js";
|
||||
import type {Constructor, SerializedGraph} from './index.js';
|
||||
|
||||
declare global {
|
||||
const LiteGraph: typeof TLiteGraph;
|
||||
const LGraph: typeof TLGraph;
|
||||
const LGraphNode: typeof TLGraphNode;
|
||||
const LGraphCanvas: typeof TLGraphCanvas;
|
||||
const LGraphGroup: typeof TLGraphGroup;
|
||||
}
|
||||
|
||||
// @rgthree: Types on ComfyApp as needed.
|
||||
export interface ComfyApp {
|
||||
extensions: ComfyExtension[];
|
||||
async queuePrompt(number?: number, batchCount = 1): Promise<void>;
|
||||
graph: TLGraph;
|
||||
canvas: TLGraphCanvas;
|
||||
clean() : void;
|
||||
registerExtension(extension: ComfyExtension): void;
|
||||
getPreviewFormatParam(): string;
|
||||
getRandParam(): string;
|
||||
loadApiJson(apiData: {}, fileName: string): void;
|
||||
async graphToPrompt(graph?: TLGraph, clean?: boolean): Promise<void>;
|
||||
// workflow: ComfyWorkflowInstance ???
|
||||
async loadGraphData(graphData: {}, clean?: boolean, restore_view?: boolean, workflow?: any|null): Promise<void>
|
||||
ui: {
|
||||
settings: {
|
||||
addSetting(config: {id: string, name: string, type: () => HTMLElement}) : void;
|
||||
}
|
||||
}
|
||||
// Just marking as any for now.
|
||||
menu?: any;
|
||||
}
|
||||
|
||||
export interface ComfyWidget extends IWidget {
|
||||
// https://github.com/comfyanonymous/ComfyUI/issues/2193 Changes from SerializedLGraphNode to
|
||||
// LGraphNode...
|
||||
serializeValue(nodeType: TLGraphNode, index: number): Promise<TValue>;
|
||||
afterQueued(): void;
|
||||
inputEl?: HTMLTextAreaElement;
|
||||
width: number;
|
||||
}
|
||||
|
||||
export interface ComfyGraphNode extends TLGraphNode {
|
||||
getExtraMenuOptions: (node: TLGraphNode, options: ContextMenuItem[]) => void;
|
||||
onExecuted(message: any): void;
|
||||
}
|
||||
|
||||
export interface ComfyNode extends TLGraphNode {
|
||||
comfyClass: string;
|
||||
}
|
||||
|
||||
// @rgthree
|
||||
export interface ComfyNodeConstructor extends Constructor<ComfyNode> {
|
||||
static title: string;
|
||||
static type?: string;
|
||||
static comfyClass: string;
|
||||
}
|
||||
|
||||
export type NodeMode = 0|1|2|3|4|undefined;
|
||||
|
||||
|
||||
export interface ComfyExtension {
|
||||
/**
|
||||
* The name of the extension
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Allows any initialisation, e.g. loading resources. Called after the canvas is created but before nodes are added
|
||||
* @param app The ComfyUI app instance
|
||||
*/
|
||||
init?(app: ComfyApp): Promise<void>;
|
||||
/**
|
||||
* Allows any additonal setup, called after the application is fully set up and running
|
||||
* @param app The ComfyUI app instance
|
||||
*/
|
||||
setup?(app: ComfyApp): Promise<void>;
|
||||
/**
|
||||
* Called before nodes are registered with the graph
|
||||
* @param defs The collection of node definitions, add custom ones or edit existing ones
|
||||
* @param app The ComfyUI app instance
|
||||
*/
|
||||
addCustomNodeDefs?(defs: Record<string, ComfyObjectInfo>, app: ComfyApp): Promise<void>;
|
||||
/**
|
||||
* Allows the extension to add custom widgets
|
||||
* @param app The ComfyUI app instance
|
||||
* @returns An array of {[widget name]: widget data}
|
||||
*/
|
||||
getCustomWidgets?(
|
||||
app: ComfyApp
|
||||
): Promise<
|
||||
Record<string, (node, inputName, inputData, app) => { widget?: IWidget; minWidth?: number; minHeight?: number }>
|
||||
>;
|
||||
/**
|
||||
* Allows the extension to add additional handling to the node before it is registered with LGraph
|
||||
* @rgthree changed nodeType from `typeof LGraphNode` to `ComfyNodeConstructor`
|
||||
* @param nodeType The node class (not an instance)
|
||||
* @param nodeData The original node object info config object
|
||||
* @param app The ComfyUI app instance
|
||||
*/
|
||||
beforeRegisterNodeDef?(nodeType: ComfyNodeConstructor, nodeData: ComfyObjectInfo, app: ComfyApp): Promise<void>;
|
||||
/**
|
||||
* Allows the extension to register additional nodes with LGraph after standard nodes are added
|
||||
* @param app The ComfyUI app instance
|
||||
*/
|
||||
// @rgthree - add void for non async
|
||||
registerCustomNodes?(app: ComfyApp): void|Promise<void>;
|
||||
/**
|
||||
* Allows the extension to modify a node that has been reloaded onto the graph.
|
||||
* If you break something in the backend and want to patch workflows in the frontend
|
||||
* This is the place to do this
|
||||
* @param node The node that has been loaded
|
||||
* @param app The ComfyUI app instance
|
||||
*/
|
||||
loadedGraphNode?(node: TLGraphNode, app: ComfyApp);
|
||||
/**
|
||||
* Allows the extension to run code after the constructor of the node
|
||||
* @param node The node that has been created
|
||||
* @param app The ComfyUI app instance
|
||||
*/
|
||||
nodeCreated?(node: TLGraphNode, app: ComfyApp);
|
||||
}
|
||||
|
||||
export type ComfyObjectInfo = {
|
||||
name: string;
|
||||
display_name?: string;
|
||||
description?: string;
|
||||
category: string;
|
||||
input?: {
|
||||
required?: Record<string, ComfyObjectInfoConfig>;
|
||||
optional?: Record<string, ComfyObjectInfoConfig>;
|
||||
hidden?: Record<string, ComfyObjectInfoConfig>;
|
||||
};
|
||||
output?: string[];
|
||||
output_name: string[];
|
||||
// @rgthree
|
||||
output_node?: boolean;
|
||||
};
|
||||
|
||||
export type ComfyObjectInfoConfig = [string | any[]] | [string | any[], any];
|
||||
|
||||
// @rgthree
|
||||
type ComfyApiInputLink = [
|
||||
/** The id string of the connected node. */
|
||||
string,
|
||||
/** The output index. */
|
||||
number,
|
||||
]
|
||||
|
||||
// @rgthree
|
||||
export type ComfyApiFormatNode = {
|
||||
"inputs": {
|
||||
[input_name: string]: string|number|boolean|ComfyApiInputLink,
|
||||
},
|
||||
"class_type": string,
|
||||
"_meta": {
|
||||
"title": string,
|
||||
}
|
||||
}
|
||||
|
||||
// @rgthree
|
||||
export type ComfyApiFormat = {
|
||||
[node_id: string]: ComfyApiFormatNode
|
||||
}
|
||||
|
||||
// @rgthree
|
||||
export type ComfyApiPrompt = {
|
||||
workflow: SerializedGraph,
|
||||
output: ComfyApiFormat,
|
||||
}
|
||||
|
||||
// @rgthree
|
||||
export type ComfyApiEventDetailStatus = {
|
||||
exec_info: {
|
||||
queue_remaining: number;
|
||||
};
|
||||
};
|
||||
|
||||
// @rgthree
|
||||
export type ComfyApiEventDetailExecutionStart = {
|
||||
prompt_id: string;
|
||||
};
|
||||
|
||||
// @rgthree
|
||||
export type ComfyApiEventDetailExecuting = null | string;
|
||||
|
||||
// @rgthree
|
||||
export type ComfyApiEventDetailProgress = {
|
||||
node: string;
|
||||
prompt_id: string;
|
||||
max: number;
|
||||
value: number;
|
||||
};
|
||||
|
||||
// @rgthree
|
||||
export type ComfyApiEventDetailExecuted = {
|
||||
node: string;
|
||||
prompt_id: string;
|
||||
output: any;
|
||||
};
|
||||
|
||||
// @rgthree
|
||||
export type ComfyApiEventDetailCached = {
|
||||
nodes: string[];
|
||||
prompt_id: string;
|
||||
};
|
||||
|
||||
// @rgthree
|
||||
export type ComfyApiEventDetailExecuted = {
|
||||
prompt_id: string;
|
||||
node: string;
|
||||
output: any;
|
||||
};
|
||||
|
||||
// @rgthree
|
||||
export type ComfyApiEventDetailError = {
|
||||
prompt_id: string;
|
||||
exception_type: string;
|
||||
exception_message: string;
|
||||
node_id: string;
|
||||
node_type: string;
|
||||
node_id: string;
|
||||
traceback: string;
|
||||
executed: any[];
|
||||
current_inputs: {[key: string]: (number[]|string[])};
|
||||
current_outputs: {[key: string]: (number[]|string[])};
|
||||
}
|
||||
55
custom_nodes/ComfyUI-Crystools/web/comfy/typings/index.d.ts
vendored
Normal file
55
custom_nodes/ComfyUI-Crystools/web/comfy/typings/index.d.ts
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
import { LGraph } from "./litegraph.js";
|
||||
|
||||
export type Constructor<T> = new(...args: any[]) => T;
|
||||
|
||||
export type SerializedLink = [
|
||||
number, // this.id,
|
||||
number, // this.origin_id,
|
||||
number, // this.origin_slot,
|
||||
number, // this.target_id,
|
||||
number, // this.target_slot,
|
||||
string, // this.type
|
||||
];
|
||||
|
||||
export interface SerializedNodeInput {
|
||||
name: string;
|
||||
type: string;
|
||||
link: number;
|
||||
}
|
||||
export interface SerializedNodeOutput {
|
||||
name: string;
|
||||
type: string;
|
||||
link: number;
|
||||
slot_index: number;
|
||||
links: number[];
|
||||
}
|
||||
export interface SerializedNode {
|
||||
id: number;
|
||||
inputs: SerializedNodeInput[];
|
||||
outputs: SerializedNodeOutput[];
|
||||
mode: number;
|
||||
order: number;
|
||||
pos: [number, number];
|
||||
properties: any;
|
||||
size: [number, number];
|
||||
type: string;
|
||||
widgets_values: Array<number | string>;
|
||||
}
|
||||
|
||||
export interface SerializedGraph {
|
||||
config: any;
|
||||
extra: any;
|
||||
groups: any;
|
||||
last_link_id: number;
|
||||
last_node_id: number;
|
||||
links: SerializedLink[];
|
||||
nodes: SerializedNode[];
|
||||
}
|
||||
|
||||
export interface BadLinksData<T = SerializedGraph|LGraph> {
|
||||
hasBadLinks: boolean;
|
||||
fixed: boolean;
|
||||
graph: T;
|
||||
patched: number;
|
||||
deleted: number;
|
||||
}
|
||||
1750
custom_nodes/ComfyUI-Crystools/web/comfy/typings/litegraph.d.ts
vendored
Normal file
1750
custom_nodes/ComfyUI-Crystools/web/comfy/typings/litegraph.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
4
custom_nodes/ComfyUI-Crystools/web/common.d.ts
vendored
Normal file
4
custom_nodes/ComfyUI-Crystools/web/common.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import type { TLGraphNode } from './comfy/index.js';
|
||||
import { ComfyApp } from './comfy/index.js';
|
||||
export declare const commonPrefix = "\uD83E\uDE9B";
|
||||
export declare function displayContext(nodeType: TLGraphNode, appFromArg: ComfyApp, index?: number, serialize_widgets?: boolean, isVirtualNode?: boolean): void;
|
||||
50
custom_nodes/ComfyUI-Crystools/web/common.js
Normal file
50
custom_nodes/ComfyUI-Crystools/web/common.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { ComfyWidgets } from './comfy/index.js';
|
||||
export const commonPrefix = '🪛';
|
||||
export function displayContext(nodeType, appFromArg, index = 0, serialize_widgets = false, isVirtualNode = false) {
|
||||
function populate(text) {
|
||||
if (this.widgets) {
|
||||
const pos = this.widgets.findIndex((w) => w.name === 'text');
|
||||
if (pos !== -1) {
|
||||
for (let i = pos; i < this.widgets.length; i++) {
|
||||
this.widgets[i]?.onRemove?.();
|
||||
}
|
||||
this.widgets.length = pos;
|
||||
}
|
||||
}
|
||||
this.serialize_widgets = serialize_widgets;
|
||||
this.isVirtualNode = isVirtualNode;
|
||||
const widget = ComfyWidgets.STRING(this, 'text', [
|
||||
'STRING', { multiline: true },
|
||||
], appFromArg).widget;
|
||||
widget.inputEl.readOnly = true;
|
||||
widget.inputEl.style.opacity = 0.6;
|
||||
if (Array.isArray(text) && index !== undefined && text[index] !== undefined) {
|
||||
text = text[index];
|
||||
}
|
||||
widget.value = text || '';
|
||||
widget.serializeValue = async () => { };
|
||||
requestAnimationFrame(() => {
|
||||
const sz = this.computeSize();
|
||||
if (sz[0] < this.size[0]) {
|
||||
sz[0] = this.size[0];
|
||||
}
|
||||
if (sz[1] < this.size[1]) {
|
||||
sz[1] = this.size[1];
|
||||
}
|
||||
this.onResize?.(sz);
|
||||
appFromArg.graph.setDirtyCanvas(true, false);
|
||||
});
|
||||
}
|
||||
const onExecutedOriginal = nodeType.prototype.onExecuted;
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
onExecutedOriginal?.apply(this, arguments);
|
||||
populate.call(this, message.text);
|
||||
};
|
||||
const onConfigureOriginal = nodeType.prototype.onConfigure;
|
||||
nodeType.prototype.onConfigure = function () {
|
||||
onConfigureOriginal?.apply(this, arguments);
|
||||
if (this.widgets_values?.length) {
|
||||
populate.call(this, this.widgets_values);
|
||||
}
|
||||
};
|
||||
}
|
||||
92
custom_nodes/ComfyUI-Crystools/web/common.ts
Normal file
92
custom_nodes/ComfyUI-Crystools/web/common.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { TLGraphNode } from './comfy/index.js';
|
||||
import { ComfyWidgets, ComfyApp } from './comfy/index.js';
|
||||
|
||||
export const commonPrefix = '🪛';
|
||||
|
||||
export function displayContext(
|
||||
nodeType: TLGraphNode,
|
||||
appFromArg: ComfyApp,
|
||||
index = 0, serialize_widgets = false, isVirtualNode = false,
|
||||
): void {
|
||||
function populate(this: TLGraphNode, text: string | string[]): void {
|
||||
if (this.widgets) {
|
||||
const pos = this.widgets.findIndex((w) => w.name === 'text');
|
||||
if (pos !== -1) {
|
||||
for (let i = pos; i < this.widgets.length; i++) {
|
||||
|
||||
this.widgets[i]?.onRemove?.();
|
||||
}
|
||||
this.widgets.length = pos;
|
||||
}
|
||||
}
|
||||
// If you want to do not save properties in the node (be careful with F5)
|
||||
// BUG on isVirtualNode, with "true", it ignores OUTPUT_NODE on py file!
|
||||
this.serialize_widgets = serialize_widgets;
|
||||
this.isVirtualNode = isVirtualNode;
|
||||
|
||||
const widget = ComfyWidgets.STRING(this, 'text', [
|
||||
'STRING', { multiline: true },
|
||||
], appFromArg).widget;
|
||||
widget.inputEl.readOnly = true;
|
||||
widget.inputEl.style.opacity = 0.6;
|
||||
if (Array.isArray(text) && index !== undefined && text[index] !== undefined) {
|
||||
// @ts-ignore
|
||||
text = text[index];
|
||||
}
|
||||
widget.value = text || '';
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
widget.serializeValue = async(): Promise<void> => {}; // just for no serialized to itself!
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const sz = this.computeSize();
|
||||
if (sz[0] < this.size[0]) {
|
||||
sz[0] = this.size[0];
|
||||
}
|
||||
if (sz[1] < this.size[1]) {
|
||||
sz[1] = this.size[1];
|
||||
}
|
||||
this.onResize?.(sz);
|
||||
appFromArg.graph.setDirtyCanvas(true, false);
|
||||
});
|
||||
}
|
||||
|
||||
// When the node is executed we will be sent the input text, display this in the widget
|
||||
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
const onExecutedOriginal = nodeType.prototype.onExecuted;
|
||||
nodeType.prototype.onExecuted = function(message: { text: string }): void {
|
||||
// @ts-ignore
|
||||
onExecutedOriginal?.apply(this, arguments);
|
||||
populate.call(this, message.text);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
const onConfigureOriginal = nodeType.prototype.onConfigure;
|
||||
nodeType.prototype.onConfigure = function(): void {
|
||||
// @ts-ignore
|
||||
onConfigureOriginal?.apply(this, arguments);
|
||||
if (this.widgets_values?.length) {
|
||||
populate.call(this, this.widgets_values);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// propagate the output value to the dependents nodes, it does not work with some nodes ¯\_(ツ)_/¯
|
||||
// const propagateOutputToDependentsNodes = function(output: serializedLGraph, value: string) {
|
||||
// if (output.links?.length) {
|
||||
// for (const l of output.links) {
|
||||
// const link_info = app.graph.links[l];
|
||||
// const outNode = app.graph.getNodeById(link_info.target_id);
|
||||
// const outIn = outNode?.inputs?.[link_info.target_slot];
|
||||
// if (outIn.widget) {
|
||||
// const widget = outNode.widgets.find((w: IWidget) => w.name === outIn.widget.name);
|
||||
// if (!widget) {
|
||||
// continue;
|
||||
// }
|
||||
// widget.value = value;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
1
custom_nodes/ComfyUI-Crystools/web/debugger.d.ts
vendored
Normal file
1
custom_nodes/ComfyUI-Crystools/web/debugger.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
77
custom_nodes/ComfyUI-Crystools/web/debugger.js
Normal file
77
custom_nodes/ComfyUI-Crystools/web/debugger.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import { app, api, ComfyWidgets, LiteGraph, TLGraphNode } from './comfy/index.js';
|
||||
import { commonPrefix, displayContext } from './common.js';
|
||||
app.registerExtension({
|
||||
name: 'Crystools.Debugger.ConsoleAny',
|
||||
beforeRegisterNodeDef(nodeType, nodeData, appFromArg) {
|
||||
if (nodeData.name === 'Show any [Crystools]') {
|
||||
displayContext(nodeType, appFromArg, 3);
|
||||
}
|
||||
},
|
||||
});
|
||||
app.registerExtension({
|
||||
name: 'Crystools.Debugger.Metadata',
|
||||
registerCustomNodes() {
|
||||
class MetadataNode extends TLGraphNode {
|
||||
constructor() {
|
||||
super(`${commonPrefix} Show Metadata `);
|
||||
Object.defineProperty(this, "fillMetadataWidget", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => {
|
||||
return app.graphToPrompt()
|
||||
.then((workflow) => {
|
||||
let result = 'inactive';
|
||||
if (this.widgets?.length !== 4) {
|
||||
console.error('Something is wrong with the widgets, should be 4!');
|
||||
return 'error';
|
||||
}
|
||||
const output = this.widgets[0];
|
||||
const active = this.widgets[1]?.value;
|
||||
const parsed = this.widgets[2]?.value;
|
||||
let what = this.widgets[3]?.value.toLowerCase();
|
||||
if (active) {
|
||||
what = what === 'prompt' ? 'output' : what;
|
||||
result = workflow[what];
|
||||
if (parsed) {
|
||||
result = JSON.stringify(result, null, 2);
|
||||
}
|
||||
else {
|
||||
result = JSON.stringify(result);
|
||||
}
|
||||
}
|
||||
if (output) {
|
||||
output.value = result;
|
||||
}
|
||||
else {
|
||||
console.error('Something is wrong with the widgets, output is undefined!');
|
||||
return 'error';
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
});
|
||||
this.serialize_widgets = false;
|
||||
this.isVirtualNode = true;
|
||||
const widget = ComfyWidgets.STRING(this, '', [
|
||||
'', { default: '', multiline: true },
|
||||
], app).widget;
|
||||
widget.inputEl.readOnly = true;
|
||||
ComfyWidgets.BOOLEAN(this, 'Active', [
|
||||
'', { default: true },
|
||||
]);
|
||||
ComfyWidgets.BOOLEAN(this, 'Parsed', [
|
||||
'', { default: true },
|
||||
]);
|
||||
ComfyWidgets.COMBO(this, 'What', [
|
||||
['Prompt', 'Workflow'], { default: 'Prompt' },
|
||||
]);
|
||||
api.addEventListener('executed', this.fillMetadataWidget, false);
|
||||
}
|
||||
}
|
||||
LiteGraph.registerNodeType('Show Metadata [Crystools]', MetadataNode);
|
||||
MetadataNode.category = `crystools ${commonPrefix}/Debugger`;
|
||||
MetadataNode.shape = LiteGraph.BOX_SHAPE;
|
||||
MetadataNode.title = `${commonPrefix} Show Metadata`;
|
||||
},
|
||||
});
|
||||
86
custom_nodes/ComfyUI-Crystools/web/debugger.ts
Normal file
86
custom_nodes/ComfyUI-Crystools/web/debugger.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { ComfyNode } from './comfy/index.js';
|
||||
import { app, api, ComfyWidgets, LiteGraph, TLGraphNode, ComfyApp } from './comfy/index.js';
|
||||
import { commonPrefix, displayContext } from './common.js';
|
||||
|
||||
// "Show any" Node
|
||||
app.registerExtension({
|
||||
name: 'Crystools.Debugger.ConsoleAny',
|
||||
beforeRegisterNodeDef(nodeType: ComfyNode, nodeData: TLGraphNode, appFromArg: ComfyApp) {
|
||||
if (nodeData.name === 'Show any [Crystools]') {
|
||||
// 3 is the index of the text field in the node
|
||||
displayContext(nodeType, appFromArg, 3);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Crystools.Debugger.Metadata',
|
||||
registerCustomNodes() {
|
||||
class MetadataNode extends TLGraphNode {
|
||||
constructor() {
|
||||
super(`${commonPrefix} Show Metadata `);
|
||||
this.serialize_widgets = false;
|
||||
this.isVirtualNode = true;
|
||||
|
||||
const widget = ComfyWidgets.STRING(this, '', [
|
||||
'', {default: '', multiline: true},
|
||||
], app).widget;
|
||||
widget.inputEl.readOnly = true;
|
||||
ComfyWidgets.BOOLEAN(this, 'Active', [
|
||||
'', {default: true},
|
||||
]);
|
||||
ComfyWidgets.BOOLEAN(this, 'Parsed', [
|
||||
'', {default: true},
|
||||
]);
|
||||
ComfyWidgets.COMBO(this, 'What', [
|
||||
['Prompt', 'Workflow'], {default: 'Prompt'},
|
||||
]);
|
||||
|
||||
// It runs at finish on each prompt queue
|
||||
api.addEventListener('executed', this.fillMetadataWidget, false);
|
||||
}
|
||||
|
||||
fillMetadataWidget = (): Promise<string> => {
|
||||
return app.graphToPrompt()
|
||||
.then((workflow: any): string => {
|
||||
let result = 'inactive';
|
||||
if (this.widgets?.length !== 4) {
|
||||
console.error('Something is wrong with the widgets, should be 4!');
|
||||
return 'error';
|
||||
}
|
||||
const output = this.widgets[0];
|
||||
const active = this.widgets[1]?.value;
|
||||
const parsed = this.widgets[2]?.value;
|
||||
let what = this.widgets[3]?.value.toLowerCase();
|
||||
|
||||
if (active) {
|
||||
what = what === 'prompt' ? 'output' : what; // little fix for better understanding
|
||||
// @ts-ignore
|
||||
result = workflow[what];
|
||||
if (parsed) {
|
||||
result = JSON.stringify(result, null, 2);
|
||||
} else {
|
||||
result = JSON.stringify(result);
|
||||
}
|
||||
}
|
||||
if (output) {
|
||||
output.value = result;
|
||||
} else {
|
||||
console.error('Something is wrong with the widgets, output is undefined!');
|
||||
return 'error';
|
||||
}
|
||||
return result;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// I'm not sure for what they're using prototype and lots of black magic, don't change the order!
|
||||
LiteGraph.registerNodeType('Show Metadata [Crystools]', MetadataNode);
|
||||
MetadataNode.category = `crystools ${commonPrefix}/Debugger`;
|
||||
MetadataNode.shape = LiteGraph.BOX_SHAPE;
|
||||
MetadataNode.title = `${commonPrefix} Show Metadata`;
|
||||
// MetadataNode.collapsable = false;
|
||||
// MetadataNode.color = '#FF2222';
|
||||
// MetadataNode.bgcolor = '#000000';
|
||||
},
|
||||
});
|
||||
1
custom_nodes/ComfyUI-Crystools/web/extensions.d.ts
vendored
Normal file
1
custom_nodes/ComfyUI-Crystools/web/extensions.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
32
custom_nodes/ComfyUI-Crystools/web/extensions.js
Normal file
32
custom_nodes/ComfyUI-Crystools/web/extensions.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { app } from './comfy/index.js';
|
||||
import { displayContext } from './common.js';
|
||||
const crystoolsExtensionsSerialized = {
|
||||
'Read JSON file [Crystools]': 'Crystools.Utils.ReadJsonFile',
|
||||
'JSON extractor [Crystools]': 'Crystools.Utils.JsonExtractor',
|
||||
};
|
||||
const crystoolsExtensions = {
|
||||
'Get resolution [Crystools]': 'Crystools.Image.GetResolution',
|
||||
'Preview from image [Crystools]': 'Crystools.Image.PreviewFromImage',
|
||||
'Preview from metadata [Crystools]': 'Crystools.Image.PreviewFromMetadata',
|
||||
'Metadata comparator [Crystools]': 'Crystools.Metadata.MetadataComparator',
|
||||
'Stats system [Crystools]': 'Crystools.Utils.StatsSystem',
|
||||
'Show any to JSON [Crystools]': 'Crystools.Debugger.ConsoleAnyToJson',
|
||||
};
|
||||
Object.keys(crystoolsExtensionsSerialized).forEach(prop => {
|
||||
crystoolsExtensions[prop] = crystoolsExtensionsSerialized[prop];
|
||||
});
|
||||
Object.keys(crystoolsExtensions).forEach(key => {
|
||||
app.registerExtension({
|
||||
name: crystoolsExtensions[key],
|
||||
beforeRegisterNodeDef(nodeType, nodeData, appFromArg) {
|
||||
if (nodeData.name === key) {
|
||||
if (nodeData.name in crystoolsExtensionsSerialized) {
|
||||
displayContext(nodeType, appFromArg, 0, true);
|
||||
}
|
||||
else {
|
||||
displayContext(nodeType, appFromArg, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
38
custom_nodes/ComfyUI-Crystools/web/extensions.ts
Normal file
38
custom_nodes/ComfyUI-Crystools/web/extensions.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { app, TLGraphNode, ComfyApp } from './comfy/index.js';
|
||||
import type { ComfyNode } from './comfy/index.js';
|
||||
import { displayContext } from './common.js';
|
||||
|
||||
const crystoolsExtensionsSerialized: Record<string, string> = {
|
||||
// 'External parameter from JSON file [Crystools]': 'Crystools.Utils.ExternalParameterFromJson',
|
||||
'Read JSON file [Crystools]': 'Crystools.Utils.ReadJsonFile',
|
||||
'JSON extractor [Crystools]': 'Crystools.Utils.JsonExtractor',
|
||||
};
|
||||
|
||||
const crystoolsExtensions: Record<string, string> = {
|
||||
'Get resolution [Crystools]': 'Crystools.Image.GetResolution',
|
||||
'Preview from image [Crystools]': 'Crystools.Image.PreviewFromImage',
|
||||
'Preview from metadata [Crystools]': 'Crystools.Image.PreviewFromMetadata',
|
||||
'Metadata comparator [Crystools]': 'Crystools.Metadata.MetadataComparator',
|
||||
'Stats system [Crystools]': 'Crystools.Utils.StatsSystem',
|
||||
'Show any to JSON [Crystools]': 'Crystools.Debugger.ConsoleAnyToJson',
|
||||
};
|
||||
|
||||
Object.keys(crystoolsExtensionsSerialized).forEach(prop => {
|
||||
// @ts-ignore
|
||||
crystoolsExtensions[prop] = crystoolsExtensionsSerialized[prop];
|
||||
});
|
||||
|
||||
Object.keys(crystoolsExtensions).forEach(key => {
|
||||
app.registerExtension({
|
||||
name: crystoolsExtensions[key],
|
||||
beforeRegisterNodeDef(nodeType: ComfyNode, nodeData: TLGraphNode, appFromArg: ComfyApp) {
|
||||
if (nodeData.name === key) {
|
||||
if (nodeData.name in crystoolsExtensionsSerialized) {
|
||||
displayContext(nodeType, appFromArg, 0, true); // serialize_widgets = true
|
||||
} else {
|
||||
displayContext(nodeType, appFromArg, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
188
custom_nodes/ComfyUI-Crystools/web/monitor.css
Normal file
188
custom_nodes/ComfyUI-Crystools/web/monitor.css
Normal file
@@ -0,0 +1,188 @@
|
||||
/* Vertical UI */
|
||||
.comfy-menu, .side-bar-panel {
|
||||
#crystools-monitors-root {
|
||||
/*background-color: red;*/
|
||||
box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.3);
|
||||
background-color: var(--comfy-menu-bg);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
margin: 7px;
|
||||
padding: 5px 0;
|
||||
cursor: crosshair;
|
||||
|
||||
/* MONITORS */
|
||||
|
||||
.crystools-monitor {
|
||||
margin: 1px 10px;
|
||||
height: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.crystools-text {
|
||||
width: 40px;
|
||||
font-size: 10px;
|
||||
text-align: right;
|
||||
margin-right: 3px;
|
||||
|
||||
&:hover {
|
||||
font-weight: 600;
|
||||
color: var(--input-text);
|
||||
}
|
||||
}
|
||||
|
||||
.crystools-label {
|
||||
&:hover {
|
||||
font-weight: 700;
|
||||
color: var(--input-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.crystools-content {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
background-color: var(--content-hover-bg)
|
||||
}
|
||||
|
||||
.crystools-slider {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 0;
|
||||
box-shadow: inset 2px 2px 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.crystools-label {
|
||||
position: relative;
|
||||
color: var(--input-text);
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* PROGRESS BAR */
|
||||
|
||||
#crystools-progressBar-root {
|
||||
cursor: pointer;
|
||||
margin: 0 12px 4px;
|
||||
height: 18px;
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
background-color: var(--comfy-input-bg);
|
||||
|
||||
.crystools-slider {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
transition: width 0.2s;
|
||||
background-color: green;
|
||||
box-shadow: inset 2px 2px 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.crystools-label {
|
||||
position: absolute;
|
||||
margin: auto 0;
|
||||
width: 100%;
|
||||
color: var(--input-text);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* side-bar-panel */
|
||||
.side-bar-panel {
|
||||
#crystools-monitors-root {
|
||||
width: 230px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.comfyui-queue-button {
|
||||
/*width: unset !important;*/
|
||||
}
|
||||
|
||||
/* Horizontal UI */
|
||||
#crystools-monitors-root {
|
||||
/*background-color: red;*/
|
||||
/*display: none;*/
|
||||
flex-direction: row;
|
||||
/*flex-grow: 5;*/
|
||||
justify-content: flex-end;
|
||||
/*flex: auto;*/
|
||||
flex-shrink: 1;
|
||||
/*min-width: max-content;*/
|
||||
/*height: 100%;*/
|
||||
/* max-width: 40vw;*/
|
||||
/* min-width: 10vw;*/
|
||||
/*gap: 5px;*/
|
||||
/*margin: 0 auto;*/
|
||||
/*align-items: center;*/
|
||||
/*align-self: center;*/
|
||||
/*width: min-content;*/
|
||||
/*width: 100%;*/
|
||||
cursor: crosshair;
|
||||
/*margin: 4px 0;*/
|
||||
display: flex; /* by progress bar base */
|
||||
flex-wrap: wrap;
|
||||
/*flex-direction: row;*/
|
||||
/*justify-content: flex-end;*/
|
||||
/*align-items: center;*/
|
||||
gap: 5px;
|
||||
align-self: center;
|
||||
|
||||
.crystools-monitor {
|
||||
background-color: var(--comfy-input-bg);
|
||||
position: relative;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
/*width: 60px;*/
|
||||
/*height: 100%;*/
|
||||
|
||||
.crystools-text {
|
||||
font-size: 10px;
|
||||
text-align: right;
|
||||
margin-left: 3px;
|
||||
position: absolute;
|
||||
font-weight: 100;
|
||||
bottom: 2px;
|
||||
z-index: 10;
|
||||
|
||||
/*&:hover {*/
|
||||
/* font-weight: 600;*/
|
||||
/* color: var(--input-text);*/
|
||||
/*}*/
|
||||
}
|
||||
|
||||
/*.crystools-label {*/
|
||||
/* &:hover {*/
|
||||
/* font-weight: 800;*/
|
||||
/* color: var(--input-text);*/
|
||||
/* }*/
|
||||
/*}*/
|
||||
}
|
||||
|
||||
.crystools-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.crystools-slider {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 0;
|
||||
box-shadow: inset 2px 2px 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.crystools-label {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
color: var(--input-text);
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
right: 2px;
|
||||
top: 2px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
1
custom_nodes/ComfyUI-Crystools/web/monitor.d.ts
vendored
Normal file
1
custom_nodes/ComfyUI-Crystools/web/monitor.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
656
custom_nodes/ComfyUI-Crystools/web/monitor.js
Normal file
656
custom_nodes/ComfyUI-Crystools/web/monitor.js
Normal file
@@ -0,0 +1,656 @@
|
||||
import { app, api, ComfyButtonGroup } from './comfy/index.js';
|
||||
import { commonPrefix } from './common.js';
|
||||
import { MonitorUI } from './monitorUI.js';
|
||||
import { Colors } from './styles.js';
|
||||
import { convertNumberToPascalCase } from './utils.js';
|
||||
import { ComfyKeyMenuDisplayOption, MenuDisplayOptions } from './progressBarUIBase.js';
|
||||
class CrystoolsMonitor {
|
||||
constructor() {
|
||||
Object.defineProperty(this, "idExtensionName", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: 'Crystools.monitor'
|
||||
});
|
||||
Object.defineProperty(this, "menuPrefix", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: commonPrefix
|
||||
});
|
||||
Object.defineProperty(this, "menuDisplayOption", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: MenuDisplayOptions.Disabled
|
||||
});
|
||||
Object.defineProperty(this, "crystoolsButtonGroup", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: null
|
||||
});
|
||||
Object.defineProperty(this, "settingsRate", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "settingsMonitorHeight", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "settingsMonitorWidth", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "monitorCPUElement", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "monitorRAMElement", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "monitorHDDElement", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "settingsHDD", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "monitorGPUSettings", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: []
|
||||
});
|
||||
Object.defineProperty(this, "monitorVRAMSettings", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: []
|
||||
});
|
||||
Object.defineProperty(this, "monitorTemperatureSettings", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: []
|
||||
});
|
||||
Object.defineProperty(this, "monitorUI", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "monitorWidthId", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: 'Crystools.MonitorWidth'
|
||||
});
|
||||
Object.defineProperty(this, "monitorWidth", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: 60
|
||||
});
|
||||
Object.defineProperty(this, "monitorHeightId", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: 'Crystools.MonitorHeight'
|
||||
});
|
||||
Object.defineProperty(this, "monitorHeight", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: 30
|
||||
});
|
||||
Object.defineProperty(this, "createSettingsRate", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => {
|
||||
this.settingsRate = {
|
||||
id: 'Crystools.RefreshRate',
|
||||
name: 'Refresh per second',
|
||||
category: ['Crystools', this.menuPrefix + ' Configuration', 'refresh'],
|
||||
tooltip: 'This is the time (in seconds) between each update of the monitors, 0 means no refresh',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 0,
|
||||
max: 2,
|
||||
step: .25,
|
||||
},
|
||||
defaultValue: .5,
|
||||
onChange: async (value) => {
|
||||
let valueNumber;
|
||||
try {
|
||||
valueNumber = parseFloat(value);
|
||||
if (isNaN(valueNumber)) {
|
||||
throw new Error('invalid value');
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.updateServer({ rate: valueNumber });
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
const data = {
|
||||
cpu_utilization: 0,
|
||||
device: 'cpu',
|
||||
gpus: [
|
||||
{
|
||||
gpu_utilization: 0,
|
||||
gpu_temperature: 0,
|
||||
vram_total: 0,
|
||||
vram_used: 0,
|
||||
vram_used_percent: 0,
|
||||
},
|
||||
],
|
||||
hdd_total: 0,
|
||||
hdd_used: 0,
|
||||
hdd_used_percent: 0,
|
||||
ram_total: 0,
|
||||
ram_used: 0,
|
||||
ram_used_percent: 0,
|
||||
};
|
||||
if (valueNumber === 0) {
|
||||
this.monitorUI.updateDisplay(data);
|
||||
}
|
||||
this.monitorUI?.updateAllAnimationDuration(valueNumber);
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "createSettingsMonitorWidth", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => {
|
||||
this.settingsMonitorWidth = {
|
||||
id: this.monitorWidthId,
|
||||
name: 'Pixel Width',
|
||||
category: ['Crystools', this.menuPrefix + ' Configuration', 'width'],
|
||||
tooltip: 'The width of the monitor in pixels on the UI (only on top/bottom UI)',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 60,
|
||||
max: 100,
|
||||
step: 1,
|
||||
},
|
||||
defaultValue: this.monitorWidth,
|
||||
onChange: (value) => {
|
||||
let valueNumber;
|
||||
try {
|
||||
valueNumber = parseInt(value);
|
||||
if (isNaN(valueNumber)) {
|
||||
throw new Error('invalid value');
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
const h = app.extensionManager.setting.get(this.monitorHeightId);
|
||||
this.monitorUI?.updateMonitorSize(valueNumber, h);
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "createSettingsMonitorHeight", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => {
|
||||
this.settingsMonitorHeight = {
|
||||
id: this.monitorHeightId,
|
||||
name: 'Pixel Height',
|
||||
category: ['Crystools', this.menuPrefix + ' Configuration', 'height'],
|
||||
tooltip: 'The height of the monitor in pixels on the UI (only on top/bottom UI)',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 16,
|
||||
max: 50,
|
||||
step: 1,
|
||||
},
|
||||
defaultValue: this.monitorHeight,
|
||||
onChange: async (value) => {
|
||||
let valueNumber;
|
||||
try {
|
||||
valueNumber = parseInt(value);
|
||||
if (isNaN(valueNumber)) {
|
||||
throw new Error('invalid value');
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
const w = await app.extensionManager.setting.get(this.monitorWidthId);
|
||||
this.monitorUI?.updateMonitorSize(w, valueNumber);
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "createSettingsCPU", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => {
|
||||
this.monitorCPUElement = {
|
||||
id: 'Crystools.ShowCpu',
|
||||
name: 'CPU Usage',
|
||||
category: ['Crystools', this.menuPrefix + ' Hardware', 'Cpu'],
|
||||
type: 'boolean',
|
||||
label: 'CPU',
|
||||
symbol: '%',
|
||||
defaultValue: true,
|
||||
htmlMonitorRef: undefined,
|
||||
htmlMonitorSliderRef: undefined,
|
||||
htmlMonitorLabelRef: undefined,
|
||||
cssColor: Colors.CPU,
|
||||
onChange: async (value) => {
|
||||
await this.updateServer({ switchCPU: value });
|
||||
this.updateWidget(this.monitorCPUElement);
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "createSettingsRAM", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => {
|
||||
this.monitorRAMElement = {
|
||||
id: 'Crystools.ShowRam',
|
||||
name: 'RAM Used',
|
||||
category: ['Crystools', this.menuPrefix + ' Hardware', 'Ram'],
|
||||
type: 'boolean',
|
||||
label: 'RAM',
|
||||
symbol: '%',
|
||||
defaultValue: true,
|
||||
htmlMonitorRef: undefined,
|
||||
htmlMonitorSliderRef: undefined,
|
||||
htmlMonitorLabelRef: undefined,
|
||||
cssColor: Colors.RAM,
|
||||
onChange: async (value) => {
|
||||
await this.updateServer({ switchRAM: value });
|
||||
this.updateWidget(this.monitorRAMElement);
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "createSettingsGPUUsage", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: (name, index, moreThanOneGPU) => {
|
||||
if (name === undefined || index === undefined) {
|
||||
console.warn('getGPUsFromServer: name or index undefined', name, index);
|
||||
return;
|
||||
}
|
||||
let label = 'GPU ';
|
||||
label += moreThanOneGPU ? index : '';
|
||||
const monitorGPUNElement = {
|
||||
id: 'Crystools.ShowGpuUsage' + convertNumberToPascalCase(index),
|
||||
name: ' Usage',
|
||||
category: ['Crystools', `${this.menuPrefix} Show GPU [${index}] ${name}`, 'Usage'],
|
||||
type: 'boolean',
|
||||
label,
|
||||
symbol: '%',
|
||||
monitorTitle: `${index}: ${name}`,
|
||||
defaultValue: true,
|
||||
htmlMonitorRef: undefined,
|
||||
htmlMonitorSliderRef: undefined,
|
||||
htmlMonitorLabelRef: undefined,
|
||||
cssColor: Colors.GPU,
|
||||
onChange: async (value) => {
|
||||
await this.updateServerGPU(index, { utilization: value });
|
||||
this.updateWidget(monitorGPUNElement);
|
||||
},
|
||||
};
|
||||
this.monitorGPUSettings[index] = monitorGPUNElement;
|
||||
app.ui.settings.addSetting(this.monitorGPUSettings[index]);
|
||||
this.monitorUI.createDOMGPUMonitor(this.monitorGPUSettings[index]);
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "createSettingsGPUVRAM", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: (name, index, moreThanOneGPU) => {
|
||||
if (name === undefined || index === undefined) {
|
||||
console.warn('getGPUsFromServer: name or index undefined', name, index);
|
||||
return;
|
||||
}
|
||||
let label = 'VRAM ';
|
||||
label += moreThanOneGPU ? index : '';
|
||||
const monitorVRAMNElement = {
|
||||
id: 'Crystools.ShowGpuVram' + convertNumberToPascalCase(index),
|
||||
name: 'VRAM',
|
||||
category: ['Crystools', `${this.menuPrefix} Show GPU [${index}] ${name}`, 'VRAM'],
|
||||
type: 'boolean',
|
||||
label: label,
|
||||
symbol: '%',
|
||||
monitorTitle: `${index}: ${name}`,
|
||||
defaultValue: true,
|
||||
htmlMonitorRef: undefined,
|
||||
htmlMonitorSliderRef: undefined,
|
||||
htmlMonitorLabelRef: undefined,
|
||||
cssColor: Colors.VRAM,
|
||||
onChange: async (value) => {
|
||||
await this.updateServerGPU(index, { vram: value });
|
||||
this.updateWidget(monitorVRAMNElement);
|
||||
},
|
||||
};
|
||||
this.monitorVRAMSettings[index] = monitorVRAMNElement;
|
||||
app.ui.settings.addSetting(this.monitorVRAMSettings[index]);
|
||||
this.monitorUI.createDOMGPUMonitor(this.monitorVRAMSettings[index]);
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "createSettingsGPUTemp", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: (name, index, moreThanOneGPU) => {
|
||||
if (name === undefined || index === undefined) {
|
||||
console.warn('getGPUsFromServer: name or index undefined', name, index);
|
||||
return;
|
||||
}
|
||||
let label = 'Temp ';
|
||||
label += moreThanOneGPU ? index : '';
|
||||
const monitorTemperatureNElement = {
|
||||
id: 'Crystools.ShowGpuTemperature' + convertNumberToPascalCase(index),
|
||||
name: 'Temperature',
|
||||
category: ['Crystools', `${this.menuPrefix} Show GPU [${index}] ${name}`, 'Temperature'],
|
||||
type: 'boolean',
|
||||
label: label,
|
||||
symbol: '°',
|
||||
monitorTitle: `${index}: ${name}`,
|
||||
defaultValue: true,
|
||||
htmlMonitorRef: undefined,
|
||||
htmlMonitorSliderRef: undefined,
|
||||
htmlMonitorLabelRef: undefined,
|
||||
cssColor: Colors.TEMP_START,
|
||||
cssColorFinal: Colors.TEMP_END,
|
||||
onChange: async (value) => {
|
||||
await this.updateServerGPU(index, { temperature: value });
|
||||
this.updateWidget(monitorTemperatureNElement);
|
||||
},
|
||||
};
|
||||
this.monitorTemperatureSettings[index] = monitorTemperatureNElement;
|
||||
app.ui.settings.addSetting(this.monitorTemperatureSettings[index]);
|
||||
this.monitorUI.createDOMGPUMonitor(this.monitorTemperatureSettings[index]);
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "createSettingsHDD", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => {
|
||||
this.monitorHDDElement = {
|
||||
id: 'Crystools.ShowHdd',
|
||||
name: 'Show HDD Used',
|
||||
category: ['Crystools', this.menuPrefix + ' Show Hard Disk', 'Show'],
|
||||
type: 'boolean',
|
||||
label: 'HDD',
|
||||
symbol: '%',
|
||||
defaultValue: false,
|
||||
htmlMonitorRef: undefined,
|
||||
htmlMonitorSliderRef: undefined,
|
||||
htmlMonitorLabelRef: undefined,
|
||||
cssColor: Colors.DISK,
|
||||
onChange: async (value) => {
|
||||
await this.updateServer({ switchHDD: value });
|
||||
this.updateWidget(this.monitorHDDElement);
|
||||
},
|
||||
};
|
||||
this.settingsHDD = {
|
||||
id: 'Crystools.WhichHdd',
|
||||
name: 'Partition to show',
|
||||
category: ['Crystools', this.menuPrefix + ' Show Hard Disk', 'Which'],
|
||||
type: 'combo',
|
||||
defaultValue: '/',
|
||||
options: [],
|
||||
onChange: async (value) => {
|
||||
await this.updateServer({ whichHDD: value });
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "createSettings", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => {
|
||||
app.ui.settings.addSetting(this.settingsRate);
|
||||
app.ui.settings.addSetting(this.settingsMonitorHeight);
|
||||
app.ui.settings.addSetting(this.settingsMonitorWidth);
|
||||
app.ui.settings.addSetting(this.monitorRAMElement);
|
||||
app.ui.settings.addSetting(this.monitorCPUElement);
|
||||
void this.getHDDsFromServer().then((data) => {
|
||||
this.settingsHDD.options = data;
|
||||
app.ui.settings.addSetting(this.settingsHDD);
|
||||
});
|
||||
app.ui.settings.addSetting(this.monitorHDDElement);
|
||||
void this.getGPUsFromServer().then((gpus) => {
|
||||
let moreThanOneGPU = false;
|
||||
if (gpus.length > 1) {
|
||||
moreThanOneGPU = true;
|
||||
}
|
||||
gpus?.forEach(({ name, index }) => {
|
||||
this.createSettingsGPUTemp(name, index, moreThanOneGPU);
|
||||
this.createSettingsGPUVRAM(name, index, moreThanOneGPU);
|
||||
this.createSettingsGPUUsage(name, index, moreThanOneGPU);
|
||||
});
|
||||
this.finishedLoad();
|
||||
});
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "finishedLoad", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => {
|
||||
this.monitorUI.orderMonitors();
|
||||
this.updateAllWidget();
|
||||
this.moveMonitor(this.menuDisplayOption);
|
||||
const w = app.extensionManager.setting.get(this.monitorWidthId);
|
||||
const h = app.extensionManager.setting.get(this.monitorHeightId);
|
||||
this.monitorUI.updateMonitorSize(w, h);
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "updateDisplay", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: (value) => {
|
||||
if (value !== this.menuDisplayOption) {
|
||||
this.menuDisplayOption = value;
|
||||
this.moveMonitor(this.menuDisplayOption);
|
||||
}
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "moveMonitor", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: (menuPosition) => {
|
||||
let parentElement;
|
||||
switch (menuPosition) {
|
||||
case MenuDisplayOptions.Disabled:
|
||||
parentElement = document.getElementById('queue-button');
|
||||
if (parentElement && this.monitorUI.rootElement) {
|
||||
parentElement.insertAdjacentElement('afterend', this.crystoolsButtonGroup.element);
|
||||
}
|
||||
else {
|
||||
console.error('Crystools: parentElement to move monitors not found!', parentElement);
|
||||
}
|
||||
break;
|
||||
case MenuDisplayOptions.Top:
|
||||
case MenuDisplayOptions.Bottom:
|
||||
app.menu?.settingsGroup.element.before(this.crystoolsButtonGroup.element);
|
||||
}
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "updateAllWidget", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => {
|
||||
this.updateWidget(this.monitorCPUElement);
|
||||
this.updateWidget(this.monitorRAMElement);
|
||||
this.updateWidget(this.monitorHDDElement);
|
||||
this.monitorGPUSettings.forEach((monitorSettings) => {
|
||||
monitorSettings && this.updateWidget(monitorSettings);
|
||||
});
|
||||
this.monitorVRAMSettings.forEach((monitorSettings) => {
|
||||
monitorSettings && this.updateWidget(monitorSettings);
|
||||
});
|
||||
this.monitorTemperatureSettings.forEach((monitorSettings) => {
|
||||
monitorSettings && this.updateWidget(monitorSettings);
|
||||
});
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "updateWidget", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: (monitorSettings) => {
|
||||
if (this.monitorUI) {
|
||||
const value = app.extensionManager.setting.get(monitorSettings.id);
|
||||
this.monitorUI.showMonitor(monitorSettings, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "updateServer", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: async (data) => {
|
||||
const resp = await api.fetchApi('/crystools/monitor', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
cache: 'no-store',
|
||||
});
|
||||
if (resp.status === 200) {
|
||||
return await resp.text();
|
||||
}
|
||||
throw new Error(resp.statusText);
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "updateServerGPU", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: async (index, data) => {
|
||||
const resp = await api.fetchApi(`/crystools/monitor/GPU/${index}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
cache: 'no-store',
|
||||
});
|
||||
if (resp.status === 200) {
|
||||
return await resp.text();
|
||||
}
|
||||
throw new Error(resp.statusText);
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "getHDDsFromServer", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: async () => {
|
||||
return this.getDataFromServer('HDD');
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "getGPUsFromServer", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: async () => {
|
||||
return this.getDataFromServer('GPU');
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "getDataFromServer", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: async (what) => {
|
||||
const resp = await api.fetchApi(`/crystools/monitor/${what}`, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
});
|
||||
if (resp.status === 200) {
|
||||
return await resp.json();
|
||||
}
|
||||
throw new Error(resp.statusText);
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "setup", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => {
|
||||
if (this.monitorUI) {
|
||||
return;
|
||||
}
|
||||
this.createSettingsRate();
|
||||
this.createSettingsMonitorHeight();
|
||||
this.createSettingsMonitorWidth();
|
||||
this.createSettingsCPU();
|
||||
this.createSettingsRAM();
|
||||
this.createSettingsHDD();
|
||||
this.createSettings();
|
||||
const currentRate = parseFloat(app.extensionManager.setting.get(this.settingsRate.id));
|
||||
this.menuDisplayOption = app.extensionManager.setting.get(ComfyKeyMenuDisplayOption);
|
||||
app.ui.settings.addEventListener(`${ComfyKeyMenuDisplayOption}.change`, (e) => {
|
||||
this.updateDisplay(e.detail.value);
|
||||
});
|
||||
this.crystoolsButtonGroup = new ComfyButtonGroup();
|
||||
app.menu?.settingsGroup.element.before(this.crystoolsButtonGroup.element);
|
||||
this.monitorUI = new MonitorUI(this.crystoolsButtonGroup.element, this.monitorCPUElement, this.monitorRAMElement, this.monitorHDDElement, this.monitorGPUSettings, this.monitorVRAMSettings, this.monitorTemperatureSettings, currentRate);
|
||||
this.updateDisplay(this.menuDisplayOption);
|
||||
this.registerListeners();
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "registerListeners", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => {
|
||||
api.addEventListener('crystools.monitor', (event) => {
|
||||
if (event?.detail === undefined) {
|
||||
return;
|
||||
}
|
||||
this.monitorUI.updateDisplay(event.detail);
|
||||
}, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
const crystoolsMonitor = new CrystoolsMonitor();
|
||||
app.registerExtension({
|
||||
name: crystoolsMonitor.idExtensionName,
|
||||
setup: crystoolsMonitor.setup,
|
||||
});
|
||||
588
custom_nodes/ComfyUI-Crystools/web/monitor.ts
Normal file
588
custom_nodes/ComfyUI-Crystools/web/monitor.ts
Normal file
@@ -0,0 +1,588 @@
|
||||
import { app, api, ComfyButtonGroup } from './comfy/index.js';
|
||||
import { commonPrefix } from './common.js';
|
||||
import { MonitorUI } from './monitorUI.js';
|
||||
import { Colors } from './styles.js';
|
||||
import { convertNumberToPascalCase } from './utils.js';
|
||||
import { ComfyKeyMenuDisplayOption, MenuDisplayOptions } from './progressBarUIBase.js';
|
||||
|
||||
// enum MonitorPosition {
|
||||
// 'Top' = 'Top',
|
||||
// 'Sidebar' = 'Sidebar',
|
||||
// 'Floating' = 'Floating',
|
||||
// }
|
||||
|
||||
class CrystoolsMonitor {
|
||||
readonly idExtensionName = 'Crystools.monitor';
|
||||
private readonly menuPrefix = commonPrefix;
|
||||
private menuDisplayOption: MenuDisplayOptions = MenuDisplayOptions.Disabled;
|
||||
private crystoolsButtonGroup: ComfyButtonGroup = null;
|
||||
|
||||
// private settingsMonitorPosition: TMonitorSettings;
|
||||
private settingsRate: TMonitorSettings;
|
||||
private settingsMonitorHeight: TMonitorSettings;
|
||||
private settingsMonitorWidth: TMonitorSettings;
|
||||
private monitorCPUElement: TMonitorSettings;
|
||||
private monitorRAMElement: TMonitorSettings;
|
||||
private monitorHDDElement: TMonitorSettings;
|
||||
private settingsHDD: TMonitorSettings;
|
||||
private monitorGPUSettings: TMonitorSettings[] = [];
|
||||
private monitorVRAMSettings: TMonitorSettings[] = [];
|
||||
private monitorTemperatureSettings: TMonitorSettings[] = [];
|
||||
|
||||
private monitorUI: MonitorUI;
|
||||
|
||||
// private readonly monitorPositionId = 'Crystools.MonitorPosition';
|
||||
private readonly monitorWidthId = 'Crystools.MonitorWidth';
|
||||
private readonly monitorWidth = 60;
|
||||
private readonly monitorHeightId = 'Crystools.MonitorHeight';
|
||||
private readonly monitorHeight = 30;
|
||||
|
||||
// NO POSIBLE TO IMPLEMENT INSIDE THE PANEL
|
||||
// createSettingsMonitorPosition = (): void => {
|
||||
// const position = app.extensionManager.setting.get(this.monitorPositionId);
|
||||
// console.log('position', position);
|
||||
// this.settingsMonitorPosition = {
|
||||
// id: this.monitorPositionId,
|
||||
// name: 'Position (floating not implemented yet)',
|
||||
// category: ['Crystools', this.menuPrefix + ' Configuration', 'position'],
|
||||
// tooltip: 'Only for new UI',
|
||||
// experimental: true,
|
||||
// // data: [],
|
||||
// type: 'combo',
|
||||
// options: [
|
||||
// MonitorPoistion.Top,
|
||||
// MonitorPoistion.Sidebar,
|
||||
// MonitorPoistion.Floating
|
||||
// ],
|
||||
//
|
||||
// defaultValue: MonitorPoistion.Sidebar,
|
||||
// // @ts-ignore
|
||||
// onChange: (_value: string): void => {
|
||||
// // if (this.monitorUI) {
|
||||
// // console.log('onChange', _value);
|
||||
// // this.moveMonitor(this.menuDisplayOption);
|
||||
// // }
|
||||
// },
|
||||
// };
|
||||
// };
|
||||
|
||||
createSettingsRate = (): void => {
|
||||
this.settingsRate = {
|
||||
id: 'Crystools.RefreshRate',
|
||||
name: 'Refresh per second',
|
||||
category: ['Crystools', this.menuPrefix + ' Configuration', 'refresh'],
|
||||
tooltip: 'This is the time (in seconds) between each update of the monitors, 0 means no refresh',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 0,
|
||||
max: 2,
|
||||
step: .25,
|
||||
},
|
||||
defaultValue: .5,
|
||||
|
||||
// @ts-ignore
|
||||
onChange: async(value: string): Promise<void> => {
|
||||
let valueNumber: number;
|
||||
|
||||
try {
|
||||
valueNumber = parseFloat(value);
|
||||
if (isNaN(valueNumber)) {
|
||||
throw new Error('invalid value');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.updateServer({rate: valueNumber});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
cpu_utilization: 0,
|
||||
device: 'cpu',
|
||||
|
||||
gpus: [
|
||||
{
|
||||
gpu_utilization: 0,
|
||||
gpu_temperature: 0,
|
||||
vram_total: 0,
|
||||
vram_used: 0,
|
||||
vram_used_percent: 0,
|
||||
},
|
||||
],
|
||||
hdd_total: 0,
|
||||
hdd_used: 0,
|
||||
hdd_used_percent: 0,
|
||||
ram_total: 0,
|
||||
ram_used: 0,
|
||||
ram_used_percent: 0,
|
||||
};
|
||||
if (valueNumber === 0) {
|
||||
this.monitorUI.updateDisplay(data);
|
||||
}
|
||||
|
||||
this.monitorUI?.updateAllAnimationDuration(valueNumber);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
createSettingsMonitorWidth = (): void => {
|
||||
this.settingsMonitorWidth = {
|
||||
id: this.monitorWidthId,
|
||||
name: 'Pixel Width',
|
||||
category: ['Crystools', this.menuPrefix + ' Configuration', 'width'],
|
||||
tooltip: 'The width of the monitor in pixels on the UI (only on top/bottom UI)',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 60,
|
||||
max: 100,
|
||||
step: 1,
|
||||
},
|
||||
defaultValue: this.monitorWidth,
|
||||
// @ts-ignore
|
||||
onChange: (value: string): void => {
|
||||
let valueNumber: number;
|
||||
|
||||
try {
|
||||
valueNumber = parseInt(value);
|
||||
if (isNaN(valueNumber)) {
|
||||
throw new Error('invalid value');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const h = app.extensionManager.setting.get(this.monitorHeightId);
|
||||
this.monitorUI?.updateMonitorSize(valueNumber, h);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
createSettingsMonitorHeight = (): void => {
|
||||
this.settingsMonitorHeight = {
|
||||
id: this.monitorHeightId,
|
||||
name: 'Pixel Height',
|
||||
category: ['Crystools', this.menuPrefix + ' Configuration', 'height'],
|
||||
tooltip: 'The height of the monitor in pixels on the UI (only on top/bottom UI)',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 16,
|
||||
max: 50,
|
||||
step: 1,
|
||||
},
|
||||
defaultValue: this.monitorHeight,
|
||||
// @ts-ignore
|
||||
onChange: async(value: string): void => {
|
||||
let valueNumber: number;
|
||||
|
||||
try {
|
||||
valueNumber = parseInt(value);
|
||||
if (isNaN(valueNumber)) {
|
||||
throw new Error('invalid value');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const w = await app.extensionManager.setting.get(this.monitorWidthId);
|
||||
this.monitorUI?.updateMonitorSize(w, valueNumber);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
createSettingsCPU = (): void => {
|
||||
// CPU Variables
|
||||
this.monitorCPUElement = {
|
||||
id: 'Crystools.ShowCpu',
|
||||
name: 'CPU Usage',
|
||||
category: ['Crystools', this.menuPrefix + ' Hardware', 'Cpu'],
|
||||
type: 'boolean',
|
||||
label: 'CPU',
|
||||
symbol: '%',
|
||||
defaultValue: true,
|
||||
htmlMonitorRef: undefined,
|
||||
htmlMonitorSliderRef: undefined,
|
||||
htmlMonitorLabelRef: undefined,
|
||||
cssColor: Colors.CPU,
|
||||
// @ts-ignore
|
||||
onChange: async(value: boolean): Promise<void> => {
|
||||
await this.updateServer({switchCPU: value});
|
||||
this.updateWidget(this.monitorCPUElement);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
createSettingsRAM = (): void => {
|
||||
// RAM Variables
|
||||
this.monitorRAMElement = {
|
||||
id: 'Crystools.ShowRam',
|
||||
name: 'RAM Used',
|
||||
category: ['Crystools', this.menuPrefix + ' Hardware', 'Ram'],
|
||||
type: 'boolean',
|
||||
label: 'RAM',
|
||||
symbol: '%',
|
||||
defaultValue: true,
|
||||
htmlMonitorRef: undefined,
|
||||
htmlMonitorSliderRef: undefined,
|
||||
htmlMonitorLabelRef: undefined,
|
||||
cssColor: Colors.RAM,
|
||||
// @ts-ignore
|
||||
onChange: async(value: boolean): Promise<void> => {
|
||||
await this.updateServer({switchRAM: value});
|
||||
this.updateWidget(this.monitorRAMElement);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
createSettingsGPUUsage = (name: string, index: number, moreThanOneGPU: boolean): void => {
|
||||
if (name === undefined || index === undefined) {
|
||||
console.warn('getGPUsFromServer: name or index undefined', name, index);
|
||||
return;
|
||||
}
|
||||
|
||||
let label = 'GPU ';
|
||||
label += moreThanOneGPU ? index : '';
|
||||
|
||||
const monitorGPUNElement: TMonitorSettings = {
|
||||
id: 'Crystools.ShowGpuUsage' + convertNumberToPascalCase(index),
|
||||
name: ' Usage',
|
||||
category: ['Crystools', `${this.menuPrefix} Show GPU [${index}] ${name}`, 'Usage'],
|
||||
type: 'boolean',
|
||||
label,
|
||||
symbol: '%',
|
||||
monitorTitle: `${index}: ${name}`,
|
||||
defaultValue: true,
|
||||
htmlMonitorRef: undefined,
|
||||
htmlMonitorSliderRef: undefined,
|
||||
htmlMonitorLabelRef: undefined,
|
||||
cssColor: Colors.GPU,
|
||||
// @ts-ignore
|
||||
onChange: async(value: boolean): Promise<void> => {
|
||||
await this.updateServerGPU(index, {utilization: value});
|
||||
this.updateWidget(monitorGPUNElement);
|
||||
},
|
||||
};
|
||||
|
||||
this.monitorGPUSettings[index] = monitorGPUNElement;
|
||||
app.ui.settings.addSetting(this.monitorGPUSettings[index]);
|
||||
this.monitorUI.createDOMGPUMonitor(this.monitorGPUSettings[index]);
|
||||
};
|
||||
|
||||
createSettingsGPUVRAM = (name: string, index: number, moreThanOneGPU: boolean): void => {
|
||||
if (name === undefined || index === undefined) {
|
||||
console.warn('getGPUsFromServer: name or index undefined', name, index);
|
||||
return;
|
||||
}
|
||||
|
||||
let label = 'VRAM ';
|
||||
label += moreThanOneGPU ? index : '';
|
||||
|
||||
// GPU VRAM Variables
|
||||
const monitorVRAMNElement: TMonitorSettings = {
|
||||
id: 'Crystools.ShowGpuVram' + convertNumberToPascalCase(index),
|
||||
name: 'VRAM',
|
||||
category: ['Crystools', `${this.menuPrefix} Show GPU [${index}] ${name}`, 'VRAM'],
|
||||
type: 'boolean',
|
||||
label: label,
|
||||
symbol: '%',
|
||||
monitorTitle: `${index}: ${name}`,
|
||||
defaultValue: true,
|
||||
htmlMonitorRef: undefined,
|
||||
htmlMonitorSliderRef: undefined,
|
||||
htmlMonitorLabelRef: undefined,
|
||||
cssColor: Colors.VRAM,
|
||||
// @ts-ignore
|
||||
onChange: async(value: boolean): Promise<void> => {
|
||||
await this.updateServerGPU(index, {vram: value});
|
||||
this.updateWidget(monitorVRAMNElement);
|
||||
},
|
||||
};
|
||||
|
||||
this.monitorVRAMSettings[index] = monitorVRAMNElement;
|
||||
app.ui.settings.addSetting(this.monitorVRAMSettings[index]);
|
||||
this.monitorUI.createDOMGPUMonitor(this.monitorVRAMSettings[index]);
|
||||
};
|
||||
|
||||
createSettingsGPUTemp = (name: string, index: number, moreThanOneGPU: boolean): void => {
|
||||
if (name === undefined || index === undefined) {
|
||||
console.warn('getGPUsFromServer: name or index undefined', name, index);
|
||||
return;
|
||||
}
|
||||
|
||||
let label = 'Temp ';
|
||||
label += moreThanOneGPU ? index : '';
|
||||
|
||||
// GPU Temperature Variables
|
||||
const monitorTemperatureNElement: TMonitorSettings = {
|
||||
id: 'Crystools.ShowGpuTemperature' + convertNumberToPascalCase(index),
|
||||
name: 'Temperature',
|
||||
category: ['Crystools', `${this.menuPrefix} Show GPU [${index}] ${name}`, 'Temperature'],
|
||||
type: 'boolean',
|
||||
label: label,
|
||||
symbol: '°',
|
||||
monitorTitle: `${index}: ${name}`,
|
||||
defaultValue: true,
|
||||
htmlMonitorRef: undefined,
|
||||
htmlMonitorSliderRef: undefined,
|
||||
htmlMonitorLabelRef: undefined,
|
||||
cssColor: Colors.TEMP_START,
|
||||
cssColorFinal: Colors.TEMP_END,
|
||||
// @ts-ignore
|
||||
onChange: async(value: boolean): Promise<void> => {
|
||||
await this.updateServerGPU(index, {temperature: value});
|
||||
this.updateWidget(monitorTemperatureNElement);
|
||||
},
|
||||
};
|
||||
|
||||
this.monitorTemperatureSettings[index] = monitorTemperatureNElement;
|
||||
app.ui.settings.addSetting(this.monitorTemperatureSettings[index]);
|
||||
this.monitorUI.createDOMGPUMonitor(this.monitorTemperatureSettings[index]);
|
||||
};
|
||||
|
||||
createSettingsHDD = (): void => {
|
||||
// HDD Variables
|
||||
this.monitorHDDElement = {
|
||||
id: 'Crystools.ShowHdd',
|
||||
name: 'Show HDD Used',
|
||||
category: ['Crystools', this.menuPrefix + ' Show Hard Disk', 'Show'],
|
||||
type: 'boolean',
|
||||
label: 'HDD',
|
||||
symbol: '%',
|
||||
// tooltip: 'See Partition to show (HDD)',
|
||||
defaultValue: false,
|
||||
htmlMonitorRef: undefined,
|
||||
htmlMonitorSliderRef: undefined,
|
||||
htmlMonitorLabelRef: undefined,
|
||||
cssColor: Colors.DISK,
|
||||
// @ts-ignore
|
||||
onChange: async(value: boolean): Promise<void> => {
|
||||
await this.updateServer({switchHDD: value});
|
||||
this.updateWidget(this.monitorHDDElement);
|
||||
},
|
||||
};
|
||||
|
||||
this.settingsHDD = {
|
||||
id: 'Crystools.WhichHdd',
|
||||
name: 'Partition to show',
|
||||
category: ['Crystools', this.menuPrefix + ' Show Hard Disk', 'Which'],
|
||||
type: 'combo',
|
||||
defaultValue: '/',
|
||||
options: [],
|
||||
// @ts-ignore
|
||||
onChange: async(value: string): Promise<void> => {
|
||||
await this.updateServer({whichHDD: value});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
createSettings = (): void => {
|
||||
app.ui.settings.addSetting(this.settingsRate);
|
||||
app.ui.settings.addSetting(this.settingsMonitorHeight);
|
||||
app.ui.settings.addSetting(this.settingsMonitorWidth);
|
||||
// app.ui.settings.addSetting(this.settingsMonitorPosition);
|
||||
app.ui.settings.addSetting(this.monitorRAMElement);
|
||||
app.ui.settings.addSetting(this.monitorCPUElement);
|
||||
|
||||
void this.getHDDsFromServer().then((data: string[]): void => {
|
||||
// @ts-ignore
|
||||
this.settingsHDD.options = data;
|
||||
app.ui.settings.addSetting(this.settingsHDD);
|
||||
});
|
||||
app.ui.settings.addSetting(this.monitorHDDElement);
|
||||
|
||||
void this.getGPUsFromServer().then((gpus: TGpuName[]): void => {
|
||||
let moreThanOneGPU = false;
|
||||
if (gpus.length > 1) {
|
||||
moreThanOneGPU = true;
|
||||
}
|
||||
|
||||
gpus?.forEach(({name, index}) => {
|
||||
this.createSettingsGPUTemp(name, index, moreThanOneGPU);
|
||||
this.createSettingsGPUVRAM(name, index, moreThanOneGPU);
|
||||
this.createSettingsGPUUsage(name, index, moreThanOneGPU);
|
||||
});
|
||||
|
||||
this.finishedLoad();
|
||||
});
|
||||
};
|
||||
|
||||
finishedLoad = (): void => {
|
||||
this.monitorUI.orderMonitors();
|
||||
this.updateAllWidget();
|
||||
this.moveMonitor(this.menuDisplayOption);
|
||||
|
||||
const w = app.extensionManager.setting.get(this.monitorWidthId);
|
||||
const h = app.extensionManager.setting.get(this.monitorHeightId);
|
||||
this.monitorUI.updateMonitorSize(w, h);
|
||||
};
|
||||
|
||||
updateDisplay = (value: MenuDisplayOptions): void => {
|
||||
if (value !== this.menuDisplayOption) {
|
||||
this.menuDisplayOption = value;
|
||||
this.moveMonitor(this.menuDisplayOption);
|
||||
}
|
||||
};
|
||||
|
||||
moveMonitor = (menuPosition: MenuDisplayOptions): void => {
|
||||
// console.log('moveMonitor', menuPosition);
|
||||
// setTimeout(() => {
|
||||
let parentElement: Element | null | undefined;
|
||||
|
||||
switch (menuPosition) {
|
||||
case MenuDisplayOptions.Disabled:
|
||||
parentElement = document.getElementById('queue-button');
|
||||
if (parentElement && this.monitorUI.rootElement) {
|
||||
parentElement.insertAdjacentElement('afterend', this.crystoolsButtonGroup.element);
|
||||
} else {
|
||||
console.error('Crystools: parentElement to move monitors not found!', parentElement);
|
||||
}
|
||||
break;
|
||||
|
||||
case MenuDisplayOptions.Top:
|
||||
case MenuDisplayOptions.Bottom:
|
||||
// const position = app.extensionManager.setting.get(this.monitorPositionId);
|
||||
// if(position === MonitorPosition.Top) {
|
||||
app.menu?.settingsGroup.element.before(this.crystoolsButtonGroup.element);
|
||||
// } else {
|
||||
// parentElement = document.getElementsByClassName('comfy-vue-side-bar-header')[0];
|
||||
// if(parentElement){
|
||||
// parentElement.insertBefore(this.crystoolsButtonGroup.element, parentElement.firstChild);
|
||||
// } else {
|
||||
// console.error('Crystools: parentElement to move monitors not found! back to top');
|
||||
// app.ui.settings.setSettingValue(this.monitorPositionId, MonitorPoistion.Top);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
// }, 100);
|
||||
};
|
||||
|
||||
updateAllWidget = (): void => {
|
||||
this.updateWidget(this.monitorCPUElement);
|
||||
this.updateWidget(this.monitorRAMElement);
|
||||
this.updateWidget(this.monitorHDDElement);
|
||||
|
||||
this.monitorGPUSettings.forEach((monitorSettings) => {
|
||||
monitorSettings && this.updateWidget(monitorSettings);
|
||||
});
|
||||
this.monitorVRAMSettings.forEach((monitorSettings) => {
|
||||
monitorSettings && this.updateWidget(monitorSettings);
|
||||
});
|
||||
this.monitorTemperatureSettings.forEach((monitorSettings) => {
|
||||
monitorSettings && this.updateWidget(monitorSettings);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* for the settings menu
|
||||
* @param monitorSettings
|
||||
*/
|
||||
updateWidget = (monitorSettings: TMonitorSettings): void => {
|
||||
if (this.monitorUI) {
|
||||
const value = app.extensionManager.setting.get(monitorSettings.id);
|
||||
this.monitorUI.showMonitor(monitorSettings, value);
|
||||
}
|
||||
};
|
||||
|
||||
updateServer = async(data: TStatsSettings): Promise<string> => {
|
||||
const resp = await api.fetchApi('/crystools/monitor', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
cache: 'no-store',
|
||||
});
|
||||
if (resp.status === 200) {
|
||||
return await resp.text();
|
||||
}
|
||||
throw new Error(resp.statusText);
|
||||
};
|
||||
|
||||
updateServerGPU = async(index: number, data: TGpuSettings): Promise<string> => {
|
||||
const resp = await api.fetchApi(`/crystools/monitor/GPU/${index}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
cache: 'no-store',
|
||||
});
|
||||
if (resp.status === 200) {
|
||||
return await resp.text();
|
||||
}
|
||||
throw new Error(resp.statusText);
|
||||
};
|
||||
|
||||
getHDDsFromServer = async(): Promise<string[]> => {
|
||||
return this.getDataFromServer('HDD');
|
||||
};
|
||||
|
||||
getGPUsFromServer = async(): Promise<TGpuName[]> => {
|
||||
return this.getDataFromServer<TGpuName>('GPU');
|
||||
};
|
||||
|
||||
getDataFromServer = async <T>(what: string): Promise<T[]> => {
|
||||
const resp = await api.fetchApi(`/crystools/monitor/${what}`, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
});
|
||||
if (resp.status === 200) {
|
||||
return await resp.json();
|
||||
}
|
||||
throw new Error(resp.statusText);
|
||||
};
|
||||
|
||||
setup = (): void => {
|
||||
if (this.monitorUI) {
|
||||
return;
|
||||
}
|
||||
// this.createSettingsMonitorPosition();
|
||||
this.createSettingsRate();
|
||||
this.createSettingsMonitorHeight();
|
||||
this.createSettingsMonitorWidth();
|
||||
this.createSettingsCPU();
|
||||
this.createSettingsRAM();
|
||||
this.createSettingsHDD();
|
||||
this.createSettings();
|
||||
|
||||
const currentRate =
|
||||
parseFloat(app.extensionManager.setting.get(this.settingsRate.id));
|
||||
|
||||
this.menuDisplayOption = app.extensionManager.setting.get(ComfyKeyMenuDisplayOption);
|
||||
app.ui.settings.addEventListener(`${ComfyKeyMenuDisplayOption}.change`, (e: any) => {
|
||||
this.updateDisplay(e.detail.value);
|
||||
},
|
||||
);
|
||||
|
||||
this.crystoolsButtonGroup = new ComfyButtonGroup();
|
||||
app.menu?.settingsGroup.element.before(this.crystoolsButtonGroup.element);
|
||||
|
||||
this.monitorUI = new MonitorUI(
|
||||
this.crystoolsButtonGroup.element,
|
||||
this.monitorCPUElement,
|
||||
this.monitorRAMElement,
|
||||
this.monitorHDDElement,
|
||||
this.monitorGPUSettings,
|
||||
this.monitorVRAMSettings,
|
||||
this.monitorTemperatureSettings,
|
||||
currentRate,
|
||||
);
|
||||
|
||||
this.updateDisplay(this.menuDisplayOption);
|
||||
this.registerListeners();
|
||||
};
|
||||
|
||||
registerListeners = (): void => {
|
||||
api.addEventListener('crystools.monitor', (event: CustomEvent) => {
|
||||
if (event?.detail === undefined) {
|
||||
return;
|
||||
}
|
||||
this.monitorUI.updateDisplay(event.detail);
|
||||
}, false);
|
||||
};
|
||||
}
|
||||
|
||||
const crystoolsMonitor = new CrystoolsMonitor();
|
||||
app.registerExtension({
|
||||
name: crystoolsMonitor.idExtensionName,
|
||||
setup: crystoolsMonitor.setup,
|
||||
});
|
||||
26
custom_nodes/ComfyUI-Crystools/web/monitorUI.d.ts
vendored
Normal file
26
custom_nodes/ComfyUI-Crystools/web/monitorUI.d.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ProgressBarUIBase } from './progressBarUIBase.js';
|
||||
export declare class MonitorUI extends ProgressBarUIBase {
|
||||
rootElement: HTMLElement;
|
||||
private monitorCPUElement;
|
||||
private monitorRAMElement;
|
||||
private monitorHDDElement;
|
||||
private monitorGPUSettings;
|
||||
private monitorVRAMSettings;
|
||||
private monitorTemperatureSettings;
|
||||
private currentRate;
|
||||
lastMonitor: number;
|
||||
styleSheet: HTMLStyleElement;
|
||||
maxVRAMUsed: Record<number, number>;
|
||||
constructor(rootElement: HTMLElement, monitorCPUElement: TMonitorSettings, monitorRAMElement: TMonitorSettings, monitorHDDElement: TMonitorSettings, monitorGPUSettings: TMonitorSettings[], monitorVRAMSettings: TMonitorSettings[], monitorTemperatureSettings: TMonitorSettings[], currentRate: number);
|
||||
createDOM: () => void;
|
||||
createDOMGPUMonitor: (monitorSettings?: TMonitorSettings) => void;
|
||||
orderMonitors: () => void;
|
||||
updateDisplay: (data: TStatsData) => void;
|
||||
updateMonitor: (monitorSettings: TMonitorSettings, percent: number, used?: number, total?: number) => void;
|
||||
updateAllAnimationDuration: (value: number) => void;
|
||||
updatedAnimationDuration: (monitorSettings: TMonitorSettings, value: number) => void;
|
||||
createMonitor: (monitorSettings?: TMonitorSettings) => HTMLDivElement;
|
||||
updateMonitorSize: (width: number, height: number) => void;
|
||||
showMonitor: (monitorSettings: TMonitorSettings, value: boolean) => void;
|
||||
resetMaxVRAM: () => void;
|
||||
}
|
||||
303
custom_nodes/ComfyUI-Crystools/web/monitorUI.js
Normal file
303
custom_nodes/ComfyUI-Crystools/web/monitorUI.js
Normal file
@@ -0,0 +1,303 @@
|
||||
import { ProgressBarUIBase } from './progressBarUIBase.js';
|
||||
import { createStyleSheet, formatBytes } from './utils.js';
|
||||
export class MonitorUI extends ProgressBarUIBase {
|
||||
constructor(rootElement, monitorCPUElement, monitorRAMElement, monitorHDDElement, monitorGPUSettings, monitorVRAMSettings, monitorTemperatureSettings, currentRate) {
|
||||
super('crystools-monitors-root', rootElement);
|
||||
Object.defineProperty(this, "rootElement", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: rootElement
|
||||
});
|
||||
Object.defineProperty(this, "monitorCPUElement", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: monitorCPUElement
|
||||
});
|
||||
Object.defineProperty(this, "monitorRAMElement", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: monitorRAMElement
|
||||
});
|
||||
Object.defineProperty(this, "monitorHDDElement", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: monitorHDDElement
|
||||
});
|
||||
Object.defineProperty(this, "monitorGPUSettings", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: monitorGPUSettings
|
||||
});
|
||||
Object.defineProperty(this, "monitorVRAMSettings", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: monitorVRAMSettings
|
||||
});
|
||||
Object.defineProperty(this, "monitorTemperatureSettings", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: monitorTemperatureSettings
|
||||
});
|
||||
Object.defineProperty(this, "currentRate", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: currentRate
|
||||
});
|
||||
Object.defineProperty(this, "lastMonitor", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: 1
|
||||
});
|
||||
Object.defineProperty(this, "styleSheet", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "maxVRAMUsed", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: {}
|
||||
});
|
||||
Object.defineProperty(this, "createDOM", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => {
|
||||
if (!this.rootElement) {
|
||||
throw Error('Crystools: MonitorUI - Container not found');
|
||||
}
|
||||
this.rootElement.appendChild(this.createMonitor(this.monitorCPUElement));
|
||||
this.rootElement.appendChild(this.createMonitor(this.monitorRAMElement));
|
||||
this.rootElement.appendChild(this.createMonitor(this.monitorHDDElement));
|
||||
this.updateAllAnimationDuration(this.currentRate);
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "createDOMGPUMonitor", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: (monitorSettings) => {
|
||||
if (!(monitorSettings && this.rootElement)) {
|
||||
return;
|
||||
}
|
||||
this.rootElement.appendChild(this.createMonitor(monitorSettings));
|
||||
this.updateAllAnimationDuration(this.currentRate);
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "orderMonitors", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => {
|
||||
try {
|
||||
this.monitorCPUElement.htmlMonitorRef.style.order = '' + this.lastMonitor++;
|
||||
this.monitorRAMElement.htmlMonitorRef.style.order = '' + this.lastMonitor++;
|
||||
this.monitorGPUSettings.forEach((_monitorSettings, index) => {
|
||||
this.monitorGPUSettings[index].htmlMonitorRef.style.order = '' + this.lastMonitor++;
|
||||
this.monitorVRAMSettings[index].htmlMonitorRef.style.order = '' + this.lastMonitor++;
|
||||
this.monitorTemperatureSettings[index].htmlMonitorRef.style.order = '' + this.lastMonitor++;
|
||||
});
|
||||
this.monitorHDDElement.htmlMonitorRef.style.order = '' + this.lastMonitor++;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('orderMonitors', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "updateDisplay", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: (data) => {
|
||||
this.updateMonitor(this.monitorCPUElement, data.cpu_utilization);
|
||||
this.updateMonitor(this.monitorRAMElement, data.ram_used_percent, data.ram_used, data.ram_total);
|
||||
this.updateMonitor(this.monitorHDDElement, data.hdd_used_percent, data.hdd_used, data.hdd_total);
|
||||
if (data.gpus === undefined || data.gpus.length === 0) {
|
||||
console.warn('UpdateAllMonitors: no GPU data');
|
||||
return;
|
||||
}
|
||||
this.monitorGPUSettings.forEach((monitorSettings, index) => {
|
||||
if (data.gpus[index]) {
|
||||
const gpu = data.gpus[index];
|
||||
if (gpu === undefined) {
|
||||
return;
|
||||
}
|
||||
this.updateMonitor(monitorSettings, gpu.gpu_utilization);
|
||||
}
|
||||
else {
|
||||
}
|
||||
});
|
||||
this.monitorVRAMSettings.forEach((monitorSettings, index) => {
|
||||
if (data.gpus[index]) {
|
||||
const gpu = data.gpus[index];
|
||||
if (gpu === undefined) {
|
||||
return;
|
||||
}
|
||||
this.updateMonitor(monitorSettings, gpu.vram_used_percent, gpu.vram_used, gpu.vram_total);
|
||||
}
|
||||
else {
|
||||
}
|
||||
});
|
||||
this.monitorTemperatureSettings.forEach((monitorSettings, index) => {
|
||||
if (data.gpus[index]) {
|
||||
const gpu = data.gpus[index];
|
||||
if (gpu === undefined) {
|
||||
return;
|
||||
}
|
||||
this.updateMonitor(monitorSettings, gpu.gpu_temperature);
|
||||
if (monitorSettings.cssColorFinal && monitorSettings.htmlMonitorSliderRef) {
|
||||
monitorSettings.htmlMonitorSliderRef.style.backgroundColor =
|
||||
`color-mix(in srgb, ${monitorSettings.cssColorFinal} ${gpu.gpu_temperature}%, ${monitorSettings.cssColor})`;
|
||||
}
|
||||
}
|
||||
else {
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "updateMonitor", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: (monitorSettings, percent, used, total) => {
|
||||
if (!(monitorSettings.htmlMonitorSliderRef && monitorSettings.htmlMonitorLabelRef)) {
|
||||
return;
|
||||
}
|
||||
if (percent < 0) {
|
||||
return;
|
||||
}
|
||||
const prefix = monitorSettings.monitorTitle ? monitorSettings.monitorTitle + ' - ' : '';
|
||||
let title = `${Math.floor(percent)}${monitorSettings.symbol}`;
|
||||
let postfix = '';
|
||||
if (used !== undefined && total !== undefined) {
|
||||
const gpuIndex = parseInt(monitorSettings.monitorTitle?.split(':')[0] || '0');
|
||||
if (!this.maxVRAMUsed[gpuIndex] || this.maxVRAMUsed[gpuIndex] > total) {
|
||||
this.maxVRAMUsed[gpuIndex] = 0;
|
||||
}
|
||||
if (used > this.maxVRAMUsed[gpuIndex]) {
|
||||
this.maxVRAMUsed[gpuIndex] = used;
|
||||
}
|
||||
postfix = ` - ${formatBytes(used)} / ${formatBytes(total)}`;
|
||||
postfix += ` Max: ${formatBytes(this.maxVRAMUsed[gpuIndex])}`;
|
||||
}
|
||||
title = `${prefix}${title}${postfix}`;
|
||||
if (monitorSettings.htmlMonitorRef) {
|
||||
monitorSettings.htmlMonitorRef.title = title;
|
||||
}
|
||||
monitorSettings.htmlMonitorLabelRef.innerHTML = `${Math.floor(percent)}${monitorSettings.symbol}`;
|
||||
monitorSettings.htmlMonitorSliderRef.style.width = `${Math.floor(percent)}%`;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "updateAllAnimationDuration", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: (value) => {
|
||||
this.updatedAnimationDuration(this.monitorCPUElement, value);
|
||||
this.updatedAnimationDuration(this.monitorRAMElement, value);
|
||||
this.updatedAnimationDuration(this.monitorHDDElement, value);
|
||||
this.monitorGPUSettings.forEach((monitorSettings) => {
|
||||
monitorSettings && this.updatedAnimationDuration(monitorSettings, value);
|
||||
});
|
||||
this.monitorVRAMSettings.forEach((monitorSettings) => {
|
||||
monitorSettings && this.updatedAnimationDuration(monitorSettings, value);
|
||||
});
|
||||
this.monitorTemperatureSettings.forEach((monitorSettings) => {
|
||||
monitorSettings && this.updatedAnimationDuration(monitorSettings, value);
|
||||
});
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "updatedAnimationDuration", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: (monitorSettings, value) => {
|
||||
const slider = monitorSettings.htmlMonitorSliderRef;
|
||||
if (!slider) {
|
||||
return;
|
||||
}
|
||||
slider.style.transition = `width ${value.toFixed(1)}s`;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "createMonitor", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: (monitorSettings) => {
|
||||
if (!monitorSettings) {
|
||||
return document.createElement('div');
|
||||
}
|
||||
const htmlMain = document.createElement('div');
|
||||
htmlMain.classList.add(monitorSettings.id);
|
||||
htmlMain.classList.add('crystools-monitor');
|
||||
monitorSettings.htmlMonitorRef = htmlMain;
|
||||
if (monitorSettings.title) {
|
||||
htmlMain.title = monitorSettings.title;
|
||||
}
|
||||
const htmlMonitorText = document.createElement('div');
|
||||
htmlMonitorText.classList.add('crystools-text');
|
||||
htmlMonitorText.innerHTML = monitorSettings.label;
|
||||
htmlMain.append(htmlMonitorText);
|
||||
const htmlMonitorContent = document.createElement('div');
|
||||
htmlMonitorContent.classList.add('crystools-content');
|
||||
htmlMain.append(htmlMonitorContent);
|
||||
const htmlMonitorSlider = document.createElement('div');
|
||||
htmlMonitorSlider.classList.add('crystools-slider');
|
||||
if (monitorSettings.cssColorFinal) {
|
||||
htmlMonitorSlider.style.backgroundColor =
|
||||
`color-mix(in srgb, ${monitorSettings.cssColorFinal} 0%, ${monitorSettings.cssColor})`;
|
||||
}
|
||||
else {
|
||||
htmlMonitorSlider.style.backgroundColor = monitorSettings.cssColor;
|
||||
}
|
||||
monitorSettings.htmlMonitorSliderRef = htmlMonitorSlider;
|
||||
htmlMonitorContent.append(htmlMonitorSlider);
|
||||
const htmlMonitorLabel = document.createElement('div');
|
||||
htmlMonitorLabel.classList.add('crystools-label');
|
||||
monitorSettings.htmlMonitorLabelRef = htmlMonitorLabel;
|
||||
htmlMonitorContent.append(htmlMonitorLabel);
|
||||
htmlMonitorLabel.innerHTML = '0%';
|
||||
return monitorSettings.htmlMonitorRef;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "updateMonitorSize", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: (width, height) => {
|
||||
this.styleSheet.innerText = `#crystools-monitors-root .crystools-monitor .crystools-content {height: ${height}px; width: ${width}px;}`;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "showMonitor", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: (monitorSettings, value) => {
|
||||
if (monitorSettings.htmlMonitorRef) {
|
||||
monitorSettings.htmlMonitorRef.style.display = value ? 'flex' : 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "resetMaxVRAM", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => {
|
||||
this.maxVRAMUsed = {};
|
||||
}
|
||||
});
|
||||
this.createDOM();
|
||||
this.styleSheet = createStyleSheet('crystools-monitors-size');
|
||||
}
|
||||
}
|
||||
250
custom_nodes/ComfyUI-Crystools/web/monitorUI.ts
Normal file
250
custom_nodes/ComfyUI-Crystools/web/monitorUI.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import { ProgressBarUIBase } from './progressBarUIBase.js';
|
||||
import { createStyleSheet, formatBytes } from './utils.js';
|
||||
|
||||
export class MonitorUI extends ProgressBarUIBase {
|
||||
lastMonitor = 1; // just for order on monitors section
|
||||
styleSheet: HTMLStyleElement;
|
||||
maxVRAMUsed: Record<number, number> = {}; // Add this to track max VRAM per GPU
|
||||
|
||||
constructor(
|
||||
public override rootElement: HTMLElement,
|
||||
private monitorCPUElement: TMonitorSettings,
|
||||
private monitorRAMElement: TMonitorSettings,
|
||||
private monitorHDDElement: TMonitorSettings,
|
||||
private monitorGPUSettings: TMonitorSettings[],
|
||||
private monitorVRAMSettings: TMonitorSettings[],
|
||||
private monitorTemperatureSettings: TMonitorSettings[],
|
||||
private currentRate: number,
|
||||
) {
|
||||
super('crystools-monitors-root', rootElement);
|
||||
this.createDOM();
|
||||
|
||||
this.styleSheet = createStyleSheet('crystools-monitors-size');
|
||||
}
|
||||
|
||||
createDOM = (): void => {
|
||||
if (!this.rootElement) {
|
||||
throw Error('Crystools: MonitorUI - Container not found');
|
||||
}
|
||||
|
||||
// this.container.style.order = '2';
|
||||
this.rootElement.appendChild(this.createMonitor(this.monitorCPUElement));
|
||||
this.rootElement.appendChild(this.createMonitor(this.monitorRAMElement));
|
||||
this.rootElement.appendChild(this.createMonitor(this.monitorHDDElement));
|
||||
this.updateAllAnimationDuration(this.currentRate);
|
||||
};
|
||||
|
||||
createDOMGPUMonitor = (monitorSettings?: TMonitorSettings): void => {
|
||||
if (!(monitorSettings && this.rootElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.rootElement.appendChild(this.createMonitor(monitorSettings));
|
||||
this.updateAllAnimationDuration(this.currentRate);
|
||||
};
|
||||
|
||||
orderMonitors = (): void => {
|
||||
try {
|
||||
// @ts-ignore
|
||||
this.monitorCPUElement.htmlMonitorRef.style.order = '' + this.lastMonitor++;
|
||||
// @ts-ignore
|
||||
this.monitorRAMElement.htmlMonitorRef.style.order = '' + this.lastMonitor++;
|
||||
// @ts-ignore
|
||||
this.monitorGPUSettings.forEach((_monitorSettings, index) => {
|
||||
// @ts-ignore
|
||||
this.monitorGPUSettings[index].htmlMonitorRef.style.order = '' + this.lastMonitor++;
|
||||
// @ts-ignore
|
||||
this.monitorVRAMSettings[index].htmlMonitorRef.style.order = '' + this.lastMonitor++;
|
||||
// @ts-ignore
|
||||
this.monitorTemperatureSettings[index].htmlMonitorRef.style.order = '' + this.lastMonitor++;
|
||||
});
|
||||
// @ts-ignore
|
||||
this.monitorHDDElement.htmlMonitorRef.style.order = '' + this.lastMonitor++;
|
||||
} catch (error) {
|
||||
console.error('orderMonitors', error);
|
||||
}
|
||||
};
|
||||
|
||||
updateDisplay = (data: TStatsData): void => {
|
||||
this.updateMonitor(this.monitorCPUElement, data.cpu_utilization);
|
||||
this.updateMonitor(this.monitorRAMElement, data.ram_used_percent, data.ram_used, data.ram_total);
|
||||
this.updateMonitor(this.monitorHDDElement, data.hdd_used_percent, data.hdd_used, data.hdd_total);
|
||||
|
||||
if (data.gpus === undefined || data.gpus.length === 0) {
|
||||
console.warn('UpdateAllMonitors: no GPU data');
|
||||
return;
|
||||
}
|
||||
|
||||
this.monitorGPUSettings.forEach((monitorSettings, index) => {
|
||||
if (data.gpus[index]) {
|
||||
const gpu = data.gpus[index];
|
||||
if (gpu === undefined) {
|
||||
// console.error('UpdateAllMonitors: no GPU data for index', index);
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateMonitor(monitorSettings, gpu.gpu_utilization);
|
||||
} else {
|
||||
// console.error('UpdateAllMonitors: no GPU data for index', index);
|
||||
}
|
||||
});
|
||||
|
||||
this.monitorVRAMSettings.forEach((monitorSettings, index) => {
|
||||
if (data.gpus[index]) {
|
||||
const gpu = data.gpus[index];
|
||||
if (gpu === undefined) {
|
||||
// console.error('UpdateAllMonitors: no GPU VRAM data for index', index);
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateMonitor(monitorSettings, gpu.vram_used_percent, gpu.vram_used, gpu.vram_total);
|
||||
} else {
|
||||
// console.error('UpdateAllMonitors: no GPU VRAM data for index', index);
|
||||
}
|
||||
});
|
||||
|
||||
this.monitorTemperatureSettings.forEach((monitorSettings, index) => {
|
||||
if (data.gpus[index]) {
|
||||
const gpu = data.gpus[index];
|
||||
if (gpu === undefined) {
|
||||
// console.error('UpdateAllMonitors: no GPU VRAM data for index', index);
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateMonitor(monitorSettings, gpu.gpu_temperature);
|
||||
if (monitorSettings.cssColorFinal && monitorSettings.htmlMonitorSliderRef) {
|
||||
monitorSettings.htmlMonitorSliderRef.style.backgroundColor =
|
||||
`color-mix(in srgb, ${monitorSettings.cssColorFinal} ${gpu.gpu_temperature}%, ${monitorSettings.cssColor})`;
|
||||
}
|
||||
} else {
|
||||
// console.error('UpdateAllMonitors: no GPU VRAM data for index', index);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
updateMonitor = (monitorSettings: TMonitorSettings, percent: number, used?: number, total?: number): void => {
|
||||
if (!(monitorSettings.htmlMonitorSliderRef && monitorSettings.htmlMonitorLabelRef)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (percent < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prefix = monitorSettings.monitorTitle ? monitorSettings.monitorTitle + ' - ' : '';
|
||||
let title = `${Math.floor(percent)}${monitorSettings.symbol}`;
|
||||
let postfix = '';
|
||||
|
||||
// Add max VRAM tracking for VRAM monitors
|
||||
if (used !== undefined && total !== undefined) {
|
||||
// Extract GPU index from monitorTitle (assuming format "X: GPU Name")
|
||||
const gpuIndex = parseInt(monitorSettings.monitorTitle?.split(':')[0] || '0');
|
||||
|
||||
// Initialize max VRAM if not set or glitch
|
||||
if (!this.maxVRAMUsed[gpuIndex] || this.maxVRAMUsed[gpuIndex]! > total) {
|
||||
this.maxVRAMUsed[gpuIndex] = 0;
|
||||
}
|
||||
|
||||
// Update max VRAM if current usage is higher
|
||||
if ( used > this.maxVRAMUsed[gpuIndex]!) {
|
||||
this.maxVRAMUsed[gpuIndex] = used;
|
||||
}
|
||||
|
||||
postfix = ` - ${formatBytes(used)} / ${formatBytes(total)}`;
|
||||
// Add max VRAM to tooltip
|
||||
postfix += ` Max: ${formatBytes(this.maxVRAMUsed[gpuIndex]!)}`;
|
||||
}
|
||||
|
||||
title = `${prefix}${title}${postfix}`;
|
||||
|
||||
if (monitorSettings.htmlMonitorRef) {
|
||||
monitorSettings.htmlMonitorRef.title = title;
|
||||
}
|
||||
monitorSettings.htmlMonitorLabelRef.innerHTML = `${Math.floor(percent)}${monitorSettings.symbol}`;
|
||||
monitorSettings.htmlMonitorSliderRef.style.width = `${Math.floor(percent)}%`;
|
||||
};
|
||||
|
||||
updateAllAnimationDuration = (value: number): void => {
|
||||
this.updatedAnimationDuration(this.monitorCPUElement, value);
|
||||
this.updatedAnimationDuration(this.monitorRAMElement, value);
|
||||
this.updatedAnimationDuration(this.monitorHDDElement, value);
|
||||
this.monitorGPUSettings.forEach((monitorSettings) => {
|
||||
monitorSettings && this.updatedAnimationDuration(monitorSettings, value);
|
||||
});
|
||||
this.monitorVRAMSettings.forEach((monitorSettings) => {
|
||||
monitorSettings && this.updatedAnimationDuration(monitorSettings, value);
|
||||
});
|
||||
this.monitorTemperatureSettings.forEach((monitorSettings) => {
|
||||
monitorSettings && this.updatedAnimationDuration(monitorSettings, value);
|
||||
});
|
||||
};
|
||||
|
||||
updatedAnimationDuration = (monitorSettings: TMonitorSettings, value: number): void => {
|
||||
const slider = monitorSettings.htmlMonitorSliderRef;
|
||||
if (!slider) {
|
||||
return;
|
||||
}
|
||||
|
||||
slider.style.transition = `width ${value.toFixed(1)}s`;
|
||||
};
|
||||
|
||||
createMonitor = (monitorSettings?: TMonitorSettings): HTMLDivElement => {
|
||||
if (!monitorSettings) {
|
||||
// just for typescript
|
||||
return document.createElement('div');
|
||||
}
|
||||
|
||||
const htmlMain = document.createElement('div');
|
||||
htmlMain.classList.add(monitorSettings.id);
|
||||
htmlMain.classList.add('crystools-monitor');
|
||||
|
||||
monitorSettings.htmlMonitorRef = htmlMain;
|
||||
|
||||
if (monitorSettings.title) {
|
||||
htmlMain.title = monitorSettings.title;
|
||||
}
|
||||
|
||||
const htmlMonitorText = document.createElement('div');
|
||||
htmlMonitorText.classList.add('crystools-text');
|
||||
htmlMonitorText.innerHTML = monitorSettings.label;
|
||||
htmlMain.append(htmlMonitorText);
|
||||
|
||||
const htmlMonitorContent = document.createElement('div');
|
||||
htmlMonitorContent.classList.add('crystools-content');
|
||||
htmlMain.append(htmlMonitorContent);
|
||||
|
||||
const htmlMonitorSlider = document.createElement('div');
|
||||
htmlMonitorSlider.classList.add('crystools-slider');
|
||||
if (monitorSettings.cssColorFinal) {
|
||||
htmlMonitorSlider.style.backgroundColor =
|
||||
`color-mix(in srgb, ${monitorSettings.cssColorFinal} 0%, ${monitorSettings.cssColor})`;
|
||||
} else {
|
||||
htmlMonitorSlider.style.backgroundColor = monitorSettings.cssColor;
|
||||
}
|
||||
monitorSettings.htmlMonitorSliderRef = htmlMonitorSlider;
|
||||
htmlMonitorContent.append(htmlMonitorSlider);
|
||||
|
||||
const htmlMonitorLabel = document.createElement('div');
|
||||
htmlMonitorLabel.classList.add('crystools-label');
|
||||
monitorSettings.htmlMonitorLabelRef = htmlMonitorLabel;
|
||||
htmlMonitorContent.append(htmlMonitorLabel);
|
||||
htmlMonitorLabel.innerHTML = '0%';
|
||||
return monitorSettings.htmlMonitorRef;
|
||||
};
|
||||
|
||||
updateMonitorSize = (width: number, height: number): void => {
|
||||
// eslint-disable-next-line max-len
|
||||
this.styleSheet.innerText = `#crystools-monitors-root .crystools-monitor .crystools-content {height: ${height}px; width: ${width}px;}`;
|
||||
};
|
||||
|
||||
showMonitor = (monitorSettings: TMonitorSettings, value: boolean): void => {
|
||||
if (monitorSettings.htmlMonitorRef) {
|
||||
monitorSettings.htmlMonitorRef.style.display = value ? 'flex' : 'none';
|
||||
}
|
||||
};
|
||||
|
||||
resetMaxVRAM = (): void => {
|
||||
this.maxVRAMUsed = {};
|
||||
};
|
||||
}
|
||||
1
custom_nodes/ComfyUI-Crystools/web/progressBar.d.ts
vendored
Normal file
1
custom_nodes/ComfyUI-Crystools/web/progressBar.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
187
custom_nodes/ComfyUI-Crystools/web/progressBar.js
Normal file
187
custom_nodes/ComfyUI-Crystools/web/progressBar.js
Normal file
@@ -0,0 +1,187 @@
|
||||
import { app, api } from './comfy/index.js';
|
||||
import { commonPrefix } from './common.js';
|
||||
import { ProgressBarUI } from './progressBarUI.js';
|
||||
import { ComfyKeyMenuDisplayOption, EStatus, MenuDisplayOptions } from './progressBarUIBase.js';
|
||||
class CrystoolsProgressBar {
|
||||
constructor() {
|
||||
Object.defineProperty(this, "idExtensionName", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: 'Crystools.progressBar'
|
||||
});
|
||||
Object.defineProperty(this, "idShowProgressBar", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: 'Crystools.ProgressBar'
|
||||
});
|
||||
Object.defineProperty(this, "defaultShowStatus", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: true
|
||||
});
|
||||
Object.defineProperty(this, "menuPrefix", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: commonPrefix
|
||||
});
|
||||
Object.defineProperty(this, "menuDisplayOption", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: MenuDisplayOptions.Disabled
|
||||
});
|
||||
Object.defineProperty(this, "currentStatus", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: EStatus.executed
|
||||
});
|
||||
Object.defineProperty(this, "currentProgress", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: 0
|
||||
});
|
||||
Object.defineProperty(this, "currentNode", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: undefined
|
||||
});
|
||||
Object.defineProperty(this, "timeStart", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: 0
|
||||
});
|
||||
Object.defineProperty(this, "progressBarUI", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "createSettings", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => {
|
||||
app.ui.settings.addSetting({
|
||||
id: this.idShowProgressBar,
|
||||
name: 'Show progress bar',
|
||||
category: ['Crystools', this.menuPrefix + ' Progress Bar', 'Show'],
|
||||
tooltip: 'This apply only on "Disabled" (old) menu',
|
||||
type: 'boolean',
|
||||
defaultValue: this.defaultShowStatus,
|
||||
onChange: this.progressBarUI.showProgressBar,
|
||||
});
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "updateDisplay", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: (menuDisplayOption) => {
|
||||
if (menuDisplayOption !== this.menuDisplayOption) {
|
||||
this.menuDisplayOption = menuDisplayOption;
|
||||
this.progressBarUI.showSection(this.menuDisplayOption === MenuDisplayOptions.Disabled);
|
||||
}
|
||||
if (this.menuDisplayOption === MenuDisplayOptions.Disabled && this.progressBarUI.showProgressBarFlag) {
|
||||
this.progressBarUI.updateDisplay(this.currentStatus, this.timeStart, this.currentProgress);
|
||||
}
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "setup", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => {
|
||||
if (this.progressBarUI) {
|
||||
this.progressBarUI
|
||||
.showProgressBar(app.extensionManager.setting.get(this.idShowProgressBar));
|
||||
return;
|
||||
}
|
||||
this.menuDisplayOption = app.extensionManager.setting.get(ComfyKeyMenuDisplayOption);
|
||||
app.ui.settings.addEventListener(`${ComfyKeyMenuDisplayOption}.change`, (e) => {
|
||||
this.updateDisplay(e.detail.value);
|
||||
});
|
||||
const progressBarElement = document.createElement('div');
|
||||
progressBarElement.classList.add('crystools-monitors-container');
|
||||
this.progressBarUI = new ProgressBarUI(progressBarElement, (this.menuDisplayOption === MenuDisplayOptions.Disabled), this.centerNode);
|
||||
const parentElement = document.getElementById('queue-button');
|
||||
if (parentElement) {
|
||||
parentElement.insertAdjacentElement('afterend', progressBarElement);
|
||||
}
|
||||
else {
|
||||
console.error('Crystools: parentElement to move monitors not found!', parentElement);
|
||||
}
|
||||
this.createSettings();
|
||||
this.updateDisplay(this.menuDisplayOption);
|
||||
this.registerListeners();
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "registerListeners", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => {
|
||||
api.addEventListener('status', ({ detail }) => {
|
||||
this.currentStatus = this.currentStatus === EStatus.execution_error ? EStatus.execution_error : EStatus.executed;
|
||||
const queueRemaining = detail?.exec_info.queue_remaining;
|
||||
if (queueRemaining) {
|
||||
this.currentStatus = EStatus.executing;
|
||||
}
|
||||
this.updateDisplay(this.menuDisplayOption);
|
||||
}, false);
|
||||
api.addEventListener('progress', ({ detail }) => {
|
||||
const { value, max, node } = detail;
|
||||
const progress = Math.floor((value / max) * 100);
|
||||
if (!isNaN(progress) && progress >= 0 && progress <= 100) {
|
||||
this.currentProgress = progress;
|
||||
this.currentNode = node;
|
||||
}
|
||||
this.updateDisplay(this.menuDisplayOption);
|
||||
}, false);
|
||||
api.addEventListener('executed', ({ detail }) => {
|
||||
if (detail?.node) {
|
||||
this.currentNode = detail.node;
|
||||
}
|
||||
this.updateDisplay(this.menuDisplayOption);
|
||||
}, false);
|
||||
api.addEventListener('execution_start', ({ _detail }) => {
|
||||
this.currentStatus = EStatus.executing;
|
||||
this.timeStart = Date.now();
|
||||
this.updateDisplay(this.menuDisplayOption);
|
||||
}, false);
|
||||
api.addEventListener('execution_error', ({ _detail }) => {
|
||||
this.currentStatus = EStatus.execution_error;
|
||||
this.updateDisplay(this.menuDisplayOption);
|
||||
}, false);
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "centerNode", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => {
|
||||
const id = this.currentNode;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const node = app.graph.getNodeById(id);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
app.canvas.centerOnNode(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
const crystoolsProgressBar = new CrystoolsProgressBar();
|
||||
app.registerExtension({
|
||||
name: crystoolsProgressBar.idExtensionName,
|
||||
setup: crystoolsProgressBar.setup,
|
||||
});
|
||||
140
custom_nodes/ComfyUI-Crystools/web/progressBar.ts
Normal file
140
custom_nodes/ComfyUI-Crystools/web/progressBar.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { app, api } from './comfy/index.js';
|
||||
import { commonPrefix } from './common.js';
|
||||
import { ProgressBarUI } from './progressBarUI.js';
|
||||
import { ComfyKeyMenuDisplayOption, EStatus, MenuDisplayOptions } from './progressBarUIBase.js';
|
||||
|
||||
class CrystoolsProgressBar {
|
||||
idExtensionName = 'Crystools.progressBar';
|
||||
idShowProgressBar = 'Crystools.ProgressBar';
|
||||
defaultShowStatus = true;
|
||||
menuPrefix = commonPrefix;
|
||||
menuDisplayOption: MenuDisplayOptions = MenuDisplayOptions.Disabled;
|
||||
|
||||
currentStatus = EStatus.executed;
|
||||
currentProgress = 0;
|
||||
currentNode?: number = undefined;
|
||||
timeStart = 0;
|
||||
|
||||
progressBarUI: ProgressBarUI;
|
||||
|
||||
// not on setup because this affect the order on settings, I prefer to options at first
|
||||
createSettings = (): void => {
|
||||
app.ui.settings.addSetting({
|
||||
id: this.idShowProgressBar,
|
||||
name: 'Show progress bar',
|
||||
category: ['Crystools', this.menuPrefix + ' Progress Bar', 'Show'],
|
||||
tooltip: 'This apply only on "Disabled" (old) menu',
|
||||
type: 'boolean',
|
||||
defaultValue: this.defaultShowStatus,
|
||||
onChange: this.progressBarUI.showProgressBar,
|
||||
});
|
||||
};
|
||||
|
||||
updateDisplay = (menuDisplayOption: MenuDisplayOptions): void => {
|
||||
if (menuDisplayOption !== this.menuDisplayOption) {
|
||||
this.menuDisplayOption = menuDisplayOption;
|
||||
this.progressBarUI.showSection(this.menuDisplayOption === MenuDisplayOptions.Disabled);
|
||||
}
|
||||
if (this.menuDisplayOption === MenuDisplayOptions.Disabled && this.progressBarUI.showProgressBarFlag) {
|
||||
this.progressBarUI.updateDisplay(this.currentStatus, this.timeStart, this.currentProgress);
|
||||
}
|
||||
};
|
||||
|
||||
// automatically called by ComfyUI
|
||||
setup = (): void => {
|
||||
if (this.progressBarUI) {
|
||||
this.progressBarUI
|
||||
.showProgressBar(app.extensionManager.setting.get(this.idShowProgressBar));
|
||||
return;
|
||||
}
|
||||
|
||||
this.menuDisplayOption = app.extensionManager.setting.get(ComfyKeyMenuDisplayOption);
|
||||
app.ui.settings.addEventListener(`${ComfyKeyMenuDisplayOption}.change`, (e: any) => {
|
||||
this.updateDisplay(e.detail.value);
|
||||
},
|
||||
);
|
||||
|
||||
const progressBarElement = document.createElement('div');
|
||||
progressBarElement.classList.add('crystools-monitors-container');
|
||||
|
||||
this.progressBarUI = new ProgressBarUI(
|
||||
progressBarElement,
|
||||
(this.menuDisplayOption === MenuDisplayOptions.Disabled),
|
||||
this.centerNode,
|
||||
);
|
||||
|
||||
const parentElement = document.getElementById('queue-button');
|
||||
if (parentElement) {
|
||||
parentElement.insertAdjacentElement('afterend', progressBarElement);
|
||||
} else {
|
||||
console.error('Crystools: parentElement to move monitors not found!', parentElement);
|
||||
}
|
||||
|
||||
this.createSettings();
|
||||
this.updateDisplay(this.menuDisplayOption);
|
||||
this.registerListeners();
|
||||
};
|
||||
|
||||
registerListeners = (): void => {
|
||||
api.addEventListener('status', ({detail}: any) => {
|
||||
this.currentStatus = this.currentStatus === EStatus.execution_error ? EStatus.execution_error : EStatus.executed;
|
||||
const queueRemaining = detail?.exec_info.queue_remaining;
|
||||
|
||||
if (queueRemaining) {
|
||||
this.currentStatus = EStatus.executing;
|
||||
}
|
||||
this.updateDisplay(this.menuDisplayOption);
|
||||
}, false);
|
||||
|
||||
api.addEventListener('progress', ({detail}: any) => {
|
||||
const {value, max, node} = detail;
|
||||
const progress = Math.floor((value / max) * 100);
|
||||
|
||||
if (!isNaN(progress) && progress >= 0 && progress <= 100) {
|
||||
this.currentProgress = progress;
|
||||
this.currentNode = node;
|
||||
}
|
||||
|
||||
this.updateDisplay(this.menuDisplayOption);
|
||||
}, false);
|
||||
|
||||
api.addEventListener('executed', ({detail}: any) => {
|
||||
if (detail?.node) {
|
||||
this.currentNode = detail.node;
|
||||
}
|
||||
|
||||
this.updateDisplay(this.menuDisplayOption);
|
||||
}, false);
|
||||
|
||||
api.addEventListener('execution_start', ({_detail}: any) => {
|
||||
this.currentStatus = EStatus.executing;
|
||||
this.timeStart = Date.now();
|
||||
|
||||
this.updateDisplay(this.menuDisplayOption);
|
||||
}, false);
|
||||
|
||||
api.addEventListener('execution_error', ({_detail}: any) => {
|
||||
this.currentStatus = EStatus.execution_error;
|
||||
|
||||
this.updateDisplay(this.menuDisplayOption);
|
||||
}, false);
|
||||
};
|
||||
|
||||
centerNode = (): void => {
|
||||
const id = this.currentNode;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const node = app.graph.getNodeById(id);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
app.canvas.centerOnNode(node);
|
||||
};
|
||||
}
|
||||
|
||||
const crystoolsProgressBar = new CrystoolsProgressBar();
|
||||
app.registerExtension({
|
||||
name: crystoolsProgressBar.idExtensionName,
|
||||
setup: crystoolsProgressBar.setup,
|
||||
});
|
||||
18
custom_nodes/ComfyUI-Crystools/web/progressBarUI.d.ts
vendored
Normal file
18
custom_nodes/ComfyUI-Crystools/web/progressBarUI.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
import { EStatus, ProgressBarUIBase } from './progressBarUIBase.js';
|
||||
export declare class ProgressBarUI extends ProgressBarUIBase {
|
||||
rootElement: HTMLElement;
|
||||
showSectionFlag: boolean;
|
||||
private centerNode;
|
||||
htmlProgressSliderRef: HTMLDivElement;
|
||||
htmlProgressLabelRef: HTMLDivElement;
|
||||
currentStatus: EStatus;
|
||||
timeStart: number;
|
||||
currentProgress: number;
|
||||
showProgressBarFlag: boolean;
|
||||
constructor(rootElement: HTMLElement, showSectionFlag: boolean, centerNode: () => void);
|
||||
createDOM: () => void;
|
||||
updateDisplay: (currentStatus: EStatus, timeStart: number, currentProgress: number) => void;
|
||||
showSection: (value: boolean) => void;
|
||||
showProgressBar: (value: boolean) => void;
|
||||
private displaySection;
|
||||
}
|
||||
142
custom_nodes/ComfyUI-Crystools/web/progressBarUI.js
Normal file
142
custom_nodes/ComfyUI-Crystools/web/progressBarUI.js
Normal file
@@ -0,0 +1,142 @@
|
||||
import { EStatus, ProgressBarUIBase } from './progressBarUIBase.js';
|
||||
export class ProgressBarUI extends ProgressBarUIBase {
|
||||
constructor(rootElement, showSectionFlag, centerNode) {
|
||||
super('crystools-progressBar-root', rootElement);
|
||||
Object.defineProperty(this, "rootElement", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: rootElement
|
||||
});
|
||||
Object.defineProperty(this, "showSectionFlag", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: showSectionFlag
|
||||
});
|
||||
Object.defineProperty(this, "centerNode", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: centerNode
|
||||
});
|
||||
Object.defineProperty(this, "htmlProgressSliderRef", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "htmlProgressLabelRef", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "currentStatus", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "timeStart", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "currentProgress", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "showProgressBarFlag", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "createDOM", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => {
|
||||
this.rootElement.setAttribute('title', 'click to see the current working node');
|
||||
this.rootElement.addEventListener('click', this.centerNode);
|
||||
const progressBar = document.createElement('div');
|
||||
progressBar.classList.add('crystools-progress-bar');
|
||||
this.rootElement.append(progressBar);
|
||||
const progressSlider = document.createElement('div');
|
||||
this.htmlProgressSliderRef = progressSlider;
|
||||
progressSlider.classList.add('crystools-slider');
|
||||
progressBar.append(this.htmlProgressSliderRef);
|
||||
const progressLabel = document.createElement('div');
|
||||
progressLabel.classList.add('crystools-label');
|
||||
progressLabel.innerHTML = '0%';
|
||||
this.htmlProgressLabelRef = progressLabel;
|
||||
progressBar.append(this.htmlProgressLabelRef);
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "updateDisplay", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: (currentStatus, timeStart, currentProgress) => {
|
||||
if (!(this.showSectionFlag && this.showProgressBarFlag)) {
|
||||
return;
|
||||
}
|
||||
if (!(this.htmlProgressLabelRef && this.htmlProgressSliderRef)) {
|
||||
console.error('htmlProgressLabelRef or htmlProgressSliderRef is undefined');
|
||||
return;
|
||||
}
|
||||
this.currentStatus = currentStatus;
|
||||
this.timeStart = timeStart;
|
||||
this.currentProgress = currentProgress;
|
||||
if (currentStatus === EStatus.executed) {
|
||||
this.htmlProgressLabelRef.innerHTML = 'cached';
|
||||
const timeElapsed = Date.now() - timeStart;
|
||||
if (timeStart > 0 && timeElapsed > 0) {
|
||||
this.htmlProgressLabelRef.innerHTML = new Date(timeElapsed).toISOString().substr(11, 8);
|
||||
}
|
||||
this.htmlProgressSliderRef.style.width = '0';
|
||||
}
|
||||
else if (currentStatus === EStatus.execution_error) {
|
||||
this.htmlProgressLabelRef.innerHTML = 'ERROR';
|
||||
this.htmlProgressSliderRef.style.backgroundColor = 'var(--error-text)';
|
||||
}
|
||||
else if (currentStatus === EStatus.executing) {
|
||||
this.htmlProgressLabelRef.innerHTML = `${currentProgress}%`;
|
||||
this.htmlProgressSliderRef.style.width = this.htmlProgressLabelRef.innerHTML;
|
||||
this.htmlProgressSliderRef.style.backgroundColor = 'green';
|
||||
}
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "showSection", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: (value) => {
|
||||
this.showSectionFlag = value;
|
||||
this.displaySection();
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "showProgressBar", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: (value) => {
|
||||
this.showProgressBarFlag = value;
|
||||
this.displaySection();
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, "displaySection", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: () => {
|
||||
this.rootElement.style.display = (this.showSectionFlag && this.showProgressBarFlag) ? 'block' : 'none';
|
||||
}
|
||||
});
|
||||
this.createDOM();
|
||||
}
|
||||
}
|
||||
95
custom_nodes/ComfyUI-Crystools/web/progressBarUI.ts
Normal file
95
custom_nodes/ComfyUI-Crystools/web/progressBarUI.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { EStatus, ProgressBarUIBase } from './progressBarUIBase.js';
|
||||
|
||||
export class ProgressBarUI extends ProgressBarUIBase {
|
||||
htmlProgressSliderRef: HTMLDivElement;
|
||||
htmlProgressLabelRef: HTMLDivElement;
|
||||
currentStatus: EStatus;
|
||||
timeStart: number;
|
||||
currentProgress: number;
|
||||
showProgressBarFlag: boolean;
|
||||
|
||||
constructor(
|
||||
public override rootElement: HTMLElement,
|
||||
public showSectionFlag: boolean,
|
||||
private centerNode: () => void,
|
||||
) {
|
||||
super('crystools-progressBar-root', rootElement);
|
||||
this.createDOM();
|
||||
}
|
||||
|
||||
createDOM = (): void => {
|
||||
this.rootElement.setAttribute('title', 'click to see the current working node');
|
||||
this.rootElement.addEventListener('click', this.centerNode);
|
||||
|
||||
const progressBar = document.createElement('div');
|
||||
progressBar.classList.add('crystools-progress-bar');
|
||||
this.rootElement.append(progressBar);
|
||||
|
||||
const progressSlider = document.createElement('div');
|
||||
this.htmlProgressSliderRef = progressSlider;
|
||||
progressSlider.classList.add('crystools-slider');
|
||||
progressBar.append(this.htmlProgressSliderRef);
|
||||
|
||||
const progressLabel = document.createElement('div');
|
||||
progressLabel.classList.add('crystools-label');
|
||||
progressLabel.innerHTML = '0%';
|
||||
this.htmlProgressLabelRef = progressLabel;
|
||||
progressBar.append(this.htmlProgressLabelRef);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
updateDisplay = (currentStatus: EStatus, timeStart: number, currentProgress: number): void => {
|
||||
if (!(this.showSectionFlag && this.showProgressBarFlag)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(this.htmlProgressLabelRef && this.htmlProgressSliderRef)) {
|
||||
console.error('htmlProgressLabelRef or htmlProgressSliderRef is undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log('only if showSection and progressBar', timeStart, currentProgress);
|
||||
|
||||
this.currentStatus = currentStatus;
|
||||
this.timeStart = timeStart;
|
||||
this.currentProgress = currentProgress;
|
||||
|
||||
if (currentStatus === EStatus.executed) {
|
||||
// finished
|
||||
this.htmlProgressLabelRef.innerHTML = 'cached';
|
||||
|
||||
const timeElapsed = Date.now() - timeStart;
|
||||
if (timeStart > 0 && timeElapsed > 0) {
|
||||
this.htmlProgressLabelRef.innerHTML = new Date(timeElapsed).toISOString().substr(11, 8);
|
||||
}
|
||||
this.htmlProgressSliderRef.style.width = '0';
|
||||
|
||||
} else if (currentStatus === EStatus.execution_error) {
|
||||
// an error occurred
|
||||
this.htmlProgressLabelRef.innerHTML = 'ERROR';
|
||||
this.htmlProgressSliderRef.style.backgroundColor = 'var(--error-text)';
|
||||
|
||||
} else if (currentStatus === EStatus.executing) {
|
||||
// on going
|
||||
this.htmlProgressLabelRef.innerHTML = `${currentProgress}%`;
|
||||
this.htmlProgressSliderRef.style.width = this.htmlProgressLabelRef.innerHTML;
|
||||
this.htmlProgressSliderRef.style.backgroundColor = 'green'; // by reset the color
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
public showSection = (value: boolean): void => {
|
||||
this.showSectionFlag = value;
|
||||
this.displaySection();
|
||||
};
|
||||
|
||||
// remember it can't have more parameters because it is used on settings automatically
|
||||
public showProgressBar = (value: boolean): void => {
|
||||
this.showProgressBarFlag = value;
|
||||
this.displaySection();
|
||||
};
|
||||
|
||||
private displaySection = (): void => {
|
||||
this.rootElement.style.display = (this.showSectionFlag && this.showProgressBarFlag) ? 'block' : 'none';
|
||||
};
|
||||
}
|
||||
18
custom_nodes/ComfyUI-Crystools/web/progressBarUIBase.d.ts
vendored
Normal file
18
custom_nodes/ComfyUI-Crystools/web/progressBarUIBase.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
export declare enum EStatus {
|
||||
executing = "Executing",
|
||||
executed = "Executed",
|
||||
execution_error = "Execution error"
|
||||
}
|
||||
export declare const ComfyKeyMenuDisplayOption = "Comfy.UseNewMenu";
|
||||
export declare enum MenuDisplayOptions {
|
||||
'Disabled' = "Disabled",
|
||||
'Top' = "Top",
|
||||
'Bottom' = "Bottom"
|
||||
}
|
||||
export declare abstract class ProgressBarUIBase {
|
||||
rootId: string;
|
||||
rootElement: HTMLElement | null | undefined;
|
||||
protected htmlClassMonitor: string;
|
||||
protected constructor(rootId: string, rootElement: HTMLElement | null | undefined);
|
||||
abstract createDOM(): void;
|
||||
}
|
||||
42
custom_nodes/ComfyUI-Crystools/web/progressBarUIBase.js
Normal file
42
custom_nodes/ComfyUI-Crystools/web/progressBarUIBase.js
Normal file
@@ -0,0 +1,42 @@
|
||||
export var EStatus;
|
||||
(function (EStatus) {
|
||||
EStatus["executing"] = "Executing";
|
||||
EStatus["executed"] = "Executed";
|
||||
EStatus["execution_error"] = "Execution error";
|
||||
})(EStatus || (EStatus = {}));
|
||||
export const ComfyKeyMenuDisplayOption = 'Comfy.UseNewMenu';
|
||||
export var MenuDisplayOptions;
|
||||
(function (MenuDisplayOptions) {
|
||||
MenuDisplayOptions["Disabled"] = "Disabled";
|
||||
MenuDisplayOptions["Top"] = "Top";
|
||||
MenuDisplayOptions["Bottom"] = "Bottom";
|
||||
})(MenuDisplayOptions || (MenuDisplayOptions = {}));
|
||||
export class ProgressBarUIBase {
|
||||
constructor(rootId, rootElement) {
|
||||
Object.defineProperty(this, "rootId", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: rootId
|
||||
});
|
||||
Object.defineProperty(this, "rootElement", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: rootElement
|
||||
});
|
||||
Object.defineProperty(this, "htmlClassMonitor", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: 'crystools-monitors-container'
|
||||
});
|
||||
if (this.rootElement && this.rootElement.children.length === 0) {
|
||||
this.rootElement.setAttribute('id', this.rootId);
|
||||
this.rootElement.classList.add(this.htmlClassMonitor);
|
||||
this.rootElement.classList.add(this.constructor.name);
|
||||
}
|
||||
else {
|
||||
}
|
||||
}
|
||||
}
|
||||
32
custom_nodes/ComfyUI-Crystools/web/progressBarUIBase.ts
Normal file
32
custom_nodes/ComfyUI-Crystools/web/progressBarUIBase.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export enum EStatus {
|
||||
executing = 'Executing',
|
||||
executed = 'Executed',
|
||||
execution_error = 'Execution error',
|
||||
}
|
||||
|
||||
export const ComfyKeyMenuDisplayOption = 'Comfy.UseNewMenu';
|
||||
export enum MenuDisplayOptions {
|
||||
'Disabled' = 'Disabled',
|
||||
'Top' = 'Top',
|
||||
'Bottom' = 'Bottom',
|
||||
}
|
||||
|
||||
export abstract class ProgressBarUIBase {
|
||||
protected htmlClassMonitor = 'crystools-monitors-container';
|
||||
|
||||
protected constructor(
|
||||
public rootId: string,
|
||||
public rootElement: HTMLElement | null | undefined,
|
||||
) {
|
||||
// IMPORTANT duplicate on crystools-save
|
||||
if (this.rootElement && this.rootElement.children.length === 0) {
|
||||
this.rootElement.setAttribute('id', this.rootId);
|
||||
this.rootElement.classList.add(this.htmlClassMonitor);
|
||||
this.rootElement.classList.add(this.constructor.name);
|
||||
} else {
|
||||
// it was created before
|
||||
}
|
||||
}
|
||||
|
||||
abstract createDOM(): void;
|
||||
}
|
||||
12
custom_nodes/ComfyUI-Crystools/web/styles.d.ts
vendored
Normal file
12
custom_nodes/ComfyUI-Crystools/web/styles.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
export declare enum Styles {
|
||||
'BARS' = "BARS"
|
||||
}
|
||||
export declare enum Colors {
|
||||
'CPU' = "#0AA015",
|
||||
'RAM' = "#07630D",
|
||||
'DISK' = "#730F92",
|
||||
'GPU' = "#0C86F4",
|
||||
'VRAM' = "#176EC7",
|
||||
'TEMP_START' = "#00ff00",
|
||||
'TEMP_END' = "#ff0000"
|
||||
}
|
||||
16
custom_nodes/ComfyUI-Crystools/web/styles.js
Normal file
16
custom_nodes/ComfyUI-Crystools/web/styles.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { utils } from './comfy/index.js';
|
||||
utils.addStylesheet('extensions/ComfyUI-Crystools/monitor.css');
|
||||
export var Styles;
|
||||
(function (Styles) {
|
||||
Styles["BARS"] = "BARS";
|
||||
})(Styles || (Styles = {}));
|
||||
export var Colors;
|
||||
(function (Colors) {
|
||||
Colors["CPU"] = "#0AA015";
|
||||
Colors["RAM"] = "#07630D";
|
||||
Colors["DISK"] = "#730F92";
|
||||
Colors["GPU"] = "#0C86F4";
|
||||
Colors["VRAM"] = "#176EC7";
|
||||
Colors["TEMP_START"] = "#00ff00";
|
||||
Colors["TEMP_END"] = "#ff0000";
|
||||
})(Colors || (Colors = {}));
|
||||
17
custom_nodes/ComfyUI-Crystools/web/styles.ts
Normal file
17
custom_nodes/ComfyUI-Crystools/web/styles.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { utils } from './comfy/index.js';
|
||||
|
||||
utils.addStylesheet('extensions/ComfyUI-Crystools/monitor.css');
|
||||
|
||||
export enum Styles {
|
||||
'BARS' = 'BARS'
|
||||
}
|
||||
|
||||
export enum Colors {
|
||||
'CPU' = '#0AA015',
|
||||
'RAM' = '#07630D',
|
||||
'DISK' = '#730F92',
|
||||
'GPU' = '#0C86F4',
|
||||
'VRAM' = '#176EC7',
|
||||
'TEMP_START' = '#00ff00',
|
||||
'TEMP_END' = '#ff0000',
|
||||
}
|
||||
61
custom_nodes/ComfyUI-Crystools/web/types.d.ts
vendored
Normal file
61
custom_nodes/ComfyUI-Crystools/web/types.d.ts
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
|
||||
type TGpuStatData = {
|
||||
gpu_utilization: number,
|
||||
gpu_temperature: number,
|
||||
vram_total: number,
|
||||
vram_used: number,
|
||||
vram_used_percent: number,
|
||||
}
|
||||
|
||||
type TGpuSettings = {
|
||||
utilization?: boolean,
|
||||
vram?: boolean,
|
||||
temperature?: boolean,
|
||||
}
|
||||
|
||||
type TGpuName = {
|
||||
name: string,
|
||||
index: number,
|
||||
}
|
||||
|
||||
type TStatsData = {
|
||||
cpu_utilization: number,
|
||||
device: string,
|
||||
gpus: TGpuStatData[],
|
||||
hdd_total: number,
|
||||
hdd_used: number,
|
||||
hdd_used_percent: number,
|
||||
ram_total: number,
|
||||
ram_used: number,
|
||||
ram_used_percent: number,
|
||||
}
|
||||
|
||||
type TStatsSettings = {
|
||||
rate?: number,
|
||||
switchCPU?: boolean,
|
||||
switchGPU?: boolean,
|
||||
switchHDD?: boolean,
|
||||
switchRAM?: boolean,
|
||||
switchVRAM?: boolean,
|
||||
whichHDD?: string,
|
||||
}
|
||||
|
||||
type TMonitorSettings = {
|
||||
id: string,
|
||||
name: string,
|
||||
category: string[], // for settings location
|
||||
label: string, // on monitor
|
||||
symbol: string, // on monitor
|
||||
monitorTitle?: string, // on monitor
|
||||
title?: string, // on monitor
|
||||
tooltip?: string, // on settings
|
||||
type: 'boolean' | 'number' | 'string' | 'slider' | 'combo',
|
||||
defaultValue: boolean | number | string,
|
||||
data?: any,
|
||||
onChange: (value: boolean | number | string) => Promise<void>,
|
||||
htmlMonitorRef?: HTMLDivElement,
|
||||
htmlMonitorSliderRef?: HTMLDivElement,
|
||||
htmlMonitorLabelRef?: HTMLDivElement,
|
||||
cssColor: string,
|
||||
cssColorFinal?: string,
|
||||
}
|
||||
5
custom_nodes/ComfyUI-Crystools/web/utils.d.ts
vendored
Normal file
5
custom_nodes/ComfyUI-Crystools/web/utils.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export declare function numberToWords(num: number): string;
|
||||
export declare function toPascalCase(strings: string[]): string;
|
||||
export declare function convertNumberToPascalCase(num: number): string;
|
||||
export declare function formatBytes(bytes: number): string;
|
||||
export declare function createStyleSheet(id: string): HTMLStyleElement;
|
||||
56
custom_nodes/ComfyUI-Crystools/web/utils.js
Normal file
56
custom_nodes/ComfyUI-Crystools/web/utils.js
Normal file
@@ -0,0 +1,56 @@
|
||||
export function numberToWords(num) {
|
||||
if (num === 0)
|
||||
return 'zero';
|
||||
const belowTwenty = [
|
||||
'', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten',
|
||||
'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen'
|
||||
];
|
||||
const tens = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'];
|
||||
const thousands = ['', 'thousand', 'million', 'billion'];
|
||||
function helper(n) {
|
||||
if (n === 0)
|
||||
return '';
|
||||
else if (n < 20)
|
||||
return belowTwenty[n] + ' ';
|
||||
else if (n < 100)
|
||||
return tens[Math.floor(n / 10)] + ' ' + helper(n % 10);
|
||||
return belowTwenty[Math.floor(n / 100)] + ' hundred ' + helper(n % 100);
|
||||
}
|
||||
let word = '';
|
||||
let i = 0;
|
||||
while (num > 0) {
|
||||
if (num % 1000 !== 0) {
|
||||
word = helper(num % 1000) + thousands[i] + ' ' + word;
|
||||
}
|
||||
num = Math.floor(num / 1000);
|
||||
i++;
|
||||
}
|
||||
return word.trim();
|
||||
}
|
||||
export function toPascalCase(strings) {
|
||||
if (!Array.isArray(strings))
|
||||
return '';
|
||||
function capitalize(word) {
|
||||
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
||||
}
|
||||
return strings.map(capitalize).join('');
|
||||
}
|
||||
export function convertNumberToPascalCase(num) {
|
||||
return toPascalCase(numberToWords(num).split(' '));
|
||||
}
|
||||
export function formatBytes(bytes) {
|
||||
if (bytes === 0)
|
||||
return '0 Bytes';
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
const formattedSize = (bytes / Math.pow(1024, i)).toFixed(2);
|
||||
return `${formattedSize} ${sizes[i]}`;
|
||||
}
|
||||
export function createStyleSheet(id) {
|
||||
const style = document.createElement('style');
|
||||
style.setAttribute('id', id);
|
||||
style.setAttribute('rel', 'stylesheet');
|
||||
style.setAttribute('type', 'text/css');
|
||||
document.head.appendChild(style);
|
||||
return style;
|
||||
}
|
||||
64
custom_nodes/ComfyUI-Crystools/web/utils.ts
Normal file
64
custom_nodes/ComfyUI-Crystools/web/utils.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
export function numberToWords(num: number): string {
|
||||
if (num === 0) return 'zero';
|
||||
|
||||
const belowTwenty = [
|
||||
'', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten',
|
||||
'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen'
|
||||
];
|
||||
const tens = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'];
|
||||
const thousands = ['', 'thousand', 'million', 'billion'];
|
||||
|
||||
function helper(n: number): string {
|
||||
if (n === 0) return '';
|
||||
else if (n < 20) return belowTwenty[n] + ' ';
|
||||
else if (n < 100) return tens[Math.floor(n / 10)] + ' ' + helper(n % 10);
|
||||
return belowTwenty[Math.floor(n / 100)] + ' hundred ' + helper(n % 100);
|
||||
}
|
||||
|
||||
let word = '';
|
||||
let i = 0;
|
||||
|
||||
while (num > 0) {
|
||||
if (num % 1000 !== 0) {
|
||||
word = helper(num % 1000) + thousands[i] + ' ' + word;
|
||||
}
|
||||
num = Math.floor(num / 1000);
|
||||
i++;
|
||||
}
|
||||
|
||||
return word.trim();
|
||||
}
|
||||
|
||||
export function toPascalCase(strings: string[]): string {
|
||||
if (!Array.isArray(strings)) return '';
|
||||
|
||||
function capitalize(word: string): string {
|
||||
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
||||
}
|
||||
|
||||
return strings.map(capitalize).join('');
|
||||
}
|
||||
|
||||
|
||||
export function convertNumberToPascalCase(num: number): string {
|
||||
return toPascalCase(numberToWords(num).split(' '));
|
||||
}
|
||||
|
||||
export function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
const formattedSize = (bytes / Math.pow(1024, i)).toFixed(2);
|
||||
|
||||
return `${formattedSize} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
export function createStyleSheet(id: string): HTMLStyleElement {
|
||||
const style = document.createElement('style');
|
||||
style.setAttribute('id', id);
|
||||
style.setAttribute('rel', 'stylesheet');
|
||||
style.setAttribute('type', 'text/css');
|
||||
document.head.appendChild(style);
|
||||
return style;
|
||||
}
|
||||
Reference in New Issue
Block a user