Files
BlakeOne-ComfyUI-NodePresets/js/extension.js
2024-04-21 06:37:28 -07:00

392 lines
10 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { ComfyDialog, $el } from "../../scripts/ui.js";
// Adds the ability to save and load node presets using the node's context menu
// To save:
// Right click on the node
// Presets -> Save -> give it a name
//
// To load:
// Right click on the node
// Presets -> click the one to add
//
// To delete/rename:
// Right click the node
// Presets -> Manage
//
// To rearrange:
// Open the manage dialog and Drag and drop elements using the "Name:" label as handle
const id = "Comfy.NodePresets";
const file = "comfy.presets.json";
class PresetManager extends ComfyDialog {
constructor() {
super();
this.load().then((v) => {
this.presets = v;
});
this.element.classList.add("comfy-manage-presets");
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 presets = {};
if (app.storageLocation === "server") {
try {
if (app.isNewUserSession) {
// New user so migrate existing presets
const json = localStorage.getItem(id);
if (json) {
presets = JSON.parse(json);
api.storeUserData(file, json, { stringify: false });
}
} else {
const res = await api.getUserData(file);
if (res.status === 200) {
presets = await res.json();
} else if (res.status !== 404) {
console.error(res.status + " " + res.statusText);
}
}
} catch (error) {
console.error(error);
}
} else {
const json = localStorage.getItem(id);
if (json) {
presets = JSON.parse(json);
}
}
return presets ?? {};
}
async store() {
if(app.storageLocation === "server") {
try {
const presets = JSON.stringify(this.presets, undefined, 4);
await api.storeUserData(file, presets, { stringify: false });
} catch (error) {
console.error(error);
}
} else {
localStorage.setItem(id, JSON.stringify(this.presets));
}
}
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?.presets) {
for (const nodeType in importFile.presets) {
if (this.presets[nodeType]) {
this.presets[nodeType] = this.presets[nodeType].concat(importFile.presets[nodeType]);
} else {
this.presets[nodeType] = importFile.presets[nodeType];
}
}
}
};
await reader.readAsText(file);
}
}
this.importInput.value = null;
this.store();
this.close();
}
exportAll() {
if (Object.keys(this.presets).length === 0) {
alert("No presets to export.");
return;
}
const json = JSON.stringify({ presets: this.presets }, 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_presets.json",
style: { display: "none" },
parent: document.body,
});
a.click();
setTimeout(function () {
a.remove();
window.URL.revokeObjectURL(url);
}, 0);
}
show(nodeType) {
const nodePresets = this.presets[nodeType];
super.show(
$el(
"div",
{},
nodePresets.flatMap((p, i) => {
let nameInput;
return [
$el(
"div",
{
dataset: { id: i },
className: "presetManagerRow",
style: {
display: "grid",
gridPresetColumns: "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('.presetManagerRow').forEach((el,i) => {
var prev_i = el.dataset.id;
if ( el == this.draggedEl && prev_i != i ) {
nodePresets.splice(i, 0, nodePresets.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: p.name,
dataset: { name: p.name },
style: {
transitionProperty: 'background-color',
transitionDuration: '0s',
},
onchange: (e) => {
clearTimeout(this.saveVisualCue);
var el = e.target;
var row = el.parentNode.parentNode;
nodePresets[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 presets = {};
presets[nodeType] = [p];
const json = JSON.stringify({presets: presets}, 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 || p.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);
nodePresets.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('.presetManagerRow').forEach((el,i) => {
el.dataset.id = i;
});
}, 0);
},
}),
]
),
]
)
];
})
)
);
}
}
app.registerExtension({
name: id,
setup() {
const presetManager = new PresetManager();
const orig = LGraphCanvas.prototype.getNodeMenuOptions;
LGraphCanvas.prototype.getNodeMenuOptions = function(node) {
const options = orig.call(this, node);
if (!node.widgets) {
return options;
}
const loadOptions = presetManager.presets[node.type]?.map((preset) => ({
content: preset.name,
callback: () => {
node.widgets.forEach((widget) => {
if (widget.name in preset.values) {
widget.value = preset.values[widget.name];
}
});
node.setDirtyCanvas(true, false);
}
}));
const saveCallback = () => {
const name = prompt("Enter name");
if (!name?.trim()) return;
const presetValues = {};
node.widgets.forEach(widget => presetValues[widget.name] = widget.value);
const preset = {name: name, values: presetValues};
if (presetManager.presets[node.type]) {
presetManager.presets[node.type].push(preset);
} else {
presetManager.presets[node.type] = [preset];
}
presetManager.store();
};
const presetOptions = [{
content: `Save`,
disabled: !node.widgets.length,
callback: saveCallback
}, {
content: "Load",
disabled: !presetManager.presets[node.type]?.length,
submenu: {options: loadOptions}
}, {
content: "Manage",
callback: () => presetManager.show(node.type)
}];
const presetMenu = {
content: "Presets",
submenu: {options: presetOptions}
};
options.push(null, presetMenu);
return options;
};
},
});