| | import { app } from "../../scripts/app.js"; |
| | import { api } from "../../scripts/api.js"; |
| | import { ComfyDialog, $el } from "../../scripts/ui.js"; |
| | import { GroupNodeConfig, GroupNodeHandler } from "./groupNode.js"; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | const id = "Comfy.NodeTemplates"; |
| | const file = "comfy.templates.json"; |
| |
|
| | class ManageTemplates extends ComfyDialog { |
| | constructor() { |
| | super(); |
| | this.load().then((v) => { |
| | this.templates = v; |
| | }); |
| |
|
| | this.element.classList.add("comfy-manage-templates"); |
| | this.draggedEl = null; |
| | this.saveVisualCue = null; |
| | this.emptyImg = new Image(); |
| | this.emptyImg.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="; |
| |
|
| | this.importInput = $el("input", { |
| | type: "file", |
| | accept: ".json", |
| | multiple: true, |
| | style: { display: "none" }, |
| | parent: document.body, |
| | onchange: () => this.importAll(), |
| | }); |
| | } |
| |
|
| | createButtons() { |
| | const btns = super.createButtons(); |
| | btns[0].textContent = "Close"; |
| | btns[0].onclick = (e) => { |
| | clearTimeout(this.saveVisualCue); |
| | this.close(); |
| | }; |
| | 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; |
| | } |
| |
|
| | async load() { |
| | let templates = []; |
| | if (app.storageLocation === "server") { |
| | if (app.isNewUserSession) { |
| | |
| | const json = localStorage.getItem(id); |
| | if (json) { |
| | templates = JSON.parse(json); |
| | } |
| | await api.storeUserData(file, json, { stringify: false }); |
| | } else { |
| | const res = await api.getUserData(file); |
| | if (res.status === 200) { |
| | try { |
| | templates = await res.json(); |
| | } catch (error) { |
| | } |
| | } else if (res.status !== 404) { |
| | console.error(res.status + " " + res.statusText); |
| | } |
| | } |
| | } else { |
| | const json = localStorage.getItem(id); |
| | if (json) { |
| | templates = JSON.parse(json); |
| | } |
| | } |
| |
|
| | return templates ?? []; |
| | } |
| |
|
| | async store() { |
| | if(app.storageLocation === "server") { |
| | const templates = JSON.stringify(this.templates, undefined, 4); |
| | localStorage.setItem(id, templates); |
| | try { |
| | await api.storeUserData(file, templates, { stringify: false }); |
| | } catch (error) { |
| | console.error(error); |
| | alert(error.message); |
| | } |
| | } else { |
| | 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 () => { |
| | const importFile = JSON.parse(reader.result); |
| | if (importFile?.templates) { |
| | for (const template of importFile.templates) { |
| | if (template?.name && template?.data) { |
| | this.templates.push(template); |
| | } |
| | } |
| | await this.store(); |
| | } |
| | }; |
| | await reader.readAsText(file); |
| | } |
| | } |
| |
|
| | this.importInput.value = null; |
| |
|
| | this.close(); |
| | } |
| |
|
| | exportAll() { |
| | if (this.templates.length == 0) { |
| | alert("No templates to export."); |
| | return; |
| | } |
| |
|
| | const json = JSON.stringify({ templates: this.templates }, null, 2); |
| | 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() { |
| | |
| | super.show( |
| | $el( |
| | "div", |
| | {}, |
| | this.templates.flatMap((t,i) => { |
| | let nameInput; |
| | return [ |
| | $el( |
| | "div", |
| | { |
| | dataset: { id: i }, |
| | className: "tempateManagerRow", |
| | style: { |
| | display: "grid", |
| | gridTemplateColumns: "1fr auto", |
| | border: "1px dashed transparent", |
| | gap: "5px", |
| | backgroundColor: "var(--comfy-menu-bg)" |
| | }, |
| | ondragstart: (e) => { |
| | this.draggedEl = e.currentTarget; |
| | e.currentTarget.style.opacity = "0.6"; |
| | e.currentTarget.style.border = "1px dashed yellow"; |
| | e.dataTransfer.effectAllowed = 'move'; |
| | e.dataTransfer.setDragImage(this.emptyImg, 0, 0); |
| | }, |
| | ondragend: (e) => { |
| | e.target.style.opacity = "1"; |
| | e.currentTarget.style.border = "1px dashed transparent"; |
| | e.currentTarget.removeAttribute("draggable"); |
| |
|
| | |
| | this.element.querySelectorAll('.tempateManagerRow').forEach((el,i) => { |
| | var prev_i = el.dataset.id; |
| |
|
| | if ( el == this.draggedEl && prev_i != i ) { |
| | this.templates.splice(i, 0, this.templates.splice(prev_i, 1)[0]); |
| | } |
| | el.dataset.id = i; |
| | }); |
| | this.store(); |
| | }, |
| | ondragover: (e) => { |
| | e.preventDefault(); |
| | if ( e.currentTarget == this.draggedEl ) |
| | return; |
| |
|
| | let rect = e.currentTarget.getBoundingClientRect(); |
| | if (e.clientY > rect.top + rect.height / 2) { |
| | e.currentTarget.parentNode.insertBefore(this.draggedEl, e.currentTarget.nextSibling); |
| | } else { |
| | e.currentTarget.parentNode.insertBefore(this.draggedEl, e.currentTarget); |
| | } |
| | } |
| | }, |
| | [ |
| | $el( |
| | "label", |
| | { |
| | textContent: "Name: ", |
| | style: { |
| | cursor: "grab", |
| | }, |
| | onmousedown: (e) => { |
| | |
| | if (e.target.localName == 'label') |
| | e.currentTarget.parentNode.draggable = 'true'; |
| | } |
| | }, |
| | [ |
| | $el("input", { |
| | value: t.name, |
| | dataset: { name: t.name }, |
| | style: { |
| | transitionProperty: 'background-color', |
| | transitionDuration: '0s', |
| | }, |
| | onchange: (e) => { |
| | clearTimeout(this.saveVisualCue); |
| | var el = e.target; |
| | var row = el.parentNode.parentNode; |
| | this.templates[row.dataset.id].name = el.value.trim() || 'untitled'; |
| | this.store(); |
| | el.style.backgroundColor = 'rgb(40, 95, 40)'; |
| | el.style.transitionDuration = '0s'; |
| | this.saveVisualCue = setTimeout(function () { |
| | el.style.transitionDuration = '.7s'; |
| | el.style.backgroundColor = 'var(--comfy-input-bg)'; |
| | }, 15); |
| | }, |
| | onkeypress: (e) => { |
| | var el = e.target; |
| | clearTimeout(this.saveVisualCue); |
| | el.style.transitionDuration = '0s'; |
| | el.style.backgroundColor = 'var(--comfy-input-bg)'; |
| | }, |
| | $: (el) => (nameInput = el), |
| | }) |
| | ] |
| | ), |
| | $el( |
| | "div", |
| | {}, |
| | [ |
| | $el("button", { |
| | textContent: "Export", |
| | style: { |
| | fontSize: "12px", |
| | fontWeight: "normal", |
| | }, |
| | onclick: (e) => { |
| | const json = JSON.stringify({templates: [t]}, null, 2); |
| | const blob = new Blob([json], {type: "application/json"}); |
| | const url = URL.createObjectURL(blob); |
| | const a = $el("a", { |
| | href: url, |
| | download: (nameInput.value || 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) => { |
| | const item = e.target.parentNode.parentNode; |
| | item.parentNode.removeChild(item); |
| | this.templates.splice(item.dataset.id*1, 1); |
| | this.store(); |
| | |
| | var that = this; |
| | setTimeout(function (){ |
| | that.element.querySelectorAll('.tempateManagerRow').forEach((el,i) => { |
| | el.dataset.id = i; |
| | }); |
| | }, 0); |
| | }, |
| | }), |
| | ] |
| | ), |
| | ] |
| | ) |
| | ]; |
| | }) |
| | ) |
| | ); |
| | } |
| | } |
| |
|
| | app.registerExtension({ |
| | name: id, |
| | setup() { |
| | const manage = new ManageTemplates(); |
| |
|
| | const clipboardAction = async (cb) => { |
| | |
| | |
| | const old = localStorage.getItem("litegrapheditor_clipboard"); |
| | await cb(); |
| | localStorage.setItem("litegrapheditor_clipboard", old); |
| | }; |
| |
|
| | const orig = LGraphCanvas.prototype.getCanvasMenuOptions; |
| | LGraphCanvas.prototype.getCanvasMenuOptions = function () { |
| | const options = orig.apply(this, arguments); |
| |
|
| | options.push(null); |
| | options.push({ |
| | content: `Save Selected as Template`, |
| | disabled: !Object.keys(app.canvas.selected_nodes || {}).length, |
| | callback: () => { |
| | const name = prompt("Enter name"); |
| | if (!name?.trim()) return; |
| |
|
| | clipboardAction(() => { |
| | app.canvas.copyToClipboard(); |
| | let data = localStorage.getItem("litegrapheditor_clipboard"); |
| | data = JSON.parse(data); |
| | const nodeIds = Object.keys(app.canvas.selected_nodes); |
| | for (let i = 0; i < nodeIds.length; i++) { |
| | const node = app.graph.getNodeById(nodeIds[i]); |
| | const nodeData = node?.constructor.nodeData; |
| | |
| | let groupData = GroupNodeHandler.getGroupData(node); |
| | if (groupData) { |
| | groupData = groupData.nodeData; |
| | if (!data.groupNodes) { |
| | data.groupNodes = {}; |
| | } |
| | data.groupNodes[nodeData.name] = groupData; |
| | data.nodes[i].type = nodeData.name; |
| | } |
| | } |
| |
|
| | manage.templates.push({ |
| | name, |
| | data: JSON.stringify(data), |
| | }); |
| | manage.store(); |
| | }); |
| | }, |
| | }); |
| |
|
| | |
| | const subItems = manage.templates.map((t) => { |
| | return { |
| | content: t.name, |
| | callback: () => { |
| | clipboardAction(async () => { |
| | const data = JSON.parse(t.data); |
| | await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {}); |
| | localStorage.setItem("litegrapheditor_clipboard", t.data); |
| | app.canvas.pasteFromClipboard(); |
| | }); |
| | }, |
| | }; |
| | }); |
| |
|
| | subItems.push(null, { |
| | content: "Manage", |
| | callback: () => manage.show(), |
| | }); |
| |
|
| | options.push({ |
| | content: "Node Templates", |
| | submenu: { |
| | options: subItems, |
| | }, |
| | }); |
| |
|
| | return options; |
| | }; |
| | }, |
| | }); |
| |
|