Spaces:
Building
on
A10G
Building
on
A10G
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"; | |
// Adds the ability to save and add multiple nodes as a template | |
// To save: | |
// Select multiple nodes (ctrl + drag to select a region or ctrl+click individual nodes) | |
// Right click the canvas | |
// Save Node Template -> give it a name | |
// | |
// To add: | |
// Right click the canvas | |
// Node templates -> click the one to add | |
// | |
// To delete/rename: | |
// Right click the canvas | |
// Node templates -> Manage | |
// | |
// To rearrange: | |
// Open the manage dialog and Drag and drop elements using the "Name:" label as handle | |
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) { | |
// New user so migrate existing templates | |
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); // Backwards compatibility | |
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); // 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( | |
$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"); | |
// rearrange the elements | |
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) => { | |
// enable dragging only from the label | |
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); // 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: (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(); | |
// update the rows index, setTimeout ensures that the list is updated | |
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) => { | |
// We use the clipboard functions but dont want to overwrite the current user clipboard | |
// Restore it after we've run our callback | |
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(); | |
}); | |
}, | |
}); | |
// Map each template to a menu item | |
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; | |
}; | |
}, | |
}); | |