Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d921b8398c | ||
|
|
20aff9bac2 | ||
|
|
fe975c0890 | ||
|
|
b1ddb4a866 | ||
|
|
4d06e3776a | ||
|
|
80c2bd07ef | ||
|
|
c1abde90e4 | ||
|
|
9cb658b44f | ||
|
|
4727a4f72d | ||
|
|
7248a21331 | ||
|
|
86c4b1570f | ||
|
|
2d8eb55a7b | ||
|
|
ea03d73b69 | ||
|
|
898ac8a18e | ||
|
|
acbece304f | ||
|
|
ef526aae54 | ||
|
|
d0c7519d55 | ||
|
|
1492788de5 | ||
|
|
cf87b95a53 | ||
|
|
dbabf9c13f | ||
|
|
5a939af67a | ||
|
|
23df17061b | ||
|
|
046be62f60 | ||
|
|
fa894d702f | ||
|
|
9bc65a7786 | ||
|
|
7feacf442b | ||
|
|
5a1ace9343 | ||
|
|
d03493a490 | ||
|
|
e3b2f0aa33 | ||
|
|
96b5e8810e | ||
|
|
e19a7f1717 | ||
|
|
97095c6705 | ||
|
|
510f971329 | ||
|
|
37b187863f | ||
|
|
f82ff6c96a | ||
|
|
256d9f9b3f | ||
|
|
fca1ad7793 | ||
|
|
73d0b410c7 | ||
|
|
24f8f272e4 | ||
|
|
d7b9b1eec1 | ||
|
|
67179e5586 | ||
|
|
f4c679d2c5 | ||
|
|
962a4e897e | ||
|
|
31ebeb70df | ||
|
|
d2581cc75a | ||
|
|
1e7772284c | ||
|
|
44bc31989f | ||
|
|
fd5c6463e8 | ||
|
|
d27c68decf | ||
|
|
f262c39266 | ||
|
|
a5c4798b09 |
@@ -1,2 +1,4 @@
|
||||
__pycache__
|
||||
settings_nodes/
|
||||
config.json
|
||||
web_alekpet_nodes/lib/idenode/**/*.json
|
||||
|
||||
@@ -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/
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
settings_nodes.json
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user