Author SHA1 Message Date
AlekPet d921b8398c Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2026-09-24 23:40:06 +03:00
AlekPet 20aff9bac2 Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2026-09-17 12:17:13 +03:00
AlekPet fe975c0890 Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2026-08-27 08:27:24 +03:00
AlekPet b1ddb4a866 Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2026-04-24 20:51:41 +03:00
AlekPet 4d06e3776a Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2026-03-03 13:47:09 +03:00
AlekPet 80c2bd07ef Merge branch 'new_init_config_disabled' into new_init_disabled_dynamic_and_not 2025-09-21 13:54:27 +03:00
AlekPet c1abde90e4 fix __init__.py for argostranslate 2025-09-21 13:26:28 +03:00
AlekPet 9cb658b44f Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-09-20 17:50:21 +03:00
AlekPet 4727a4f72d Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-09-11 12:24:22 +03:00
AlekPet 7248a21331 Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-09-03 16:53:15 +03:00
AlekPet 86c4b1570f Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-08-30 20:26:37 +03:00
AlekPet 2d8eb55a7b Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-08-29 20:19:23 +03:00
AlekPet ea03d73b69 Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-08-28 22:17:47 +03:00
AlekPet 898ac8a18e Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-08-22 17:18:35 +03:00
AlekPet acbece304f Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-08-16 13:18:30 +03:00
AlekPet ef526aae54 Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-08-04 16:04:13 +03:00
AlekPet d0c7519d55 Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-08-01 23:01:35 +03:00
AlekPet 1492788de5 Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-08-01 21:36:17 +03:00
AlekPet cf87b95a53 Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-07-29 18:20:43 +03:00
AlekPet dbabf9c13f Merge branch 'new_init_config_disabled' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-07-10 13:34:13 +03:00
AlekPet 5a939af67a Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-07-10 13:33:40 +03:00
AlekPet 23df17061b Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-07-10 13:30:25 +03:00
AlekPet 046be62f60 Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-06-20 19:56:07 +03:00
AlekPet fa894d702f Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-06-05 11:18:28 +03:00
AlekPet 9bc65a7786 Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-05-17 16:07:20 +03:00
AlekPet 7feacf442b Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-05-07 10:47:14 +03:00
AlekPet 5a1ace9343 Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-04-25 12:38:27 +03:00
AlekPet d03493a490 Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-04-15 21:28:31 +03:00
AlekPet e3b2f0aa33 Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-04-15 06:24:33 +03:00
AlekPet 96b5e8810e Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-04-13 12:50:11 +03:00
AlekPet e19a7f1717 update, add DeepLX 2025-04-13 12:31:29 +03:00
AlekPet 97095c6705 Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-04-07 10:22:51 +03:00
AlekPet 510f971329 Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-03-28 22:48:55 +03:00
AlekPet 37b187863f Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-03-06 14:47:04 +03:00
AlekPet f82ff6c96a Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-02-28 10:19:21 +03:00
AlekPet 256d9f9b3f Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-02-22 16:39:07 +03:00
AlekPet fca1ad7793 Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-02-22 16:11:57 +03:00
AlekPet 73d0b410c7 Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-02-22 14:42:20 +03:00
AlekPet 24f8f272e4 Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-02-18 19:47:05 +03:00
AlekPet d7b9b1eec1 Stop tracking config.json, and new version __init__.py 2025-02-10 18:11:25 +03:00
AlekPet 67179e5586 Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-01-31 12:58:56 +03:00
AlekPet f4c679d2c5 Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2025-01-24 18:25:37 +03:00
AlekPet 962a4e897e Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2024-12-08 11:56:51 +03:00
AlekPet 31ebeb70df Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2024-11-25 17:02:59 +03:00
AlekPet d2581cc75a Merge branch 'master' of https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet into new_init_config_disabled 2024-11-21 18:59:56 +03:00
AlekPet 1e7772284c mend 2024-11-02 18:33:48 +03:00
AlekPet 44bc31989f mend 2024-11-02 18:23:56 +03:00
AlekPet fd5c6463e8 mend 2024-11-02 17:58:57 +03:00
AlekPet d27c68decf mend 2024-11-02 17:58:17 +03:00
AlekPet f262c39266 update structure enabled nodes and view 2024-11-02 17:56:16 +03:00
AlekPet a5c4798b09 implement disabled nodes used config 2024-11-02 16:53:37 +03:00
13 changed files with 1893 additions and 1180 deletions
+2
View File
@@ -1,2 +1,4 @@
__pycache__
settings_nodes/
config.json
web_alekpet_nodes/lib/idenode/**/*.json
+6 -2
View File
@@ -1,8 +1,12 @@
from server import PromptServer
from aiohttp import web
import argostranslate.package
import argostranslate.translate
try:
import argostranslate.package
import argostranslate.translate
except:
pass
# Find packages https://www.argosopentech.com/argospm/index/
+1
View File
@@ -0,0 +1 @@
settings_nodes.json
-23
View File
@@ -4,8 +4,6 @@
## Changelog:
> 2026.09.27 - Canvas data saving method has been changed.
> 2025.07.29 - Add tool Crop canvas
> 2024.05.10 - Implement piping in an image ([issue in an image](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet/issues/24)) ([example Piping in an image](#piping))
@@ -23,27 +21,6 @@
- Use the resize mod to move, rotate and resize drawn objects
- Prohibition of certain modifications in the change mode, prohibition of movement along certain axes, scaling and rotation
- Support brushes mypaint (MyPaint Brush tool)
- Crop image
- Connect image on input (piping) and change type insert background or as image and other settings
- Drag and drop image on canvas node
- Paste image on canvas node (Ctrl+Shift+V or Ctrl+K V)
- Select image in combo and insert to the canvas
# Saved canvas data, version 2.0
All canvas data is saved in the `ComfyUI\user\__alekpet\painter_assets` folder
and has the following structure:
```ComfyUI
└── user
└── __alekpet/
└── painter_assets/
└── 8f3c.../
└── state.json ← Fabric/Painter node data
└── 1fn6.../
└── state.json ← Fabric/Painter node data
...
```
# Example
-233
View File
@@ -1,233 +0,0 @@
"""
Backend storage for PainterNode canvas state.
Workflow files contain only a small asset reference. The Fabric.js canvas JSON
(which may contain base64 encoded images) is stored server-side instead.
"""
import json
import os
import re
import shutil
import tempfile
from datetime import datetime, timezone
from aiohttp import web
from server import PromptServer
import folder_paths
ASSET_ID_RE = re.compile(
r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-"
r"[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"
)
def _asset_root():
# Keep Painter data outside the workflow and outside the custom-node directory so updating/reinstalling the extension does not delete it.
if hasattr(folder_paths, "get_system_user_directory"):
root = folder_paths.get_system_user_directory("alekpet")
else:
root = os.path.join(folder_paths.get_user_directory(), "__alekpet")
root = os.path.join(root, "painter_assets")
os.makedirs(root, exist_ok=True)
return root
def _asset_dir(asset_id):
if not isinstance(asset_id, str) or not ASSET_ID_RE.fullmatch(asset_id):
raise ValueError("Invalid Painter asset_id")
return os.path.join(_asset_root(), asset_id)
def _json_response_error(message, status=400):
return web.json_response({"success": False, "error": message}, status=status)
def file_time_iso(timestamp):
return datetime.fromtimestamp(
timestamp,
timezone.utc,
).isoformat()
@PromptServer.instance.routes.get("/alekpet/painter_assets")
async def list_painter_assets(request):
try:
root = _asset_root()
assets = []
for asset_id in os.listdir(root):
if not ASSET_ID_RE.fullmatch(asset_id):
continue
asset_dir = os.path.join(root, asset_id)
state_path = os.path.join(asset_dir, "state.json")
if not os.path.isfile(state_path):
continue
file_stat = os.stat(state_path)
width = height = None
try:
with open(state_path, "r", encoding="utf-8") as f:
state = json.load(f)
metadata = state.get("metadata")
if not metadata:
metadata = {}
created_at = metadata.get("created_at") or file_time_iso(file_stat.st_mtime)
updated_at = metadata.get("updated_at") or file_time_iso(file_stat.st_mtime)
workflow_name = metadata.get("workflow_name", "Unsaved Workflow")
size = state.get("settings", {}).get("currentCanvasSize", {})
width = size.get("width")
height = size.get("height")
except (OSError, json.JSONDecodeError, AttributeError):
pass
assets.append(
{
"asset_id": asset_id,
"size": file_stat.st_size,
"created_at": created_at,
"updated_at": updated_at,
"width": width,
"height": height,
"workflow_name": workflow_name
}
)
assets.sort(key=lambda item: item["updated_at"], reverse=True)
return web.json_response({"success": True, "assets": assets})
except OSError as e:
return _json_response_error(str(e), 500)
@PromptServer.instance.routes.get("/alekpet/painter_asset/{asset_id}")
async def get_painter_asset(request):
try:
asset_id = request.match_info["asset_id"]
state_path = os.path.join(_asset_dir(asset_id), "state.json")
if not os.path.isfile(state_path):
return _json_response_error("Painter asset not found", 404)
with open(state_path, "r", encoding="utf-8") as f:
state = json.load(f)
return web.json_response(
{
"success": True,
"asset_id": asset_id,
"state": state,
}
)
except ValueError as e:
return _json_response_error(str(e), 400)
except (OSError, json.JSONDecodeError) as e:
return _json_response_error(str(e), 500)
@PromptServer.instance.routes.post("/alekpet/painter_asset/{asset_id}")
async def save_painter_asset(request):
try:
asset_id = request.match_info["asset_id"]
asset_dir = _asset_dir(asset_id)
payload = await request.json()
state = payload.get("state")
workflow_name = payload.get("workflow_name")
if not isinstance(state, dict):
return _json_response_error("state must be an object", 400)
# Get metadata
metadata = state.get("metadata")
now = datetime.now(timezone.utc).isoformat()
if not isinstance(metadata, dict):
metadata = {}
state["metadata"] = metadata
# created_at - not change if asset exist
existing_created_at = None
state_path = os.path.join(asset_dir, "state.json")
if os.path.isfile(state_path):
try:
with open(state_path, "r", encoding="utf-8") as f:
existing_state = json.load(f)
existing_metadata = existing_state.get("metadata")
if isinstance(existing_metadata, dict):
existing_created_at = existing_metadata.get("created_at")
if not existing_created_at:
existing_created_at = file_time_iso(os.path.getmtime(state_path))
except (OSError, json.JSONDecodeError):
pass
# Check created date
if existing_created_at:
metadata["created_at"] = existing_created_at
else:
metadata["created_at"] = now
# Update update date
metadata["updated_at"] = now
# Update workflow_name
if workflow_name:
metadata["workflow_name"] = workflow_name
os.makedirs(asset_dir, exist_ok=True)
fd, tmp_path = tempfile.mkstemp(
prefix=".state-",
suffix=".tmp",
dir=asset_dir,
)
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(
state,
f,
ensure_ascii=False,
separators=(",", ":"),
)
f.flush()
os.fsync(f.fileno())
os.replace(tmp_path, os.path.join(asset_dir, "state.json"))
finally:
if os.path.exists(tmp_path):
os.unlink(tmp_path)
return web.json_response(
{
"success": True,
"asset_id": asset_id,
}
)
except ValueError as e:
return _json_response_error(str(e), 400)
except (OSError, json.JSONDecodeError) as e:
return _json_response_error(str(e), 500)
@PromptServer.instance.routes.delete("/alekpet/painter_asset/{asset_id}")
async def delete_painter_asset(request):
try:
asset_dir = _asset_dir(request.match_info["asset_id"])
if os.path.isdir(asset_dir):
shutil.rmtree(asset_dir)
return web.json_response({"success": True})
except ValueError as e:
return _json_response_error(str(e), 400)
except OSError as e:
return _json_response_error(str(e), 500)
+300 -3
View File
@@ -1,5 +1,6 @@
import hashlib
import os
import json
from server import PromptServer
from aiohttp import web
import base64
@@ -8,10 +9,306 @@ import time
from PIL import Image, ImageOps, ImageSequence
import torch
import numpy as np
import glob
import folder_paths
import node_helpers
from . import painter_asset_storage
# Directory node save settings
CHUNK_SIZE = 1024
dir_painter_node = os.path.dirname(__file__)
extension_path = os.path.join(os.path.abspath(dir_painter_node))
nodes_settings_path = os.path.join(extension_path, "settings_nodes")
# Create directory settings_nodes if not exists
if not os.path.exists(nodes_settings_path):
os.mkdir(nodes_settings_path)
tipsfile = os.path.join(nodes_settings_path, "Stores painter nodes settings.txt")
with open(tipsfile, "w+", encoding="utf-8") as tipsfile:
tipsfile.write("Painter node saved settings!")
# Function create file json file
PREFIX = "_setting.json"
def isFileName(filename):
if (
not filename
or not isinstance(filename, str)
or filename.strip() == ""
or "/" in filename
or "\\" in filename
or ".." in filename
):
print("Filename is incorrect")
return False
return True
def create_settings_json(filename):
try:
json_file = os.path.join(nodes_settings_path, filename)
if not os.path.isfile(json_file):
print(f"File settings for '{filename}' is not found! Create file!")
with open(json_file, "w") as f:
json.dump({}, f)
except Exception as e:
print(f"Error: ${e}")
def get_settings_json(filename, notExistCreate=True):
if not isFileName(filename):
return {}
json_file = os.path.join(nodes_settings_path, filename)
if os.path.isfile(json_file):
f = open(json_file, "rb")
try:
load_data = json.load(f)
return load_data
except Exception as e:
print("Error load json file: ", e)
if notExistCreate:
f.close()
os.remove(json_file)
create_settings_json(filename)
finally:
f.close()
else:
create_settings_json(filename)
return {}
# Load json file
@PromptServer.instance.routes.get("/alekpet/loading_node_settings/{nodeName}")
async def loadingSettings(request):
filename = request.match_info.get("nodeName", None)
if not isFileName(filename):
load_data = {}
else:
load_data = get_settings_json(filename + PREFIX)
return web.json_response({"settings_nodes": load_data}, status=200)
# Load json's files
@PromptServer.instance.routes.get("/alekpet/loading_all_node_settings")
async def loadingAllSettings(request):
load_data = []
jsonFiles = glob.glob("*.json", root_dir=nodes_settings_path)
for f in jsonFiles:
path_to_file = os.path.join(nodes_settings_path, f)
if os.path.isfile(path_to_file):
file = open(path_to_file, "rb")
try:
jsonData = json.load(file)
load_data.append({"name": f.replace(PREFIX, ""), "value": jsonData})
except Exception as e:
print("Error load json file: ", e)
finally:
file.close()
else:
print(f"File {f} not file!")
return web.json_response({"all_settings_nodes": load_data}, status=200)
# Save data to json file
@PromptServer.instance.routes.post("/alekpet/save_node_settings")
async def saveSettings(request):
try:
if not request.content_type.startswith("multipart/"):
return web.json_response(
{"error": "multipart/* content type expected"}, status=400
)
reader = await request.multipart()
filename_reader = await reader.next()
filename = await filename_reader.text()
data_reader = await reader.next()
if isFileName(filename):
filename = filename + PREFIX
json_file = os.path.join(nodes_settings_path, filename)
if os.path.isfile(json_file):
with open(json_file, "wb") as f:
while True:
chunk = await data_reader.read_chunk(size=CHUNK_SIZE)
if not chunk:
break
f.write(chunk)
return web.json_response(
{"message": "Painter data saved successfully"}, status=200
)
else:
create_settings_json(filename)
return web.json_response(
{"message": "Painter file settings created!"}, status=200
)
else:
raise Exception("Filename is not found or incorrect!")
except Exception as e:
print("Error save json file: ", e)
return web.json_response({"error": str(e)}, status=500)
# Rename file settings painter node data
@PromptServer.instance.routes.get("/alekpet/file_exist_node_settings/{workflowName}")
async def existFileSettings(request):
try:
workflowName = request.match_info.get("workflowName", None)
if not isFileName(workflowName):
return web.json_response(
{"message": "Workflows names is invalid!", "isExists": False}, status=400
)
else:
filename_new_prefix = workflowName + PREFIX
json_file_new_prefix = os.path.join(
nodes_settings_path, filename_new_prefix
)
if os.path.isfile(json_file_new_prefix):
return web.json_response(
{"message": "Workflows is found!", "isExists": True}, status=200
)
else:
return web.json_response(
{"message": "Workflows is not found!", "isExists": False}, status=200
)
except OSError as e:
return web.json_response(
{"error": "Error: %s - %s." % (e.filename, e.strerror)}, status=500
)
@PromptServer.instance.routes.post("/alekpet/rename_node_settings")
async def renameSettings(request):
try:
json_data = await request.json()
filename_old = json_data.get("old_name")
filename_new = json_data.get("new_name")
overwrite = json_data.get("overwrite", False)
if not isFileName(filename_old) or not isFileName(filename_new):
return web.json_response(
{"message": "Workflows names is invalid!"}, status=400
)
filename_old_prefix = filename_old + PREFIX
json_file_old_prefix = os.path.join(nodes_settings_path, filename_old_prefix)
filename_new_prefix = filename_new + PREFIX
json_file_new_prefix = os.path.join(nodes_settings_path, filename_new_prefix)
if os.path.isfile(json_file_old_prefix):
# File exist remove him, if overwrite = True
if overwrite and os.path.isfile(json_file_new_prefix):
os.remove(json_file_new_prefix)
os.rename(json_file_old_prefix, json_file_new_prefix)
return web.json_response(
{
"message": f"Workflow '{filename_old}' renamed to '{filename_new}' successfully!"
},
status=200,
)
except OSError as e:
return web.json_response(
{"error": "Error: %s - %s." % (e.filename, e.strerror)}, status=500
)
# Remove file settings painter node data
@PromptServer.instance.routes.post("/alekpet/remove_node_settings")
async def removeSettings(request):
try:
json_data = await request.json()
filename = json_data.get("name")
painters_data = json_data.get("painters_data")
if painters_data is None or (
type(painters_data) == str and painters_data.strip() == ""
):
return web.json_response(
{"message": "Painters data is invalid!"}, status=400
)
if not isFileName:
return web.json_response(
{"message": "Workflow filename settings invalid!"}, status=400
)
filename_ = filename + PREFIX
json_file = os.path.join(nodes_settings_path, filename_)
if os.path.isfile(json_file):
remove_info = {"workflow_record": False, "paint_record": False}
if len(painters_data) == 0:
os.remove(json_file)
remove_info["workflow_record"] = True
return web.json_response(
{
"message": f"Workflow '{filename}' removed successfully!",
"workflow_info": remove_info,
},
status=200,
)
# Open json to delete painters
painters_remove = []
with open(json_file, "r") as f:
data = json.load(f)
for w in painters_data:
if w in data.get("painters_data"):
painters_remove.append(w)
data["painters_data"].pop(w)
remove_info["paint_record"] = True
# Save to json file if remove workflow
with open(json_file, "w") as f:
json.dump(data, f)
# Check if workflow is empty, remove file json
if len(data.get("painters_data").keys()) == 0:
os.remove(json_file)
remove_info["workflow_record"] = True
return web.json_response(
{
"message": f"Painters '{','.join(painters_remove)}' data removed from the workflow '{filename}' successfully! Workflow is empty remove workflow!",
"workflow_info": remove_info,
},
status=200,
)
else:
return web.json_response(
{
"message": f"Painters '{','.join(painters_remove)}' data removed from the workflow '{filename}' successfully!",
"workflow_info": remove_info,
},
status=200,
)
except OSError as e:
return web.json_response(
{"error": "Error: %s - %s." % (e.filename, e.strerror)}, status=500
)
# Piping image
@@ -102,7 +399,7 @@ class PainterNode(object):
PromptServer.instance.send_sync(
"alekpet_get_image", {"unique_id": unique_id, "images": input_images}
)
if not wait_canvas_change(unique_id):
print(f"Painter_{unique_id}: Failed to get image!")
else:
@@ -110,7 +407,7 @@ class PainterNode(object):
# end - Piping image input
image_path = folder_paths.get_annotated_filepath(image)
img = node_helpers.pillow(Image.open, image_path)
output_images = []
+498 -297
View File
@@ -1,12 +1,13 @@
# Title: ComfyUI Install Customs Nodes and javascript files
# Author: AlekPet
# Version: 2025.06.26
# Version: 2024.11.02
import os
import importlib.util
import subprocess
import sys
import shutil
import __main__
import json
# import pkgutil
import re
@@ -31,15 +32,155 @@ extension_dirs = [
DEBUG = False
NODE_CLASS_MAPPINGS = dict() # dynamic class nodes append in mappings
# dynamic display names nodes append mappings names
NODE_DISPLAY_NAME_MAPPINGS = dict()
NODE_DISPLAY_NAME_MAPPINGS = dict() # dynamic display names nodes append mappings names
wordsExludeSpace = "|".join(["ChatGLM"])
humanReadableTextReg = re.compile(
rf"({wordsExludeSpace}-?\d*)|(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])"
)
humanReadableTextReg = re.compile("(?<=[a-z0-9])([A-Z])|(?<=[A-Z0-9])([A-Z][a-z]+)")
module_name_cut_version = re.compile("[>=<]")
renameTextUnderline = re.compile("(?<=[a-z])([A-Z])")
installed_modules = {}
# installed_modules = {m[1] for m in pkgutil.iter_modules()}
# -- Config settings --
CONFIG = {}
NODES_SETTINGS = {}
GLOBAL_SETTINGS = {}
DYNAMIC_NODES_ADD = True
is_check_enabled_nodes = True
# -- All nodes
def get_classes(code):
tree = ast.parse(code)
return [
n.name
for n in ast.walk(tree)
if isinstance(n, ast.ClassDef) and "Node" in n.name
]
def getNamesNodesInsidePyFile(nodeElement):
node_folder = os.path.join(extension_folder, nodeElement)
cls_names = []
py_files = []
for f in os.listdir(node_folder):
ext = os.path.splitext(f)
if (
os.path.isfile(os.path.join(node_folder, f))
and not f.startswith("__")
and ext[1] == ".py"
):
py_files.append(f)
with open(os.path.join(node_folder, f), "r") as pyf:
cls_names.extend(get_classes(pyf.read()))
return cls_names, py_files
# List nodes dirs
ALL_NODES_DIRS = {
dir.name: dict(zip(["nodes", "py_files"], getNamesNodesInsidePyFile(dir.name)))
for dir in os.scandir(extension_folder)
if dir.is_dir()
and not dir.name.startswith(".")
and not dir.name.startswith("_")
and dir.name != extension_dirs[0] and dir.name != "example_workflows"
}
def addChangesToJson(json_data):
isChanged = False
_json_data = json.loads(json.dumps(json_data))
nodes_data = _json_data.get("nodes_settings")
if not nodes_data and len(ALL_NODES_DIRS.keys()) == 0:
isChanged = False
for node_check_folder in ALL_NODES_DIRS:
# If folder not found, create folder key and nodes children keys
if node_check_folder not in nodes_data:
DEBUG and print(f" -> Folder node {node_check_folder} not exists!")
nodes_data.update(
{
node_check_folder: {
"active": True,
"nodes": {
node: True
for node in ALL_NODES_DIRS[node_check_folder]["nodes"]
},
}
}
)
isChanged = True
else:
# Else check children nodes
all_childen_nodes_in_folder = ALL_NODES_DIRS[node_check_folder]["nodes"]
json_data_nodes_in_folder = nodes_data[node_check_folder]["nodes"]
for child_node in all_childen_nodes_in_folder:
if child_node not in json_data_nodes_in_folder:
DEBUG and print(
f" -> Folder node {node_check_folder} children node {child_node} not exists!"
)
json_data_nodes_in_folder.update({child_node: True})
isChanged = True
return _json_data, isChanged
# end -- All nodes
# Directory config file
config_path = os.path.join(extension_folder, "config.json")
# Load config.js file
if not os.path.exists(config_path):
DEBUG and print(" -> File config.js file not found! Created new config!")
with open(config_path, "w") as f:
json.dump(
{"nodes_settings": {}, "global_settings": {"check_enabled_nodes": False}}, f
)
# -- Open config check nodes
isChanged = False
json_data = None
with open(config_path, "r") as f:
data = json.load(f)
json_data, isChanged = addChangesToJson(data)
if isChanged:
with open(config_path, "w") as f:
json.dump(json_data, f, indent=2)
# -- end - Open config check nodes
CONFIG = json_data
NODES_SETTINGS = CONFIG.get("nodes_settings")
GLOBAL_SETTINGS = CONFIG.get("global_settings")
NODES_LOAD_SUCCESS = {}
NODES_LOAD_FAILED = {}
# Global settings
if GLOBAL_SETTINGS is not None:
is_check_enabled_nodes = GLOBAL_SETTINGS.get("check_enabled_nodes", True)
# Nodes settings
if NODES_SETTINGS is None:
is_check_enabled_nodes = False
# -- end - Config settings --
try:
from filelock import FileLock
except ImportError:
@@ -59,7 +200,6 @@ def get_version_extension():
return f" \033[1;34mv{version}\033[0m\033[1;35m"
except Exception as e:
print(e)
return version
@@ -74,7 +214,7 @@ def information(datas):
try:
print(info, end="\r", flush=True)
except UnicodeError:
safe_text = info.encode("utf-8", errors="replace").decode("utf-8", errors="replace")
safe_text = info.encode('utf-8', errors='replace').decode('utf-8', errors='replace')
print(safe_text, end="\r", flush=True)
@@ -83,59 +223,77 @@ def printColorInfo(text, color="\033[92m"):
print(f"{color}{text}{CLEAR}")
def get_classes(code):
tree = ast.parse(code)
return [n.name for n in ast.walk(tree) if isinstance(n, ast.ClassDef) and "Node" in n.name]
def addComfyUINodesToMapping(nodeElement):
def addComfyUINodesToMapping(nodeElement, nodesOptions):
log(f" -> Find class execute node <{nodeElement}>, add NODE_CLASS_MAPPINGS ...")
node_folder = os.path.join(extension_folder, nodeElement)
for f in os.listdir(node_folder):
classesNames = []
for f in ALL_NODES_DIRS[nodeElement]["py_files"]:
ext = os.path.splitext(f)
# Find files extensions .py
if (
os.path.isfile(os.path.join(node_folder, f))
and not f.startswith("__")
and ext[1] == ".py"
and ext[0] != "__init__"
):
# remove extensions .py
module_without_py = f.replace(ext[1], "")
# Import module
spec = importlib.util.spec_from_file_location(module_without_py, os.path.join(node_folder, f))
module = importlib.util.module_from_spec(spec)
# remove extensions .py
module_without_py = f.replace(ext[1], "")
# Import module
spec = importlib.util.spec_from_file_location(
module_without_py, os.path.join(node_folder, f)
)
module = importlib.util.module_from_spec(spec)
try:
spec.loader.exec_module(module)
classes_names = list(
filter(
lambda p: callable(getattr(module, p)) and p.find("Node") != -1,
dir(module),
)
except Exception as e:
NODES_LOAD_FAILED[nodeElement] = e
continue
classes_names = list(
filter(
lambda p: callable(getattr(module, p)) and p.find("Node") != -1,
dir(module),
)
for class_module_name in classes_names:
# Check module
if class_module_name and class_module_name not in NODE_CLASS_MAPPINGS.keys():
log(f" [*] Class node found '{class_module_name}' add to NODE_CLASS_MAPPINGS...")
NODE_CLASS_MAPPINGS.update({class_module_name: getattr(module, class_module_name)})
NODE_DISPLAY_NAME_MAPPINGS.update(
{class_module_name: humanReadableTextReg.sub(" \\1\\2", class_module_name)}
)
NODES_LOAD_SUCCESS[nodeElement] = classes_names
for class_module_name in classes_names:
# Check module
if (
class_module_name
and class_module_name not in NODE_CLASS_MAPPINGS.keys()
):
classesNames.append(class_module_name)
if is_check_enabled_nodes and class_module_name in nodesOptions:
if nodesOptions[class_module_name]:
log(
f" [*] Class node found '{class_module_name}' add to NODE_CLASS_MAPPINGS..."
)
NODE_CLASS_MAPPINGS.update(
{class_module_name: getattr(module, class_module_name)}
)
NODE_DISPLAY_NAME_MAPPINGS.update(
{
class_module_name: humanReadableTextReg.sub(
r" \1", class_module_name
).strip()
}
)
else:
log(
f" [*] Class node found '{class_module_name}' add to NODE_CLASS_MAPPINGS..."
)
def getNamesNodesInsidePyFile(nodeElement):
node_folder = os.path.join(extension_folder, nodeElement)
cls_names = []
for f in os.listdir(node_folder):
ext = os.path.splitext(f)
if (
os.path.isfile(os.path.join(node_folder, f))
and not f.startswith("__")
and ext[1] == ".py"
and ext[0] != "__init__"
):
with open(os.path.join(node_folder, f), "r", encoding="utf-8", errors="replace") as pyf:
cls_names.extend(get_classes(pyf.read()))
return cls_names
NODE_CLASS_MAPPINGS.update(
{class_module_name: getattr(module, class_module_name)}
)
NODE_DISPLAY_NAME_MAPPINGS.update(
{
class_module_name: humanReadableTextReg.sub(
r" \1", class_module_name
).strip()
}
)
return classesNames
def module_install(commands, cwd="."):
@@ -146,8 +304,8 @@ def module_install(commands, cwd="."):
stderr=subprocess.PIPE,
text=True,
bufsize=1,
encoding="utf-8",
errors="replace",
encoding='utf-8',
errors='replace'
)
out = threading.Thread(target=information, args=(result.stdout,))
err = threading.Thread(target=information, args=(result.stderr,))
@@ -202,36 +360,66 @@ def checkModules(nodeElement):
# [Hack] argostranslate module checking install
if nodeElement == "ArgosTranslateNode" and "argostranslate" in installed_modules and len(lines):
if DEBUG:
if not DEBUG:
printColorInfo("* Argostranslate installed removed from required modules!")
required_modules.remove(lines[0])
modules_to_install = required_modules - installed_modules
if modules_to_install:
module_install([sys.executable, "-m", "pip", "install", *modules_to_install])
nodes_list_dict = {}
module_install(
[sys.executable, "-m", "pip", "install", *modules_to_install]
)
def install_node(nodeElement):
global nodes_list_dict
# Nodes direcories and nodes in extension
folderNodes = ALL_NODES_DIRS.get(nodeElement)
isNotNodes = len(folderNodes.get("nodes", [])) == 0
log(f"* Node <{nodeElement}> is found, installing...")
clsNodes = getNamesNodesInsidePyFile(nodeElement)
clsNodesText = "\033[93m" + ", ".join(clsNodes) + "\033[0m" if clsNodes else ""
# printColorInfo(f"Node -> {nodeElement}: {clsNodesText} \033[92m[Loading] ")
nodes_list_dict[nodeElement].update(
{
"nodes": clsNodes,
"message": f"Node -> {nodeElement}: {clsNodesText} \033[92m",
}
)
# Install dependencies
checkModules(nodeElement)
# addComfyUINodesToMapping(nodeElement) # dynamic class nodes append in mappings
if DYNAMIC_NODES_ADD:
currentNodeSettings = NODES_SETTINGS.get(nodeElement)
isActiveNode = True
nodesOptions = {}
# Disable extensions and JavaScript files:
# file_js = renameTextUnderline.sub("_\\1", nodeElement).lower().strip()
# path_js = os.path.join(web_extensions_dir, f"{file_js}.js")
# path_off = os.path.join(web_extensions_dir, f"{file_js}.off")
if currentNodeSettings is not None:
isActiveNode = currentNodeSettings.get("active", True)
nodesOptions = currentNodeSettings.get("nodes", {})
if is_check_enabled_nodes and isActiveNode == False or isNotNodes:
# stateValue = "Nodes missing" if isNotNodes else "off"
# printColorInfo(
# f"Node -> \033[93m{nodeElement}\033[0m: [\033[91m{stateValue}\033[0m] "
# )
# Disable extensions and JavaScript files, rename extension *.js -> *.off:
# if os.path.isfile(path_js):
# print(file_js, "OK")
# os.rename(path_js, path_off)
return
# Disable extensions and JavaScript files, return normal extension *.off -> *.js:
# if os.path.isfile(path_off):
# print(path_off, "OK")
# os.rename(path_off, path_js)
log(f"* Node <{nodeElement}> is found, installing...")
# -- Add to mapping
# dynamic class nodes append in mappings
clsNames = addComfyUINodesToMapping(nodeElement, nodesOptions)
def safe_rmtree(path):
@@ -252,10 +440,8 @@ def installNodes():
def _installNodes_internal():
global installed_modules
global nodes_list_dict
log(f"\n-------> AlekPet Node Installing [DEBUG] <-------")
log("\n-------> AlekPet Node Installing [DEBUG] <-------")
# printColorInfo(f"### [START] ComfyUI AlekPet Nodes{get_version_extension()} ###", "\033[1;35m")
# Remove files in lib directory
libfiles = ["fabric.js"]
for file in libfiles:
@@ -272,20 +458,7 @@ def _installNodes_internal():
installed_modules = get_installed_modules()
nodes = []
for nodeElement in os.listdir(extension_folder):
if (
not nodeElement.startswith("__")
and nodeElement.endswith("Node")
and os.path.isdir(os.path.join(extension_folder, nodeElement))
):
nodes_list_dict[nodeElement] = {
"error": None,
"message": f"Node -> {nodeElement}\033[92m",
}
nodes.append(nodeElement)
for n in nodes:
for n in ALL_NODES_DIRS:
install_node(n)
@@ -296,237 +469,265 @@ WEB_DIRECTORY = f"./{extension_dirs[0]}"
# Install nodes
installNodes()
# Import classes nodes and add in mappings
# ArgosTranslateNode
try:
from .ArgosTranslateNode.argos_translate_node import (
ArgosTranslateCLIPTextEncodeNode,
ArgosTranslateTextNode,
)
NODE_CLASS_MAPPINGS.update(
{
"ArgosTranslateCLIPTextEncodeNode": ArgosTranslateCLIPTextEncodeNode,
"ArgosTranslateTextNode": ArgosTranslateTextNode,
}
)
NODE_DISPLAY_NAME_MAPPINGS.update(
{
"ArgosTranslateCLIPTextEncodeNode": "Argos Translate CLIP Text Encode Node",
"ArgosTranslateTextNode": "Argos Translate Text Node",
}
)
# For ComfyUI Manager 😄
if not DYNAMIC_NODES_ADD:
# Import classes nodes and add in mappings
# ArgosTranslateNode
try:
from .ArgosTranslateNode.argos_translate_node import (
ArgosTranslateCLIPTextEncodeNode,
ArgosTranslateTextNode,
)
except Exception as e:
if "ArgosTranslateNode" in nodes_list_dict:
nodes_list_dict["ArgosTranslateNode"]["error"] = e
NODE_CLASS_MAPPINGS.update(
{
"ArgosTranslateCLIPTextEncodeNode": ArgosTranslateCLIPTextEncodeNode,
"ArgosTranslateTextNode": ArgosTranslateTextNode,
}
)
NODE_DISPLAY_NAME_MAPPINGS.update(
{
"ArgosTranslateCLIPTextEncodeNode": "Argos Translate CLIP Text Encode Node",
"ArgosTranslateTextNode": "Argos Translate Text Node",
}
)
except Exception as e:
NODES_LOAD_FAILED["ArgosTranslateNode"] = e
# DeepTranslatorNode
try:
from .DeepTranslatorNode.deep_translator_node import (
DeepTranslatorCLIPTextEncodeNode,
DeepTranslatorTextNode,
)
# DeepTranslatorNode
try:
from .DeepTranslatorNode.deep_translator_node import (
DeepTranslatorCLIPTextEncodeNode,
DeepTranslatorTextNode,
)
NODE_CLASS_MAPPINGS.update(
{
"DeepTranslatorCLIPTextEncodeNode": DeepTranslatorCLIPTextEncodeNode,
"DeepTranslatorTextNode": DeepTranslatorTextNode,
}
)
NODE_DISPLAY_NAME_MAPPINGS.update(
{
"DeepTranslatorCLIPTextEncodeNode": "Deep Translator CLIP Text Encode Node",
"DeepTranslatorTextNode": "Deep Translator Text Node",
}
)
NODE_CLASS_MAPPINGS.update(
{
"DeepTranslatorCLIPTextEncodeNode": DeepTranslatorCLIPTextEncodeNode,
"DeepTranslatorTextNode": DeepTranslatorTextNode,
}
)
NODE_DISPLAY_NAME_MAPPINGS.update(
{
"DeepTranslatorCLIPTextEncodeNode": "Deep Translator CLIP Text Encode Node",
"DeepTranslatorTextNode": "Deep Translator Text Node",
}
)
except Exception as e:
if "DeepTranslatorNode" in nodes_list_dict:
nodes_list_dict["DeepTranslatorNode"]["error"] = e
except Exception as e:
NODES_LOAD_FAILED["DeepTranslatorNode"] = e
# GoogleTranslateNode
try:
from .GoogleTranslateNode.google_translate_node import (
GoogleTranslateCLIPTextEncodeNode,
GoogleTranslateTextNode,
)
NODE_CLASS_MAPPINGS.update(
{
"GoogleTranslateCLIPTextEncodeNode": GoogleTranslateCLIPTextEncodeNode,
"GoogleTranslateTextNode": GoogleTranslateTextNode,
}
)
NODE_DISPLAY_NAME_MAPPINGS.update(
{
"GoogleTranslateCLIPTextEncodeNode": "Google Translate CLIP Text Encode Node",
"GoogleTranslateTextNode": "Google Translate Text Node",
}
)
except Exception as e:
NODES_LOAD_FAILED["GoogleTranslateNode"] = e
# ChatGLMNode
try:
from .ChatGLMNode.chatglm_node import (
ChatGLM4TranslateCLIPTextEncodeNode,
ChatGLM4TranslateTextNode,
ChatGLM4InstructNode,
ChatGLM4InstructMediaNode,
ChatGLMImageGenerateNode,
ChatGLMVideoGenerateNode
)
NODE_CLASS_MAPPINGS.update(
{
"ChatGLM4TranslateCLIPTextEncodeNode": ChatGLM4TranslateCLIPTextEncodeNode,
"ChatGLM4TranslateTextNode": ChatGLM4TranslateTextNode,
"ChatGLM4InstructNode": ChatGLM4InstructNode,
"ChatGLM4InstructMediaNode": ChatGLM4InstructMediaNode,
# Generate
"ChatGLMImageGenerateNode": ChatGLMImageGenerateNode,
"ChatGLMVideoGenerateNode": ChatGLMVideoGenerateNode,
}
)
NODE_DISPLAY_NAME_MAPPINGS.update(
{
"ChatGLM4TranslateCLIPTextEncodeNode": "ChatGLM-4 Translate CLIP Text Encode Node",
"ChatGLM4TranslateTextNode": "ChatGLM-4 Translate Text Node",
"ChatGLM4InstructNode": "ChatGLM-4 Instruct Node",
"ChatGLM4InstructMediaNode": "ChatGLM-4 Instruct Media Node",
# Generate
"ChatGLMImageGenerateNode": "Chat GLM Image Generate Node",
"ChatGLMVideoGenerateNode": "Chat GLM Video Generate Node",
}
)
except Exception as e:
NODES_LOAD_FAILED["ChatGLMNode"] = e
# GoogleTranslateNode
try:
from .GoogleTranslateNode.google_translate_node import (
GoogleTranslateCLIPTextEncodeNode,
GoogleTranslateTextNode,
)
NODE_CLASS_MAPPINGS.update(
{
"GoogleTranslateCLIPTextEncodeNode": GoogleTranslateCLIPTextEncodeNode,
"GoogleTranslateTextNode": GoogleTranslateTextNode,
}
)
NODE_DISPLAY_NAME_MAPPINGS.update(
{
"GoogleTranslateCLIPTextEncodeNode": "Google Translate CLIP Text Encode Node",
"GoogleTranslateTextNode": "Google Translate Text Node",
}
)
except Exception as e:
if "GoogleTranslateNode" in nodes_list_dict:
nodes_list_dict["GoogleTranslateNode"]["error"] = e
# ChatGLMNode
try:
from .ChatGLMNode.chatglm_node import (
ChatGLM4TranslateCLIPTextEncodeNode,
ChatGLM4TranslateTextNode,
ChatGLM4InstructNode,
ChatGLM4InstructMediaNode,
ChatGLMImageGenerateNode,
ChatGLMVideoGenerateNode
)
NODE_CLASS_MAPPINGS.update(
{
"ChatGLM4TranslateCLIPTextEncodeNode": ChatGLM4TranslateCLIPTextEncodeNode,
"ChatGLM4TranslateTextNode": ChatGLM4TranslateTextNode,
"ChatGLM4InstructNode": ChatGLM4InstructNode,
"ChatGLM4InstructMediaNode": ChatGLM4InstructMediaNode,
# Generate
"ChatGLMImageGenerateNode": ChatGLMImageGenerateNode,
"ChatGLMVideoGenerateNode": ChatGLMVideoGenerateNode,
}
)
NODE_DISPLAY_NAME_MAPPINGS.update(
{
"ChatGLM4TranslateCLIPTextEncodeNode": "ChatGLM-4 Translate CLIP Text Encode Node",
"ChatGLM4TranslateTextNode": "ChatGLM-4 Translate Text Node",
"ChatGLM4InstructNode": "ChatGLM-4 Instruct Node",
"ChatGLM4InstructMediaNode": "ChatGLM-4 Instruct Media Node",
# Generate
"ChatGLMImageGenerateNode": "Chat GLM Image Generate Node",
"ChatGLMVideoGenerateNode": "Chat GLM Video Generate Node",
}
)
except Exception as e:
if "ChatGLMNode" in nodes_list_dict:
nodes_list_dict["ChatGLMNode"]["error"] = e
# ExtrasNode
try:
# ExtrasNode
from .ExtrasNode.extras_node import PreviewTextNode, HexToHueNode, ColorsCorrectNode
NODE_CLASS_MAPPINGS.update(
{
"PreviewTextNode": PreviewTextNode,
"HexToHueNode": HexToHueNode,
"ColorsCorrectNode": ColorsCorrectNode,
}
)
NODE_DISPLAY_NAME_MAPPINGS.update(
{
"PreviewTextNode": "Preview Text Node",
"HexToHueNode": "HEX to HUE Node",
"ColorsCorrectNode": "Colors Correct Node",
}
)
except Exception as e:
if "ExtrasNode" in nodes_list_dict:
nodes_list_dict["ExtrasNode"]["error"] = e
# PainterNode
try:
# PainterNode
from .PainterNode.painter_node import PainterNode
NODE_CLASS_MAPPINGS.update(
{
"PainterNode": PainterNode,
}
)
NODE_DISPLAY_NAME_MAPPINGS.update(
{
"PainterNode": "Painter Node",
}
)
except Exception as e:
if "PainterNode" in nodes_list_dict:
nodes_list_dict["PainterNode"]["error"] = e
# PoseNode
try:
# PoseNode
from .PoseNode.pose_node import PoseNode
NODE_CLASS_MAPPINGS.update(
{
"PoseNode": PoseNode,
}
)
NODE_DISPLAY_NAME_MAPPINGS.update(
{
"PoseNode": "Pose Node",
}
)
except Exception as e:
if "PoseNode" in nodes_list_dict:
nodes_list_dict["PoseNode"]["error"] = e
# IDENode
try:
# IDENode
from .IDENode.ide_node import IDENode
NODE_CLASS_MAPPINGS.update(
{
"IDENode": IDENode,
}
)
NODE_CLASS_MAPPINGS.update({
"PreviewTextNode": PreviewTextNode,
"HexToHueNode": HexToHueNode,
"ColorsCorrectNode": ColorsCorrectNode,
"PainterNode": PainterNode,
"PoseNode": PoseNode,
"IDENode": IDENode,
})
NODE_DISPLAY_NAME_MAPPINGS.update(
{
"IDENode": "IDE Node",
}
)
NODE_DISPLAY_NAME_MAPPINGS.update({
"PreviewTextNode": "Preview Text Node",
"HexToHueNode": "HEX to HUE Node",
"ColorsCorrectNode": "Colors Correct Node",
"PainterNode": "Painter Node",
"PoseNode": "Pose Node",
"IDENode": "IDE Node",
})
except Exception as e:
if "IDENode" in nodes_list_dict:
nodes_list_dict["IDENode"]["error"] = e
else:
# For ComfyUI Manager 😄, if DYNAMIC_NODES_ADD = True
'''
# Import classes nodes and add in mappings
NODE_CLASS_MAPPINGS = {
"ArgosTranslateCLIPTextEncodeNode": ArgosTranslateCLIPTextEncodeNode,
"ArgosTranslateTextNode": ArgosTranslateTextNode,
"ChatGLM4TranslateCLIPTextEncodeNode": ChatGLM4TranslateCLIPTextEncodeNode,
"ChatGLM4TranslateTextNode": ChatGLM4TranslateTextNode,
"ChatGLM4InstructNode": ChatGLM4InstructNode,
"ChatGLM4InstructMediaNode": ChatGLM4InstructMediaNode,
"ChatGLMImageGenerateNode": ChatGLMImageGenerateNode,
"ChatGLMVideoGenerateNode": ChatGLMVideoGenerateNode,
"DeepTranslatorCLIPTextEncodeNode": DeepTranslatorCLIPTextEncodeNode,
"DeepTranslatorTextNode": DeepTranslatorTextNode,
"PreviewTextNode": PreviewTextNode,
"HexToHueNode": HexToHueNode,
"ColorsCorrectNode": ColorsCorrectNode,
"GoogleTranslateCLIPTextEncodeNode": GoogleTranslateCLIPTextEncodeNode,
"GoogleTranslateTextNode": GoogleTranslateTextNode,
"PainterNode": PainterNode,
"PoseNode": PoseNode,
"IDENode": IDENode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ArgosTranslateCLIPTextEncodeNode": "Argos Translate CLIP Text Encode Node",
"ArgosTranslateTextNode": "Argos Translate Text Node",
"ChatGLM4TranslateCLIPTextEncodeNode": "ChatGLM-4 Translate CLIP Text Encode Node",
"ChatGLM4TranslateTextNode": "ChatGLM-4 Translate Text Node",
"ChatGLM4InstructNode": "ChatGLM-4 Instruct Node",
"ChatGLM4InstructMediaNode": "ChatGLM-4 Instruct Media Node",
"ChatGLMImageGenerateNode": "Chat GLM Image Generate Node",
"ChatGLMVideoGenerateNode": "Chat GLM Video Generate Node",
"DeepTranslatorCLIPTextEncodeNode": "Deep Translator CLIP Text Encode Node",
"DeepTranslatorTextNode": "Deep Translator Text Node",
"PreviewTextNode": "Preview Text Node",
"HexToHueNode": "HEX to HUE Node",
"ColorsCorrectNode": "Colors Correct Node",
"GoogleTranslateCLIPTextEncodeNode": "Google Translate CLIP Text Encode Node",
"GoogleTranslateTextNode": "Google Translate Text Node",
"PainterNode": "Painter Node",
"PoseNode": "Pose Node",
"IDENode": "IDE Node",
}'''
# Information
printColorInfo(f"\n### [START] ComfyUI AlekPet Nodes{get_version_extension()} ###", "\033[1;35m")
printColorInfo(
f"### [START] ComfyUI AlekPet Nodes{get_version_extension()} ###", "\033[1;35m"
)
failed_nodes_text = ""
len_nodes = len(nodes_list_dict)
for key, node in enumerate(nodes_list_dict):
currentNode = nodes_list_dict[node]
for nodeElement in ALL_NODES_DIRS:
folderNodes = ALL_NODES_DIRS.get(nodeElement)
clsNodes = folderNodes.get("nodes", [])
isNotNodes = len(clsNodes) == 0
currentNodeVals = currentNode.get("error")
# Method views nodes DYNAMIC_NODES_ADD = False
if not DYNAMIC_NODES_ADD:
status = "\033[1;36;40m [Success] "
if nodeElement in NODES_LOAD_FAILED:
status = "\033[91m [Failed] "
if currentNodeVals is None:
status = "\033[92m[Loading]"
else:
color_error = "\033[1;31;40m"
status = color_error + " [Failed]"
error_message = currentNode.get("error", "Error")
if(len(currentNodeVals.args) == 2 and currentNodeVals.args[1] == "warning"):
color_error = "\033[33m"
status = color_error + "[Warning]"
error_message = currentNodeVals.args[0]
failed_nodes_text += f"\033[93m{node} -> {color_error}{error_message}\033[0m\n"
message = currentNode.get("message", "No message available")
printColorInfo(f"{message}{status}\033[0m")
if key == len_nodes - 1 and failed_nodes_text:
printColorInfo(
f"\n\033[1;31;40m* Nodes have been temporarily disabled due to the error or specially *\033[0m\n{failed_nodes_text}"
clsNodesText = (
"\033[93m" + ", ".join(clsNodes) + status
if not isNotNodes
else "\033[91m[Node missing]"
)
printColorInfo(f"### [END] ComfyUI AlekPet Nodes ###", "\033[1;35m")
# Show list nodes
printColorInfo(f"Node -> {nodeElement}: {clsNodesText} \033[92m")
else:
isActiveNode = True
nodesOptions = {}
currentNodeSettings = NODES_SETTINGS.get(nodeElement)
if currentNodeSettings is not None:
isActiveNode = currentNodeSettings.get("active", True)
nodesOptions = currentNodeSettings.get("nodes", {})
# Method views nodes DYNAMIC_NODES_ADD = True
if nodeElement not in NODES_LOAD_SUCCESS:
state = "Off" if not isActiveNode else "Failed"
printColorInfo(f"Node -> \033[93m{nodeElement}: \033[91m[{state}]")
continue
namesNodes = NODES_LOAD_SUCCESS[nodeElement]
lenNodesNames = len(namesNodes) - 1
listNodes = f"Node -> \033[93m{nodeElement}\033[0m: "
for k, n in enumerate(namesNodes):
sep = "" if k == lenNodesNames else ", "
active = "off"
if n in nodesOptions:
active = (
"\033[94mon" if nodesOptions[n] else "\033[91moff"
) + "\033[0m"
else:
active = "\033[94mon\033[0m"
listNodes += f"\033[92m{n}\033[0m [{active}]{sep} "
listNodes += "\033[1;36;40m[Success]"
# -- end Add to mapping
# Show list nodes
printColorInfo(listNodes)
# View nodes failed loading
if NODES_LOAD_FAILED.keys():
print(
f"\n\033[1;31;40m* Nodes have been temporarily disabled due to the error *\033[0m"
)
for failed_node in NODES_LOAD_FAILED:
error_text = NODES_LOAD_FAILED[failed_node]
print("\033[93m" + failed_node + " -> \033[1;31;40m" + str(error_text) + "\033[0m")
printColorInfo("### [END] ComfyUI AlekPet Nodes ###", "\033[1;35m")
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui_custom_nodes_alekpet"
description = "Nodes: PoseNode, PainterNode, TranslateTextNode, TranslateCLIPTextEncodeNode, DeepTranslatorTextNode, DeepTranslatorCLIPTextEncodeNode, ArgosTranslateTextNode, ArgosTranslateCLIPTextEncodeNode, ChatGLM4TranslateCLIPTextEncodeNode, ChatGLM4TranslateTextNode, ChatGLM4InstructNode, ChatGLM4InstructMediaNode, ChatGLMImageGenerateNode, ChatGLMVideoGenerateNode, PreviewTextNode, HexToHueNode, ColorsCorrectNode, IDENode."
version = "1.2.0"
version = "1.1.11"
license = { file = "LICENSE" }
[project.urls]
@@ -462,146 +462,79 @@
/* -- end Styles Settigs Kistey -- */
/* Settings store managing data */
.alekpet_painter_storage_overlay {
position: fixed;
inset: 0;
z-index: 9998;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.55);
}
.alekpet_painter_storage_box {
width: min(900px, 90vw);
max-height: 85vh;
.painter_storage_box {
width: 30vw;
display: flex;
flex-direction: column;
background: var(--comfy-menu-bg, #202020);
color: var(--input-text, #ddd);
border: 1px solid var(--border-color, #555);
gap: 20px;
color: var(--primary-fg);
}
.painter_storage_header {
display: flex;
justify-content: space-around;
background: var(--content-bg);
padding: 3px;
border-radius: 8px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
}
.alekpet_painter_storage_header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
border-bottom: 1px solid var(--border-color, #444);
}
.alekpet_painter_storage_toolbar {
display: flex;
gap: 8px;
padding: 10px 14px;
}
.alekpet_painter_storage_items {
overflow: auto;
padding: 0 14px 14px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 10px;
}
.alekpet_painter_storage_item {
grid-column: 1/-1;
padding: 20px;
.painter_storage_body {
text-align: center;
}
.alekpet_painter_storage_item_card {
position: relative;
.painter_storage_items_box {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
border: 1px solid var(--border-color, #444);
border-radius: 6px;
background: var(--comfy-input-bg, #181818);
}
.alekpet_painter_storage_item_preview_wrapper {
height: 180px;
display: flex;
align-items: center;
gap: 5px;
align-items: stretch;
justify-content: center;
overflow: hidden;
background: #000;
}
.alekpet_painter_storage_button_close {
position: absolute;
top: 8px;
right: 8px;
transition: all 0.4s;
border: 0;
border-radius: 0 0 0 50%;
background: var(--comfy-menu-bg);
.painter_storage_workflows_item {
cursor: pointer;
}
.alekpet_painter_storage_button_close:hover {
border-radius: 50% 50% 50% 0;
padding: 2px;
border: 1px solid rgb(255 151 0);
background: rgb(255 151 0 / 80%);
.painter_storage_workflows_item:hover {
background: var(--primary-hover-bg) !important;
}
.alekpet_painter_storage_item_meta {
font-size: 11px;
display: flex;
flex-direction: column;
gap: 3px;
flex: 1 1 0;
.painter_storage_grid {
display: grid;
grid-template-columns: 0.2fr 0.2fr repeat(3, 1fr);
justify-content: center;
place-items: center;
}
.alekpet_painter_storage_item_id {
font-family: monospace;
font-size: 11px;
.painter_storage_items_header {
background: var(--primary-bg);
text-transform: uppercase;
}
.painter_storage_painters_item_preview_canvas {
width: 60% !important;
height: auto !important;
}
/* Workflows */
.painter_storage_painters_wrapper {
border: 1px solid var(--border-color);
background: var(--p-button-outlined-contrast-hover-background);
}
.painter_storage_painters_grid {
grid-template-columns: 0.2fr repeat(4, 1fr);
}
.painter_storage_painters_header {
background: var(--descrip-text);
}
.painter_storage_painters_item_name {
text-wrap: nowrap;
width: 90%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.alekpet_painter_storage_item_skew_box {
background: linear-gradient(
90deg,
var(--comfy-menu-bg) 60%,
var(--comfy-menu-secondary-bg)
);
padding: 3px 0px;
flex-direction: column;
clip-path: polygon(0px 0%, 100% 0%, calc(100% - 15px) 100%, 0% 100%);
}
.alekpet_painter_storage_item_size {
background: linear-gradient(90deg, #14377a 60%, transparent);
}
.alekpet_painter_storage_item_workflow_name {
background: linear-gradient(90deg, #096e00 60%, transparent);
}
.alekpet_painter_storage_item_created_date {
background: linear-gradient(90deg, #6e0044 60%, transparent);
}
.alekpet_painter_storage_item_updated_date {
background: linear-gradient(90deg, #52006e 60%, transparent);
}
.alekpet_painter_storage_item_size > span,
.alekpet_painter_storage_item_workflow_name > span {
color: var(--success-color);
}
.alekpet_painter_storage_item_created_date > span {
color: var(--error-text);
}
.alekpet_painter_storage_item_updated_date > span {
color: var(--color-datatype-LATENT);
}
/* -- end Settings store managing data */
@@ -0,0 +1,485 @@
import { api } from "../../../../scripts/api.js";
import { fabric } from "./fabric.js";
import { formatBytes } from "./helpers.js";
import { ComfyDialog } from "../../../../../scripts/ui.js";
import {
makeElement,
createWindowModal,
animateClick,
THEMES_MODAL_WINDOW,
comfyuiDesktopConfirm,
} from "../../utils.js";
export class PainterStorageDialog extends ComfyDialog {
// Remove record in LocalStorage or JSON file
async removeRecord(workflow_name, painters_data = []) {
if (!(await comfyuiDesktopConfirm(`Delete record "${workflow_name}"?`)))
return;
try {
const rawResponse = await fetch("/alekpet/remove_node_settings", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: workflow_name,
painters_data: painters_data,
}),
});
if (rawResponse.status !== 200) {
throw new Error(
`Error painter remove file settings: ${rawResponse.statusText}`
);
}
const jsonData = await rawResponse.json();
return {
status: true,
remove_info: jsonData?.workflow_info,
message: jsonData.message,
};
} catch (e) {
console.log(e);
return { status: false, remove_info: null, message: e.message };
}
}
// Create menu elements
createMenuElements(_data) {
const data = _data.sort((a, b) => {
a = JSON.stringify(a.value).length;
b = JSON.stringify(b.value).length;
return b - a;
});
if (!data?.length) {
return makeElement("div", {
class: ["painter_storage_items_box"],
textContent: "Records is not found!",
});
}
const itemBox = makeElement("div", {
class: ["painter_storage_items_box"],
children: [
makeElement("div", {
class: ["painter_storage_grid", "painter_storage_items_header"],
children: [
makeElement("div", { textContent: "👓" }),
makeElement("div", { textContent: "#" }),
makeElement("div", { textContent: "Name workflow" }),
makeElement("div", { textContent: "Size" }),
makeElement("div", { textContent: "Action" }),
],
}),
],
});
// Check if body item is empty, show message no records
async function checkNoItems() {
if (
document.querySelector(".painter_storage_body").children.length === 1
) {
const data = await this.loadingDataJSON();
if (data) this.updateBodyData(data);
}
}
// Sort workflows for size
function sortWorkflowSize(paint_data) {
return Object.keys(paint_data.painters_data)
.map((w) => ({
painter_name: w,
painter_data: paint_data.painters_data[w],
}))
.sort((a, b) => {
a = JSON.stringify(a.painter_data).length;
b = JSON.stringify(b.painter_data).length;
return b - a;
});
}
// Create painters elements
function createItemsPainters(workflow_name, workflow_data) {
// Wrapper
const itemPaintersWrapper = makeElement("div", {
class: ["painter_storage_painters_wrapper"],
style: { display: "none", opacity: 0, transition: "all 0.4s" },
});
// Header
const itemPainterHeader = makeElement("div", {
class: [
"painter_storage_grid",
"painter_storage_painters_header",
"painter_storage_painters_grid",
],
children: [
makeElement("div", { textContent: "#" }),
makeElement("div", { textContent: "Painter name" }),
makeElement("div", { textContent: "Preview" }),
makeElement("div", { textContent: "Size" }),
makeElement("div", { textContent: "Action" }),
],
parent: itemPaintersWrapper,
});
for (const [idx, { painter_name, painter_data }] of sortWorkflowSize(
workflow_data
).entries()) {
const itemPainter = makeElement("div", {
class: [
"painter_storage_grid",
"painter_storage_painters_item",
"painter_storage_painters_grid",
],
});
const numPainter = makeElement("div", { textContent: idx + 1 });
const namePainter = makeElement("div", {
class: ["painter_storage_painters_item_name"],
textContent: painter_name,
title: painter_name,
});
const previewPainter = makeElement("div", {
class: ["painter_storage_painters_item_preview"],
});
// Create canvas from preview
const canvasEl = makeElement("canvas", {
class: ["painter_storage_painters_item_preview_canvas"],
});
previewPainter.append(canvasEl);
// Fabric canvas load
const fcanvas = new fabric.StaticCanvas(canvasEl);
const canvasValue =
typeof painter_data === "string"
? JSON.parse(painter_data)
: painter_data;
if (canvasValue?.settings?.currentCanvasSize) {
const { width, height } = canvasValue.settings.currentCanvasSize;
fcanvas.setDimensions({ width, height }, { backstoreOnly: true });
}
fcanvas.loadFromJSON(canvasValue.canvas_settings, () => {
fcanvas.renderAll();
});
const sizePainter = makeElement("div", {
innerHTML: formatBytes(new Blob([JSON.stringify(painter_data)]).size),
});
const removeButtonPainter = makeElement("button", {
style: {
color: "var(--error-text)",
padding: "7px",
fontSize: "1rem",
},
textContent: "Delete",
title: "Delete painter!",
onclick: async (e) => {
e.stopPropagation();
if (!confirm(`Delete painter record "${painter_name}"?`)) return;
const result = await this.removeRecord(workflow_name, [
painter_name,
]);
if (!result.status) {
createWindowModal({
...THEMES_MODAL_WINDOW.error,
stylesBox: {
...THEMES_MODAL_WINDOW.error.stylesBox,
lineHeight: 1.5,
},
textTitle: "Error",
textBody: result.message,
options: {
parent: document.body,
overlay: { overlay_enabled: true },
auto: {
autoshow: true,
autoremove: true,
autohide: true,
timewait: 500,
},
close: { showClose: false },
},
});
return;
}
let textMessage = `<div>Painter name <span style="color: chartreuse; font-weight: 600;">"${painter_name}"</span> deleted from json file workflow <span style="color: darkseagreen; font-weight: 600;">"${workflow_name}"</span>!</div>`;
if (
result.remove_info.workflow_record &&
result.remove_info.paint_record
) {
textMessage = `<div>Workflow name <span style="color: chartreuse; font-weight: 600;">"${workflow_name}"</span> and record <span style="color: darkseagreen; font-weight: 600;">"${painter_name}"</span> deleted from json file!</div>`;
}
createWindowModal({
...THEMES_MODAL_WINDOW.normal,
stylesBox: {
...THEMES_MODAL_WINDOW.normal.stylesBox,
lineHeight: 1.5,
},
textTitle: "Information",
textBody: textMessage,
options: {
parent: document.body,
overlay: { overlay_enabled: true },
auto: {
autoshow: true,
autoremove: true,
autohide: true,
timewait: 500,
},
close: { showClose: false },
},
});
if (
result.remove_info.workflow_record &&
result.remove_info.paint_record
) {
itemPainter.closest(".painter_storage_painters_wrapper").remove();
checkNoItems.call(this);
return;
}
itemPainter.remove();
},
});
itemPainter.append(
numPainter,
namePainter,
previewPainter,
sizePainter,
removeButtonPainter
);
itemPaintersWrapper.append(itemPainter);
}
return itemPaintersWrapper;
}
// Create workflows elements
function createWorkflowStorage(idx, workflow_name, workflow_value) {
const itemWorkflow = makeElement("div", {
class: ["painter_storage_grid", "painter_storage_workflows_item"],
title: `Show painters records workflow ${workflow_name}`,
onclick: (e) => {
const target = e.currentTarget;
animateClick(target.nextElementSibling).then((res) => {
target.children[0].textContent = res ? "▼" : "►";
target.title = `Hide painters records workflow ${workflow_name}`;
});
},
});
const showWorkflow = makeElement("div", { textContent: "►" });
const numEl = makeElement("div", { textContent: idx + 1 });
const nameEl = makeElement("div", { textContent: workflow_name });
const sizeEl = makeElement("div", {
innerHTML: formatBytes(new Blob([JSON.stringify(workflow_value)]).size),
});
const removeButtonEl = makeElement("button", {
style: { color: "var(--error-text)", padding: "7px", fontSize: "1rem" },
textContent: "Delete",
title: "Delete workflow!",
onclick: async (e) => {
e.stopPropagation();
if (!confirm(`Delete workflow "${workflow_name}"?`)) return;
const result = await this.removeRecord(workflow_name);
if (!result.status) {
createWindowModal({
...THEMES_MODAL_WINDOW.error,
stylesBox: {
...THEMES_MODAL_WINDOW.error.stylesBox,
lineHeight: 1.5,
},
textTitle: "Error",
textBody: result.message,
options: {
parent: document.body,
overlay: { overlay_enabled: true },
auto: {
autoshow: true,
autoremove: true,
autohide: true,
timewait: 500,
},
close: { showClose: false },
},
});
return;
}
createWindowModal({
...THEMES_MODAL_WINDOW.normal,
stylesBox: {
...THEMES_MODAL_WINDOW.normal.stylesBox,
lineHeight: 1.5,
},
textTitle: "Information",
textBody: `<div>Workflow name <span style="color: chartreuse; font-weight: 600;">"${workflow_name}"</span> deleted from json files!</div>`,
options: {
parent: document.body,
overlay: { overlay_enabled: true },
auto: {
autoshow: true,
autoremove: true,
autohide: true,
timewait: 500,
},
close: { showClose: false },
},
});
itemWorkflow.closest(".painter_storage_painters_wrapper").remove();
checkNoItems.call(this);
},
});
itemWorkflow.append(showWorkflow, numEl, nameEl, sizeEl, removeButtonEl);
return itemWorkflow;
}
for (const [idx, { name, value }] of data.entries()) {
const itemWrapper = makeElement("div", {
class: ["painter_storage_painters_wrapper"],
style: {
background: `var(${
(idx + 1) % 2 !== 0 ? "--tr-odd-bg-color" : "--tr-even-bg-color"
})`,
},
});
const itemPaint = createWorkflowStorage.call(this, idx, name, value);
const itemWorkflows = createItemsPainters.call(this, name, value);
itemWrapper.append(itemPaint, itemWorkflows);
itemBox.append(itemWrapper);
}
return itemBox;
}
// Get data form JSON's files
async loadingDataJSON() {
try {
const rawResponse = await api.fetchApi(
"/alekpet/loading_all_node_settings"
);
if (rawResponse.status !== 200)
throw new Error(
`Error painter get json data settings: ${rawResponse.statusText}`
);
const data = await rawResponse?.json();
if (!data?.all_settings_nodes) return {};
return data.all_settings_nodes;
} catch (e) {
console.log(e);
return {};
}
}
updateBodyData(data) {
this.body.innerHTML = "";
this.body.append(this.createMenuElements(data));
}
// Get data form LocalStorage
loadingDataLocalStorage() {
return Object.keys(localStorage)
.filter((v) => v.indexOf("Painter_") !== -1)
.map((v) => ({ name: v, value: JSON.parse(localStorage.getItem(v)) }));
}
async updateDataStorages() {
try {
this.body.innerHTML = "";
const storagePaintersData = await this.loadingDataJSON();
console.log("Loading JSON files data");
this.body.append(this.createMenuElements(storagePaintersData));
return true;
} catch (err) {
return err;
}
}
// Main function show
async show() {
this.body = makeElement("div", {
class: ["painter_storage_body"],
});
const box = makeElement("div", {
class: ["painter_storage_box"],
children: [
makeElement("div", {
class: ["painter_storage_header"],
children: [
makeElement("div", {
innerHTML: "<b>JSON</b> storage data",
style: { flexBasis: "100%", textAlign: "center" },
}),
makeElement("button", {
textContent: "Refresh",
style: { color: "limegreen", fontSize: "1rem", padding: "7px" },
onclick: async (e) => {
const target = e.target;
target.style.color = "var(--descrip-text)";
target.disabled = true;
target.textContent = "Wait...";
await new Promise((res, rej) => {
const result = this.updateDataStorages();
result ? res(true) : rej(result);
})
.then(() => {
setTimeout(() => {
target.style.color = "limegreen";
target.disabled = false;
target.textContent = "Refresh";
}, 500);
})
.catch((e) => {
console.log(e);
});
},
}),
],
}),
this.body,
],
});
await this.updateDataStorages();
super.show(box);
this.element.style.zIndex = 9999;
}
close() {
this.element.remove();
}
}
+443 -22
View File
@@ -1,17 +1,13 @@
import { api } from "../../../../scripts/api.js";
import { app } from "../../../../scripts/app.js";
import { fabric } from "./fabric.js";
function createPainterAssetId() {
if (globalThis.crypto?.randomUUID) {
return globalThis.crypto.randomUUID();
}
// Fallback for older embedded browsers.
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
import {
makeElement,
createWindowModal,
isEmptyObject,
THEMES_MODAL_WINDOW,
comfyuiDesktopConfirm,
} from "../../utils.js";
// RGB, HSV, and HSL color conversion algorithms in JavaScript https://gist.github.com/mjackson/5311256
function rgbToHsv(r, g, b) {
@@ -100,14 +96,293 @@ function HsvToRgb(brush_settings) {
return { red, green, blue };
}
function formatBytes(bytes) {
if (!bytes) return "0 B";
const units = ["B", "KB", "MB", "GB"];
const i = Math.min(
Math.floor(Math.log(bytes) / Math.log(1024)),
units.length - 1
);
return `${(bytes / 1024 ** i).toFixed(i ? 1 : 0)} ${units[i]}`;
function formatBytes(bytes, decimals = 2) {
if (!+bytes) return "0 Bytes";
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
}
// Workflow state manager class
class WorkflowStateManager {
constructor(debug) {
if (WorkflowStateManager.instance) {
return WorkflowStateManager.instance;
}
this.debug = debug;
this._workflowManager = app?.extensionManager?.workflow;
this._currentWorkflow = null;
this._previousWorkflow = null;
this.workflowsStores = [];
this.countLoaded = 0;
this.nodeStores = [];
this.newSave = false;
WorkflowStateManager.instance = this;
}
static getInstance(debug) {
if (!WorkflowStateManager.instance) {
WorkflowStateManager.instance = new WorkflowStateManager(debug);
}
return WorkflowStateManager.instance;
}
get workflowManager() {
return this._workflowManager;
}
get previousWorkflow() {
return this._previousWorkflow;
}
set previousWorkflow(new_value) {
this._previousWorkflow = new_value;
}
get currentWorkflow() {
return this._currentWorkflow;
}
set currentWorkflow(new_value) {
this._currentWorkflow = new_value;
}
// Load settings from json file
async loadData(workflowName = null) {
if (!this.currentWorkflow) {
this.debug && console.log("❗ [PainterNode] No currentWorkflow");
return;
} else {
this.debug &&
console.log(
"🆗 [PainterNode] Data loaded workflow: ",
this.currentWorkflow
);
}
try {
const rawResponse = await api.fetchApi(
`/alekpet/loading_node_settings/Painter_${
workflowName ?? this.currentWorkflow
}`
);
if (rawResponse.status !== 200)
throw new Error(
`Error painter load file settings: ${rawResponse.statusText}`
);
const data = await rawResponse?.json();
if (!data) return {};
return data.settings_nodes;
} catch (e) {
console.log(e);
return {};
}
}
// Remove settings from json file
async removeData(workflow_name, painters_data = []) {
try {
console.log(workflow_name, painters_data);
const rawResponse = await fetch("/alekpet/remove_node_settings", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: workflow_name ?? `Painter_${this.currentWorkflow}`,
painters_data: painters_data,
}),
});
if (rawResponse.status !== 200)
throw new Error(
`Error painter remove file settings: ${rawResponse.statusText}`
);
const jsonData = await rawResponse?.json();
console.log(jsonData.message);
} catch (e) {
console.log(e);
}
}
// Get all data form JSON's files
async loadingAllDataJSON() {
try {
const rawResponse = await api.fetchApi(
"/alekpet/loading_all_node_settings"
);
if (rawResponse.status !== 200)
throw new Error(
`Error painter get json data settings: ${rawResponse.statusText}`
);
const data = await rawResponse?.json();
if (!data?.all_settings_nodes) return [];
return data.all_settings_nodes;
} catch (e) {
console.log(e);
return [];
}
}
checkWorkflowExist(workflow) {
return !this.workflowsStores.includes(workflow);
}
updateWorkflows() {
return this.workflowManager.workflows.map((w) => w.filename);
}
updateWorkflow(newValue) {
this.previousWorkflow = this.currentWorkflow;
this.currentWorkflow = newValue;
this.debug &&
console.log(
`🔀 Previous workflow: ${this.previousWorkflow}, Current workflow: ${this.currentWorkflow}`
);
}
openWorkflow(filename) {
this.updateWorkflow(filename);
this.nodeStores = app.graph._nodes.filter((n) => n.type === "PainterNode");
this.countLoaded = 0;
this.workflowsStores = this.updateWorkflows();
this.debug && console.log("🔄 Change workflow:", this.currentWorkflow);
}
async setEvents() {
// Проверяем доступность объекта
if (!this.workflowManager) {
throw new Error("Workflow manager is not available");
}
const painters_settings_json = JSON.parse(
localStorage.getItem("alekpet.PainterNode.SaveSettingsJson", false)
);
if (!painters_settings_json) {
console.log("Painter settings save to JSON -> OFF");
return;
}
const self = this;
await new Promise((resolve) => {
this.workflowManager.$onAction(({ name, args, after }) => {
if (name === "createTemporary") {
if (app.graph._nodes.filter((n) => n.type === "PainterNode").length) {
this.newSave = true;
}
}
if (name === "deleteWorkflow") {
const filename = args[0].filename;
if (filename) {
this.removeData(`Painter_${filename}`);
}
}
if (name === "openWorkflow") {
after((result) => {
this.openWorkflow(result.filename);
resolve(true);
// Rename
const originalRename = result.rename;
result.rename = async function () {
const prevName = this.filename;
// Check if confirm dialog workflow exist overwrite
let confirmed = false;
// const originalConfirm = window.confirm;
// window.confirm = (message) => {
// confirmed = originalConfirm(message);
// return confirmed;
// };
const res = await originalRename.apply(this, arguments);
const newName = this.filename;
if (newName === prevName) return res; // names equals, nothing rename
// Rename backend
// let updateComplete = false;
try {
// -- Check if new name exist inside folder JSON files
// const checkResponse = await api.fetchApi(
// `/alekpet/file_exist_node_settings/Painter_${newName}`
// );
// if (checkResponse.status !== 200)
// throw new Error(
// `Error check exist workflow name file settings: ${checkResponse.statusText}`
// );
// const checkData = await checkResponse?.json();
// if (checkData?.isExists) {
// confirmed = await comfyuiDesktopConfirm(
// `Workflow new name '${newName}' already exists among the JSON files! Rename?`
// );
// }
// -- end - Check if new name exist inside folder JSON files
const rawResponse = await fetch(
"/alekpet/rename_node_settings",
{
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
old_name: `Painter_${prevName}`,
new_name: `Painter_${newName}`,
overwrite: confirmed,
}),
}
);
if (rawResponse.status === 200) {
const json_data = await rawResponse?.json();
console.log(json_data?.message);
// updateComplete = true;
} else {
console.error(
`Error rename workflow file settings ${rawResponse.statusText}`
);
}
} catch (e) {
console.log(e);
}
self.debug && console.log("Rename workflow!");
return res;
};
return result;
});
}
});
});
}
}
// LocalStorage Init
@@ -124,6 +399,8 @@ class StorageClass {
this.node = node;
this.name = node.name;
this.workflowStateManager = WorkflowStateManager.getInstance(debug);
this.settings_painter_node_default = {
undo_history: [],
redo_history: [],
@@ -152,10 +429,154 @@ class StorageClass {
},
};
this.settings_painter_node_all = {};
this.settings_painter_node = JSON.parse(
JSON.stringify(this.settings_painter_node_default)
);
}
getSettingsPainterNode() {
return this.settings_painter_node;
}
async getData(showModal = true) {
// Get settings node
if (showModal) {
const parent = this.node.painter.panelPaintBoxRight;
const message = createWindowModal({
...THEMES_MODAL_WINDOW.warning,
textTitle: "Loading",
textBody: [
makeElement("div", {
innerHTML:
"Please wait, <span style='font-weight: bold; color: orange'>Painter node</span> settings are loading. Loading times may take a long time if large images have been added to the canvas!",
}),
],
options: {
auto: {
autohide: true,
autoshow: true,
autoremove: true,
autoremove: true,
timewait: 500,
propStyles: { opacity: 0 },
propPreStyles: { display: "flex" },
},
parent,
},
});
}
this.settings_painter_node_all = await this.workflowStateManager.loadData();
// -- end - Get settings node
if (
!this.settings_painter_node_all ||
isEmptyObject(this.settings_painter_node_all)
) {
this.settings_painter_node_all = {
painters_data: { [this.name]: {} },
};
this.settings_painter_node = {};
}
// --- Realize save copy before change workflow. Dev....
if (this.workflowStateManager.newSave) {
const json_data = await this.workflowStateManager.loadingAllDataJSON();
let previousData = json_data.filter(
(d) =>
d.name === `Painter_${this.workflowStateManager.previousWorkflow}`
);
previousData = previousData.length ? previousData[0].value : null;
if (
previousData &&
this.workflowStateManager.currentWorkflow !==
this.workflowStateManager.previousWorkflow
) {
this.settings_painter_node_all.painters_data = JSON.parse(
JSON.stringify(previousData.painters_data)
);
console.log(
`Copy painters data '${Object.keys(previousData.painters_data).join(
","
)}' from previous workflow '${
this.workflowStateManager.previousWorkflow
}' to '${this.workflowStateManager.currentWorkflow}' completed!`
);
}
}
// --- end - Realize save copy before change workflow. Dev....
if (
!this.settings_painter_node_all.painters_data.hasOwnProperty(this.name)
) {
this.settings_painter_node_all.painters_data[this.name] = {};
}
if (
this.settings_painter_node_all.painters_data[this.name] &&
isEmptyObject(this.settings_painter_node_all.painters_data[this.name])
) {
this.settings_painter_node_all.painters_data[this.name] = JSON.parse(
JSON.stringify(this.settings_painter_node_default)
);
}
this.settings_painter_node =
this.settings_painter_node_all.painters_data[this.name];
this.workflowStateManager.countLoaded += 1;
if (
this.workflowStateManager.newSave &&
this.workflowStateManager.countLoaded ===
this.workflowStateManager.nodeStores.length
) {
this.workflowStateManager.newSave = false;
this.workflowStateManager.countLoaded = 0;
}
return this.settings_painter_node;
}
// Write settings in the file json
async saveData(workflowName = null) {
try {
let savedData = await this.workflowStateManager.loadData(workflowName);
if (Object.keys(savedData).length > 0) {
savedData.painters_data[this.name] = JSON.parse(
JSON.stringify(this.settings_painter_node)
);
} else {
savedData = this.settings_painter_node_all;
}
const formData = new FormData();
formData.append(
"name",
`Painter_${workflowName ?? this.workflowStateManager.currentWorkflow}`
);
formData.append(
"data",
new Blob([JSON.stringify(savedData)], {
type: "application/json",
})
);
const rawResponse = await fetch("/alekpet/save_node_settings", {
method: "POST",
body: formData,
});
if (rawResponse.status !== 200) {
throw new Error(
`Error painter save file settings ${rawResponse.statusText}`
);
}
} catch (e) {
console.log(e);
}
}
}
export {
@@ -166,5 +587,5 @@ export {
HsvToRgb,
StorageClass,
formatBytes,
createPainterAssetId,
WorkflowStateManager,
};
@@ -1,294 +0,0 @@
import { api } from "../../../../scripts/api.js";
import { app } from "../../../../scripts/app.js";
import { fabric } from "./fabric.js";
import { formatBytes, createPainterAssetId } from "./helpers.js";
import { comfyuiDesktopConfirm } from "../../utils.js";
const ASSET_VERSION = 2;
function assetRef(assetId) {
return { version: ASSET_VERSION, asset_id: assetId };
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function waitForPainter(node, timeout = 5000) {
return new Promise((resolve) => {
const started = Date.now();
const tick = () => {
if (node.painter?.canvas) return resolve(node.painter);
if (Date.now() - started >= timeout) return resolve(null);
setTimeout(tick, 50);
};
tick();
});
}
export class PainterAssetDialog {
constructor() {
this.overlay = null;
this.items = null;
}
show() {
if (this.overlay) return;
this.overlay = document.createElement("div");
this.overlay.className = "alekpet_painter_storage_overlay";
const box = document.createElement("div");
box.className = "alekpet_painter_storage_box";
const header = document.createElement("div");
header.className = "alekpet_painter_storage_header";
header.innerHTML = `<strong>Painter Assets</strong>`;
const close = document.createElement("button");
close.textContent = "✕";
close.onclick = () => this.close();
header.appendChild(close);
const toolbar = document.createElement("div");
toolbar.className = "alekpet_painter_storage_toolbar";
const refresh = document.createElement("button");
refresh.textContent = "Refresh";
refresh.onclick = () => this.load();
toolbar.appendChild(refresh);
this.items = document.createElement("div");
this.items.className = "alekpet_painter_storage_items";
box.append(header, toolbar, this.items);
this.overlay.appendChild(box);
this.overlay.addEventListener("click", (event) => {
if (event.target === this.overlay) this.close();
});
document.body.appendChild(this.overlay);
this.load();
}
close() {
this.overlay?.remove();
this.overlay = null;
}
async load() {
if (!this.items) return;
this.items.innerHTML =
"<div class='alekpet_painter_storage_item'>Loading...</div>";
try {
const response = await api.fetchApi("/alekpet/painter_assets");
if (!response.ok)
throw new Error(`${response.status} ${response.statusText}`);
const result = await response.json();
const assets = Array.isArray(result?.assets) ? result.assets : [];
this.items.innerHTML = "";
if (!assets.length) {
this.items.innerHTML =
"<div class='alekpet_painter_storage_item'>No Painter assets</div>";
return;
}
assets.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
for (const asset of assets) this.renderAsset(asset);
} catch (error) {
this.items.innerHTML = `<div class='alekpet_painter_storage_item' style='text-align:left;color:var(--error-text,#f66);'>${escapeHtml(error.message)}</div>`;
console.error("[PainterAssetDialog] Failed to load assets:", error);
}
}
renderAsset(asset) {
const card = document.createElement("div");
card.className = "alekpet_painter_storage_item_card";
// Preview canvas saved data
const previewWrap = document.createElement("div");
previewWrap.className = "alekpet_painter_storage_item_preview_wrapper";
const canvasEl = document.createElement("canvas");
canvasEl.style.maxWidth = "100%";
canvasEl.style.maxHeight = "100%";
previewWrap.appendChild(canvasEl);
// Metadata
const meta = document.createElement("div");
meta.className = "alekpet_painter_storage_item_meta";
const id = document.createElement("div");
id.innerHTML = `Id: <span>${asset.asset_id}</span>`;
id.title = `Unique id: ${asset.asset_id}`;
id.className =
"alekpet_painter_storage_item_skew_box alekpet_painter_storage_item_id";
const size = document.createElement("div");
size.title = "Data size · Pixel resolution";
size.innerHTML = `Size: <span>${formatBytes(asset.size)}</span> · WxH: <span>${asset.width && asset.height ? `${asset.width}×${asset.height}` : ""}</span>`;
size.className =
"alekpet_painter_storage_item_skew_box alekpet_painter_storage_item_size";
const workflow_name = document.createElement("div");
workflow_name.innerHTML = `Workflow: <span>${asset.workflow_name}</span>`;
workflow_name.title = `Last used workflow: ${asset.workflow_name}`;
workflow_name.className =
"alekpet_painter_storage_item_skew_box alekpet_painter_storage_item_workflow_name";
const created_date = new Date(asset.created_at).toLocaleString();
const file_date_created = document.createElement("div");
file_date_created.title = "Date create at";
file_date_created.innerHTML = `Create at: <span>${created_date}</span>`;
file_date_created.className =
"alekpet_painter_storage_item_skew_box alekpet_painter_storage_item_created_date";
const updated_date = new Date(asset.updated_at).toLocaleString();
const file_date_updated = document.createElement("div");
file_date_updated.title = "Date update at";
file_date_updated.innerHTML = `Update at: <span>${updated_date}</span>`;
file_date_updated.className =
"alekpet_painter_storage_item_skew_box alekpet_painter_storage_item_updated_date";
// Buttons panel
const buttons = document.createElement("div");
Object.assign(buttons.style, { display: "flex", gap: "6px" });
const add = document.createElement("button");
add.textContent = "➕ Add as copy";
add.title = "Add to workflow as copy.";
add.onclick = () => this.addToWorkflow(asset.asset_id);
const addRef = document.createElement("button");
addRef.textContent = "🔗 Add as reference";
addRef.title =
"Add to workflow as reference. Attention: the change will affect all copies in workflows!";
addRef.onclick = () => this.addToWorkflow(asset.asset_id, true);
const remove = document.createElement("button");
remove.textContent = "🗑️";
remove.title = "Remove canvas data";
remove.className = "alekpet_painter_storage_button_close";
remove.onclick = () => this.deleteAsset(asset.asset_id, card);
buttons.append(add, addRef, remove);
meta.append(id, workflow_name, size, file_date_created, file_date_updated);
card.append(previewWrap, id, meta, buttons);
this.items.appendChild(card);
this.loadPreview(asset.asset_id, canvasEl).catch((error) => {
console.warn("[PainterAssetDialog] Preview failed:", error);
});
}
async loadPreview(assetId, canvasEl) {
const response = await api.fetchApi(
`/alekpet/painter_asset/${encodeURIComponent(assetId)}`
);
if (!response.ok)
throw new Error(`${response.status} ${response.statusText}`);
const state = (await response.json())?.state;
if (!state?.canvas_settings) return;
const size = state.settings?.currentCanvasSize || {
width: 512,
height: 512,
};
const scale = Math.min(1, 170 / Math.max(size.width, size.height));
const width = Math.max(1, Math.round(size.width * scale));
const height = Math.max(1, Math.round(size.height * scale));
canvasEl.width = width;
canvasEl.height = height;
const preview = new fabric.StaticCanvas(canvasEl, {
width,
height,
backgroundColor: state.canvas_settings.background || "#000000",
});
await new Promise((resolve) => {
preview.loadFromJSON(state.canvas_settings, () => {
preview.setDimensions({ width, height });
preview.setViewportTransform([scale, 0, 0, scale, 0, 0]);
preview.renderAll();
resolve();
});
});
}
async addToWorkflow(assetId, persist = false) {
const LiteGraph = globalThis.LiteGraph;
if (!LiteGraph) {
console.error("[PainterAssetDialog] LiteGraph is unavailable");
return;
}
const node = LiteGraph.createNode("PainterNode");
if (!node) return;
const mouse = app.canvas?.graph_mouse;
const selected = app.canvas?.selected_nodes
? Object.values(app.canvas.selected_nodes)[0]
: null;
node.pos =
mouse?.length === 2
? [mouse[0], mouse[1]]
: selected?.pos
? [selected.pos[0] + 40, selected.pos[1] + 40]
: [100, 100];
app.graph.add(node);
const painter = await waitForPainter(node);
if (!painter) {
console.error(
"[PainterAssetDialog] PainterNode initialization timed out"
);
return;
}
const painterIndex = node.widgets?.findIndex?.(
(widget) => widget.type === "painter_node_alekpet"
);
if (painterIndex < 0) return;
await painter.loadCanvasData(assetRef(assetId), painterIndex, {
migrateImage: false,
persist,
});
if (!persist) {
painter.node.painterAsset.asset_id = createPainterAssetId();
await painter.persistPainterState();
}
app.graph.setDirtyCanvas(true, false);
}
async deleteAsset(assetId, card) {
if (!(await comfyuiDesktopConfirm(`Delete Painter asset ${assetId}?`)))
return;
try {
const response = await api.fetchApi(
`/alekpet/painter_asset/${encodeURIComponent(assetId)}`,
{ method: "DELETE" }
);
if (!response.ok)
throw new Error(`${response.status} ${response.statusText}`);
card.remove();
if (!this.items.children.length) {
this.items.innerHTML =
"<div class='alekpet_painter_storage_item'>No Painter assets</div>";
}
} catch (error) {
console.error("[PainterAssetDialog] Failed to delete asset:", error);
alert(`Failed to delete Painter asset: ${error.message}`);
}
}
}
+106 -187
View File
@@ -13,9 +13,8 @@ import {
toRGBA,
getColorHEX,
StorageClass,
createPainterAssetId,
} from "./lib/painternode/helpers.js";
import { PainterAssetDialog } from "./lib/painternode/painter_asset_dialog.js";
import { PainterStorageDialog } from "./lib/painternode/dialogs.js";
import { addStylesheet } from "../../scripts/utils.js";
import {
showHide,
@@ -36,17 +35,12 @@ import { MyPaintManager } from "./lib/painternode/manager_mypaint.js";
const DEBUG = false;
const extensionName = "alekpet.PainterNode";
const CLONE_CACHE = new Map();
const PAINTER_ASSET_VERSION = 2;
function isPainterAssetRef(value) {
return (
value &&
typeof value === "object" &&
value.version === PAINTER_ASSET_VERSION &&
typeof value.asset_id === "string" &&
value.asset_id.length > 0
);
}
// Save settings in JSON file on the extension folder [big data settings includes images] if true else localStorage
let painters_settings_json = JSON.parse(
localStorage.getItem(`${extensionName}.SaveSettingsJson`, false)
);
//
const removeIcon =
"data:image/svg+xml,%3Csvg version='1.1' id='Ebene_1' x='0px' y='0px' width='595.275px' height='595.275px' viewBox='200 215 230 470' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3C/defs%3E%3Crect x='125.3' y='264.6' width='350.378' height='349.569' style='fill: rgb(237, 0, 0); stroke: rgb(197, 2, 2);' rx='58.194' ry='58.194'%3E%3C/rect%3E%3Cg%3E%3Crect x='267.162' y='307.978' transform='matrix(0.7071 -0.7071 0.7071 0.7071 -222.6202 340.6915)' style='fill:white;' width='65.545' height='262.18' rx='32.772' ry='32.772'%3E%3C/rect%3E%3Crect x='266.988' y='308.153' transform='matrix(0.7071 0.7071 -0.7071 0.7071 398.3889 -83.3116)' style='fill:white;' width='65.544' height='262.179' rx='32.772' ry='32.772'%3E%3C/rect%3E%3C/g%3E%3C/svg%3E";
@@ -118,16 +112,6 @@ async function getLoadedFonts() {
});
});
}
function getActiveWorkflowName() {
const workflow = app?.extensionManager?.workflow?.activeWorkflow;
return (
workflow?.filename?.replace(/\.json$/i, "") ||
workflow?.key ||
"Unsaved Workflow"
);
}
// ================= END FUNCTIONS ================
// ================= CLASS PAINTER ================
@@ -156,11 +140,6 @@ class Painter {
this.redo_history = [];
this.storageCls = this.node.storageCls;
this.node.painterAsset = this.node.painterAsset || {
version: PAINTER_ASSET_VERSION,
asset_id: createPainterAssetId(),
};
this._persistPromise = Promise.resolve();
// [Crop] Object
this.crop_object = null;
@@ -226,12 +205,10 @@ class Painter {
async saveSettingsPainterNode() {
this.canvasSaveSettingsPainter();
const persistPromise = this.persistPainterState();
// Save data
app?.extensionManager?.workflow?.activeWorkflow?.changeTracker?.captureCanvasState();
await persistPromise;
if (painters_settings_json) await this.node.storageCls.saveData();
}
initCanvas(canvasEl) {
@@ -2483,86 +2460,6 @@ class Painter {
} catch (e) {}
}
getWorkflowValue() {
return {
version: PAINTER_ASSET_VERSION,
asset_id: this.node.painterAsset.asset_id,
};
}
async persistPainterState(state = null) {
const save_data =
this.storageCls.settings_painter_node.settings.saveImage ?? true;
if (!save_data) return;
const nextState = state || {
...this.storageCls.settings_painter_node,
canvas_settings: this.canvas.toJSON(["mypaintlib"]),
};
const request = async () => {
const response = await api.fetchApi(
`/alekpet/painter_asset/${encodeURIComponent(
this.node.painterAsset.asset_id
)}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
state: nextState,
workflow_name: getActiveWorkflowName(),
}),
}
);
if (!response.ok) {
throw new Error(
`Painter asset save failed: ${response.status} ${response.statusText}`
);
}
return response.json();
};
// Serialize writes
this._persistPromise = this._persistPromise.catch(() => {}).then(request);
try {
return await this._persistPromise;
} catch (error) {
console.error("[PainterNode] Failed to save backend asset:", error);
return null;
}
}
async loadPainterAsset(assetId) {
const response = await api.fetchApi(
`/alekpet/painter_asset/${encodeURIComponent(assetId)}`
);
if (response.status === 404) {
app.extensionManager.toast.add({
severity: "warn",
summary: "Warning",
detail: `Painter asset '${assetId}' load failed: ${response.status} ${response.statusText}`,
life: 8000,
});
return null;
}
if (!response.ok) {
throw new Error(
`Painter asset load failed: ${response.status} ${response.statusText}`
);
}
const result = await response.json();
return result?.state ?? null;
}
undoRedoLoadData(data) {
this.canvas.loadFromJSON(data, () => {
this.canvas.renderAll();
@@ -2893,41 +2790,21 @@ class Painter {
// - end
}
async loadCanvasData(data, painter_idx, options = { persist: true }) {
// New workflows store only {version, asset_id}
if (isPainterAssetRef(data)) {
this.node.painterAsset = {
version: PAINTER_ASSET_VERSION,
asset_id: options.assetId || data.asset_id,
};
async loadCanvasData(data, painter_idx) {
if (painters_settings_json && data === null) {
data = await this.storageCls.getData();
let backendState = null;
try {
backendState = await this.loadPainterAsset(
this.node.painterAsset.asset_id
);
} catch (error) {
console.error("[PainterNode] Failed to load backend asset:", error);
}
if (backendState) {
data = backendState;
} else {
// The workflow may reference an asset that was deleted
if (!data) {
data = JSON.parse(
JSON.stringify(this.storageCls.settings_painter_node_default)
);
}
} else if (data && data.canvas_settings) {
// Legacy workflow migration
this.node.painterAsset = {
version: PAINTER_ASSET_VERSION,
asset_id: createPainterAssetId(),
};
this.storageCls.settings_painter_node = data;
}
if (data) {
this.storageCls.settings_painter_node = data;
Object.assign(this.node.widgets[painter_idx].value, data);
if (data?.settings) {
const {
@@ -2950,22 +2827,14 @@ class Painter {
// Loading canvas data
if (data?.canvas_settings) {
await this.canvasLoadSettingPainter(data).then((result) => {
this.canvasLoadSettingPainter(data).then((result) => {
if (result) {
this.canvas.renderAll();
this.uploadPaintFile(this.name);
}
});
}
// Migrate legacy workflow data, or repair a missing backend asset
if (options.persist !== false) {
await this.persistPainterState(data);
}
if (data?.canvas_settings && options.migrateImage !== false) {
await this.uploadPaintFile(this.node.name);
}
this.node.setSize(this.node.size);
app.graph.setDirtyCanvas(true, false);
}
@@ -3002,26 +2871,10 @@ function PainterWidget(node, inputName, inputData, app) {
wrapperPainter,
{
setValue(v) {
if (isPainterAssetRef(v)) {
node.painter.node.painterAsset = {
version: PAINTER_ASSET_VERSION,
asset_id: v.asset_id,
};
return;
}
// Legacy workflows are loaded asynchronously by onConfigure
if (v?.canvas_settings) {
node.painter.node._legacyPainterData = v;
return;
}
if (v) {
node.painter.storageCls.settings_painter_node = v;
}
node.painter.storageCls.settings_painter_node = v;
},
getValue() {
return node.painter.getWorkflowValue();
return node.painter.storageCls.settings_painter_node;
},
}
);
@@ -3109,11 +2962,8 @@ function PainterWidget(node, inputName, inputData, app) {
// Node serialize
node.onSerialize = (n) => {
const painterIndex = n.widgets_values?.findIndex?.(
(_, index) => node.widgets[index]?.type === "painter_node_alekpet"
);
if (painterIndex >= 0) {
n.widgets_values[painterIndex] = node.painter.getWorkflowValue();
if (painters_settings_json) {
n.widgets_values[3] = null;
}
};
@@ -3246,6 +3096,8 @@ function PainterWidget(node, inputName, inputData, app) {
// ================= CREATE EXTENSION ================
let setEventsPromise = null;
app.registerExtension({
name: extensionName,
async init(app) {
@@ -3257,7 +3109,7 @@ app.registerExtension({
// Managing data
app.ui.settings.addSetting({
id: `${extensionName}.ManagingData`,
name: "🔸 Managing JSON data assets",
name: "🔸 Managing JSON data storage",
defaultValue: false,
type: (name, sett, val) => {
return makeElement("tr", {
@@ -3265,13 +3117,12 @@ app.registerExtension({
makeElement("td", {
children: [
makeElement("button", {
textContent: "Painter Assets",
textContent: "Managing Data",
onclick: () => {
new PainterAssetDialog().show();
new PainterStorageDialog().show();
},
style: {
display: "block",
marginTop: "5px",
},
}),
],
@@ -3280,6 +3131,21 @@ app.registerExtension({
});
},
});
// Add settings params painter node
app.ui.settings.addSetting({
id: `${extensionName}.SaveSettingsJson`,
name: "🔸 Save settings to JSON file (BETA)",
defaultValue: false,
type: "boolean",
onChange: (e) => {
painters_settings_json = !!e;
localStorage.setItem(
`${extensionName}.SaveSettingsJson`,
painters_settings_json
);
},
});
// end -- Settings
},
async setup(app) {},
@@ -3328,6 +3194,10 @@ app.registerExtension({
this.title = `${this.type} - ${this.painter.storageCls.settings_painter_node.settings.currentCanvasSize.width}x${this.painter.storageCls.settings_painter_node.settings.currentCanvasSize.height}`;
if (!painters_settings_json && !this?.widgets_values) {
this.painter.uploadPaintFile(nodeNamePNG);
}
// Resize window
window.addEventListener("resize", (e) => resizeCanvas(this), false);
return r;
@@ -3338,11 +3208,10 @@ app.registerExtension({
const cloneNode = origClone?.apply(this, arguments);
if (!cloneNode) return origClone;
CLONE_CACHE.set(this.id, {
state: structuredClone(this.painter.storageCls.settings_painter_node),
assetId: createPainterAssetId(),
});
CLONE_CACHE.set(
this.id,
structuredClone(this.painter.storageCls.settings_painter_node)
);
return cloneNode;
};
@@ -3352,6 +3221,34 @@ app.registerExtension({
await this.getTitle();
if (painters_settings_json) {
if (this.storageCls.workflowStateManager.currentWorkflow) {
DEBUG &&
console.log(
`⚠️ [PainterNode] currentWorkflow is already set, skip: ${this.name} -> ${this.storageCls.workflowStateManager.currentWorkflow}`
);
}
// Если `setEvents` уже выполняется, ждем его завершения
if (setEventsPromise) {
DEBUG &&
console.log(
`⏳ [PainterNode] Waiting for setEvents to complete...`
);
await setEventsPromise;
DEBUG &&
console.log(`✅ [PainterNode] setEvents completed, continue`);
} else {
// Если это первый узел, запускаем `setEvents`
DEBUG &&
console.log(`🚀 [PainterNode] The first node calls setEvents...`);
setEventsPromise = this.storageCls.workflowStateManager.setEvents();
await setEventsPromise;
DEBUG && console.log(`✅ [PainterNode] setEvents completed`);
}
}
setTimeout(async () => {
console.log(`🔧 Configure PainterNode: ${this.name}`);
@@ -3361,24 +3258,19 @@ app.registerExtension({
if (painter_idx < 0) return;
let data = widget.widgets_values[painter_idx];
let cloneState = null;
if (!data) {
for (const [originalId, cached] of CLONE_CACHE.entries()) {
for (const [originalId, cachedSettings] of CLONE_CACHE.entries()) {
console.log(
`♻️ Recovering cloned data from node ${originalId} to node ${this.id}`
);
cloneState = cached.state;
data = cloneState;
data = cachedSettings;
CLONE_CACHE.delete(originalId);
break;
}
}
await this.painter.loadCanvasData(data, painter_idx, {
assetId: cloneState ? createPainterAssetId() : undefined,
migrateImage: true,
});
this.painter.loadCanvasData(data, painter_idx);
});
};
@@ -3413,6 +3305,33 @@ app.registerExtension({
},
});
}
setTimeout(() => {
const removeIndex = options.findIndex((m) => m?.content === "Remove"),
removeButton = options[removeIndex];
if (!!removeButton) {
const remove_callback = removeButton.callback;
const self = this;
removeButton.callback = async function () {
const nodeName = Array.from(arguments).find((f) => f?.name).name;
remove_callback.apply(this, arguments);
if (!painters_settings_json) return;
if (
await comfyuiDesktopConfirm(
`Remove data ${nodeName} from JSON?`
)
) {
self.storageCls.workflowStateManager.removeData(null, [
nodeName,
]);
}
};
}
}, 0);
};
// end - ExtraMenuOptions
}