diff --git a/PainterNode/painter_asset_storage.py b/PainterNode/painter_asset_storage.py index e981bce..58deb3a 100644 --- a/PainterNode/painter_asset_storage.py +++ b/PainterNode/painter_asset_storage.py @@ -61,6 +61,12 @@ async def list_painter_assets(request): try: with open(state_path, "r", encoding="utf-8") as f: state = json.load(f) + + metadata = state.get("metadata") + if not metadata: + metadata = {} + + workflow_name = metadata.get("workflow_name", "Unsaved Workflow") size = state.get("settings", {}).get("currentCanvasSize", {}) width = size.get("width") height = size.get("height") @@ -74,6 +80,7 @@ async def list_painter_assets(request): "updated_at": stat.st_mtime, "width": width, "height": height, + "workflow_name": workflow_name } ) @@ -115,10 +122,16 @@ async def save_painter_asset(request): 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) + if not hasattr(state, "metadata"): + state.setdefault("metadata", {}) + + state["metadata"]["workflow_name"] = workflow_name + os.makedirs(asset_dir, exist_ok=True) fd, tmp_path = tempfile.mkstemp( prefix=".state-", diff --git a/web_alekpet_nodes/css/painternode/painter_node_styles.css b/web_alekpet_nodes/css/painternode/painter_node_styles.css index 8883864..d55a2d2 100644 --- a/web_alekpet_nodes/css/painternode/painter_node_styles.css +++ b/web_alekpet_nodes/css/painternode/painter_node_styles.css @@ -513,6 +513,7 @@ } .alekpet_painter_storage_item_card { + position: relative; display: flex; flex-direction: column; gap: 8px; @@ -531,11 +532,58 @@ background: #000; } -.alekpet_painter_storage_item_id_text { +.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; +} + +.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%); +} + +.alekpet_painter_storage_item_meta { + font-size: 11px; + display: flex; + flex-direction: column; + gap: 3px; + flex: 1 1 0; +} + +.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, #727272, transparent); + 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, transparent); +} +.alekpet_painter_storage_item_size > span { + color: var(----success-color); +} + +.alekpet_painter_storage_item_workflow_name { + background: linear-gradient(90deg, #096e00, transparent); +} +.alekpet_painter_storage_item_workflow_name > span { + color: var(--success-color); +} /* -- end Settings store managing data */ diff --git a/web_alekpet_nodes/lib/painternode/painter_asset_dialog.js b/web_alekpet_nodes/lib/painternode/painter_asset_dialog.js index 6d14437..9dff372 100644 --- a/web_alekpet_nodes/lib/painternode/painter_asset_dialog.js +++ b/web_alekpet_nodes/lib/painternode/painter_asset_dialog.js @@ -118,27 +118,48 @@ export class PainterAssetDialog { canvasEl.style.maxHeight = "100%"; previewWrap.appendChild(canvasEl); + const meta = document.createElement("div"); + meta.className = "alekpet_painter_storage_item_meta"; + const id = document.createElement("div"); id.textContent = asset.asset_id; id.title = asset.asset_id; - id.className = "alekpet_painter_storage_item_id_text"; + id.className = "alekpet_painter_storage_item_id"; - const meta = document.createElement("div"); - meta.textContent = `${formatBytes(asset.size)}${asset.width && asset.height ? ` · ${asset.width}×${asset.height}` : ""}`; - meta.style.fontSize = "11px"; + const size = document.createElement("div"); + size.title = "Data size · Pixel resolution"; + size.innerHTML = `Size: ${formatBytes(asset.size)} · WxH: ${asset.width && asset.height ? ` · ${asset.width}×${asset.height}` : ""}`; + size.className = + "alekpet_painter_storage_item_skew_box alekpet_painter_storage_item_size"; + + const workflow_name = document.createElement("div"); + workflow_name.innerHTML = `Workflow: ${asset.workflow_name}`; + 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 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); + 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 = "🗑 Delete"; + 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, remove); + buttons.append(add, addRef, remove); + meta.append(id, workflow_name, size); card.append(previewWrap, id, meta, buttons); this.items.appendChild(card); @@ -182,7 +203,7 @@ export class PainterAssetDialog { }); } - async addToScene(assetId) { + async addToWorkflow(assetId, persist = false) { const LiteGraph = globalThis.LiteGraph; if (!LiteGraph) { console.error("[PainterAssetDialog] LiteGraph is unavailable"); @@ -219,10 +240,13 @@ export class PainterAssetDialog { await painter.loadCanvasData(assetRef(assetId), painterIndex, { migrateImage: false, + persist, }); - painter.node.painterAsset.asset_id = createPainterAssetId(); - await painter.persistPainterState(); + if (!persist) { + painter.node.painterAsset.asset_id = createPainterAssetId(); + await painter.persistPainterState(); + } app.graph.setDirtyCanvas(true, false); } diff --git a/web_alekpet_nodes/painter_node.js b/web_alekpet_nodes/painter_node.js index 953e0e5..b394a50 100644 --- a/web_alekpet_nodes/painter_node.js +++ b/web_alekpet_nodes/painter_node.js @@ -118,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 ================ @@ -2501,7 +2511,10 @@ class Painter { headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ state: nextState }), + body: JSON.stringify({ + state: nextState, + workflow_name: getActiveWorkflowName(), + }), } ); @@ -2874,7 +2887,7 @@ class Painter { // - end } - async loadCanvasData(data, painter_idx, options = {}) { + async loadCanvasData(data, painter_idx, options = { persist: true }) { // New workflows store only {version, asset_id} if (isPainterAssetRef(data)) { this.node.painterAsset = { @@ -2939,7 +2952,10 @@ class Painter { } // Migrate legacy workflow data, or repair a missing backend asset - await this.persistPainterState(data); + if (options.persist !== false) { + await this.persistPainterState(data); + } + if (data?.canvas_settings && options.migrateImage !== false) { await this.uploadPaintFile(this.node.name); } @@ -3237,7 +3253,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", {