From 6dbb18df928b97ef7858d8df1bf0bc0003d5f302 Mon Sep 17 00:00:00 2001 From: Jairo Correa Date: Tue, 17 Oct 2023 17:53:57 -0300 Subject: [PATCH 1/3] Export and import templates --- web/extensions/core/nodeTemplates.js | 146 ++++++++++++++++++++++----- web/scripts/app.js | 44 +++++++- 2 files changed, 164 insertions(+), 26 deletions(-) diff --git a/web/extensions/core/nodeTemplates.js b/web/extensions/core/nodeTemplates.js index 7059f826..11856516 100644 --- a/web/extensions/core/nodeTemplates.js +++ b/web/extensions/core/nodeTemplates.js @@ -22,6 +22,15 @@ class ManageTemplates extends ComfyDialog { super(); this.element.classList.add("comfy-manage-templates"); this.templates = this.load(); + + this.importInput = $el("input", { + type: "file", + accept: ".json", + multiple: true, + style: {display: "none"}, + parent: document.body, + onchange: () => this.importAll(), + }); } createButtons() { @@ -34,6 +43,22 @@ class ManageTemplates extends ComfyDialog { onclick: () => this.save(), }) ); + btns.unshift( + $el("button", { + type: "button", + textContent: "Export", + onclick: () => this.exportAll(), + }) + ); + btns.unshift( + $el("button", { + type: "button", + textContent: "Import", + onclick: () => { + this.importInput.click(); + }, + }) + ); return btns; } @@ -69,6 +94,50 @@ class ManageTemplates extends ComfyDialog { localStorage.setItem(id, JSON.stringify(this.templates)); } + async importAll() { + for (const file of this.importInput.files) { + if (file.type === "application/json" || file.name.endsWith(".json")) { + const reader = new FileReader(); + reader.onload = async () => { + var importFile = JSON.parse(reader.result); + if (importFile && importFile?.templates) { + for (const template of importFile.templates) { + if (template?.name && template?.data) { + this.templates.push(template); + } + } + this.store(); + } + }; + await reader.readAsText(file); + } + } + + this.close(); + } + + exportAll() { + if (this.templates.length == 0) { + alert("No templates to export."); + return; + } + + const json = JSON.stringify({templates: this.templates}, null, 2); // convert the data to a JSON string + const blob = new Blob([json], {type: "application/json"}); + const url = URL.createObjectURL(blob); + const a = $el("a", { + href: url, + download: "node_templates.json", + style: {display: "none"}, + parent: document.body, + }); + a.click(); + setTimeout(function () { + a.remove(); + window.URL.revokeObjectURL(url); + }, 0); + } + show() { // Show list of template names + delete button super.show( @@ -97,19 +166,48 @@ class ManageTemplates extends ComfyDialog { }), ] ), - $el("button", { - textContent: "Delete", - style: { - fontSize: "12px", - color: "red", - fontWeight: "normal", - }, - onclick: (e) => { - nameInput.value = ""; - e.target.style.display = "none"; - e.target.previousElementSibling.style.display = "none"; - }, - }), + $el( + "div", + {}, + [ + $el("button", { + textContent: "Export", + style: { + fontSize: "12px", + fontWeight: "normal", + }, + onclick: (e) => { + const json = JSON.stringify({templates: [t]}, null, 2); // convert the data to a JSON string + const blob = new Blob([json], {type: "application/json"}); + const url = URL.createObjectURL(blob); + const a = $el("a", { + href: url, + download: t.name + ".json", + style: {display: "none"}, + parent: document.body, + }); + a.click(); + setTimeout(function () { + a.remove(); + window.URL.revokeObjectURL(url); + }, 0); + }, + }), + $el("button", { + textContent: "Delete", + style: { + fontSize: "12px", + color: "red", + fontWeight: "normal", + }, + onclick: (e) => { + nameInput.value = ""; + e.target.parentElement.style.display = "none"; + e.target.parentElement.previousElementSibling.style.display = "none"; + }, + }), + ] + ), ]; }) ) @@ -164,19 +262,17 @@ app.registerExtension({ }, })); - if (subItems.length) { - subItems.push(null, { - content: "Manage", - callback: () => manage.show(), - }); + subItems.push(null, { + content: "Manage", + callback: () => manage.show(), + }); - options.push({ - content: "Node Templates", - submenu: { - options: subItems, - }, - }); - } + options.push({ + content: "Node Templates", + submenu: { + options: subItems, + }, + }); return options; }; diff --git a/web/scripts/app.js b/web/scripts/app.js index 1a07d69b..acbd30b2 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -1416,6 +1416,43 @@ export class ComfyApp { } } + loadTemplateData(templateData) { + if (!templateData?.templates) { + return; + } + + const old = localStorage.getItem("litegrapheditor_clipboard"); + + var maxY, nodeBottom, node; + + for (const template of templateData.templates) { + if (!template?.data) { + continue; + } + + localStorage.setItem("litegrapheditor_clipboard", template.data); + app.canvas.pasteFromClipboard(); + + // Move mouse position down to paste the next template below + + maxY = false; + + for (const i in app.canvas.selected_nodes) { + node = app.canvas.selected_nodes[i]; + + nodeBottom = node.pos[1] + node.size[1]; + + if (maxY === false || nodeBottom > maxY) { + maxY = nodeBottom; + } + } + + app.canvas.graph_mouse[1] = maxY + 50; + } + + localStorage.setItem("litegrapheditor_clipboard", old); + } + /** * Populates the graph with the specified workflow data * @param {*} graphData A serialized graph object @@ -1756,7 +1793,12 @@ export class ComfyApp { } else if (file.type === "application/json" || file.name?.endsWith(".json")) { const reader = new FileReader(); reader.onload = () => { - this.loadGraphData(JSON.parse(reader.result)); + var jsonContent = JSON.parse(reader.result); + if (jsonContent?.templates) { + this.loadTemplateData(jsonContent); + } else { + this.loadGraphData(jsonContent); + } }; reader.readAsText(file); } else if (file.name?.endsWith(".latent") || file.name?.endsWith(".safetensors")) { From a5550747370984714caa859c1c58cd77f43f9008 Mon Sep 17 00:00:00 2001 From: Jairo Correa Date: Tue, 17 Oct 2023 19:44:26 -0300 Subject: [PATCH 2/3] Use name from input to export single node template --- web/extensions/core/nodeTemplates.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/extensions/core/nodeTemplates.js b/web/extensions/core/nodeTemplates.js index 11856516..92d57f9d 100644 --- a/web/extensions/core/nodeTemplates.js +++ b/web/extensions/core/nodeTemplates.js @@ -182,7 +182,7 @@ class ManageTemplates extends ComfyDialog { const url = URL.createObjectURL(blob); const a = $el("a", { href: url, - download: t.name + ".json", + download: (nameInput.value || t.name) + ".json", style: {display: "none"}, parent: document.body, }); From 484bfe46c21cf108a687b579354b5996f867a4f7 Mon Sep 17 00:00:00 2001 From: Jairo Correa Date: Fri, 20 Oct 2023 15:19:29 -0300 Subject: [PATCH 3/3] Clear importInput after import so change event works with same file --- web/extensions/core/nodeTemplates.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/extensions/core/nodeTemplates.js b/web/extensions/core/nodeTemplates.js index 92d57f9d..43449107 100644 --- a/web/extensions/core/nodeTemplates.js +++ b/web/extensions/core/nodeTemplates.js @@ -113,6 +113,8 @@ class ManageTemplates extends ComfyDialog { } } + this.importInput.value = null; + this.close(); }