Files
2025-04-14 14:50:22 +05:30

903 lines
30 KiB
JavaScript

import { app } from "../../scripts/app.js";
const NODE_WIDGET_MAP = {
"JDCN_BatchImageLoadFromList": "Index",
"JDCN_AnyFileSelector": "Index",
"JDCN_AnyFileListRandom": "random_seed",
"JDCN_BatchCounter": "Lap",
"JDCN_BatchCounterAdvance": "Lap",
"JDCN_StringManipulator": "index",
"JDCN_ShowAny": "source",
};
let unAttachedControl = {}
class JDCN_BatchImageLoadFromListControl {
constructor(node, indexWidget) {
for (const [i, w] of node.widgets.entries()) {
switch (w.name) {
case indexWidget:
this.indexWidget = w
break;
}
}
this.indexWidget.afterQueued = () => {
this.indexWidget.value = this.indexWidget.value + 1
this.indexWidget.callback(this.indexWidget.value)
}
}
}
class JDCN_AnyFileSelectorControl {
constructor(node, seedWidget, changeWidget) {
for (const [i, w] of node.widgets.entries()) {
switch (w.name) {
case seedWidget:
this.seedWidget = w
break;
case changeWidget:
this.changeWidget = w
break;
}
}
this.seedWidget.afterQueued = () => {
let change = this.changeWidget.value
switch (change) {
case 'increment':
this.seedWidget.value = this.seedWidget.value + 1
break;
case 'decrement':
this.seedWidget.value = this.seedWidget.value - 1
break;
default:
this.seedWidget.value = this.seedWidget.value + 0
break;
}
this.seedWidget.value = this.seedWidget.value < 0 ? 0 : this.seedWidget.value
this.seedWidget.callback(this.seedWidget.value)
}
}
}
class JDCN_BatchCounterControl {
constructor(node, seedWidget, changeWidget, rangeWidget, logWidget) {
for (const [i, w] of node.widgets.entries()) {
switch (w.name) {
case seedWidget:
this.seedWidget = w
break;
case changeWidget:
this.changeWidget = w
break;
case rangeWidget:
this.rangeWidget = w
break;
case logWidget:
this.logWidget = w
break;
}
}
this.seedWidget.afterQueued = () => {
let change = this.changeWidget.value
switch (change) {
case 'increment':
this.seedWidget.value = this.seedWidget.value + 1
break;
case 'decrement':
this.seedWidget.value = this.seedWidget.value - 1
break;
default:
this.seedWidget.value = this.seedWidget.value + 0
break;
}
this.seedWidget.value = this.seedWidget.value < 0 ? 0 : this.seedWidget.value
this.seedWidget.callback(this.seedWidget.value)
}
this.seedWidget.beforeQueued = () => {
let lap = this.seedWidget.value
let range = this.rangeWidget.value
this.logWidget.value = "" + (lap * range)
}
}
}
class JDCN_BatchCounterAdvanceControl {
constructor(node, seedWidget, changeWidget) {
for (const [i, w] of node.widgets.entries()) {
switch (w.name) {
case seedWidget:
this.seedWidget = w
break;
case changeWidget:
this.changeWidget = w
break;
}
}
this.seedWidget.afterQueued = () => {
let change = this.changeWidget.value
switch (change) {
case 'increment':
this.seedWidget.value = this.seedWidget.value + 1
break;
case 'decrement':
this.seedWidget.value = this.seedWidget.value - 1
break;
default:
this.seedWidget.value = this.seedWidget.value + 0
break;
}
this.seedWidget.value = this.seedWidget.value < 0 ? 0 : this.seedWidget.value
this.seedWidget.callback(this.seedWidget.value)
}
}
}
class JDCN_AnyFileListRandomControl {
constructor(node, seedWidget, changeWidget) {
for (const [i, w] of node.widgets.entries()) {
switch (w.name) {
case seedWidget:
this.seedWidget = w
break;
case changeWidget:
this.changeWidget = w
break;
}
}
this.seedWidget.afterQueued = () => {
let change = this.changeWidget.value
switch (change) {
case 'increment':
this.seedWidget.value = this.seedWidget.value + 1
break;
case 'decrement':
this.seedWidget.value = this.seedWidget.value - 1
break;
case 'fixed':
this.seedWidget.value = this.seedWidget.value + 0
break;
default:
this.seedWidget.value = Date.now()
break;
}
this.seedWidget.value = this.seedWidget.value < 0 ? 0 : this.seedWidget.value
this.seedWidget.callback(this.seedWidget.value)
}
}
}
class JDCN_StringManipulatorControl {
constructor(node, seedWidget, changeWidget) {
for (const [i, w] of node.widgets.entries()) {
switch (w.name) {
case seedWidget:
this.seedWidget = w
break;
case changeWidget:
this.changeWidget = w
break;
}
}
this.seedWidget.afterQueued = () => {
let change = this.changeWidget.value
switch (change) {
case 'increment':
this.seedWidget.value = this.seedWidget.value + 1
break;
case 'decrement':
this.seedWidget.value = this.seedWidget.value - 1
break;
case 'fixed':
this.seedWidget.value = this.seedWidget.value + 0
break;
default:
this.seedWidget.value = Date.now()
break;
}
this.seedWidget.value = this.seedWidget.value < 0 ? 0 : this.seedWidget.value
this.seedWidget.callback(this.seedWidget.value)
}
}
}
const JDCN_BatchImageLoadFromList = {
name: "JDCN_BatchImageLoadFromList",
async beforeRegisterNodeDef(nodeType, nodeData, _app) {
if (nodeData.name === "JDCN_BatchImageLoadFromList") {
nodeType.prototype.onNodeCreated = function () {
this.JDCN_BatchImageLoadFromListControl = new JDCN_BatchImageLoadFromListControl(this, NODE_WIDGET_MAP[nodeData.name]);
this.JDCN_BatchImageLoadFromListControl.indexWidget.value = 1;
};
}
},
};
const JDCN_AnyFileSelector = {
name: "JDCN_AnyFileSelector",
async beforeRegisterNodeDef(nodeType, nodeData, _app) {
if (nodeData.name === "JDCN_AnyFileSelector") {
nodeType.prototype.onNodeCreated = function () {
this.JDCN_AnyFileSelectorControl = new JDCN_AnyFileSelectorControl(this, NODE_WIDGET_MAP[nodeData.name], "Change");
this.JDCN_AnyFileSelectorControl.seedWidget.value = 1;
};
}
},
};
const JDCN_BatchCounter = {
name: "JDCN_BatchCounter",
async beforeRegisterNodeDef(nodeType, nodeData, _app) {
if (nodeData.name === "JDCN_BatchCounter") {
nodeType.prototype.onNodeCreated = function () {
this.JDCN_BatchCounterControl = new JDCN_BatchCounterControl(this, NODE_WIDGET_MAP[nodeData.name], "LapChange", "Range", "Log");
this.JDCN_BatchCounterControl.seedWidget.value = 1;
};
}
},
};
const JDCN_BatchCounterAdvance = {
name: "JDCN_BatchCounterAdvance",
async beforeRegisterNodeDef(nodeType, nodeData, _app) {
if (nodeData.name === "JDCN_BatchCounterAdvance") {
nodeType.prototype.onNodeCreated = function () {
this.JDCN_BatchCounterAdvanceControl = new JDCN_BatchCounterAdvanceControl(this, NODE_WIDGET_MAP[nodeData.name], "LapChange");
this.JDCN_BatchCounterAdvanceControl.seedWidget.value = 1;
};
}
},
};
const JDCN_AnyFileListRandom = {
name: "JDCN_AnyFileListRandom",
async beforeRegisterNodeDef(nodeType, nodeData, _app) {
if (nodeData.name === "JDCN_AnyFileListRandom") {
nodeType.prototype.onNodeCreated = function () {
this.JDCN_AnyFileListRandomControl = new JDCN_AnyFileListRandomControl(this, NODE_WIDGET_MAP[nodeData.name], "seed_change");
this.JDCN_AnyFileListRandomControl.seedWidget.value = Date.now()
};
}
},
};
const JDCN_StringManipulator = {
name: "JDCN_StringManipulator",
async beforeRegisterNodeDef(nodeType, nodeData, _app) {
if (nodeData.name === "JDCN_StringManipulator") {
nodeType.prototype.onNodeCreated = function () {
this.JDCN_StringManipulatorControl = new JDCN_StringManipulatorControl(this, NODE_WIDGET_MAP[nodeData.name], "index_change");
this.JDCN_StringManipulatorControl.seedWidget.value = 1;
};
}
},
};
class JDCN_ShowAnyControl {
constructor(node, logWidget) {
this.node = node
for (const [i, w] of node.widgets.entries()) {
switch (w.name) {
case logWidget:
this.logWidget = w
break;
}
}
}
}
let JDCN_ShowAny = {
name: "JDCN_ShowAny",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "JDCN_ShowAny") {
nodeType.prototype.onNodeCreated = function () {
unAttachedControl["JDCN_ShowAny"] = new JDCN_ShowAnyControl(this, "text");
unAttachedControl["JDCN_ShowAny"].node.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
console.log(originNode, this)
}
unAttachedControl["JDCN_ShowAny"].node.onSerialize = function () {
if (this.isOutputConnected(0)) {
console.log(this.getOutputNodes(0))
}
}
}
}
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted === null || onExecuted === void 0 ? void 0 : onExecuted.apply(this, [message]);
console.log(this)
};
},
};
class Store {
constructor(storeName) {
this.dbName = 'JDCN';
this.storeName = storeName || 'LoadImage';
this.dbVersion = 1;
this.db = null;
this.isIndexedDBAvailable = 'indexedDB' in window;
}
openDB() {
return new Promise((resolve, reject) => {
if (!this.isIndexedDBAvailable) {
resolve(null);
return;
}
if (this.db) {
resolve(this.db);
return;
}
const request = indexedDB.open(this.dbName, this.dbVersion);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
}
};
request.onsuccess = (event) => {
this.db = event.target.result;
resolve(this.db);
};
request.onerror = (event) => {
console.error('IndexedDB error:', event.target.errorCode);
resolve(null);
};
});
}
/**
* Stores a value under a key.
* @param {string} key - The key to store the value under.
* @param {*} value - The value to store (can be an object).
* @returns {Promise<void>}
*/
set(key, value) {
if (this.isIndexedDBAvailable) {
return this.openDB().then((db) => {
if (db) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.put(value, key);
request.onsuccess = () => {
resolve();
};
request.onerror = (event) => {
console.error('IndexedDB set error:', event.target.errorCode);
reject(event.target.errorCode);
};
});
} else {
try {
localStorage.setItem(key, JSON.stringify(value));
return Promise.resolve();
} catch (e) {
return Promise.reject(e);
}
}
});
} else {
try {
localStorage.setItem(key, JSON.stringify(value));
return Promise.resolve();
} catch (e) {
return Promise.reject(e);
}
}
}
/**
* Retrieves a value by key.
* @param {string} key - The key of the value to retrieve.
* @returns {Promise<*>} - The stored value or null if not found.
*/
get(key) {
if (this.isIndexedDBAvailable) {
return this.openDB().then((db) => {
if (db) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.get(key);
request.onsuccess = (event) => {
const data = event.target.result;
resolve(data);
};
request.onerror = (event) => {
console.error('IndexedDB get error:', event.target.errorCode);
reject(event.target.errorCode);
};
});
} else {
const item = localStorage.getItem(key);
const data = item ? JSON.parse(item) : null;
return Promise.resolve(data);
}
});
} else {
const item = localStorage.getItem(key);
const data = item ? JSON.parse(item) : null;
return Promise.resolve(data);
}
}
/**
* Deletes a value by key.
* @param {string} key - The key of the value to delete.
* @returns {Promise<void>}
*/
remove(key) {
if (this.isIndexedDBAvailable) {
return this.openDB().then((db) => {
if (db) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.delete(key);
request.onsuccess = () => {
resolve();
};
request.onerror = (event) => {
console.error('IndexedDB remove error:', event.target.errorCode);
reject(event.target.errorCode);
};
});
} else {
localStorage.removeItem(key);
return Promise.resolve();
}
});
} else {
localStorage.removeItem(key);
return Promise.resolve();
}
}
}
class JDCN_LoadImageF {
constructor(app) {
this.app = app;
this.nodes = [];
this.onNodeAdded = this.onNodeAdded.bind(this);
this.onNodeRemoved = this.onNodeRemoved.bind(this);
this.nodeType = "JDCN_LoadImage";
this.store = new Store();
this.workflow_name = ''; // Initialize workflow_name
this.dialogObserver = null; // To keep reference to the observer
this.dialogElement = null; // To keep reference to the dialog element
this.inputElement = null; // To keep reference to the input element
this.buttonElement = null; // To keep reference to the button element
this.images = {}
this.attachDialog()
this.setupJDCNFileInput()
}
onNodeAdded(node) {
if (node.type === this.nodeType) {
this.registerNode(node, 1)
}
}
onNodeRemoved(node) {
if (node.type === this.nodeType) {
this.registerNode(node, 0)
}
}
isValidBase64Image(base64String) {
const base64ImageRegex = /^data:image\/[a-zA-Z0-9.+-]+;base64,[A-Za-z0-9+/]+={0,2}$/;
return base64ImageRegex.test(base64String);
}
async postImage(id, base64) {
try {
await this.store.set(id, base64)
console.log('Image saved:', id);
} catch (error) {
console.error('Error saving image:', error);
}
}
async getImage(id) {
try {
let base64 = await this.store.get(id)
if (base64) {
return base64;
}
else {
return null
}
} catch (error) {
console.error('Error retrieving image:', error);
return null;
}
}
async isImagePresentOnServer(name) {
const url = `/api/view?filename=${name}&type=input&subfolder=`;
try {
const response = await fetch(url, { method: 'HEAD', cache: 'no-cache' });
return response.status !== 404;
} catch (error) {
console.error("Error checking image presence:", error);
return false;
}
}
async defineElements(node) {
try {
if (node.properties['image']) {
let name = node.properties['image']
let base64 = await this.getImage(name)
if (base64) {
this.images[name] = base64
let onServer = await this.isImagePresentOnServer(name)
if (!onServer) {
node.pasteFile(this.base64ToFile(base64, name))
}
}
}
const imageWidget = node.widgets.find((w) => w.name === "image");
if (imageWidget) {
imageWidget.afterQueued = this.afterQueued.bind(this, node)
}
} catch (error) {
console.log(error)
}
}
clearDefinedElemnts(node) {
try {
if (node.properties['image']) {
let name = node.properties['image']
delete this.images[name]
}
const imageWidget = node.widgets.find((w) => w.name === "image");
if (imageWidget) {
imageWidget.afterQueued = null
}
} catch (error) {
console.log(error)
}
}
convertImageObjectToBase64(image) {
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const context = canvas.getContext('2d');
context.drawImage(image, 0, 0);
return canvas.toDataURL();
}
base64ToFile(base64String, filename) {
const arr = base64String.split(',');
const mime = arr[0].match(/:(.*?);/)[1];
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, { type: mime });
}
async afterQueued(node) {
try {
if (node.imgs && node.imgs.length > 0) {
const save_in_export = node.widgets.find(w => w.name === "save_in_export");
if (save_in_export && save_in_export.value) {
let name = node.widgets[0].value || node.widgets[0]._real_value
let image = node.imgs[0]
let base64 = this.convertImageObjectToBase64(image)
console.log("Trying:", name)
node.setProperty("image", name)
this.images[name] = base64
await this.postImage(name, base64)
}
}
} catch (error) {
console.log(error)
}
}
async collectImage(node) {
try {
if (node.imgs && node.imgs.length > 0) {
let name = node.widgets[0].value || node.widgets[0]._real_value
console.log(node.widgets[0].value, node.widgets[0]._real_value)
let image = node.imgs[0]
let base64 = this.convertImageObjectToBase64(image)
console.log("Trying:", name)
node.setProperty("image", name)
this.images[name] = base64
}
} catch (error) {
console.log(error)
}
}
registerNode(node, mode) {
if (mode == 1) {
if (!this.nodes.includes(node)) {
this.nodes.push(node)
this.defineElements(node)
}
else {
if (this.nodes.includes(node)) {
this.clearDefinedElemnts(node)
this.nodes.splice(this.nodes.indexOf(node), 1)
}
if (!this.nodes.includes(node)) {
this.nodes.push(node)
this.defineElements(node)
}
}
}
else if (mode == 0) {
if (this.nodes.includes(node)) {
this.nodes.splice(this.nodes.indexOf(node), 1)
}
}
}
registerNodes() {
this.app.graph._nodes.forEach(node => {
if (node.type === this.nodeType) {
this.registerNode(node, 1)
}
})
}
attachDialog() {
if (this.dialogObserver) {
this.dialogObserver.disconnect();
}
this.dialogObserver = new MutationObserver((mutationsList) => {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
this.checkAndAttachToDialog(node);
}
});
mutation.removedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
this.checkAndDetachFromDialog(node);
}
});
}
}
});
this.dialogObserver.observe(document.body, { childList: true, subtree: true });
}
checkAndAttachToDialog(node) {
if (node) {
const dialogNode = node.querySelector('h3');
if (dialogNode && dialogNode.textContent == "Export Workflow") {
this.attachEventsToDialog(node);
}
}
}
attachEventsToDialog(dialogNode) {
this.dialogElement = dialogNode;
this.inputElement = dialogNode.querySelector('[data-pc-name="inputtext"]');
this.buttonElement = dialogNode.querySelector('[data-pc-name="button"]');
if (this.inputElement) {
this.workflow_name = this.inputElement.value;
this.inputElement.addEventListener('input', this.onInputChange);
}
if (this.buttonElement) {
this.buttonElement.addEventListener('click', this.onButtonClick);
}
}
onInputChange = (event) => {
this.workflow_name = event.target.value;
};
onButtonClick = () => {
this.saveAsJDCN()
};
checkAndDetachFromDialog(node) {
if (node === this.dialogElement) {
this.detachEventsFromDialog();
} else if (node.contains(this.dialogElement)) {
this.detachEventsFromDialog();
}
}
detachEventsFromDialog() {
if (this.inputElement) {
this.inputElement.removeEventListener('input', this.onInputChange);
this.inputElement = null;
}
if (this.buttonElement) {
this.buttonElement.removeEventListener('click', this.onButtonClick);
this.buttonElement = null;
}
this.dialogElement = null;
}
async forJD() {
this.images = {}
let nodes = 0;
for (const node of this.nodes) {
const save_in_export = node.widgets.find(w => w.name === "save_in_export");
if (save_in_export && save_in_export.value) {
await this.collectImage(node);
nodes++;
}
}
return nodes;
}
setupJDCNFileInput() {
document.body.addEventListener('dragover', (event) => {
event.preventDefault();
});
document.body.addEventListener('drop', (event) => {
event.preventDefault();
const file = event.dataTransfer.files[0];
if (file && file.name.endsWith('.json')) {
handleJDCNFile(file);
}
});
let obj = this
async function handleJDCNFile(file) {
const reader = new FileReader();
reader.onload = async (e) => {
try {
let graph = JSON.parse(e.target.result);
let images = { ...graph.workflow_images }; // Clone to avoid mutating original object
delete graph.workflow_images;
for (const name in images) {
if (images.hasOwnProperty(name) && obj.isValidBase64Image(images[name])) {
await obj.postImage(name, images[name]);
}
}
obj.registerNodes()
} catch (error) {
console.error("Error parsing .json file:", error);
}
};
reader.readAsText(file);
}
}
async saveAsJDCN() {
let nodes = await this.forJD();
console.log("Save in export:", nodes)
if (nodes > 0) {
let name = this.workflow_name + "_with_media";
let graph;
if (typeof this.app.serializeGraph === 'function') {
graph = this.app.serializeGraph();
} else if (typeof this.app.serialize === 'function') {
graph = this.app.serialize();
} else {
throw new Error("Neither serializeGraph() nor serialize() method is available on app.");
}
let images = this.images;
graph.workflow_images = images
const jsonString = JSON.stringify(graph, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${name}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
}
}
let JDCN_LoadImageFObj = new JDCN_LoadImageF(app);
let JDCN_LoadImageFExt = {
name: "ComfyUI-JDCN_LoadImageF",
async setup() {
let originalNodeAdded = app.graph.onNodeAdded ? app.graph.onNodeAdded.bind(app.graph) : null;
let originalNodeRemoved = app.graph.onNodeRemoved ? app.graph.onNodeRemoved.bind(app.graph) : null;
app.graph.onNodeAdded = function (node) {
if (originalNodeAdded) originalNodeAdded(node);
JDCN_LoadImageFObj.onNodeAdded(node);
};
app.graph.onNodeRemoved = function (node) {
if (originalNodeRemoved) originalNodeRemoved(node);
JDCN_LoadImageFObj.onNodeRemoved(node);
};
},
async afterConfigureGraph() {
console.log("JDCN Load Image Extension Loaded");
JDCN_LoadImageFObj.registerNodes();
}
};
// app.registerExtension(JDCN_BatchImageLoadFromList);
app.registerExtension(JDCN_AnyFileSelector);
app.registerExtension(JDCN_LoadImageFExt);
app.registerExtension(JDCN_AnyFileListRandom);
app.registerExtension(JDCN_BatchCounter);
app.registerExtension(JDCN_BatchCounterAdvance);
app.registerExtension(JDCN_StringManipulator);
// app.registerExtension(JDCN_ShowAny);