@@ -1,6 +1,6 @@
|
||||
# ComfyUI-Crystools [](https://hits.seeyoufarm.com) [](https://paypal.me/crystian77)
|
||||
|
||||
**_A powerful set of tools for your belt when you work with ComfyUI_**
|
||||
**_🪛 A powerful set of tools for your belt when you work with ComfyUI 🪛_**
|
||||
|
||||
You can see the progress bar & time elapsed also you can see metadata and compare between two images, compare between two JSONs, show any value to console/display, pipes, and more!
|
||||
This provides better nodes to load images, previews, etc, and see "hidden" data, but without loading a new workflow.
|
||||
@@ -23,6 +23,29 @@ This provides better nodes to load images, previews, etc, and see "hidden" data,
|
||||
|
||||
## General
|
||||
|
||||
### Resources monitor
|
||||
|
||||
**🎉Finally, you can see the resources used by ComfyUI (CPU, GPU, RAM, VRAM, and space) on the menu in real-time!**
|
||||
|
||||

|
||||
|
||||
Now you can identify the bottlenecks in your workflow and know when it's time to restart the server, unload models or even close some tabs!
|
||||
|
||||
You can configure the refresh rate which resources to show:
|
||||
|
||||

|
||||
|
||||
> **Notes:**
|
||||
> - The GPU data is only available when you use CUDA (only NVIDIA cards, sorry AMD users).
|
||||
> - It is not perfect, but it is a good approximation.
|
||||
> - Data comes from these libraries:
|
||||
> - [psutil](https://pypi.org/project/psutil/)
|
||||
> - [torch](https://pytorch.org/)
|
||||
> - [pynvml](https://pypi.org/project/pynvml/) (official NVIDIA library)
|
||||
> - This is a beta version, so please report any issue you find.
|
||||
|
||||
|
||||
|
||||
### Progress bar
|
||||
|
||||
You can see the progress of your workflow with a progress bar on the menu!
|
||||
@@ -33,8 +56,6 @@ https://github.com/crystian/ComfyUI-Crystools/assets/3886806/35cc1257-2199-4b85-
|
||||
|
||||
Additionally, it shows the time elapsed at the end of the workflow, and you can `click` on it to see the **current working node.**
|
||||
|
||||
Additionally, it shows the time elapsed at the end of the workflow, and you can `click` on it to see the **current working node.**
|
||||
|
||||
> **Notes:**
|
||||
> - If you don't want to see it, you can turn it off from settings (`Show progress bar in menu`)
|
||||
|
||||
@@ -483,14 +504,19 @@ You have predefined switches (string, latent, image, conditioning) but you can u
|
||||
---
|
||||
|
||||
## To do
|
||||
- [✅] Review the priority of the list of nodes
|
||||
- [❌] Add support for other image formats (jpg, gif, etc.) - nope, I don't need it; it's complicated
|
||||
- [ ] Several unit tests
|
||||
- [ ] Add permanent cache for preview/metadata image (to survive to F5! or restart the server)
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
### 1.3.0 (08/01/2024)
|
||||
- Added in general Resources monitor (CPU, GPU, RAM, VRAM, and space)
|
||||
- Added this icon to identify this set of tools: 🪛
|
||||
|
||||
### Crystools-save: 1.1.0 (07/01/2024)
|
||||
- Labeling updated according to the new version of Crystools (this project)
|
||||
|
||||
### 1.2.0 (05/01/2024)
|
||||
- progress bar added
|
||||
|
||||
|
||||
+2
-1
@@ -2,7 +2,7 @@
|
||||
@author: Crystian
|
||||
@title: Crystools
|
||||
@nickname: Crystools
|
||||
@version: 1.2.0
|
||||
@version: 1.3.0
|
||||
@project: "https://github.com/crystian/ComfyUI-Crystools",
|
||||
@description: Plugins for multiples uses, mainly for debugging, you need them! IG: https://www.instagram.com/crystian.ia
|
||||
"""
|
||||
@@ -18,6 +18,7 @@ from .nodes.list import CListAny, CListString
|
||||
from .nodes.pipe import CPipeToAny, CPipeFromAny
|
||||
from .nodes.utils import CUtilsCompareJsons, CUtilsStatSystem
|
||||
from .nodes.metadata import CMetadataExtractor, CMetadataCompare
|
||||
from .general import *
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
CLASSES.CBOOLEAN_NAME.value: CBoolean,
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ class TEXTS(Enum):
|
||||
|
||||
class CATEGORY(Enum):
|
||||
TESTING = "_for_testing"
|
||||
MAIN = "crystools"
|
||||
MAIN = "crystools 🪛"
|
||||
PRIMITIVE = "/Primitive"
|
||||
DEBUGGER = "/Debugger"
|
||||
LIST = "/List"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
@@ -0,0 +1 @@
|
||||
from .monitor import *
|
||||
@@ -0,0 +1,264 @@
|
||||
import asyncio
|
||||
import server
|
||||
import time
|
||||
import threading
|
||||
import psutil
|
||||
import torch
|
||||
import pynvml
|
||||
from comfy.model_management import get_torch_device_name, get_torch_device
|
||||
from server import PromptServer
|
||||
from aiohttp import web
|
||||
from ..core import logger
|
||||
|
||||
lock = threading.Lock()
|
||||
|
||||
class CMonitor:
|
||||
monitorThread = None
|
||||
threadController = threading.Event()
|
||||
rate = 0
|
||||
nvidia = False
|
||||
pynvmlLoaded = False
|
||||
cudaAvailable = False
|
||||
torchDevice = 'cpu'
|
||||
cudaDevice = 'cpu'
|
||||
cudaDevicesFound = 0
|
||||
switchCPU = False
|
||||
switchGPU = False
|
||||
switchHDD = False
|
||||
switchRAM = False
|
||||
switchVRAM = False
|
||||
|
||||
def __init__(self, rate=5, switchCPU=False, switchGPU=False, switchHDD=False, switchRAM=False, switchVRAM=False):
|
||||
self.rate = rate
|
||||
self.switchCPU = switchCPU
|
||||
self.switchGPU = switchGPU
|
||||
self.switchHDD = switchHDD
|
||||
self.switchRAM = switchRAM
|
||||
self.switchVRAM = switchVRAM
|
||||
|
||||
try:
|
||||
pynvml.nvmlInit()
|
||||
self.pynvmlLoaded = True
|
||||
except Exception as e:
|
||||
self.pynvmlLoaded = False
|
||||
logger.error('Could not init pynvml.' + str(e))
|
||||
|
||||
self.startMonitor()
|
||||
|
||||
def buildMonitorData(self):
|
||||
cpu = -1
|
||||
ramTotal = -1
|
||||
ramUsed = -1
|
||||
ramUsedPercent = -1
|
||||
hddTotal = -1
|
||||
hddUsed = -1
|
||||
hddUsedPercent = -1
|
||||
gpuUtilization = -1
|
||||
vramUsed = -1
|
||||
vramTotal = -1
|
||||
vramPercent = -1
|
||||
|
||||
if self.switchCPU:
|
||||
cpu = psutil.cpu_percent()
|
||||
|
||||
if self.switchRAM:
|
||||
ram = psutil.virtual_memory()
|
||||
ramTotal = ram.total
|
||||
ramUsed = ram.used
|
||||
ramUsedPercent = ram.percent
|
||||
|
||||
if self.switchHDD:
|
||||
hdd = psutil.disk_usage('/')
|
||||
hddTotal = hdd.total
|
||||
hddUsed = hdd.used
|
||||
hddUsedPercent = hdd.percent
|
||||
|
||||
deviceType = 'cpu'
|
||||
gpus = []
|
||||
|
||||
if self.cudaDevice == 'cpu':
|
||||
gpus.append({
|
||||
'gpu_utilization': -1,
|
||||
'vram_total': -1,
|
||||
'vram_used': -1,
|
||||
'vram_used_percent': -1,
|
||||
})
|
||||
else:
|
||||
deviceType = self.cudaDevice
|
||||
|
||||
if self.pynvmlLoaded and self.nvidia and self.cudaAvailable:
|
||||
for deviceIndex in range(self.cudaDevicesFound):
|
||||
deviceHandle = pynvml.nvmlDeviceGetHandleByIndex(deviceIndex)
|
||||
|
||||
# GPU Utilization
|
||||
if self.switchGPU:
|
||||
utilization = pynvml.nvmlDeviceGetUtilizationRates(deviceHandle)
|
||||
gpuUtilization = utilization.gpu
|
||||
|
||||
# VRAM
|
||||
if self.switchVRAM:
|
||||
# Torch or pynvml?, pynvml is more accurate with the system, torch is more accurate with comfyUI
|
||||
memory = pynvml.nvmlDeviceGetMemoryInfo(deviceHandle)
|
||||
vramUsed = memory.used
|
||||
vramTotal = memory.total
|
||||
|
||||
# device = torch.device(deviceType)
|
||||
# vramUsed = torch.cuda.memory_allocated(device)
|
||||
# vramTotal = torch.cuda.get_device_properties(device).total_memory
|
||||
|
||||
vramPercent = vramUsed / vramTotal * 100
|
||||
|
||||
gpus.append({
|
||||
'gpu_utilization': gpuUtilization,
|
||||
'vram_total': vramTotal,
|
||||
'vram_used': vramUsed,
|
||||
'vram_used_percent': vramPercent,
|
||||
})
|
||||
|
||||
return {
|
||||
'cpu_utilization': cpu,
|
||||
'ram_total': ramTotal,
|
||||
'ram_used': ramUsed,
|
||||
'ram_used_percent': ramUsedPercent,
|
||||
'hdd_total': hddTotal,
|
||||
'hdd_used': hddUsed,
|
||||
'hdd_used_percent': hddUsedPercent,
|
||||
'device_type': deviceType,
|
||||
'gpus': gpus,
|
||||
}
|
||||
|
||||
|
||||
async def send_message(self, data) -> None:
|
||||
s = server.PromptServer.instance
|
||||
# logger.debug(f'Sending message... {data}')
|
||||
await s.send_json('crystools.monitor', data)
|
||||
|
||||
def monitorLoop(self):
|
||||
while self.rate > 0 and not self.threadController.is_set():
|
||||
lock.acquire()
|
||||
asyncio.run(self.send_message(self.buildMonitorData()))
|
||||
lock.release()
|
||||
time.sleep(self.rate)
|
||||
|
||||
def startMonitor(self):
|
||||
if self.monitorThread is not None:
|
||||
self.stopMonitor()
|
||||
logger.info('Restarting monitor...')
|
||||
else:
|
||||
if self.rate == 0:
|
||||
logger.debug('Monitor rate is 0, not starting monitor.')
|
||||
return None
|
||||
|
||||
logger.info('Starting monitor...')
|
||||
|
||||
if self.pynvmlLoaded and pynvml.nvmlDeviceGetCount() > 0:
|
||||
self.cudaDevicesFound = pynvml.nvmlDeviceGetCount()
|
||||
self.nvidia = True
|
||||
logger.info(f'NVIDIA Driver detected - {pynvml.nvmlSystemGetDriverVersion()}')
|
||||
else:
|
||||
logger.warn('No NVIDIA GPU detected.')
|
||||
|
||||
try:
|
||||
self.torchDevice = get_torch_device_name(get_torch_device())
|
||||
except Exception as e:
|
||||
logger.error('Could not pick default device.' + str(e))
|
||||
|
||||
self.cudaDevice = 'cpu' if self.torchDevice == 'cpu' else 'cuda'
|
||||
self.cudaAvailable = torch.cuda.is_available()
|
||||
|
||||
if self.nvidia and self.cudaAvailable and self.torchDevice == 'cpu':
|
||||
logger.warn('CUDA is available, but torch is using CPU.')
|
||||
|
||||
self.threadController.clear()
|
||||
|
||||
if self.monitorThread is None or not self.monitorThread.is_alive():
|
||||
self.monitorThread = threading.Thread(target=self.monitorLoop)
|
||||
self.monitorThread.daemon = True
|
||||
self.monitorThread.start()
|
||||
|
||||
|
||||
def stopMonitor(self):
|
||||
logger.debug('Stopping monitor...')
|
||||
self.threadController.set()
|
||||
|
||||
|
||||
cmonitor = CMonitor(1, True, True, True, True, True)
|
||||
|
||||
|
||||
@PromptServer.instance.routes.patch("/crystools/monitor")
|
||||
async def newSettings(request):
|
||||
try:
|
||||
settings = await request.json()
|
||||
|
||||
if 'rate' in settings is not None:
|
||||
rate = settings['rate']
|
||||
if type(rate) is not int and type(rate) is not float:
|
||||
raise Exception('Rate must be an number.')
|
||||
|
||||
if cmonitor.rate == 0 and rate > 0:
|
||||
cmonitor.rate = rate
|
||||
cmonitor.startMonitor()
|
||||
else:
|
||||
cmonitor.rate = rate
|
||||
|
||||
|
||||
if 'switchCPU' in settings is not None:
|
||||
switchCPU = settings['switchCPU']
|
||||
if type(switchCPU) is not bool:
|
||||
raise Exception('switchCPU must be an boolean.')
|
||||
|
||||
cmonitor.switchCPU = switchCPU
|
||||
|
||||
if 'switchGPU' in settings is not None:
|
||||
switchGPU = settings['switchGPU']
|
||||
if type(switchGPU) is not bool:
|
||||
raise Exception('switchGPU must be an boolean.')
|
||||
|
||||
cmonitor.switchGPU = switchGPU
|
||||
|
||||
if 'switchHDD' in settings is not None:
|
||||
switchHDD = settings['switchHDD']
|
||||
if type(switchHDD) is not bool:
|
||||
raise Exception('switchHDD must be an boolean.')
|
||||
|
||||
cmonitor.switchHDD = switchHDD
|
||||
|
||||
if 'switchRAM' in settings is not None:
|
||||
switchRAM = settings['switchRAM']
|
||||
if type(switchRAM) is not bool:
|
||||
raise Exception('switchRAM must be an boolean.')
|
||||
|
||||
cmonitor.switchRAM = switchRAM
|
||||
|
||||
if 'switchVRAM' in settings is not None:
|
||||
switchVRAM = settings['switchVRAM']
|
||||
if type(switchVRAM) is not bool:
|
||||
raise Exception('switchVRAM must be an boolean.')
|
||||
|
||||
cmonitor.switchVRAM = switchVRAM
|
||||
|
||||
return web.Response(status=200)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return web.Response(status=400, text=str(e))
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/crystools/monitor/switch")
|
||||
async def monitorSwitch(request):
|
||||
try:
|
||||
switch = await request.json()
|
||||
|
||||
if 'monitor' in switch is not None:
|
||||
monitor = switch['monitor']
|
||||
if type(monitor) is not bool:
|
||||
raise Exception('monitor must be an boolean.')
|
||||
|
||||
if monitor:
|
||||
cmonitor.startMonitor()
|
||||
else:
|
||||
cmonitor.stopMonitor()
|
||||
|
||||
return web.Response(status=200)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return web.Response(status=400, text=str(e))
|
||||
+55
-54
@@ -1,66 +1,67 @@
|
||||
from enum import Enum
|
||||
|
||||
postfix = ' 🪛'
|
||||
|
||||
# IMPORTANT DON'T CHANGE THE "NAME" AND "TYPE" OF THE ENUMS, IT WILL BREAK THE COMPATIBILITY!
|
||||
# IMPORTANT DON'T CHANGE THE 'NAME' AND 'TYPE' OF THE ENUMS, IT WILL BREAK THE COMPATIBILITY!
|
||||
# remember: NAME is for search, DESC is for contextual
|
||||
class CLASSES(Enum):
|
||||
CBOOLEAN_NAME = "Primitive boolean [Crystools]"
|
||||
CBOOLEAN_DESC = "Primitive boolean [Crystools]"
|
||||
CTEXT_NAME = "Primitive string [Crystools]"
|
||||
CTEXT_DESC = "Primitive string [Crystools]"
|
||||
CTEXTML_NAME = "Primitive string multiline [Crystools]"
|
||||
CTEXTML_DESC = "Primitive string multiline [Crystools]"
|
||||
CINTEGER_NAME = "Primitive integer [Crystools]"
|
||||
CINTEGER_DESC = "Primitive integer [Crystools]"
|
||||
CFLOAT_NAME = "Primitive float [Crystools]"
|
||||
CFLOAT_DESC = "Primitive float [Crystools]"
|
||||
CBOOLEAN_NAME = 'Primitive boolean [Crystools]'
|
||||
CBOOLEAN_DESC = 'Primitive boolean' + postfix
|
||||
CTEXT_NAME = 'Primitive string [Crystools]'
|
||||
CTEXT_DESC = 'Primitive string' + postfix
|
||||
CTEXTML_NAME = 'Primitive string multiline [Crystools]'
|
||||
CTEXTML_DESC = 'Primitive string multiline' + postfix
|
||||
CINTEGER_NAME = 'Primitive integer [Crystools]'
|
||||
CINTEGER_DESC = 'Primitive integer' + postfix
|
||||
CFLOAT_NAME = 'Primitive float [Crystools]'
|
||||
CFLOAT_DESC = 'Primitive float' + postfix
|
||||
|
||||
CDEBUGGER_CONSOLE_ANY_NAME = "Show any [Crystools]"
|
||||
CDEBUGGER_ANY_DESC = "Show any value to console/display [Crystools]"
|
||||
CDEBUGGER_CONSOLE_ANY_TO_JSON_NAME = "Show any to JSON [Crystools]"
|
||||
CDEBUGGER_CONSOLE_ANY_TO_JSON_DESC = "Show any to JSON [Crystools]"
|
||||
CDEBUGGER_CONSOLE_ANY_NAME = 'Show any [Crystools]'
|
||||
CDEBUGGER_ANY_DESC = 'Show any value to console/display' + postfix
|
||||
CDEBUGGER_CONSOLE_ANY_TO_JSON_NAME = 'Show any to JSON [Crystools]'
|
||||
CDEBUGGER_CONSOLE_ANY_TO_JSON_DESC = 'Show any to JSON' + postfix
|
||||
|
||||
CLIST_ANY_TYPE = "ListAny"
|
||||
CLIST_ANY_NAME = "List of any [Crystools]"
|
||||
CLIST_ANY_DESC = "List of any [Crystools]"
|
||||
CLIST_STRING_TYPE = "ListString"
|
||||
CLIST_STRING_NAME = "List of strings [Crystools]"
|
||||
CLIST_STRING_DESC = "List of strings [Crystools]"
|
||||
CLIST_ANY_TYPE = 'ListAny'
|
||||
CLIST_ANY_NAME = 'List of any [Crystools]'
|
||||
CLIST_ANY_DESC = 'List of any' + postfix
|
||||
CLIST_STRING_TYPE = 'ListString'
|
||||
CLIST_STRING_NAME = 'List of strings [Crystools]'
|
||||
CLIST_STRING_DESC = 'List of strings' + postfix
|
||||
|
||||
CSWITCH_ANY_NAME = "Switch any [Crystools]"
|
||||
CSWITCH_ANY_DESC = "Switch any [Crystools]"
|
||||
CSWITCH_STRING_NAME = "Switch string [Crystools]"
|
||||
CSWITCH_STRING_DESC = "Switch string [Crystools]"
|
||||
CSWITCH_CONDITIONING_NAME = "Switch conditioning [Crystools]"
|
||||
CSWITCH_CONDITIONING_DESC = "Switch conditioning [Crystools]"
|
||||
CSWITCH_IMAGE_NAME = "Switch image [Crystools]"
|
||||
CSWITCH_IMAGE_DESC = "Switch image [Crystools]"
|
||||
CSWITCH_LATENT_NAME = "Switch latent [Crystools]"
|
||||
CSWITCH_LATENT_DESC = "Switch latent [Crystools]"
|
||||
CSWITCH_ANY_NAME = 'Switch any [Crystools]'
|
||||
CSWITCH_ANY_DESC = 'Switch any' + postfix
|
||||
CSWITCH_STRING_NAME = 'Switch string [Crystools]'
|
||||
CSWITCH_STRING_DESC = 'Switch string' + postfix
|
||||
CSWITCH_CONDITIONING_NAME = 'Switch conditioning [Crystools]'
|
||||
CSWITCH_CONDITIONING_DESC = 'Switch conditioning' + postfix
|
||||
CSWITCH_IMAGE_NAME = 'Switch image [Crystools]'
|
||||
CSWITCH_IMAGE_DESC = 'Switch image' + postfix
|
||||
CSWITCH_LATENT_NAME = 'Switch latent [Crystools]'
|
||||
CSWITCH_LATENT_DESC = 'Switch latent' + postfix
|
||||
|
||||
CPIPE_ANY_TYPE = "CPipeAny"
|
||||
CPIPE_TO_ANY_NAME = "Pipe to/edit any [Crystools]"
|
||||
CPIPE_TO_ANY_DESC = "Pipe to/edit any [Crystools]"
|
||||
CPIPE_FROM_ANY_NAME = "Pipe from any [Crystools]"
|
||||
CPIPE_FROM_ANY_DESC = "Pipe from any [Crystools]"
|
||||
CPIPE_ANY_TYPE = 'CPipeAny'
|
||||
CPIPE_TO_ANY_NAME = 'Pipe to/edit any [Crystools]'
|
||||
CPIPE_TO_ANY_DESC = 'Pipe to/edit any' + postfix
|
||||
CPIPE_FROM_ANY_NAME = 'Pipe from any [Crystools]'
|
||||
CPIPE_FROM_ANY_DESC = 'Pipe from any' + postfix
|
||||
|
||||
CIMAGE_LOAD_METADATA_NAME = "Load image with metadata [Crystools]"
|
||||
CIMAGE_LOAD_METADATA_DESC = "Load image with metadata [Crystools]"
|
||||
CIMAGE_GET_RESOLUTION_NAME = "Get resolution [Crystools]"
|
||||
CIMAGE_GET_RESOLUTION_DESC = "Get resolution [Crystools]"
|
||||
CIMAGE_PREVIEW_IMAGE_NAME = "Preview from image [Crystools]"
|
||||
CIMAGE_PREVIEW_IMAGE_DESC = "Preview from image [Crystools]"
|
||||
CIMAGE_PREVIEW_METADATA_NAME = "Preview from metadata [Crystools]"
|
||||
CIMAGE_PREVIEW_METADATA_DESC = "Preview from metadata [Crystools]"
|
||||
CIMAGE_SAVE_METADATA_NAME = "Save image with extra metadata [Crystools]"
|
||||
CIMAGE_SAVE_METADATA_DESC = "Save image with extra metadata [Crystools]"
|
||||
CIMAGE_LOAD_METADATA_NAME = 'Load image with metadata [Crystools]'
|
||||
CIMAGE_LOAD_METADATA_DESC = 'Load image with metadata' + postfix
|
||||
CIMAGE_GET_RESOLUTION_NAME = 'Get resolution [Crystools]'
|
||||
CIMAGE_GET_RESOLUTION_DESC = 'Get resolution' + postfix
|
||||
CIMAGE_PREVIEW_IMAGE_NAME = 'Preview from image [Crystools]'
|
||||
CIMAGE_PREVIEW_IMAGE_DESC = 'Preview from image' + postfix
|
||||
CIMAGE_PREVIEW_METADATA_NAME = 'Preview from metadata [Crystools]'
|
||||
CIMAGE_PREVIEW_METADATA_DESC = 'Preview from metadata' + postfix
|
||||
CIMAGE_SAVE_METADATA_NAME = 'Save image with extra metadata [Crystools]'
|
||||
CIMAGE_SAVE_METADATA_DESC = 'Save image with extra metadata' + postfix
|
||||
|
||||
CMETADATA_EXTRACTOR_NAME = "Metadata extractor [Crystools]"
|
||||
CMETADATA_EXTRACTOR_DESC = "Metadata extractor [Crystools]"
|
||||
CMETADATA_COMPARATOR_NAME = "Metadata comparator [Crystools]"
|
||||
CMETADATA_COMPARATOR_DESC = "Metadata comparator [Crystools]"
|
||||
CMETADATA_EXTRACTOR_NAME = 'Metadata extractor [Crystools]'
|
||||
CMETADATA_EXTRACTOR_DESC = 'Metadata extractor' + postfix
|
||||
CMETADATA_COMPARATOR_NAME = 'Metadata comparator [Crystools]'
|
||||
CMETADATA_COMPARATOR_DESC = 'Metadata comparator' + postfix
|
||||
|
||||
CUTILS_JSON_COMPARATOR_NAME = "Json comparator [Crystools]"
|
||||
CUTILS_JSON_COMPARATOR_DESC = "Json comparator [Crystools]"
|
||||
CUTILS_STAT_SYSTEM_NAME = "Stats system [Crystools]"
|
||||
CUTILS_STAT_SYSTEM_DESC = "Stats system (powered by WAS) [Crystools]"
|
||||
CUTILS_JSON_COMPARATOR_NAME = 'Json comparator [Crystools]'
|
||||
CUTILS_JSON_COMPARATOR_DESC = 'Json comparator' + postfix
|
||||
CUTILS_STAT_SYSTEM_NAME = 'Stats system [Crystools]'
|
||||
CUTILS_STAT_SYSTEM_DESC = 'Stats system (powered by WAS)' + postfix
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
import json
|
||||
from ..core import CONFIG, CATEGORY, BOOLEAN, BOOLEAN_FALSE, KEYS, TEXTS, STRING, logger, setWidgetValues, any
|
||||
|
||||
from ..core import CONFIG, CATEGORY, BOOLEAN, BOOLEAN_FALSE, KEYS, TEXTS, STRING, logger, any
|
||||
|
||||
class CConsoleAny:
|
||||
def __init__(self):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
deepdiff
|
||||
torch
|
||||
numpy
|
||||
Pillow>=9.5.0
|
||||
pynvml
|
||||
|
||||
+2
-1
@@ -1,5 +1,7 @@
|
||||
import { ComfyWidgets } from '../../../scripts/widgets.js';
|
||||
|
||||
export const commonPrefix = '🪛';
|
||||
|
||||
export function displayContext(nodeType, app, index = 0, serialize_widgets = false, isVirtualNode = false) {
|
||||
function populate(text) {
|
||||
if (this.widgets) {
|
||||
@@ -54,7 +56,6 @@ export function displayContext(nodeType, app, index = 0, serialize_widgets = fal
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// propagate the output value to the dependents nodes, it does not work with some nodes ¯\_(ツ)_/¯
|
||||
const propagateOutputToDependentsNodes = function(output, value) {
|
||||
if (output.links && output.links.length) {
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { app } from '../../../scripts/app.js';
|
||||
import { api } from '../../../scripts/api.js';
|
||||
import { ComfyWidgets } from '../../../scripts/widgets.js';
|
||||
import { displayContext } from './common.js';
|
||||
import { commonPrefix, displayContext } from './common.js';
|
||||
|
||||
// "Show any" Node
|
||||
app.registerExtension({
|
||||
@@ -22,8 +22,8 @@ app.registerExtension({
|
||||
name: 'Crystools.Debugger.Metadata',
|
||||
|
||||
registerCustomNodes() {
|
||||
const category = 'crystools/Debugger';
|
||||
const friendlyName = 'Show Metadata [Crystools]';
|
||||
const category = `crystools ${commonPrefix}/Debugger`;
|
||||
const friendlyName = 'Show Metadata 🪛';
|
||||
|
||||
class MetadataNode {
|
||||
// color = "#FF2222";
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
import { app } from '../../../scripts/app.js';
|
||||
import { api } from '../../../scripts/api.js';
|
||||
import { commonPrefix } from './common.js';
|
||||
|
||||
class CrystoolsMonitor {
|
||||
idExtensionName = 'Crystools.monitor';
|
||||
menuPrefix = commonPrefix;
|
||||
htmlIdCrystoolsRoot = 'crystools-root';
|
||||
htmlIdCrystoolsMonitorContainer = 'crystools-monitor-container';
|
||||
|
||||
idInputRate = 'Crystools.inputRate';
|
||||
defaultRate = .5;
|
||||
|
||||
/** MONITORS */
|
||||
// CPU Variables
|
||||
idSwitchCPU = 'Crystools.switchCPU';
|
||||
defaultSwitchCPU = true;
|
||||
htmlMonitorCPURef = null;
|
||||
htmlMonitorCPUSliderRef = null;
|
||||
htmlMonitorCPULabelRef = null;
|
||||
cssColorCPU = '#0AA015';
|
||||
|
||||
// RAM Variables
|
||||
idSwitchRAM = 'Crystools.switchRAM';
|
||||
defaultSwitchRAM = true;
|
||||
htmlMonitorRAMRef = null;
|
||||
htmlMonitorRAMSliderRef = null;
|
||||
htmlMonitorRAMLabelRef = null;
|
||||
cssColorRAM = '#07630D';
|
||||
|
||||
// GPU Variables
|
||||
idSwitchGPU = 'Crystools.switchGPU';
|
||||
defaultSwitchGPU = true;
|
||||
htmlMonitorGPURef = null;
|
||||
htmlMonitorGPUSliderRef = null;
|
||||
htmlMonitorGPULabelRef = null;
|
||||
cssColorGPU = '#0C86F4';
|
||||
|
||||
// VRAM Variables
|
||||
idSwitchVRAM = 'Crystools.switchVRAM';
|
||||
defaultSwitchVRAM = true;
|
||||
htmlMonitorVRAMRef = null;
|
||||
htmlMonitorVRAMSliderRef = null;
|
||||
htmlMonitorVRAMLabelRef = null;
|
||||
cssColorVRAM = '#176EC7';
|
||||
|
||||
// HDD Variables
|
||||
idSwitchHDD = 'Crystools.switchHDD';
|
||||
defaultSwitchHDD = false;
|
||||
htmlMonitorHDDRef = null;
|
||||
htmlMonitorHDDSliderRef = null;
|
||||
htmlMonitorHDDLabelRef = null;
|
||||
cssColorHDD = '#730F92';
|
||||
|
||||
constructor() {
|
||||
this.createSettings();
|
||||
}
|
||||
|
||||
createSettings = () => {
|
||||
app.ui.settings.addSetting({
|
||||
id: this.idSwitchCPU,
|
||||
name: this.menuPrefix + 'Display CPU monitor [menu]',
|
||||
type: 'boolean',
|
||||
defaultValue: this.defaultSwitchCPU,
|
||||
onChange: async(value) => {
|
||||
this.updateWidget(this.htmlMonitorCPURef, value);
|
||||
await this.updateServer({switchCPU: value});
|
||||
},
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: this.idSwitchRAM,
|
||||
name: this.menuPrefix + 'Display RAM monitor [menu]',
|
||||
type: 'boolean',
|
||||
defaultValue: this.defaultSwitchRAM,
|
||||
onChange: async(value) => {
|
||||
this.updateWidget(this.htmlMonitorRAMRef, value);
|
||||
await this.updateServer({switchRAM: value});
|
||||
},
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: this.idSwitchGPU,
|
||||
name: this.menuPrefix + 'Display GPU monitor [menu]',
|
||||
type: 'boolean',
|
||||
defaultValue: this.defaultSwitchGPU,
|
||||
onChange: async(value) => {
|
||||
this.updateWidget(this.htmlMonitorGPURef, value);
|
||||
await this.updateServer({switchGPU: value});
|
||||
},
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: this.idSwitchVRAM,
|
||||
name: this.menuPrefix + 'Display Video RAM monitor [menu]',
|
||||
type: 'boolean',
|
||||
defaultValue: this.defaultSwitchVRAM,
|
||||
onChange: async(value) => {
|
||||
this.updateWidget(this.htmlMonitorVRAMRef, value);
|
||||
await this.updateServer({switchVRAM: value});
|
||||
},
|
||||
});
|
||||
app.ui.settings.addSetting({
|
||||
id: this.idSwitchHDD,
|
||||
name: this.menuPrefix + 'Display Hard disk monitor [menu]',
|
||||
tooltip: 'Only the drive where the comfyUI is installed',
|
||||
type: 'boolean',
|
||||
defaultValue: this.defaultSwitchHDD,
|
||||
onChange: async(value) => {
|
||||
this.updateWidget(this.htmlMonitorHDDRef, value);
|
||||
await this.updateServer({switchHDD: value});
|
||||
},
|
||||
});
|
||||
|
||||
app.ui.settings.addSetting({
|
||||
id: this.idInputRate,
|
||||
name: this.menuPrefix + 'Monitors refresh rate (in seconds) [menu]',
|
||||
tooltip: 'This is the time between each update of the monitors, 0 means no refresh',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 0,
|
||||
max: 5,
|
||||
step: 0.25,
|
||||
},
|
||||
defaultValue: this.defaultRate,
|
||||
onChange: async(value) => {
|
||||
try {
|
||||
value = parseFloat(value);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
await this.updateServer({rate: value});
|
||||
|
||||
if (value === 0) {
|
||||
this.updateDisplay({
|
||||
cpu_utilization: -1,
|
||||
device: 'cpu',
|
||||
|
||||
gpus: [{
|
||||
gpu_utilization: -1,
|
||||
vram_total: -1,
|
||||
vram_used: -1,
|
||||
vram_used_percent: -1,
|
||||
}],
|
||||
hdd_total: -1,
|
||||
hdd_used: -1,
|
||||
hdd_used_percent: -1,
|
||||
ram_total: -1,
|
||||
ram_used: -1,
|
||||
ram_used_percent: -1,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.htmlMonitorCPUSliderRef) { // validation because this run before setup
|
||||
value = value.toFixed(1);
|
||||
const animationConfig = `width ${value}s`;
|
||||
this.htmlMonitorCPUSliderRef.style.transition = animationConfig;
|
||||
this.htmlMonitorGPUSliderRef.style.transition = animationConfig;
|
||||
this.htmlMonitorHDDSliderRef.style.transition = animationConfig;
|
||||
this.htmlMonitorRAMSliderRef.style.transition = animationConfig;
|
||||
this.htmlMonitorVRAMSliderRef.style.transition = animationConfig;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
updateServer = 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();
|
||||
} else {
|
||||
console.error(resp);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
updateAllWidget = () => {
|
||||
this.updateWidget(this.htmlMonitorCPURef, app.ui.settings.getSettingValue(this.idSwitchCPU, this.defaultSwitchCPU));
|
||||
this.updateWidget(this.htmlMonitorGPURef, app.ui.settings.getSettingValue(this.idSwitchGPU, this.defaultSwitchGPU));
|
||||
this.updateWidget(this.htmlMonitorHDDRef, app.ui.settings.getSettingValue(this.idSwitchHDD, this.defaultSwitchHDD));
|
||||
this.updateWidget(this.htmlMonitorRAMRef, app.ui.settings.getSettingValue(this.idSwitchRAM, this.defaultSwitchRAM));
|
||||
this.updateWidget(this.htmlMonitorVRAMRef, app.ui.settings.getSettingValue(this.idSwitchVRAM, this.defaultSwitchVRAM));
|
||||
};
|
||||
|
||||
updateWidget = (container, value) => {
|
||||
if (container) {
|
||||
container.style.display = value ? 'flex' : 'none';
|
||||
}
|
||||
};
|
||||
|
||||
updateDisplay = (data) => {
|
||||
// console.debug('updateDisplay', data);
|
||||
this.htmlMonitorCPULabelRef.innerHTML = `${Math.floor(data.cpu_utilization)}%`;
|
||||
this.htmlMonitorCPUSliderRef.style.width = this.htmlMonitorCPULabelRef.innerHTML;
|
||||
|
||||
const gpu = data.gpus[0];
|
||||
if (gpu) {
|
||||
this.htmlMonitorGPULabelRef.innerHTML = `${Math.floor(gpu.gpu_utilization)}%`;
|
||||
this.htmlMonitorGPUSliderRef.style.width = this.htmlMonitorGPULabelRef.innerHTML;
|
||||
this.htmlMonitorVRAMLabelRef.innerHTML = `${Math.floor(gpu.vram_used_percent)}%`;
|
||||
this.htmlMonitorVRAMSliderRef.style.width = this.htmlMonitorVRAMLabelRef.innerHTML;
|
||||
} else {
|
||||
console.error('no gpu found');
|
||||
}
|
||||
|
||||
this.htmlMonitorHDDLabelRef.innerHTML = `${Math.floor(data.hdd_used_percent)}%`;
|
||||
this.htmlMonitorHDDSliderRef.style.width = this.htmlMonitorHDDLabelRef.innerHTML;
|
||||
|
||||
this.htmlMonitorRAMLabelRef.innerHTML = `${Math.floor(data.ram_used_percent)}%`;
|
||||
this.htmlMonitorRAMSliderRef.style.width = this.htmlMonitorRAMLabelRef.innerHTML;
|
||||
};
|
||||
|
||||
setup() {
|
||||
const parentElement = document.getElementById('queue-button');
|
||||
|
||||
let ctoolsRoot = document.getElementById(this.htmlIdCrystoolsRoot);
|
||||
if (!ctoolsRoot) {
|
||||
ctoolsRoot = document.createElement('div');
|
||||
ctoolsRoot.setAttribute('id', this.htmlIdCrystoolsRoot);
|
||||
ctoolsRoot.style.display = 'flex';
|
||||
ctoolsRoot.style.width = '100%';
|
||||
ctoolsRoot.style.flexDirection = 'column';
|
||||
parentElement.insertAdjacentElement('afterend', ctoolsRoot);
|
||||
}
|
||||
|
||||
const htmlContainer = document.createElement('div');
|
||||
htmlContainer.setAttribute('id', this.htmlIdCrystoolsMonitorContainer);
|
||||
htmlContainer.style.width = '100%';
|
||||
htmlContainer.style.cursor = 'crosshair';
|
||||
htmlContainer.style.order = '3';
|
||||
htmlContainer.style.margin = '4px 0';
|
||||
ctoolsRoot.append(htmlContainer);
|
||||
|
||||
htmlContainer.append(this.createMonitorCPU());
|
||||
htmlContainer.append(this.createMonitorRAM());
|
||||
htmlContainer.append(this.createMonitorGPU());
|
||||
htmlContainer.append(this.createMonitorVRAM());
|
||||
htmlContainer.append(this.createMonitorHDD());
|
||||
|
||||
this.updateAllWidget();
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
createMonitor = (id, label, color) => {
|
||||
const option = {
|
||||
rootRef: null,
|
||||
sliderRef: null,
|
||||
labelRef: null,
|
||||
};
|
||||
|
||||
const htmlMain = document.createElement('div');
|
||||
htmlMain.setAttribute('id', id);
|
||||
htmlMain.style.margin = '2px 10px';
|
||||
htmlMain.style.height = '12px';
|
||||
htmlMain.style.position = 'relative';
|
||||
htmlMain.style.display = 'flex';
|
||||
htmlMain.style.alignItems = 'center';
|
||||
htmlMain.style.flexDirection = 'row';
|
||||
option.rootRef = htmlMain;
|
||||
|
||||
const htmlMonitorText = document.createElement('div');
|
||||
htmlMonitorText.style.width = '35px';
|
||||
htmlMonitorText.style.fontSize = '10px';
|
||||
htmlMonitorText.innerHTML = label;
|
||||
htmlMain.append(htmlMonitorText);
|
||||
|
||||
const htmlMonitorContent = document.createElement('div');
|
||||
htmlMonitorContent.style.height = '100%';
|
||||
htmlMonitorContent.style.flexGrow = '1';
|
||||
htmlMonitorContent.style.position = 'relative';
|
||||
htmlMonitorContent.style.backgroundColor = 'var(--bg-color)';
|
||||
htmlMain.append(htmlMonitorContent);
|
||||
|
||||
const htmlMonitorSlider = document.createElement('div');
|
||||
htmlMonitorSlider.style.position = 'absolute';
|
||||
htmlMonitorSlider.style.height = '100%';
|
||||
htmlMonitorSlider.style.width = '0';
|
||||
htmlMonitorSlider.style.backgroundColor = color;
|
||||
htmlMonitorSlider.style.transition = 'width 0.5s';
|
||||
option.sliderRef = htmlMonitorSlider;
|
||||
htmlMonitorContent.append(htmlMonitorSlider);
|
||||
|
||||
const htmlMonitorLabel = document.createElement('div');
|
||||
htmlMonitorLabel.style.position = 'relative';
|
||||
htmlMonitorLabel.style.width = '100%';
|
||||
htmlMonitorLabel.style.color = 'var(--drag-text)';
|
||||
htmlMonitorLabel.style.fontSize = '10px';
|
||||
htmlMonitorLabel.innerHTML = '0%';
|
||||
option.labelRef = htmlMonitorLabel;
|
||||
htmlMonitorContent.append(htmlMonitorLabel);
|
||||
|
||||
return option;
|
||||
};
|
||||
|
||||
createMonitorCPU = () => {
|
||||
const options = this.createMonitor('crystools-monitor-cpu', 'CPU', this.cssColorCPU);
|
||||
|
||||
this.htmlMonitorCPURef = options.rootRef;
|
||||
this.htmlMonitorCPUSliderRef = options.sliderRef;
|
||||
this.htmlMonitorCPULabelRef = options.labelRef;
|
||||
return this.htmlMonitorCPURef;
|
||||
};
|
||||
|
||||
createMonitorRAM = () => {
|
||||
const options = this.createMonitor('crystools-monitor-ram', 'RAM', this.cssColorRAM);
|
||||
|
||||
this.htmlMonitorRAMRef = options.rootRef;
|
||||
this.htmlMonitorRAMSliderRef = options.sliderRef;
|
||||
this.htmlMonitorRAMLabelRef = options.labelRef;
|
||||
return this.htmlMonitorRAMRef;
|
||||
};
|
||||
|
||||
createMonitorGPU = () => {
|
||||
const options = this.createMonitor('crystools-monitor-gpu', 'GPU', this.cssColorGPU);
|
||||
|
||||
this.htmlMonitorGPURef = options.rootRef;
|
||||
this.htmlMonitorGPUSliderRef = options.sliderRef;
|
||||
this.htmlMonitorGPULabelRef = options.labelRef;
|
||||
return this.htmlMonitorGPURef;
|
||||
};
|
||||
|
||||
createMonitorVRAM = () => {
|
||||
const options = this.createMonitor('crystools-monitor-vram', 'VRAM', this.cssColorVRAM);
|
||||
|
||||
this.htmlMonitorVRAMRef = options.rootRef;
|
||||
this.htmlMonitorVRAMSliderRef = options.sliderRef;
|
||||
this.htmlMonitorVRAMLabelRef = options.labelRef;
|
||||
return this.htmlMonitorVRAMRef;
|
||||
};
|
||||
|
||||
createMonitorHDD = () => {
|
||||
const options = this.createMonitor('crystools-monitor-hdd', 'HDD', this.cssColorHDD);
|
||||
|
||||
this.htmlMonitorHDDRef = options.rootRef;
|
||||
this.htmlMonitorHDDSliderRef = options.sliderRef;
|
||||
this.htmlMonitorHDDLabelRef = options.labelRef;
|
||||
return this.htmlMonitorHDDRef;
|
||||
};
|
||||
|
||||
registerListeners = () => {
|
||||
api.addEventListener('crystools.monitor', (event) => {
|
||||
if (event?.detail === undefined) {
|
||||
return;
|
||||
}
|
||||
this.updateDisplay(event.detail);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const crystoolsMonitor = new CrystoolsMonitor();
|
||||
app.registerExtension({
|
||||
name: crystoolsMonitor.idExtensionName,
|
||||
setup: crystoolsMonitor.setup.bind(crystoolsMonitor),
|
||||
});
|
||||
+20
-20
@@ -1,5 +1,6 @@
|
||||
import { api } from '../../../scripts/api.js';
|
||||
import { app } from '../../../scripts/app.js';
|
||||
import { commonPrefix } from './common.js';
|
||||
|
||||
const status = {
|
||||
executing: 'Executing',
|
||||
@@ -11,7 +12,7 @@ class CrystoolsProgressBar {
|
||||
idExtensionName = 'Crystools.progressBar';
|
||||
idShowProgressBar = 'Crystools.showStatus';
|
||||
defaultShowStatus = true;
|
||||
menuPrefix = '';
|
||||
menuPrefix = commonPrefix;
|
||||
htmlIdCrystoolsRoot = 'crystools-root';
|
||||
htmlIdCrystoolsProgressBarContainer = 'crystools-progress-bar-container';
|
||||
|
||||
@@ -29,12 +30,9 @@ class CrystoolsProgressBar {
|
||||
|
||||
// not on setup because this affect the order on settings, I prefer to options at first
|
||||
createSettings = () => {
|
||||
/** Form on settings: (the order is important) */
|
||||
|
||||
// use new save button
|
||||
app.ui.settings.addSetting({
|
||||
id: this.idShowProgressBar,
|
||||
name: this.menuPrefix + 'Show progress bar in menu',
|
||||
name: this.menuPrefix + 'Show progress bar [menu]',
|
||||
type: 'boolean',
|
||||
defaultValue: this.defaultShowStatus,
|
||||
onChange: this.showProgressBar,
|
||||
@@ -42,11 +40,11 @@ class CrystoolsProgressBar {
|
||||
};
|
||||
|
||||
showProgressBar = (value) => {
|
||||
const ctools = document.getElementById(this.htmlIdCrystoolsProgressBarContainer);
|
||||
const container = document.getElementById(this.htmlIdCrystoolsProgressBarContainer);
|
||||
|
||||
// validation because this run before setup
|
||||
if (ctools) {
|
||||
ctools.style.display = value ? 'block' : 'none';
|
||||
if (container) {
|
||||
container.style.display = value ? 'block' : 'none';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -59,7 +57,7 @@ class CrystoolsProgressBar {
|
||||
if (this.timeStart > 0 && timeElapsed > 0) {
|
||||
this.htmlProgressLabelRef.innerHTML = new Date(timeElapsed).toISOString().substr(11, 8);
|
||||
}
|
||||
this.htmlProgressSliderRef.style.backgroundColor = 'var(--bg-color)';
|
||||
this.htmlProgressSliderRef.style.width = '0';
|
||||
|
||||
} else if (this.currentStatus === status.execution_error) {
|
||||
// an error occurred
|
||||
@@ -71,7 +69,7 @@ class CrystoolsProgressBar {
|
||||
// on going
|
||||
this.htmlProgressLabelRef.innerHTML = `${this.currentProgress}%`;
|
||||
this.htmlProgressSliderRef.style.width = this.htmlProgressLabelRef.innerHTML;
|
||||
this.htmlProgressSliderRef.style.backgroundColor = 'green';
|
||||
this.htmlProgressSliderRef.style.backgroundColor = 'green'; // by reset the color
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,27 +86,29 @@ class CrystoolsProgressBar {
|
||||
parentElement.insertAdjacentElement('afterend', ctoolsRoot);
|
||||
}
|
||||
|
||||
const progressBarContainer = document.createElement('div');
|
||||
progressBarContainer.setAttribute('id', this.htmlIdCrystoolsProgressBarContainer);
|
||||
progressBarContainer.setAttribute('title', 'click to see the current working node');
|
||||
progressBarContainer.style.margin = '4px 0';
|
||||
progressBarContainer.style.width = '100%';
|
||||
progressBarContainer.style.cursor = 'pointer';
|
||||
progressBarContainer.style.order = '1';
|
||||
progressBarContainer.addEventListener('click', this.centerNode);
|
||||
ctoolsRoot.append(progressBarContainer);
|
||||
const htmlContainer = document.createElement('div');
|
||||
htmlContainer.setAttribute('id', this.htmlIdCrystoolsProgressBarContainer);
|
||||
htmlContainer.setAttribute('title', 'click to see the current working node');
|
||||
htmlContainer.style.margin = '4px 0';
|
||||
htmlContainer.style.width = '100%';
|
||||
htmlContainer.style.cursor = 'pointer';
|
||||
htmlContainer.style.order = '1';
|
||||
htmlContainer.addEventListener('click', this.centerNode);
|
||||
ctoolsRoot.append(htmlContainer);
|
||||
|
||||
const progressBar = document.createElement('div');
|
||||
progressBar.style.margin = '0 10px';
|
||||
progressBar.style.height = '18px';
|
||||
progressBar.style.position = 'relative';
|
||||
progressBar.style.backgroundColor = 'var(--bg-color)';
|
||||
progressBarContainer.append(progressBar);
|
||||
htmlContainer.append(progressBar);
|
||||
|
||||
const progressSlider = document.createElement('div');
|
||||
progressSlider.style.position = 'absolute';
|
||||
progressSlider.style.height = '100%';
|
||||
progressSlider.style.width = '0';
|
||||
progressSlider.style.transition = 'width 0.2s';
|
||||
progressSlider.style.backgroundColor = 'green';
|
||||
this.htmlProgressSliderRef = progressSlider;
|
||||
progressBar.append(this.htmlProgressSliderRef);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user