From f037c4a4e96fec21796f7d1810481307a255b06b Mon Sep 17 00:00:00 2001 From: AlekPet Date: Sun, 27 Sep 2026 15:42:01 +0300 Subject: [PATCH] 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`. --- PainterNode/painter_asset_storage.py | 49 +- pyproject.toml | 2 +- .../css/painternode/painter_node_styles.css | 119 ++--- web_alekpet_nodes/lib/painternode/dialogs.js | 485 ------------------ web_alekpet_nodes/lib/painternode/helpers.js | 41 +- .../lib/painternode/painter_asset_dialog.js | 250 +++++++++ web_alekpet_nodes/painter_node.js | 21 +- 7 files changed, 382 insertions(+), 585 deletions(-) delete mode 100644 web_alekpet_nodes/lib/painternode/dialogs.js create mode 100644 web_alekpet_nodes/lib/painternode/painter_asset_dialog.js diff --git a/PainterNode/painter_asset_storage.py b/PainterNode/painter_asset_storage.py index ef7216e..e981bce 100644 --- a/PainterNode/painter_asset_storage.py +++ b/PainterNode/painter_asset_storage.py @@ -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}) diff --git a/pyproject.toml b/pyproject.toml index 18e0790..24ccbd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] diff --git a/web_alekpet_nodes/css/painternode/painter_node_styles.css b/web_alekpet_nodes/css/painternode/painter_node_styles.css index 7ff5d2d..8883864 100644 --- a/web_alekpet_nodes/css/painternode/painter_node_styles.css +++ b/web_alekpet_nodes/css/painternode/painter_node_styles.css @@ -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 */ diff --git a/web_alekpet_nodes/lib/painternode/dialogs.js b/web_alekpet_nodes/lib/painternode/dialogs.js deleted file mode 100644 index 6ac92c9..0000000 --- a/web_alekpet_nodes/lib/painternode/dialogs.js +++ /dev/null @@ -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 = `
Painter name "${painter_name}" deleted from json file workflow "${workflow_name}"!
`; - - if ( - result.remove_info.workflow_record && - result.remove_info.paint_record - ) { - textMessage = `
Workflow name "${workflow_name}" and record "${painter_name}" deleted from json file!
`; - } - - 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: `
Workflow name "${workflow_name}" deleted from json files!
`, - 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: "JSON 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(); - } -} diff --git a/web_alekpet_nodes/lib/painternode/helpers.js b/web_alekpet_nodes/lib/painternode/helpers.js index f6901e3..da1049b 100644 --- a/web_alekpet_nodes/lib/painternode/helpers.js +++ b/web_alekpet_nodes/lib/painternode/helpers.js @@ -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, }; diff --git a/web_alekpet_nodes/lib/painternode/painter_asset_dialog.js b/web_alekpet_nodes/lib/painternode/painter_asset_dialog.js new file mode 100644 index 0000000..6d14437 --- /dev/null +++ b/web_alekpet_nodes/lib/painternode/painter_asset_dialog.js @@ -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 = `Painter Assets`; + + 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 = + "
Loading...
"; + + 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 = + "
No Painter assets
"; + 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 = `
${escapeHtml(error.message)}
`; + 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 = + "
No Painter assets
"; + } + } catch (error) { + console.error("[PainterAssetDialog] Failed to delete asset:", error); + alert(`Failed to delete Painter asset: ${error.message}`); + } + } +} diff --git a/web_alekpet_nodes/painter_node.js b/web_alekpet_nodes/painter_node.js index e7e69c7..953e0e5 100644 --- a/web_alekpet_nodes/painter_node.js +++ b/web_alekpet_nodes/painter_node.js @@ -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", }, }), ],