diff --git a/__init__.py b/__init__.py index 9fb84db..a6d0368 100644 --- a/__init__.py +++ b/__init__.py @@ -1,14 +1,14 @@ -from .api.dependency_installer import * -from .elegant_resource_monitor import ElegantResourceMonitor from .api.http_server import * +from .elegant_resource_monitor import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS +import random -NODE_CLASS_MAPPINGS = { - "ElegantResourceMonitor": ElegantResourceMonitor, -} +tech_rambling = [ + "Beepity beep beep!", "Clickity-clackity!", "Algorithm: Activated!", "Nerd mode: OFF [ON]", + "Geeks unite!", "Data crunch time!", "Reboot-a-rama!", "Quantum leap engaged!" +] -NODE_DISPLAY_NAME_MAPPINGS = { - "ElegantResourceMonitor": "📈 Resource Monitor", -} +print(f"\033[1;34m[CCTech Suite]: 🤖🤖🤖 \033[96m\033[3m{random.choice(tech_rambling)}\033[0m 🤖🤖🤖") +print(f"\033[1;34m[CCTech Suite]:\033[0m Activated \033[96m{len(NODE_CLASS_MAPPINGS)}\033[0m server nodes.") __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/elegant_resource_monitor.py b/elegant_resource_monitor.py index 09206d3..a7bdd2f 100644 --- a/elegant_resource_monitor.py +++ b/elegant_resource_monitor.py @@ -7,17 +7,14 @@ class ElegantResourceMonitor(): @classmethod def INPUT_TYPES(s): - return { - "required": { - "directory_path": ("STRING", {"default": ""}), - }, - } + return { } - RETURN_TYPES = ("INT", "FLOAT", "STRING",) + RETURN_TYPES = () + RETURN_NAMES = () FUNCTION = "run_resource_monitor" - CATEGORY = "CCTech/Utilities" + CATEGORY = "🤖 CCTech/Utilities" def run_resource_monitor(self): current_time = time.time() @@ -25,9 +22,9 @@ class ElegantResourceMonitor(): NODE_CLASS_MAPPINGS = { - "ElegantResourceMonitor": ElegantResourceMonitor, + "Resource Monitor": ElegantResourceMonitor, } NODE_DISPLAY_NAME_MAPPINGS = { - "ElegantResourceMonitor": "📈 Resource Monitor", + "Resource Monitor": "Resource Monitor 📈", } diff --git a/pyproject.toml b/pyproject.toml index 7cdb5e7..a416768 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui-elegant-resource-monitor" description = "This custom node for ComfyUI will add a simple and elegant resource monitor." -version = "1.0.8" +version = "1.0.6" license = {file = "LICENSE"} [project.urls] diff --git a/web/assets/js/dependencies.js b/web/assets/js/dependencies.js index 4ed8961..72cd4d2 100644 --- a/web/assets/js/dependencies.js +++ b/web/assets/js/dependencies.js @@ -4,7 +4,6 @@ styles.property = 'stylesheet' styles.rel = 'stylesheet' styles.onload = async function () { - console.log('Stylesheet loaded and applied.') if (localStorage.getItem('lastClass') && localStorage.getItem('lastInactiveClass')) { var lastClass = JSON.parse(localStorage.getItem('lastClass')) var lastInactiveClass = JSON.parse(localStorage.getItem('lastInactiveClass')) diff --git a/web/assets/js/elegant-base-server-node.js b/web/assets/js/elegant-base-server-node.js new file mode 100644 index 0000000..6ad5c23 --- /dev/null +++ b/web/assets/js/elegant-base-server-node.js @@ -0,0 +1,287 @@ +import { ComfyWidgets } from '/scripts/widgets.js' +import { app } from '/scripts/app.js' +export class ElegantResourceMonitorBaseNode extends LGraphNode { + constructor(title = ElegantResourceMonitorBaseNode.title, skipOnConstructedCall = true) { + super(title) + this.comfyClass = '__NEED_COMFY_CLASS__' + this.nickname = 'ElegantResourceMonitor' + this.isVirtualNode = false + this.isDropEnabled = false + this.removed = false + this.configuring = false + this._tempWidth = 0 + this.__constructed__ = false + this.helpDialog = null + if (title == '__NEED_CLASS_TITLE__') { + throw new Error('ElegantResourceMonitorBaseNode needs overrides.') + } + this.widgets = this.widgets || [] + this.properties = this.properties || {} + setTimeout(() => { + if (this.comfyClass == '__NEED_COMFY_CLASS__') { + throw new Error('ElegantResourceMonitorBaseNode needs a comfy class override.') + } + this.checkAndRunOnConstructed() + }) + } + checkAndRunOnConstructed() { + var _a + if (!this.__constructed__) { + this.onConstructed() + console.log( + `[ElegantResourceMonitorBaseNode] Child class did not call onConstructed for "${this.type}.`, + ) + } + return this.__constructed__ + } + onDragOver(e) { + if (!this.isDropEnabled) return false + return importIndividualNodesInnerOnDragOver(this, e) + } + async onDragDrop(e) { + if (!this.isDropEnabled) return false + return importIndividualNodesInnerOnDragDrop(this, e) + } + async invokeExtensionsAsync(method, ...args) { + return Promise.resolve() + } + onConstructed() { + var _a + if (this.__constructed__) return false + this.type = (_a = this.type) !== null && _a !== void 0 ? _a : undefined + this.__constructed__ = true + this.invokeExtensionsAsync('nodeCreated', this) + return this.__constructed__ + } + configure(info) { + this.configuring = true + super.configure(info) + for (const w of this.widgets || []) { + w.last_y = w.last_y || 0 + } + this.configuring = false + } + clone() { + const cloned = super.clone() + if (cloned.properties && !!window.structuredClone) { + cloned.properties = structuredClone(cloned.properties) + } + return cloned + } + set mode(mode) { + if (this.mode_ != mode) { + const oldMode = this.mode_ + this.mode_ = mode + this.onModeChange(oldMode, mode) + } + } + get mode() { + return this.mode_ + } + onModeChange(from, to) {} + async handleAction(action) { + action + } + removeWidget(widgetOrSlot) { + if (typeof widgetOrSlot === 'number') { + this.widgets.splice(widgetOrSlot, 1) + } else if (widgetOrSlot) { + const index = this.widgets.indexOf(widgetOrSlot) + if (index > -1) { + this.widgets.splice(index, 1) + } + } + } + defaultGetSlotMenuOptions(slot) { + var _a, _b + const menu_info = [] + if ( + (_b = + (_a = slot === null || slot === void 0 ? void 0 : slot.output) === null || _a === void 0 + ? void 0 + : _a.links) === null || _b === void 0 + ? void 0 + : _b.length + ) { + menu_info.push({ content: 'Disconnect Links', slot: slot }) + } + let inputOrOutput = slot.input || slot.output + if (inputOrOutput) { + if (inputOrOutput.removable) { + menu_info.push( + inputOrOutput.locked ? { content: 'Cannot remove' } : { content: 'Remove Slot', slot }, + ) + } + if (!inputOrOutput.nameLocked) { + menu_info.push({ content: 'Rename Slot', slot }) + } + } + return menu_info + } + onRemoved() { + var _a + ;(_a = super.onRemoved) === null || _a === void 0 ? void 0 : _a.call(this) + this.removed = true + } + static setUp(...args) {} + getHelp() { + return '' + } + showHelp() { + const help = this.getHelp() || this.constructor.help + if (help) { + this.helpDialog = new ElegantResourceMonitorHelpDialog(this, help).show() + this.helpDialog.addEventListener('close', (e) => { + this.helpDialog = null + }) + } + } + onKeyDown(event) { + KEY_EVENT_SERVICE.handleKeyDownOrUp(event) + if (event.key == '?' && !this.helpDialog) { + this.showHelp() + } + } + onKeyUp(event) { + KEY_EVENT_SERVICE.handleKeyDownOrUp(event) + } + getExtraMenuOptions(canvas, options) { + var _a, _b, _c, _d, _e, _f + if (super.getExtraMenuOptions) { + ;(_a = super.getExtraMenuOptions) === null || _a === void 0 + ? void 0 + : _a.apply(this, [canvas, options]) + } else if ( + (_c = (_b = this.constructor.nodeType) === null || _b === void 0 ? void 0 : _b.prototype) === + null || _c === void 0 + ? void 0 + : _c.getExtraMenuOptions + ) { + ;(_f = + (_e = + (_d = this.constructor.nodeType) === null || _d === void 0 ? void 0 : _d.prototype) === + null || _e === void 0 + ? void 0 + : _e.getExtraMenuOptions) === null || _f === void 0 + ? void 0 + : _f.apply(this, [canvas, options]) + } + const help = this.getHelp() || this.constructor.help + if (help) { + addHelpMenuItem(this, help, options) + } + } +} + +export class ElegantResourceMonitorBaseServerNode extends ElegantResourceMonitorBaseNode { + constructor(title) { + super(title, true) + this.isDropEnabled = true + this.serialize_widgets = true + this.setupFromServerNodeData() + this.onConstructed() + } + getWidgets() { + return ComfyWidgets + } + async setupFromServerNodeData() { + var _a, _b, _c + const nodeData = this.constructor.nodeData + if (!nodeData) { + throw Error('No node data') + } + this.comfyClass = nodeData.name + let inputs = nodeData['input']['required'] + if (nodeData['input']['optional'] != undefined) { + inputs = Object.assign({}, inputs, nodeData['input']['optional']) + } + const WIDGETS = this.getWidgets() + const config = { + minWidth: 1, + minHeight: 1, + widget: null, + } + for (const inputName in inputs) { + const inputData = inputs[inputName] + const type = inputData[0] + if ((_a = inputData[1]) === null || _a === void 0 ? void 0 : _a.forceInput) { + this.addInput(inputName, type) + } else { + let widgetCreated = true + if (Array.isArray(type)) { + Object.assign(config, WIDGETS.COMBO(this, inputName, inputData, app) || {}) + } else if (`${type}:${inputName}` in WIDGETS) { + Object.assign( + config, + WIDGETS[`${type}:${inputName}`](this, inputName, inputData, app) || {}, + ) + } else if (type in WIDGETS) { + Object.assign(config, WIDGETS[type](this, inputName, inputData, app) || {}) + } else { + this.addInput(inputName, type) + widgetCreated = false + } + if ( + widgetCreated && + ((_b = inputData[1]) === null || _b === void 0 ? void 0 : _b.forceInput) && + (config === null || config === void 0 ? void 0 : config.widget) + ) { + if (!config.widget.options) config.widget.options = {} + config.widget.options.forceInput = inputData[1].forceInput + } + if ( + widgetCreated && + ((_c = inputData[1]) === null || _c === void 0 ? void 0 : _c.defaultInput) && + (config === null || config === void 0 ? void 0 : config.widget) + ) { + if (!config.widget.options) config.widget.options = {} + config.widget.options.defaultInput = inputData[1].defaultInput + } + } + } + for (const o in nodeData['output']) { + let output = nodeData['output'][o] + if (output instanceof Array) output = 'COMBO' + const outputName = nodeData['output_name'][o] || output + const outputShape = nodeData['output_is_list'][o] + ? LiteGraph.GRID_SHAPE + : LiteGraph.CIRCLE_SHAPE + this.addOutput(outputName, output, { shape: outputShape }) + } + const s = this.computeSize() + s[0] = Math.max(config.minWidth, s[0] * 1.5) + s[1] = Math.max(config.minHeight, s[1]) + this.size = s + this.serialize_widgets = true + } + static registerForOverride(comfyClass, nodeData, ElegantResourceMonitorClass) { + if (OVERRIDDEN_SERVER_NODES.has(comfyClass)) { + throw Error( + `Already have a class to override ${ + comfyClass.type || comfyClass.name || comfyClass.title + }`, + ) + } + OVERRIDDEN_SERVER_NODES.set(comfyClass, ElegantResourceMonitorClass) + if (!ElegantResourceMonitorClass.__registeredForOverride__) { + ElegantResourceMonitorClass.__registeredForOverride__ = true + ElegantResourceMonitorClass.nodeType = comfyClass + ElegantResourceMonitorClass.nodeData = nodeData + ElegantResourceMonitorClass.onRegisteredForOverride(comfyClass, ElegantResourceMonitorClass) + } + } + static onRegisteredForOverride(comfyClass, ElegantResourceMonitorClass) {} +} +ElegantResourceMonitorBaseServerNode.nodeData = null +ElegantResourceMonitorBaseServerNode.nodeType = null +ElegantResourceMonitorBaseServerNode.__registeredForOverride__ = false +const OVERRIDDEN_SERVER_NODES = new Map() +const oldregisterNodeType = LiteGraph.registerNodeType +LiteGraph.registerNodeType = async function (nodeId, baseClass) { + var _a + const clazz = OVERRIDDEN_SERVER_NODES.get(baseClass) || baseClass + if (clazz !== baseClass) { + const classLabel = clazz.type || clazz.name || clazz.title + } + return oldregisterNodeType.call(LiteGraph, nodeId, clazz) +} diff --git a/web/assets/js/elegant-resource-monitor-node.js b/web/assets/js/elegant-resource-monitor-node.js new file mode 100644 index 0000000..bf60b39 --- /dev/null +++ b/web/assets/js/elegant-resource-monitor-node.js @@ -0,0 +1,208 @@ +import { app } from '/scripts/app.js' +import { ComfyWidgets } from '/scripts/widgets.js' +import { ElegantResourceMonitorBaseServerNode } from './elegant-base-server-node.js' +import { elegant } from '../../comfyui/elegant.js' +const LAST_SEED_BUTTON_LABEL = '♻️ (Use Last Queued Seed)' +const SPECIAL_SEED_RANDOM = -1 +const SPECIAL_SEED_INCREMENT = -2 +const SPECIAL_SEED_DECREMENT = -3 +const SPECIAL_SEEDS = [SPECIAL_SEED_RANDOM, SPECIAL_SEED_INCREMENT, SPECIAL_SEED_DECREMENT] +class ElegantResourceMonitor extends ElegantResourceMonitorBaseServerNode { + constructor(title = ElegantResourceMonitor.title) { + super(title) + this.title = 'ElegantResourceMonitor' + this.serialize_widgets = true + this.lastSeed = undefined + this.serializedCtx = {} + this.lastSeedValue = null + this.randMax = 1125899906842624 + this.randMin = 0 + this.randomRange = 1125899906842624 + this.handleApiHijackingBound = this.handleApiHijacking.bind(this) + elegant.addEventListener('comfy-api-queue-prompt-before', this.handleApiHijackingBound) + } + onRemoved() { + elegant.addEventListener('comfy-api-queue-prompt-before', this.handleApiHijackingBound) + } + configure(info) { + var _a + super.configure(info) + if ((_a = this.properties) === null || _a === void 0 ? void 0 : _a['showLastSeed']) { + this.addLastSeedValue() + } + } + async handleAction(action) { + if (action === 'Randomize Each Time') { + this.seedWidget.value = SPECIAL_SEED_RANDOM + } else if (action === 'Use Last Queued Seed') { + this.seedWidget.value = this.lastSeed != null ? this.lastSeed : this.seedWidget.value + this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL + this.lastSeedButton.disabled = true + } + } + onNodeCreated() { + var _a + ;(_a = super.onNodeCreated) === null || _a === void 0 ? void 0 : _a.call(this) + + for (const [i, w] of this.widgets.entries()) { + } + this.addWidget( + 'button', + '🎲 Randomize Each Time', + null, + () => { + console.log('random') + }, + { serialize: false }, + ) + this.addWidget( + 'button', + '🎲 New Fixed Random', + null, + () => { + console.log('fixed') + }, + { serialize: false }, + ) + this.lastSeedButton = this.addWidget( + 'button', + '🎲 Last Seed', + null, + () => { + console.log('last seed') + }, + { width: 50, serialize: false }, + ) + this.lastSeedButton.disabled = true + } + getExtraMenuOptions(canvas, options) { + var _a + ;(_a = super.getExtraMenuOptions) === null || _a === void 0 + ? void 0 + : _a.apply(this, [...arguments]) + options.splice(options.length - 1, 0, { + content: 'Show/Hide Last Seed Value', + callback: (_value, _options, _event, _parentMenu, _node) => { + this.properties['showLastSeed'] = !this.properties['showLastSeed'] + if (this.properties['showLastSeed']) { + this.addLastSeedValue() + } else { + this.removeLastSeedValue() + } + }, + }) + } + addLastSeedValue() { + if (this.lastSeedValue) return + this.lastSeedValue = ComfyWidgets['STRING']( + this, + 'last_seed', + ['STRING', { multiline: true }], + app, + ).widget + this.lastSeedValue.inputEl.readOnly = true + this.lastSeedValue.inputEl.style.fontSize = '0.75rem' + this.lastSeedValue.inputEl.style.textAlign = 'center' + this.computeSize() + } + removeLastSeedValue() { + if (!this.lastSeedValue) return + this.lastSeedValue.inputEl.remove() + this.widgets.splice(this.widgets.indexOf(this.lastSeedValue), 1) + this.lastSeedValue = null + this.computeSize() + } + handleApiHijacking(e) { + var _a, _b, _c, _d + if (this.mode === LiteGraph.NEVER || this.mode === 4) { + return + } + const workflow = e.detail.workflow + const output = e.detail.output + let workflowNode = + (_b = + (_a = workflow === null || workflow === void 0 ? void 0 : workflow.nodes) === null || + _a === void 0 + ? void 0 + : _a.find((n) => n.id === this.id)) !== null && _b !== void 0 + ? _b + : null + let outputInputs = + (_c = output === null || output === void 0 ? void 0 : output[this.id]) === null || + _c === void 0 + ? void 0 + : _c.inputs + if ( + !workflowNode || + !outputInputs || + outputInputs[this.seedWidget.name || 'seed'] === undefined + ) { + const [n, v] = this.logger.warnParts( + `Node ${this.id} not found in prompt data sent to server. This may be fine if only ` + + `queuing part of the workflow. If not, then this could be a bug.`, + ) + ;(_d = console[n]) === null || _d === void 0 ? void 0 : _d.call(console, ...v) + return + } + const seedToUse = this.getSeedToUse() + const seedWidgetndex = this.widgets.indexOf(this.seedWidget) + workflowNode.widgets_values[seedWidgetndex] = seedToUse + outputInputs[this.seedWidget.name || 'seed'] = seedToUse + this.lastSeed = seedToUse + if (seedToUse != this.seedWidget.value) { + this.lastSeedButton.name = `♻️ ${this.lastSeed}` + this.lastSeedButton.disabled = false + } else { + this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL + this.lastSeedButton.disabled = true + } + if (this.lastSeedValue) { + this.lastSeedValue.value = `Last Seed: ${this.lastSeed}` + } + } + getSeedToUse() { + const inputSeed = this.seedWidget.value + let seedToUse = null + if (SPECIAL_SEEDS.includes(inputSeed)) { + if (typeof this.lastSeed === 'number' && !SPECIAL_SEEDS.includes(this.lastSeed)) { + if (inputSeed === SPECIAL_SEED_INCREMENT) { + seedToUse = this.lastSeed + 1 + } else if (inputSeed === SPECIAL_SEED_DECREMENT) { + seedToUse = this.lastSeed - 1 + } + } + if (seedToUse == null || SPECIAL_SEEDS.includes(seedToUse)) { + seedToUse = + Math.floor(Math.random() * this.randomRange) * + ((this.seedWidget.options.step || 1) / 10) + + this.randMin + } + } + return seedToUse !== null && seedToUse !== void 0 ? seedToUse : inputSeed + } + static setUp(comfyClass, nodeData) { + ElegantResourceMonitorBaseServerNode.registerForOverride( + comfyClass, + nodeData, + ElegantResourceMonitor, + ) + } + static onRegisteredForOverride(comfyClass, ctxClass) { + setTimeout(() => { + ElegantResourceMonitor.category = comfyClass.category + }) + } +} + +ElegantResourceMonitor.title = 'Resource Monitor 📈' +ElegantResourceMonitor.type = 'Resource Monitor' +ElegantResourceMonitor.comfyClass = 'Resource Monitor' +ElegantResourceMonitor.exposedActions = ['Randomize Each Time', 'Use Last Queued Seed'] +app.registerExtension({ + name: 'Resource Monitor', + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData.name === ElegantResourceMonitor.type) { + ElegantResourceMonitor.setUp(nodeType, nodeData) + } + }, +}) diff --git a/web/assets/js/elegant-resource-monitor-service.js b/web/assets/js/elegant-resource-monitor-service.js index 798d4ec..a577e96 100644 --- a/web/assets/js/elegant-resource-monitor-service.js +++ b/web/assets/js/elegant-resource-monitor-service.js @@ -1,5 +1,5 @@ import { api } from '/scripts/api.js' -import { getResolver } from './shared_utils.js' +import { getResolver } from '../../comfyui/shared_utils.js' export class ElegantResourceMonitorExecution { constructor(id) { this.promptApi = null diff --git a/web/assets/js/elegant-resource-monitor.js b/web/assets/js/elegant-resource-monitor.js index e3a88ce..23ad58f 100644 --- a/web/assets/js/elegant-resource-monitor.js +++ b/web/assets/js/elegant-resource-monitor.js @@ -255,7 +255,6 @@ class ElegantResourceMonitor extends HTMLElement { if (e === null || e === void 0 ? void 0 : e.detail.queue) { console.log(`(${e.detail.queue}) Running... in another tab`) } else { - console.log('Idle') } } @@ -265,7 +264,6 @@ class ElegantResourceMonitor extends HTMLElement { connectedCallback() { if (!this.connected) { - console.log('Adding event listener to MONITOR_SERVICE') MONITOR_SERVICE.addEventListener( 'elegant-resource-monitor-update', this.onProgressUpdateBound, diff --git a/web/assets/js/script.js b/web/assets/js/script.js index 51c64ce..e2ab92b 100644 --- a/web/assets/js/script.js +++ b/web/assets/js/script.js @@ -406,7 +406,7 @@ setTimeout(() => { } function getCoordinates(isOutside) { - var position = localStorage.getItem('perf-monitor-position') ?? "bottom-right" + var position = localStorage.getItem('perf-monitor-position') if (isOutside) { var outsidePosition = getNearestPosition() diff --git a/web/comfyui/elegant.css b/web/comfyui/elegant.css new file mode 100644 index 0000000..6b58652 --- /dev/null +++ b/web/comfyui/elegant.css @@ -0,0 +1,623 @@ +.elegant-top-messages-container { + position: fixed; + z-index: 9999; + top: 0; + left: 0; + width: 100%; + height: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: start; +} + +.elegant-top-messages-container > div { + position: relative; + height: fit-content; + padding: 4px; + margin-top: -100px; /* re-set by JS */ + opacity: 0; + transition: all 0.33s ease-in-out; + z-index: 3; +} + +.elegant-top-messages-container > div:last-child { + z-index: 2; +} + +.elegant-top-messages-container > div:not(.-show) { + z-index: 1; +} + +.elegant-top-messages-container > div.-show { + opacity: 1; + margin-top: 0px !important; +} + +.elegant-top-messages-container > div.-show { + opacity: 1; + transform: translateY(0%); +} + +.elegant-top-messages-container > div > div { + position: relative; + background: #353535; + color: #fff; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + height: fit-content; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.88); + padding: 6px 12px; + border-radius: 4px; + font-family: Arial, sans-serif; + font-size: 14px; +} + +.elegant-top-messages-container > div > div > span { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} + +.elegant-top-messages-container > div > div > span svg { + width: 20px; + height: auto; + margin-right: 8px; +} + +.elegant-top-messages-container > div > div > span svg.icon-checkmark { + fill: #2e9720; +} + +.elegant-top-messages-container [type='warn']::before, +.elegant-top-messages-container [type='success']::before { + content: '⚠️'; + display: inline-block; + flex: 0 0 auto; + font-size: 18px; + margin-right: 4px; + line-height: 1; +} + +.elegant-top-messages-container [type='success']::before { + content: '🎉'; +} + +.elegant-top-messages-container a { + cursor: pointer; + text-decoration: underline; + color: #fc0; + margin-left: 4px; + display: inline-block; + line-height: 1; +} + +.elegant-top-messages-container a:hover { + color: #fc0; + text-decoration: none; +} + +/* Fix node selector being crazy long b/c of array types. */ +.litegraph.litesearchbox input, +.litegraph.litesearchbox select { + max-width: 250px; +} + +/* There's no reason for this z-index to be so high. It layers on top of things it shouldn't, + (like pythongssss' image gallery, the properties panel, etc.) */ +.comfy-multiline-input { + z-index: 1 !important; +} + +.comfy-multiline-input:focus { + z-index: 2 !important; +} + +.litegraph .dialog { + z-index: 3 !important; /* This is set to 1, but goes under the multi-line inputs, so bump it. */ +} + +:not(#fakeid) .elegant-button-reset { + position: relative; + appearance: none; + cursor: pointer; + border: 0; + background: transparent; + color: inherit; + padding: 0; + margin: 0; +} + +:not(#fakeid) .elegant-button { + --padding-top: 7px; + --padding-bottom: 9px; + --padding-x: 16px; + position: relative; + cursor: pointer; + border: 0; + border-radius: 0.25rem; + background: rgba(0, 0, 0, 0.5); + color: white; + font-family: system-ui, sans-serif; + font-size: 1rem; + line-height: 1; + white-space: nowrap; + text-decoration: none; + margin: 0.25rem; + box-shadow: 0px 0px 2px rgb(0, 0, 0); + background: #212121; + transition: all 0.1s ease-in-out; + padding: var(--padding-top) var(--padding-x) var(--padding-bottom); + display: inline-flex; + flex-direction: row; + align-items: center; + justify-content: center; +} +:not(#fakeid) .elegant-button::before, +:not(#fakeid) .elegant-button::after { + content: ''; + display: block; + position: absolute; + border-radius: 0.25rem; + left: 0; + top: 0; + width: 100%; + height: 100%; + box-shadow: inset 1px 1px 0px rgba(255, 255, 255, 0.12), inset -1px -1px 0px rgba(0, 0, 0, 0.75); + background: linear-gradient(to bottom, rgba(255, 255, 255, 0.06), rgba(0, 0, 0, 0.15)); + mix-blend-mode: screen; +} +:not(#fakeid) .elegant-button::after { + mix-blend-mode: multiply; +} +:not(#fakeid) .elegant-button:hover { + background: #303030; +} +:not(#fakeid) .elegant-button:active { + box-shadow: 0px 0px 0px rgba(0, 0, 0, 0); + background: #121212; + padding: calc(var(--padding-top) + 1px) calc(var(--padding-x) - 1px) + calc(var(--padding-bottom) - 1px) calc(var(--padding-x) + 1px); +} +:not(#fakeid) .elegant-button:active::before, +:not(#fakeid) .elegant-button:active::after { + box-shadow: 1px 1px 0px rgba(255, 255, 255, 0.15), inset 1px 1px 0px rgba(0, 0, 0, 0.5), + inset 1px 3px 5px rgba(0, 0, 0, 0.33); +} +:not(#fakeid) .elegant-button.-blue { + background: #346599 !important; +} +:not(#fakeid) .elegant-button.-blue:hover { + background: #3b77b8 !important; +} +:not(#fakeid) .elegant-button.-blue:active { + background: #1d5086 !important; +} +:not(#fakeid) .elegant-button.-green { + background: linear-gradient(to bottom, rgba(255, 255, 255, 0.06), rgba(0, 0, 0, 0.15)), #14580b; +} +:not(#fakeid) .elegant-button.-green:hover { + background: linear-gradient(to bottom, rgba(255, 255, 255, 0.06), rgba(0, 0, 0, 0.15)), #1a6d0f; +} +:not(#fakeid) .elegant-button.-green:active { + background: linear-gradient(to bottom, rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.06)), #0f3f09; +} +:not(#fakeid) .elegant-button[disabled] { + box-shadow: none; + background: #666 !important; + color: #aaa; + pointer-events: none; +} +:not(#fakeid) .elegant-button[disabled]::before, +:not(#fakeid) .elegant-button[disabled]::after { + display: none; +} + +.elegant-dialog { + outline: 0; + border: 0; + border-radius: 6px; + background: #414141; + color: #fff; + box-shadow: inset 1px 1px 0px rgba(255, 255, 255, 0.05), inset -1px -1px 0px rgba(0, 0, 0, 0.5), + 2px 2px 20px rgb(0, 0, 0); + max-width: 800px; + box-sizing: border-box; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + font-size: 1rem; + padding: 0; + max-height: calc(100% - 32px); +} +.elegant-dialog *, +.elegant-dialog *::before, +.elegant-dialog *::after { + box-sizing: inherit; +} + +.elegant-dialog-container > * { + padding: 8px 16px; +} +.elegant-dialog-container > *:first-child { + padding-top: 16px; +} +.elegant-dialog-container > *:last-child { + padding-bottom: 16px; +} + +.elegant-dialog.-iconed::after { + content: ''; + font-size: 276px; + position: absolute; + right: 0px; + bottom: 0px; + opacity: 0.15; + display: block; + width: 237px; + overflow: hidden; + height: 186px; + line-height: 1; + pointer-events: none; + z-index: -1; +} + +.elegant-dialog.-iconed.-help::after { + content: '🛟'; +} + +.elegant-dialog.-iconed.-settings::after { + content: '⚙️'; +} + +@media (max-width: 832px) { + .elegant-dialog { + max-width: calc(100% - 32px); + } +} +.elegant-dialog-container-title { + display: flex; + flex-direction: row; + align-items: center; + justify-content: start; +} + +.elegant-dialog-container-title > svg:first-child { + width: 36px; + height: 36px; + margin-right: 16px; +} + +.elegant-dialog-container-title h2 { + font-size: 1.375rem; + margin: 0; + font-weight: bold; +} + +.elegant-dialog-container-title h2 small { + font-size: 0.8125rem; + font-weight: normal; + opacity: 0.75; +} + +.elegant-dialog-container-content { + overflow: auto; + max-height: calc(100vh - 200px); /* Arbitrary height to copensate for margin, title, and footer.*/ +} + +.elegant-dialog-container-content p { + font-size: 0.8125rem; + margin-top: 0; +} + +.elegant-dialog-container-content ul li p { + margin-bottom: 4px; +} + +.elegant-dialog-container-content ul li p + p { + margin-top: 0.5em; +} + +.elegant-dialog-container-content ul li ul { + margin-top: 0.5em; + margin-bottom: 1em; +} + +.elegant-dialog-container-content p code { + display: inline-block; + padding: 2px 4px; + margin: 0px 2px; + border: 1px solid rgba(255, 255, 255, 0.25); + border-radius: 3px; + background: rgba(255, 255, 255, 0.1); +} + +.elegant-dialog-container-footer { + display: flex; + align-items: center; + justify-content: center; +} + +body.elegant-dialog-open > *:not(.elegant-dialog):not(.elegant-top-messages-container) { + filter: blur(5px); +} + +.elegant-menu { + list-style: none; + padding: 0; + margin: 0; + position: fixed; + z-index: 999999; + pointer-events: none; + opacity: 0; + transition: opacity 0.08s ease-in-out; + color: #dde; + background-color: #111; + font-size: 12px; + box-shadow: 0 0 10px black !important; +} +.elegant-menu > li { + position: relative; + padding: 4px 6px; + z-index: 9999; + white-space: nowrap; +} +.elegant-menu > li[role='button'] { + background-color: var(--comfy-menu-bg) !important; + color: var(--input-text); + cursor: pointer; +} +.elegant-menu > li[role='button']:hover { + filter: brightness(155%); +} +.elegant-menu[state^='measuring'] { + display: block; + opacity: 0; +} +.elegant-menu[state='open'] { + display: block; + opacity: 1; + pointer-events: all; +} + +.elegant-top-menu { + box-sizing: border-box; + white-space: nowrap; + background: var(--content-bg); + color: var(--content-fg); + display: flex; + flex-direction: column; +} +.elegant-top-menu * { + box-sizing: inherit; +} +.elegant-top-menu menu { + list-style: none; + padding: 0; + margin: 0; +} +.elegant-top-menu menu > li:not(#fakeid) { + list-style: none; + padding: 0; + margin: 0; +} +.elegant-top-menu menu > li:not(#fakeid) > button { + cursor: pointer; + padding: 8px 12px 8px 8px; + width: 100%; + text-align: start; + display: flex; + flex-direction: row; + align-items: center; + justify-content: start; +} +.elegant-top-menu menu > li:not(#fakeid) > button:hover { + background-color: var(--comfy-input-bg); +} +.elegant-top-menu menu > li:not(#fakeid) > button svg { + height: 16px; + width: auto; + margin-inline-end: 0.6em; +} +.elegant-top-menu menu > li:not(#fakeid) > button svg.github-star { + fill: rgb(227, 179, 65); +} +.elegant-top-menu menu > li:not(#fakeid).elegant-message { + min-height: 32px; +} +.elegant-top-menu menu > li:not(#fakeid).elegant-message > span { + padding: 8px 12px; + display: block; + width: 100%; + text-align: center; + font-style: italic; + font-size: 12px; +} + +.elegant-dialog.-settings { + width: 100%; +} + +.elegant-dialog.-settings fieldset { + border: 1px solid rgba(255, 255, 255, 0.25); + padding: 0 12px 8px; + margin-bottom: 16px; +} + +.elegant-dialog.-settings fieldset > legend { + margin-left: 8px; + padding: 0 8px; + opacity: 0.5; +} + +.elegant-dialog.-settings .formrow { + display: flex; + flex-direction: column; +} + +.elegant-dialog.-settings .formrow + .formrow { + border-top: 1px solid rgba(255, 255, 255, 0.25); +} + +.elegant-dialog.-settings .fieldrow { + display: flex; + flex-direction: row; +} + +.elegant-dialog.-settings .fieldrow > label { + flex: 1 1 auto; + user-select: none; + padding: 8px 12px 12px; +} + +.elegant-dialog.-settings .fieldrow > label span { + font-weight: bold; +} + +.elegant-dialog.-settings .fieldrow > label small { + display: block; + margin-top: 4px; + font-size: 0.6875rem; + opacity: 0.75; + padding-left: 16px; +} + +.elegant-dialog.-settings .fieldrow ~ .fieldrow { + font-size: 0.9rem; + border-top: 1px dotted rgba(255, 255, 255, 0.25); +} + +.elegant-dialog.-settings .fieldrow ~ .fieldrow label { + padding-left: 28px; +} + +.elegant-dialog.-settings .fieldrow:first-child:not(.-checked) ~ .fieldrow { + display: none; +} + +.elegant-dialog.-settings .fieldrow:hover { + background: rgba(255, 255, 255, 0.1); +} + +.elegant-dialog.-settings .fieldrow ~ .fieldrow span { + font-weight: normal; +} + +.elegant-dialog.-settings .fieldrow > .fieldrow-value { + display: flex; + align-items: center; + justify-content: end; + flex: 0 0 auto; + width: 50%; + max-width: 230px; +} + +.elegant-dialog.-settings .fieldrow.-type-boolean > .fieldrow-value { + max-width: 64px; +} + +.elegant-dialog.-settings .fieldrow.-type-number input { + width: 48px; + text-align: right; +} + +.elegant-dialog.-settings .fieldrow input[type='checkbox'] { + width: 24px; + height: 24px; + cursor: pointer; +} + +.elegant-comfyui-settings-row div { + display: flex; + flex-direction: row; + align-items: center; + justify-content: end; +} + +.elegant-comfyui-settings-row div svg { + width: 36px; + height: 36px; + margin-right: 16px; +} + +.litegraph.litecontextmenu .litemenu-title .elegant-contextmenu-title-elegant-comfy, +.litegraph.litecontextmenu .litemenu-entry.elegant-contextmenu-item { + display: flex; + flex-direction: row; + align-items: center; + justify-content: start; +} + +.litegraph.litecontextmenu .litemenu-title .elegant-contextmenu-title-elegant-comfy svg, +.litegraph.litecontextmenu .litemenu-entry.elegant-contextmenu-item svg { + fill: currentColor; + width: auto; + height: 16px; + margin-right: 6px; +} + +.litegraph.litecontextmenu .litemenu-entry.elegant-contextmenu-item svg.github-star { + fill: rgb(227, 179, 65); +} + +.litegraph.litecontextmenu .litemenu-title .elegant-contextmenu-title-elegant-comfy, +.litegraph.litecontextmenu .litemenu-entry.elegant-contextmenu-label { + color: #dde; + background-color: #212121 !important; + margin: 0; + padding: 2px; + cursor: default; + opacity: 1; + padding: 4px; + font-weight: bold; +} + +.litegraph.litecontextmenu .litemenu-title .elegant-contextmenu-title-elegant-comfy { + font-size: 1.1em; + color: #fff; + background-color: #090909 !important; + justify-content: center; + padding: 4px 8px; +} + +elegant-progress-bar { + display: block; + position: relative; + z-index: 999; + top: 0; + left: 0; + height: 14px; + font-size: 10px; + width: 100%; + overflow: hidden; + box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.25); + box-shadow: inset 0px -1px 0px rgba(0, 0, 0, 0.25), 0px 1px 0px rgba(255, 255, 255, 0.125); +} + +* ~ elegant-progress-bar, +.comfyui-body-bottom elegant-progress-bar { + box-shadow: 0px -1px 0px rgb(0, 0, 0), inset 0px 1px 0px rgba(255, 255, 255, 0.15), + inset 0px -1px 0px rgba(0, 0, 0, 0.25), 0px 1px 0px rgba(255, 255, 255, 0.125); +} + +body:not([style*='grid']) elegant-progress-bar { + position: fixed; + top: 0px; + bottom: auto; +} +body:not([style*='grid']) elegant-progress-bar.elegant-pos-bottom { + top: auto; + bottom: 0px; +} + +.elegant-debug-keydowns { + display: block; + position: fixed; + z-index: 999; + top: 3px; + right: 8px; + font-size: 10px; + color: #fff; + font-family: sans-serif; +} diff --git a/web/comfyui/elegant.js b/web/comfyui/elegant.js new file mode 100644 index 0000000..f29f0c1 --- /dev/null +++ b/web/comfyui/elegant.js @@ -0,0 +1,291 @@ +import { app, ComfyApp } from '/scripts/app.js' +import { api } from '/scripts/api.js' +import { iconGear, iconNode, iconReplace, iconStarFilled } from './svgs.js' +import { injectCss } from './shared_utils.js' + +class Elegant extends EventTarget { + constructor() { + var _a, _b, _c, _d + super() + this.api = api + this.settingsDialog = null + this.progressBarEl = null + this.queueNodeIds = null + this.monitorBadLinksAlerted = false + this.monitorLinkTimeout = null + this.processingQueue = false + this.loadingApiJson = false + this.replacingReroute = null + this.processingMouseDown = false + this.processingMouseUp = false + this.processingMouseMove = false + this.lastAdjustedMouseEvent = null + this.canvasCurrentlyCopyingToClipboard = false + this.canvasCurrentlyCopyingToClipboardWithMultipleNodes = false + this.initialGraphToPromptSerializedWorkflowBecauseComfyUIBrokeStuff = null + this.elDebugKeydowns = null + this.nodes = [] + this.isMac = !!( + ((_a = navigator.platform) === null || _a === void 0 + ? void 0 + : _a.toLocaleUpperCase().startsWith('MAC')) || + ((_c = (_b = navigator.userAgentData) === null || _b === void 0 ? void 0 : _b.platform) === + null || _c === void 0 + ? void 0 + : _c.toLocaleUpperCase().startsWith('MAC')) + ) + + this.initializeGraphAndCanvasHooks() + // Wrap the method + const originalGetNodeDefs = api.getNodeDefs.bind(api) + + api.getNodeDefs = async function (...args) { + // Trigger a custom event before calling the original method + const event = new CustomEvent('getNodeDefsCalled', { + detail: { args }, + }) + window.dispatchEvent(event) + + // Call the original method + const result = await originalGetNodeDefs(...args) + + // Optionally, you can also dispatch another event after the method completes + window.dispatchEvent( + new CustomEvent('getNodeDefsCompleted', { + detail: { result }, + }), + ) + + return result + } + + // Add event listeners + window.addEventListener('getNodeDefsCalled', (event) => {}) + + window.addEventListener('getNodeDefsCompleted', (event) => { + this.nodes = Object.values(event.detail.result).filter((n) => n.category.includes('CCTech')) + }) + + this.initializeContextMenu() + this.cssPromise = injectCss('extensions/ComfyUI-Elegant-Resource-Monitor/comfyui/elegant.css') + } + + async initializeGraphAndCanvasHooks() { + const elegant = this + const graphSerialize = LGraph.prototype.serialize + LGraph.prototype.serialize = function () { + const response = graphSerialize.apply(this, [...arguments]) + elegant.initialGraphToPromptSerializedWorkflowBecauseComfyUIBrokeStuff = response + return response + } + const processMouseDown = LGraphCanvas.prototype.processMouseDown + LGraphCanvas.prototype.processMouseDown = function (e) { + elegant.processingMouseDown = true + const returnVal = processMouseDown.apply(this, [...arguments]) + elegant.dispatchCustomEvent('on-process-mouse-down', { originalEvent: e }) + elegant.processingMouseDown = false + return returnVal + } + const adjustMouseEvent = LGraphCanvas.prototype.adjustMouseEvent + LGraphCanvas.prototype.adjustMouseEvent = function (e) { + adjustMouseEvent.apply(this, [...arguments]) + elegant.lastAdjustedMouseEvent = e + } + const copyToClipboard = LGraphCanvas.prototype.copyToClipboard + LGraphCanvas.prototype.copyToClipboard = function (nodes) { + elegant.canvasCurrentlyCopyingToClipboard = true + elegant.canvasCurrentlyCopyingToClipboardWithMultipleNodes = + Object.values(nodes || this.selected_nodes || []).length > 1 + copyToClipboard.apply(this, [...arguments]) + elegant.canvasCurrentlyCopyingToClipboard = false + elegant.canvasCurrentlyCopyingToClipboardWithMultipleNodes = false + } + const onGroupAdd = LGraphCanvas.onGroupAdd + LGraphCanvas.onGroupAdd = function (...args) { + const graph = app.graph + onGroupAdd.apply(this, [...args]) + LGraphCanvas.onShowPropertyEditor( + {}, + null, + null, + null, + graph._groups[graph._groups.length - 1], + ) + } + } + + async invokeExtensionsAsync(method, ...args) { + return Promise.resolve() + } + dispatchCustomEvent(event, detail) { + if (detail != null) { + return this.dispatchEvent(new CustomEvent(event, { detail })) + } + return this.dispatchEvent(new CustomEvent(event)) + } + async initializeContextMenu() { + const that = this + setTimeout(async () => { + const getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions + LGraphCanvas.prototype.getCanvasMenuOptions = function (...args) { + let existingOptions = getCanvasMenuOptions.apply(this, [...args]) + const options = [] + options.push(null) + options.push(null) + options.push(null) + options.push({ + content: `🤖 CCTech`, + className: 'elegant-contextmenu-item elegant-contextmenu-main-item-elegant-comfy', + submenu: { + options: that.getElegantContextMenuItems(), + }, + }) + options.push(null) + options.push(null) + let idx = null + idx = + idx || + existingOptions.findIndex((o) => { + var _a, _b + return (_b = + (_a = o === null || o === void 0 ? void 0 : o.content) === null || _a === void 0 + ? void 0 + : _a.startsWith) === null || _b === void 0 + ? void 0 + : _b.call(_a, 'Queue Group') + }) + 1 + idx = + idx || + existingOptions.findIndex((o) => { + var _a, _b + return (_b = + (_a = o === null || o === void 0 ? void 0 : o.content) === null || _a === void 0 + ? void 0 + : _a.startsWith) === null || _b === void 0 + ? void 0 + : _b.call(_a, 'Queue Selected') + }) + 1 + idx = + idx || + existingOptions.findIndex((o) => { + var _a, _b + return (_b = + (_a = o === null || o === void 0 ? void 0 : o.content) === null || _a === void 0 + ? void 0 + : _a.startsWith) === null || _b === void 0 + ? void 0 + : _b.call(_a, 'Convert to Group') + }) + idx = + idx || + existingOptions.findIndex((o) => { + var _a, _b + return (_b = + (_a = o === null || o === void 0 ? void 0 : o.content) === null || _a === void 0 + ? void 0 + : _a.startsWith) === null || _b === void 0 + ? void 0 + : _b.call(_a, 'Arrange (') + }) + idx = idx || existingOptions.findIndex((o) => !o) + 1 + idx = idx || 3 + existingOptions.splice(idx, 0, ...options) + for (let i = existingOptions.length; i > 0; i--) { + if (existingOptions[i] === null && existingOptions[i + 1] === null) { + existingOptions.splice(i, 1) + } + } + return existingOptions + } + }, 1016) + } + getElegantContextMenuItems() { + const [canvas, graph] = [app.canvas, app.graph] + const selectedNodes = Object.values(canvas.selected_nodes || {}) + let rerouteNodes = [] + if (selectedNodes.length) { + rerouteNodes = selectedNodes.filter((n) => n.type === 'Reroute') + } else { + rerouteNodes = graph._nodes.filter((n) => n.type == 'Reroute') + } + const rerouteLabel = selectedNodes.length ? 'selected' : 'all' + const bookmarkMenuItems = getBookmarks() ?? [] + return [ + { + content: 'Nodes', + disabled: true, + className: 'elegant-contextmenu-item elegant-contextmenu-label', + }, + { + content: iconNode + 'All', + className: 'elegant-contextmenu-item', + has_submenu: true, + submenu: { + options: this.nodes.map((n) => n.display_name), + callback: (value, options, event) => { + const nodeValue = this.nodes.find((n) => n.display_name.includes(value)) + const node = LiteGraph.createNode(nodeValue.name) + node.pos = [this.lastAdjustedMouseEvent.canvasX, this.lastAdjustedMouseEvent.canvasY] + canvas.graph.add(node) + canvas.selectNode(node) + app.graph.setDirtyCanvas(true, true) + }, + extra: { elegant_doNotNest: true }, + }, + }, + // { + // content: 'Actions', + // disabled: true, + // className: 'elegant-contextmenu-item elegant-contextmenu-label', + // }, + // { + // content: iconGear + 'Settings (coming soon)', + // disabled: true, + // className: 'elegant-contextmenu-item', + // callback: (...args) => { + // this.settingsDialog.addEventListener('close', (e) => { + // this.settingsDialog = null + // }) + // }, + // }, + ...bookmarkMenuItems, + { + content: 'More...', + disabled: true, + className: 'elegant-contextmenu-item elegant-contextmenu-label', + }, + { + content: iconStarFilled + 'Star on Github', + className: 'elegant-contextmenu-item elegant-contextmenu-github', + callback: (...args) => { + window.open('https://github.com/ChrisColeTech/', '_blank') + }, + }, + ] + } +} +function getBookmarks() { + const graph = app.graph + const bookmarks = graph._nodes + .filter((n) => n.type === 'NodeTypesString.BOOKMARK') + .sort((a, b) => a.title.localeCompare(b.title)) + .map((n) => ({ + content: `[${n.shortcutKey}] ${n.title}`, + className: 'elegant-contextmenu-item', + callback: () => { + n.canvasToBookmark() + }, + })) + return !bookmarks.length + ? [] + : [ + { + content: '🔖 Bookmarks', + disabled: true, + className: 'elegant-contextmenu-item elegant-contextmenu-label', + }, + ...bookmarks, + ] +} +export const elegant = new Elegant() +window.elegant = elegant diff --git a/web/comfyui/shared_utils.js b/web/comfyui/shared_utils.js new file mode 100644 index 0000000..dad89f6 --- /dev/null +++ b/web/comfyui/shared_utils.js @@ -0,0 +1,115 @@ +export function getResolver(timeout = 5000) { + const resolver = {} + resolver.id = generateId(8) + resolver.completed = false + resolver.resolved = false + resolver.rejected = false + resolver.promise = new Promise((resolve, reject) => { + resolver.reject = () => { + resolver.completed = true + resolver.rejected = true + reject() + } + resolver.resolve = (data) => { + resolver.completed = true + resolver.resolved = true + resolve(data) + } + }) + resolver.timeout = setTimeout(() => { + if (!resolver.completed) { + resolver.reject() + } + }, timeout) + return resolver +} +const DEBOUNCE_FN_TO_PROMISE = new WeakMap() +export function debounce(fn, ms = 64) { + if (!DEBOUNCE_FN_TO_PROMISE.get(fn)) { + DEBOUNCE_FN_TO_PROMISE.set( + fn, + wait(ms).then(() => { + DEBOUNCE_FN_TO_PROMISE.delete(fn) + fn() + }), + ) + } + return DEBOUNCE_FN_TO_PROMISE.get(fn) +} +export function wait(ms = 16) { + if (ms === 16) { + return new Promise((resolve) => { + requestAnimationFrame(() => { + resolve() + }) + }) + } + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, ms) + }) +} +function dec2hex(dec) { + return dec.toString(16).padStart(2, '0') +} +export function generateId(length) { + const arr = new Uint8Array(length / 2) + crypto.getRandomValues(arr) + return Array.from(arr, dec2hex).join('') +} +export function getObjectValue(obj, objKey, def) { + if (!obj || !objKey) return def + const keys = objKey.split('.') + const key = keys.shift() + const found = obj[key] + if (keys.length) { + return getObjectValue(found, keys.join('.'), def) + } + return found +} +export function setObjectValue(obj, objKey, value, createMissingObjects = true) { + if (!obj || !objKey) return obj + const keys = objKey.split('.') + const key = keys.shift() + if (obj[key] === undefined) { + if (!createMissingObjects) { + return + } + obj[key] = {} + } + if (!keys.length) { + obj[key] = value + } else { + if (typeof obj[key] != 'object') { + obj[key] = {} + } + setObjectValue(obj[key], keys.join('.'), value, createMissingObjects) + } + return obj +} +export function moveArrayItem(arr, itemOrFrom, to) { + const from = typeof itemOrFrom === 'number' ? itemOrFrom : arr.indexOf(itemOrFrom) + arr.splice(to, 0, arr.splice(from, 1)[0]) +} +export function removeArrayItem(arr, itemOrIndex) { + const index = typeof itemOrIndex === 'number' ? itemOrIndex : arr.indexOf(itemOrIndex) + arr.splice(index, 1) +} +export function injectCss(href) { + if (document.querySelector(`link[href^="${href}"]`)) { + return Promise.resolve() + } + return new Promise((resolve) => { + const link = document.createElement('link') + link.setAttribute('rel', 'stylesheet') + link.setAttribute('type', 'text/css') + const timeout = setTimeout(resolve, 1000) + link.addEventListener('load', (e) => { + clearInterval(timeout) + resolve() + }) + link.href = href + document.head.appendChild(link) + }) +} diff --git a/web/comfyui/svgs.js b/web/comfyui/svgs.js new file mode 100644 index 0000000..d21c72f --- /dev/null +++ b/web/comfyui/svgs.js @@ -0,0 +1,160 @@ +import { createElement as $el } from './utils_dom.js' +export const logoElegant = `` +export const github = `` +export const iconStarFilled = ` + + ` +export const iconReplace = ` + + + + + ` +export const iconNode = ` + + + ` +export const iconGear = ` + + ` +export const checkmark = ` + + + + ` +export const logoCivitai = ` + + + + + + + + + + ` +export const iconOutLink = ` + + ` +export const link = ` + + ` +export const pencil = ` + + ` +export const dotdotdot = ` + + + +` +export const models = ` + + + + +` +export const pencilColored = ` + + + + + + + + + ` +export const diskColored = ` + + + + + + + +` +export const folderColored = ` + + +` +export const modelsColored = ` + + + + +` +export const legoBlocksColored = ` + + + + + + + + + + + + +` +export const legoBlockColored = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +` +export const gearColored = ` + + + + +` +export function $svg(markup, attrs) { + if (!markup.match(/^\s* !!n) +} +export function queryOne(selectors, parent = document) { + var _a + return (_a = parent.querySelector(selectors)) !== null && _a !== void 0 ? _a : null +} +export function createText(text) { + return document.createTextNode(text) +} +export function getClosestOrSelf(element, query) { + const el = element + return ( + ((el === null || el === void 0 ? void 0 : el.closest) && + ((el.matches(query) && el) || el.closest(query))) || + null + ) +} +export function createElement(selectorOrMarkup, attrs) { + const frag = getHtmlFragment(selectorOrMarkup) + let element = frag === null || frag === void 0 ? void 0 : frag.firstElementChild + let selector = '' + if (!element) { + selector = selectorOrMarkup.replace(/[\r\n]\s*/g, '') + const tag = getSelectorTag(selector) || 'div' + element = document.createElement(tag) + selector = selector.replace(RGX_TAG, '$2') + selector = selector.replace(RGX_ATTR_ID, '[id="$1"]') + selector = selector.replace( + RGX_ATTR_CLASS, + (match, p1, p2) => `${p1}[class="${p2.replace(/\./g, ' ')}"]`, + ) + } + const selectorAttrs = getSelectorAttributes(selector) + if (selectorAttrs) { + for (const attr of selectorAttrs) { + let matches = attr.substring(1, attr.length - 1).split('=') + let key = localAssertNotFalsy(matches.shift()) + let value = matches.join('=') + if (value === undefined) { + setAttribute(element, key, true) + } else { + value = value.replace(/^['"](.*)['"]$/, '$1') + setAttribute(element, key, value) + } + } + } + if (attrs) { + setAttributes(element, attrs) + } + return element +} +function getSelectorTag(str) { + return tryMatch(str, RGX_TAG) +} +function getSelectorAttributes(selector) { + RGX_ATTRS_MAYBE_OPEN.lastIndex = 0 + let attrs = [] + let result + while ((result = RGX_ATTRS_MAYBE_OPEN.exec(selector))) { + let attr = result[0] + if (attr.endsWith(']')) { + attrs.push(attr) + } else { + attr = + result[0] + getOpenAttributesRecursive(selector.substr(RGX_ATTRS_MAYBE_OPEN.lastIndex), 2) + RGX_ATTRS_MAYBE_OPEN.lastIndex += attr.length - result[0].length + attrs.push(attr) + } + } + return attrs +} +function getOpenAttributesRecursive(selectorSubstring, openCount) { + let matches = selectorSubstring.match(RGX_ATTRS_FOLLOW_OPEN) + let result = '' + if (matches && matches.length) { + result = matches[0] + openCount += result.endsWith(']') ? -1 : 1 + if (openCount > 0) { + result += getOpenAttributesRecursive(selectorSubstring.substr(result.length), openCount) + } + } + return result +} +function tryMatch(str, rgx, index = 1) { + var _a + let found = '' + try { + found = ((_a = str.match(rgx)) === null || _a === void 0 ? void 0 : _a[index]) || '' + } catch (e) { + found = '' + } + return found +} +export function setAttributes(element, data) { + let attr + for (attr in data) { + if (data.hasOwnProperty(attr)) { + setAttribute(element, attr, data[attr]) + } + } +} +function getHtmlFragment(value) { + if (value.match(/^\s*<.*?>[\s\S]*<\/[a-z0-9]+>\s*$/)) { + return document.createRange().createContextualFragment(value.trim()) + } + return null +} +function getChild(value) { + if (value instanceof Node) { + return value + } + if (typeof value === 'string') { + let child = getHtmlFragment(value) + if (child) { + return child + } + if (getSelectorTag(value)) { + return createElement(value) + } + return createText(value) + } + if (value && typeof value.toElement === 'function') { + return value.toElement() + } + return null +} +export function setAttribute(element, attribute, value) { + let isRemoving = value == null + if (attribute === 'default') { + attribute = RGX_DEFAULT_VALUE_PROP.test(element.nodeName) ? 'value' : 'text' + } + if (attribute === 'text') { + empty(element).appendChild(createText(value != null ? String(value) : '')) + } else if (attribute === 'html') { + empty(element).innerHTML += value != null ? String(value) : '' + } else if (attribute == 'style') { + if (typeof value === 'string') { + element.style.cssText = isRemoving ? '' : value != null ? String(value) : '' + } else { + for (const [styleKey, styleValue] of Object.entries(value)) { + element.style[styleKey] = styleValue + } + } + } else if (attribute == 'events') { + for (const [key, fn] of Object.entries(value)) { + addEvent(element, key, fn) + } + } else if (attribute === 'parent') { + value.appendChild(element) + } else if (attribute === 'child' || attribute === 'children') { + if (typeof value === 'string' && /^\[[^\[\]]+\]$/.test(value)) { + const parseable = value.replace(/^\[([^\[\]]+)\]$/, '["$1"]').replace(/,/g, '","') + try { + const parsed = JSON.parse(parseable) + value = parsed + } catch (e) { + console.error(e) + } + } + if (attribute === 'children') { + empty(element) + } + let children = value instanceof Array ? value : [value] + for (let child of children) { + child = getChild(child) + if (child instanceof Node) { + if (element instanceof HTMLTemplateElement) { + element.content.appendChild(child) + } else { + element.appendChild(child) + } + } + } + } else if (attribute == 'for') { + element.htmlFor = value != null ? String(value) : '' + if (isRemoving) { + element.removeAttribute('for') + } + } else if (attribute === 'class' || attribute === 'className' || attribute === 'classes') { + element.className = isRemoving ? '' : Array.isArray(value) ? value.join(' ') : String(value) + } else if (attribute === 'dataset') { + if (typeof value !== 'object') { + console.error('Expecting an object for dataset') + return + } + for (const [key, val] of Object.entries(value)) { + element.dataset[key] = String(val) + } + } else if (attribute == 'onclick' && typeof value === 'function') { + element.addEventListener('click', value) + } else if (['checked', 'disabled', 'readonly', 'required', 'selected'].includes(attribute)) { + element[attribute] = !!value + if (!value) { + element.removeAttribute(attribute) + } else { + element.setAttribute(attribute, attribute) + } + } else if (DIRECT_ATTRIBUTE_MAP.hasOwnProperty(attribute)) { + if (isRemoving) { + element.removeAttribute(DIRECT_ATTRIBUTE_MAP[attribute]) + } else { + element.setAttribute(DIRECT_ATTRIBUTE_MAP[attribute], String(value)) + } + } else if (isRemoving) { + element.removeAttribute(attribute) + } else { + let oldVal = element.getAttribute(attribute) + if (oldVal !== value) { + element.setAttribute(attribute, String(value)) + } + } +} +function addEvent(element, key, fn) { + element.addEventListener(key, fn) +} +function setStyles(element, styles = null) { + if (styles) { + for (let name in styles) { + setStyle(element, name, styles[name]) + } + } + return element +} +function setStyle(element, name, value) { + name = name.indexOf('float') > -1 ? 'cssFloat' : name + if (name.indexOf('-') != -1) { + name = name.replace(/-\D/g, (match) => { + return match.charAt(1).toUpperCase() + }) + } + if (value == String(Number(value)) && RGX_NUMERIC_STYLE.test(name)) { + value = value + RGX_NUMERIC_STYLE_UNIT + } + if (name === 'display' && typeof value !== 'string') { + value = !!value ? null : 'none' + } + element.style[name] = value === null ? null : String(value) + return element +} +export function empty(element) { + while (element.firstChild) { + element.removeChild(element.firstChild) + } + return element +} +export function appendChildren(el, children) { + children = !Array.isArray(children) ? [children] : children + for (let child of children) { + child = getChild(child) + if (child instanceof Node) { + if (el instanceof HTMLTemplateElement) { + el.content.appendChild(child) + } else { + el.appendChild(child) + } + } + } +}