PainterNode update
+ Added a path in `painter_asset_storage.py` to retrieve saved assets for management. + Refactored the saved data manager. + Added the ability to insert saved canvases into the current workflow in PainterNode. + Added a button to permanently delete saved canvas data. + Added new style classes and removed unused ones. + Cleaned up the code. + Updated the version in `pyproject.toml`.
This commit is contained in:
@@ -7,6 +7,7 @@ Workflow files contain only a small asset reference. The Fabric.js canvas JSON
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from aiohttp import web
|
||||
@@ -42,11 +43,51 @@ def _json_response_error(message, status=400):
|
||||
return web.json_response({"success": False, "error": message}, status=status)
|
||||
|
||||
|
||||
@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
|
||||
|
||||
stat = os.stat(state_path)
|
||||
width = height = None
|
||||
try:
|
||||
with open(state_path, "r", encoding="utf-8") as f:
|
||||
state = json.load(f)
|
||||
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": stat.st_size,
|
||||
"updated_at": stat.st_mtime,
|
||||
"width": width,
|
||||
"height": height,
|
||||
}
|
||||
)
|
||||
|
||||
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_dir = _asset_dir(request.match_info["asset_id"])
|
||||
state_path = os.path.join(asset_dir, "state.json")
|
||||
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)
|
||||
@@ -57,7 +98,7 @@ async def get_painter_asset(request):
|
||||
return web.json_response(
|
||||
{
|
||||
"success": True,
|
||||
"asset_id": request.match_info["asset_id"],
|
||||
"asset_id": asset_id,
|
||||
"state": state,
|
||||
}
|
||||
)
|
||||
@@ -116,8 +157,6 @@ async def delete_painter_asset(request):
|
||||
try:
|
||||
asset_dir = _asset_dir(request.match_info["asset_id"])
|
||||
if os.path.isdir(asset_dir):
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(asset_dir)
|
||||
|
||||
return web.json_response({"success": True})
|
||||
|
||||
+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,80 @@
|
||||
/* -- end Styles Settigs Kistey -- */
|
||||
|
||||
/* Settings store managing data */
|
||||
.painter_storage_box {
|
||||
width: 30vw;
|
||||
.alekpet_painter_storage_overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10000;
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
align-items: stretch;
|
||||
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;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.painter_storage_workflows_item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.painter_storage_workflows_item:hover {
|
||||
background: var(--primary-hover-bg) !important;
|
||||
}
|
||||
|
||||
.painter_storage_grid {
|
||||
display: grid;
|
||||
grid-template-columns: 0.2fr 0.2fr repeat(3, 1fr);
|
||||
justify-content: center;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.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_text {
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* -- 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,16 +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]}`;
|
||||
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
|
||||
@@ -164,4 +166,5 @@ export {
|
||||
HsvToRgb,
|
||||
StorageClass,
|
||||
formatBytes,
|
||||
createPainterAssetId,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
import { api } from "../../../../scripts/api.js";
|
||||
import { app } from "../../../../scripts/app.js";
|
||||
import { fabric } from "./fabric.js";
|
||||
import { formatBytes, createPainterAssetId } from "./helpers.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";
|
||||
|
||||
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);
|
||||
|
||||
const id = document.createElement("div");
|
||||
id.textContent = asset.asset_id;
|
||||
id.title = asset.asset_id;
|
||||
id.className = "alekpet_painter_storage_item_id_text";
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.textContent = `${formatBytes(asset.size)}${asset.width && asset.height ? ` · ${asset.width}×${asset.height}` : ""}`;
|
||||
meta.style.fontSize = "11px";
|
||||
|
||||
const buttons = document.createElement("div");
|
||||
Object.assign(buttons.style, { display: "flex", gap: "6px" });
|
||||
|
||||
const add = document.createElement("button");
|
||||
add.textContent = "➕ Add to scene";
|
||||
add.onclick = () => this.addToScene(asset.asset_id);
|
||||
|
||||
const remove = document.createElement("button");
|
||||
remove.textContent = "🗑 Delete";
|
||||
remove.onclick = () => this.deleteAsset(asset.asset_id, card);
|
||||
|
||||
buttons.append(add, remove);
|
||||
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 addToScene(assetId) {
|
||||
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,
|
||||
});
|
||||
|
||||
painter.node.painterAsset.asset_id = createPainterAssetId();
|
||||
await painter.persistPainterState();
|
||||
|
||||
app.graph.setDirtyCanvas(true, false);
|
||||
}
|
||||
|
||||
async deleteAsset(assetId, card) {
|
||||
if (!confirm(`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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
@@ -37,19 +38,6 @@ const extensionName = "alekpet.PainterNode";
|
||||
const CLONE_CACHE = new Map();
|
||||
const PAINTER_ASSET_VERSION = 2;
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
function isPainterAssetRef(value) {
|
||||
return (
|
||||
value &&
|
||||
@@ -3257,12 +3245,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",
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user