Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9a2ad42d1 | ||
|
|
1169fe83fe | ||
|
|
add7a5dca2 | ||
|
|
2ee14a8708 | ||
|
|
b334b11ad1 | ||
|
|
f037c4a4e9 | ||
|
|
8502b20091 |
+1
-2
@@ -1,3 +1,2 @@
|
||||
__pycache__
|
||||
settings_nodes/
|
||||
web_alekpet_nodes/lib/idenode/**/*.json
|
||||
web_alekpet_nodes/lib/idenode/**/*.json
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
settings_nodes.json
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
## 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))
|
||||
@@ -21,6 +23,27 @@
|
||||
- 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
|
||||
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
"""
|
||||
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)
|
||||
+3
-300
@@ -1,6 +1,5 @@
|
||||
import hashlib
|
||||
import os
|
||||
import json
|
||||
from server import PromptServer
|
||||
from aiohttp import web
|
||||
import base64
|
||||
@@ -9,306 +8,10 @@ import time
|
||||
from PIL import Image, ImageOps, ImageSequence
|
||||
import torch
|
||||
import numpy as np
|
||||
import glob
|
||||
|
||||
import folder_paths
|
||||
import node_helpers
|
||||
|
||||
# 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
|
||||
)
|
||||
from . import painter_asset_storage
|
||||
|
||||
|
||||
# Piping image
|
||||
@@ -399,7 +102,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:
|
||||
@@ -407,7 +110,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 = []
|
||||
|
||||
+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.1.11"
|
||||
version = "1.2.0"
|
||||
license = { file = "LICENSE" }
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -462,79 +462,146 @@
|
||||
/* -- end Styles Settigs Kistey -- */
|
||||
|
||||
/* Settings store managing data */
|
||||
.painter_storage_box {
|
||||
width: 30vw;
|
||||
.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;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
color: var(--primary-fg);
|
||||
}
|
||||
|
||||
.painter_storage_header {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
background: var(--content-bg);
|
||||
padding: 3px;
|
||||
background: var(--comfy-menu-bg, #202020);
|
||||
color: var(--input-text, #ddd);
|
||||
border: 1px solid var(--border-color, #555);
|
||||
border-radius: 8px;
|
||||
align-items: center;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.painter_storage_body {
|
||||
.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;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.painter_storage_items_box {
|
||||
.alekpet_painter_storage_item_card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--border-color, #444);
|
||||
border-radius: 6px;
|
||||
background: var(--comfy-input-bg, #181818);
|
||||
}
|
||||
|
||||
.painter_storage_workflows_item {
|
||||
.alekpet_painter_storage_item_preview_wrapper {
|
||||
height: 180px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.painter_storage_workflows_item:hover {
|
||||
background: var(--primary-hover-bg) !important;
|
||||
.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_grid {
|
||||
display: grid;
|
||||
grid-template-columns: 0.2fr 0.2fr repeat(3, 1fr);
|
||||
justify-content: center;
|
||||
place-items: center;
|
||||
.alekpet_painter_storage_item_meta {
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.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%;
|
||||
.alekpet_painter_storage_item_id {
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
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 */
|
||||
|
||||
@@ -1,485 +0,0 @@
|
||||
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,13 +1,17 @@
|
||||
import { api } from "../../../../scripts/api.js";
|
||||
import { app } from "../../../../scripts/app.js";
|
||||
import { fabric } from "./fabric.js";
|
||||
import {
|
||||
makeElement,
|
||||
createWindowModal,
|
||||
isEmptyObject,
|
||||
THEMES_MODAL_WINDOW,
|
||||
comfyuiDesktopConfirm,
|
||||
} from "../../utils.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);
|
||||
});
|
||||
}
|
||||
|
||||
// RGB, HSV, and HSL color conversion algorithms in JavaScript https://gist.github.com/mjackson/5311256
|
||||
function rgbToHsv(r, g, b) {
|
||||
@@ -96,293 +100,14 @@ function HsvToRgb(brush_settings) {
|
||||
return { red, green, blue };
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
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]}`;
|
||||
}
|
||||
|
||||
// LocalStorage Init
|
||||
@@ -399,8 +124,6 @@ class StorageClass {
|
||||
this.node = node;
|
||||
this.name = node.name;
|
||||
|
||||
this.workflowStateManager = WorkflowStateManager.getInstance(debug);
|
||||
|
||||
this.settings_painter_node_default = {
|
||||
undo_history: [],
|
||||
redo_history: [],
|
||||
@@ -429,154 +152,10 @@ 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 {
|
||||
@@ -587,5 +166,5 @@ export {
|
||||
HsvToRgb,
|
||||
StorageClass,
|
||||
formatBytes,
|
||||
WorkflowStateManager,
|
||||
createPainterAssetId,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
+187
-106
@@ -13,8 +13,9 @@ import {
|
||||
toRGBA,
|
||||
getColorHEX,
|
||||
StorageClass,
|
||||
createPainterAssetId,
|
||||
} from "./lib/painternode/helpers.js";
|
||||
import { PainterStorageDialog } from "./lib/painternode/dialogs.js";
|
||||
import { PainterAssetDialog } from "./lib/painternode/painter_asset_dialog.js";
|
||||
import { addStylesheet } from "../../scripts/utils.js";
|
||||
import {
|
||||
showHide,
|
||||
@@ -35,12 +36,17 @@ 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;
|
||||
|
||||
// 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)
|
||||
);
|
||||
//
|
||||
function isPainterAssetRef(value) {
|
||||
return (
|
||||
value &&
|
||||
typeof value === "object" &&
|
||||
value.version === PAINTER_ASSET_VERSION &&
|
||||
typeof value.asset_id === "string" &&
|
||||
value.asset_id.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
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";
|
||||
@@ -112,6 +118,16 @@ 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 ================
|
||||
@@ -140,6 +156,11 @@ 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;
|
||||
@@ -205,10 +226,12 @@ class Painter {
|
||||
|
||||
async saveSettingsPainterNode() {
|
||||
this.canvasSaveSettingsPainter();
|
||||
|
||||
const persistPromise = this.persistPainterState();
|
||||
// Save data
|
||||
app?.extensionManager?.workflow?.activeWorkflow?.changeTracker?.captureCanvasState();
|
||||
|
||||
if (painters_settings_json) await this.node.storageCls.saveData();
|
||||
await persistPromise;
|
||||
}
|
||||
|
||||
initCanvas(canvasEl) {
|
||||
@@ -2460,6 +2483,86 @@ 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();
|
||||
@@ -2790,21 +2893,41 @@ class Painter {
|
||||
// - end
|
||||
}
|
||||
|
||||
async loadCanvasData(data, painter_idx) {
|
||||
if (painters_settings_json && data === null) {
|
||||
data = await this.storageCls.getData();
|
||||
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,
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
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
|
||||
data = JSON.parse(
|
||||
JSON.stringify(this.storageCls.settings_painter_node_default)
|
||||
);
|
||||
}
|
||||
|
||||
this.storageCls.settings_painter_node = data;
|
||||
} else if (data && data.canvas_settings) {
|
||||
// Legacy workflow migration
|
||||
this.node.painterAsset = {
|
||||
version: PAINTER_ASSET_VERSION,
|
||||
asset_id: createPainterAssetId(),
|
||||
};
|
||||
}
|
||||
|
||||
if (data) {
|
||||
Object.assign(this.node.widgets[painter_idx].value, data);
|
||||
this.storageCls.settings_painter_node = data;
|
||||
|
||||
if (data?.settings) {
|
||||
const {
|
||||
@@ -2827,14 +2950,22 @@ class Painter {
|
||||
|
||||
// Loading canvas data
|
||||
if (data?.canvas_settings) {
|
||||
this.canvasLoadSettingPainter(data).then((result) => {
|
||||
await 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);
|
||||
}
|
||||
@@ -2871,10 +3002,26 @@ function PainterWidget(node, inputName, inputData, app) {
|
||||
wrapperPainter,
|
||||
{
|
||||
setValue(v) {
|
||||
node.painter.storageCls.settings_painter_node = 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;
|
||||
}
|
||||
},
|
||||
getValue() {
|
||||
return node.painter.storageCls.settings_painter_node;
|
||||
return node.painter.getWorkflowValue();
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -2962,8 +3109,11 @@ function PainterWidget(node, inputName, inputData, app) {
|
||||
|
||||
// Node serialize
|
||||
node.onSerialize = (n) => {
|
||||
if (painters_settings_json) {
|
||||
n.widgets_values[3] = null;
|
||||
const painterIndex = n.widgets_values?.findIndex?.(
|
||||
(_, index) => node.widgets[index]?.type === "painter_node_alekpet"
|
||||
);
|
||||
if (painterIndex >= 0) {
|
||||
n.widgets_values[painterIndex] = node.painter.getWorkflowValue();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3096,8 +3246,6 @@ function PainterWidget(node, inputName, inputData, app) {
|
||||
|
||||
// ================= CREATE EXTENSION ================
|
||||
|
||||
let setEventsPromise = null;
|
||||
|
||||
app.registerExtension({
|
||||
name: extensionName,
|
||||
async init(app) {
|
||||
@@ -3109,7 +3257,7 @@ app.registerExtension({
|
||||
// Managing data
|
||||
app.ui.settings.addSetting({
|
||||
id: `${extensionName}.ManagingData`,
|
||||
name: "🔸 Managing JSON data storage",
|
||||
name: "🔸 Managing JSON data assets",
|
||||
defaultValue: false,
|
||||
type: (name, sett, val) => {
|
||||
return makeElement("tr", {
|
||||
@@ -3117,12 +3265,13 @@ app.registerExtension({
|
||||
makeElement("td", {
|
||||
children: [
|
||||
makeElement("button", {
|
||||
textContent: "Managing Data",
|
||||
textContent: "Painter Assets",
|
||||
onclick: () => {
|
||||
new PainterStorageDialog().show();
|
||||
new PainterAssetDialog().show();
|
||||
},
|
||||
style: {
|
||||
display: "block",
|
||||
marginTop: "5px",
|
||||
},
|
||||
}),
|
||||
],
|
||||
@@ -3131,21 +3280,6 @@ 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) {},
|
||||
@@ -3194,10 +3328,6 @@ 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;
|
||||
@@ -3208,10 +3338,11 @@ app.registerExtension({
|
||||
const cloneNode = origClone?.apply(this, arguments);
|
||||
if (!cloneNode) return origClone;
|
||||
|
||||
CLONE_CACHE.set(
|
||||
this.id,
|
||||
structuredClone(this.painter.storageCls.settings_painter_node)
|
||||
);
|
||||
CLONE_CACHE.set(this.id, {
|
||||
state: structuredClone(this.painter.storageCls.settings_painter_node),
|
||||
assetId: createPainterAssetId(),
|
||||
});
|
||||
|
||||
return cloneNode;
|
||||
};
|
||||
|
||||
@@ -3221,34 +3352,6 @@ 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}`);
|
||||
|
||||
@@ -3258,19 +3361,24 @@ app.registerExtension({
|
||||
|
||||
if (painter_idx < 0) return;
|
||||
let data = widget.widgets_values[painter_idx];
|
||||
let cloneState = null;
|
||||
|
||||
if (!data) {
|
||||
for (const [originalId, cachedSettings] of CLONE_CACHE.entries()) {
|
||||
for (const [originalId, cached] of CLONE_CACHE.entries()) {
|
||||
console.log(
|
||||
`♻️ Recovering cloned data from node ${originalId} to node ${this.id}`
|
||||
);
|
||||
data = cachedSettings;
|
||||
cloneState = cached.state;
|
||||
data = cloneState;
|
||||
CLONE_CACHE.delete(originalId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.painter.loadCanvasData(data, painter_idx);
|
||||
await this.painter.loadCanvasData(data, painter_idx, {
|
||||
assetId: cloneState ? createPainterAssetId() : undefined,
|
||||
migrateImage: true,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3305,33 +3413,6 @@ 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