491 lines
20 KiB
JavaScript
491 lines
20 KiB
JavaScript
import { app } from "../../scripts/app.js"
|
|
import { api } from "../../scripts/api.js";
|
|
import { ComfyWidgets } from "../../scripts/widgets.js";
|
|
import { uploader } from './libUploadFile.js';
|
|
|
|
|
|
app.registerExtension({
|
|
name: "hetima.SLPSingleLinePicker",
|
|
|
|
// https://docs.comfy.org/custom-nodes/js/context-menu-migration
|
|
getNodeMenuItems(node) {
|
|
const items = []
|
|
// Add items only for specific node types
|
|
if (node.comfyClass === "HetimaSingleLinePicker") {
|
|
items.push(null); // Separator
|
|
items.push({
|
|
content: "Get Lora List",
|
|
callback: () => {
|
|
node?.listLoraFiles?.();
|
|
}
|
|
});
|
|
items.push({
|
|
content: "Upload File...",
|
|
callback: () => {
|
|
node?.uploadFile?.();
|
|
}
|
|
})
|
|
}
|
|
return items
|
|
},
|
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
|
if (nodeData.name === "HetimaSingleLinePicker") {
|
|
const style = document.createElement("style");
|
|
style.innerText = `
|
|
.single-line-picker-container {
|
|
width: 100%;
|
|
height: 100%;
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
overflow-x: hidden;
|
|
overflow-y: auto;
|
|
white-space: nowrap;
|
|
|
|
background-color: var(--bg-color);
|
|
color: var(--fg-color);
|
|
}
|
|
.single-line-picker-list-item {
|
|
cursor: default;
|
|
user-select: none;
|
|
padding: 3px;
|
|
font-size: 12px;
|
|
}
|
|
.single-line-picker-list-item-selected {
|
|
background-color: var(--fg-color);
|
|
color: var(--bg-color);
|
|
}
|
|
|
|
`;
|
|
document.head.appendChild(style);
|
|
|
|
const base64prefix = ":::base64:::";
|
|
const decodeBase64Unicode = function (input) {
|
|
let rawBase64 = input;
|
|
|
|
if (input.startsWith(base64prefix)) {
|
|
rawBase64 = input.slice(base64prefix.length);
|
|
}else{
|
|
return "";
|
|
}
|
|
//bytes = Uint8Array.fromBase64(rawBase64);
|
|
const binString = atob(rawBase64);
|
|
const bytes = Uint8Array.from(binString, (m) => m.charCodeAt(0));
|
|
return new TextDecoder().decode(bytes);
|
|
}
|
|
|
|
const addListItem = function (container) {
|
|
const itm = document.createElement("div");
|
|
itm.className = "single-line-picker-list-item";
|
|
container.appendChild(itm);
|
|
};
|
|
|
|
const setItemData = function (itm, label, realText) {
|
|
itm.innerText = label;
|
|
itm.dataset.realText = realText;
|
|
}
|
|
|
|
const selectItem = function (itm) {
|
|
if (!itm.classList.contains("single-line-picker-list-item-selected")) {
|
|
itm.classList.add("single-line-picker-list-item-selected");
|
|
}
|
|
};
|
|
|
|
const deselectItem = function (itm) {
|
|
if (itm.classList.contains("single-line-picker-list-item-selected")){
|
|
itm.classList.remove("single-line-picker-list-item-selected");
|
|
}
|
|
};
|
|
|
|
const selectedText = function (container) {
|
|
var result = "";
|
|
const lineCount = container.childElementCount;
|
|
for (let index = 0; index < lineCount; index++) {
|
|
const div = container.children[index];
|
|
if (div.classList.contains("single-line-picker-list-item-selected")) {
|
|
if (result != "") {
|
|
result += "\n";
|
|
}
|
|
result += div.dataset.realText;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
const selectedIndexes = function (container) {
|
|
var result = "";
|
|
const lineCount = container.childElementCount;
|
|
for (let index = 0; index < lineCount; index++) {
|
|
const div = container.children[index];
|
|
if (div.classList.contains("single-line-picker-list-item-selected")) {
|
|
if (result != "") {
|
|
result += ",";
|
|
}
|
|
result += index.toString();
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
const ret = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
|
const node = this;
|
|
this.initialized = false;
|
|
|
|
this.dictMode = false;
|
|
this.sectionMode = false;
|
|
|
|
node.properties = node.properties || {};
|
|
node.properties.selected_indexes = node.properties.selected_indexes || "";
|
|
|
|
|
|
this.textListWidget = ComfyWidgets["STRING"](this, "source_text", ["STRING", { multiline: true }], app).widget;
|
|
this.textListWidget.inputEl.readOnly = true;
|
|
this.textListWidget.callback = async (v) => {
|
|
this.textListChanged();
|
|
}
|
|
this.textListWidget.hidden = true;
|
|
|
|
this.selectedTextWidget = ComfyWidgets["STRING"](this, "selected_text", ["STRING", { multiline: true }], app).widget;
|
|
this.selectedTextWidget.inputEl.readOnly = true;
|
|
this.selectedTextWidget.hidden = true;
|
|
|
|
// this.stemTextWidget = ComfyWidgets["STRING"](this, "stem", ["STRING", { multiline: false }], app).widget;
|
|
// this.stemTextWidget.hidden = true;
|
|
|
|
const container = document.createElement("div");
|
|
container.className = "single-line-picker-container";
|
|
// container.style.width = "100%";
|
|
// container.style.height = "100%";
|
|
// container.style.position = "relative";
|
|
// container.style.display = "flex";
|
|
// container.style.flexDirection = "column";
|
|
// container.style.overflowX = "hidden";
|
|
// container.style.overflowY = "auto";
|
|
// container.style.whiteSpace = "nowrap";
|
|
// container.style.background = "transparent";
|
|
|
|
node.container = container;
|
|
node.container.addEventListener('click', (e) => {
|
|
const lists = Array.from(e.currentTarget.children);
|
|
const clicked_index = lists.findIndex(list => list === e.target);
|
|
// -1 is deselect all
|
|
const lineCount = this.container.childElementCount;
|
|
for (let index = 0; index < lineCount; index++) {
|
|
const div = this.container.children[index];
|
|
if (index == clicked_index) {
|
|
selectItem(div);
|
|
}else{
|
|
deselectItem(div);
|
|
}
|
|
}
|
|
if (clicked_index > 0) {
|
|
}
|
|
node.selectionChanged(null);
|
|
e.stopPropagation();
|
|
});
|
|
|
|
node.listWidget = node.addDOMWidget("list", "TEXTLIST", container, {
|
|
serialize: false,
|
|
computeSize: () => {
|
|
const width = Math.max(node.size[0] || 200, 200);
|
|
const height = Math.max(node.size[1] || 200, 200);
|
|
return [width, height];
|
|
},
|
|
});
|
|
|
|
this.properties.selected_indexes = "";
|
|
this.selectedTextWidget.value = "";
|
|
this.initialized = true;
|
|
return ret;
|
|
};
|
|
|
|
const onExecuted = nodeType.prototype.onExecuted;
|
|
nodeType.prototype.onExecuted = function (message) {
|
|
onExecuted?.apply(this, arguments);
|
|
this.setTextList(message?.text);
|
|
};
|
|
|
|
nodeType.prototype.selectionChanged = function (forceValue = null) {
|
|
if (forceValue === null) {
|
|
forceValue = selectedIndexes(this.container);
|
|
} else {
|
|
const idxs = forceValue.split(",").map(str => parseInt(str, 10));
|
|
const lineCount = this.container.childElementCount;
|
|
for (let index = 0; index < lineCount; index++) {
|
|
const div = this.container.children[index];
|
|
if (idxs.includes(index)) {
|
|
selectItem(div);
|
|
} else {
|
|
deselectItem(div);
|
|
}
|
|
}
|
|
}
|
|
this.properties.selected_indexes = forceValue;
|
|
const text = selectedText(this.container);
|
|
this.selectedTextWidget.value = text;
|
|
|
|
// propagate value to the output nodes
|
|
if (this.outputs?.length > 0 && this.initialized){
|
|
let outputText = text;
|
|
for (const output of this.outputs) {
|
|
if (output?.links?.length > 0) {
|
|
for (const linkId of output.links) {
|
|
const link = this.graph.links[linkId];
|
|
if (!link) continue;
|
|
const targetNode = this.graph.getNodeById(link.target_id);
|
|
if (targetNode?.type === "HetimaSingleLinePicker") {
|
|
const targetWidget = targetNode.widgets?.find(w => w.name === "source_text");
|
|
if (targetWidget) {
|
|
setTimeout(() => {
|
|
targetWidget.value = outputText;
|
|
}, 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.initialized) {
|
|
setTimeout(() => {
|
|
this.setDirtyCanvas(true, true);
|
|
}, 0);
|
|
}
|
|
|
|
// const firstLine = text.split('\n')[0];
|
|
// const stem = firstLine.substring(
|
|
// Math.max(firstLine.lastIndexOf("/"), firstLine.lastIndexOf("\\")) + 1,
|
|
// firstLine.lastIndexOf("."));
|
|
// this.stemTextWidget.value = stem || firstLine;
|
|
}
|
|
|
|
nodeType.prototype.textListChanged = function () {
|
|
if (!this.container) {
|
|
return;
|
|
}
|
|
|
|
const text = this.textListWidget.value;
|
|
const texts = text.split('\n');
|
|
let lineCount = texts.length;
|
|
|
|
// read the configuration from the first line
|
|
if (lineCount > 1 && this.parseConfig(texts[0])) {
|
|
texts.shift();
|
|
lineCount--;
|
|
}
|
|
|
|
let sections = null;
|
|
if (this.sectionMode){
|
|
sections = this.parseSectionFormat(texts);
|
|
lineCount = sections.length / 2;
|
|
}
|
|
|
|
// match the number of elements
|
|
const needs = lineCount - this.container.childElementCount;
|
|
if (needs > 0){
|
|
for (let index = 0; index < needs; index++) {
|
|
addListItem(this.container);
|
|
}
|
|
} else if (needs < 0) {
|
|
for (let index = 0; index > needs; index--) {
|
|
this.container.lastElementChild.remove();
|
|
}
|
|
}
|
|
|
|
//section
|
|
if (this.sectionMode) {
|
|
if (sections?.length > 0) {
|
|
if (sections.length % 2 !== 0) {
|
|
sections.pop();
|
|
}
|
|
let count = 0;
|
|
for (let i = 0; i < sections.length; i += 2) {
|
|
const title = sections[i];
|
|
const body = sections[i + 1];
|
|
const div = this.container.children[count];
|
|
setItemData(div, title, body);
|
|
deselectItem(div);
|
|
count++;
|
|
}
|
|
}
|
|
} else {
|
|
// normal, dict
|
|
for (let index = 0; index < lineCount; index++) {
|
|
const div = this.container.children[index];
|
|
let realText = texts[index];
|
|
let label = texts[index];
|
|
if (this.dictMode) {
|
|
const items = texts[index].split(':');
|
|
if (items.length > 1) {
|
|
label = items.shift().trim();
|
|
realText = items.join(':').trim();
|
|
}
|
|
}
|
|
|
|
setItemData(div, label, realText);
|
|
deselectItem(div);
|
|
}
|
|
}
|
|
|
|
this.properties.selected_indexes = "";
|
|
this.selectedTextWidget.value = "";
|
|
// this.stemTextWidget.value = "";
|
|
this.setDirtyCanvas(true, true);
|
|
};
|
|
|
|
nodeType.prototype.setTextList = function (texts) {
|
|
let text = "";
|
|
if (typeof texts === 'string') {
|
|
text = texts;
|
|
} else {
|
|
text = texts?.join?.("") || "";
|
|
}
|
|
if (this.textListWidget) {
|
|
if (this.textListWidget.value != text) {
|
|
this.textListWidget.value = text;
|
|
}
|
|
}
|
|
};
|
|
|
|
nodeType.prototype.listLoraFiles = function () {
|
|
async function fetchCheckpoints() {
|
|
try {
|
|
const response = await api.fetchApi("/models/loras");
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error("Failed to retrieve the model:", error);
|
|
}
|
|
}
|
|
|
|
fetchCheckpoints().then((value) => {
|
|
this.setTextList(value?.join?.("\n"));
|
|
});
|
|
};
|
|
|
|
nodeType.prototype.uploadFile = function () {
|
|
uploader.uploadFile((success, text) => {
|
|
if (success) {
|
|
this.setTextList(text);
|
|
}
|
|
});
|
|
};
|
|
|
|
const configRegex = /(#\w+)(?::("([^"\\]|\\.)*"|\S+))?/g;
|
|
// const escapeRegex = /\\(.)/g
|
|
nodeType.prototype.parseConfig = function (txt) {
|
|
// reset
|
|
this.dictMode = false;
|
|
this.sectionMode = false;
|
|
|
|
if (!txt.startsWith("#")) {
|
|
return false;
|
|
}
|
|
configRegex.lastIndex = 0;
|
|
const result = Object.fromEntries(
|
|
Array.from(txt.matchAll(configRegex), m => {
|
|
const key = m[1].toLowerCase();
|
|
let value = m[2] || "";
|
|
if (value.endsWith(',')) {
|
|
value = value.slice(0, -1);
|
|
}
|
|
// When enclosed in double quotation, perform quote removal and escape decoding.
|
|
// if (value.startsWith('"') && value.endsWith('"')) {
|
|
// escapeRegex.lastIndex = 0;
|
|
// value = value.slice(1, -1).replace(escapeRegex, '$1');
|
|
// }
|
|
|
|
return [key, value];
|
|
})
|
|
);
|
|
const keys = Object.keys(result);
|
|
if (keys.length == 0) {
|
|
return false;
|
|
}
|
|
if (keys.includes("#dict")) {
|
|
this.dictMode = true;
|
|
} else if (keys.includes("#section")) {
|
|
this.sectionMode = true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
nodeType.prototype.parseSectionFormat = function (lineArray) {
|
|
const result = [];
|
|
let currentBody = [];
|
|
let currentTitle = null;
|
|
let includesBase64 = false;
|
|
|
|
lineArray.forEach((line) => {
|
|
const trimmedLine = line.trim();
|
|
if (!trimmedLine) return;
|
|
// [label]
|
|
if (trimmedLine.startsWith('[') && trimmedLine.endsWith(']')) {
|
|
// prev section
|
|
if (currentTitle !== null && currentBody.length > 0) {
|
|
result.push(currentTitle, currentBody.join('\n'));
|
|
}
|
|
// reset
|
|
currentTitle = trimmedLine.substring(1, trimmedLine.length - 1);
|
|
currentBody = [];
|
|
} else {
|
|
// accumulate the body
|
|
if (currentTitle !== null) {
|
|
if (currentBody.length == 0 && trimmedLine === base64prefix) {
|
|
includesBase64 = true;
|
|
}
|
|
currentBody.push(trimmedLine);
|
|
}
|
|
}
|
|
});
|
|
|
|
// push last section
|
|
if (currentTitle !== null && currentBody.length > 0) {
|
|
result.push(currentTitle, currentBody.join('\n'));
|
|
}
|
|
|
|
if (includesBase64) {
|
|
for (let index = 0; index < result.length; index++) {
|
|
const itm = result[index];
|
|
if (itm.startsWith(base64prefix)) {
|
|
result[index] = decodeBase64Unicode(result[index]);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
nodeType.prototype.onSerialize = function (info) {
|
|
info.properties = {
|
|
source_text: this.textListWidget.value || "",
|
|
selected_indexes: this.properties.selected_indexes || ""
|
|
};
|
|
//info.widgets_values = [""];
|
|
};
|
|
|
|
nodeType.prototype.onConfigure = function (info) {
|
|
this.initialized = false;
|
|
this.properties = this.properties || {};
|
|
|
|
if (info.properties) {
|
|
this.textListWidget.value = info.properties.source_text || ""; //call textListChanged()
|
|
this.properties.selected_indexes = info.properties.selected_indexes || "";
|
|
}
|
|
this.selectionChanged(this.properties.selected_indexes);
|
|
this.initialized = true;
|
|
};
|
|
}
|
|
},
|
|
});
|
|
|
|
|