Files

603 lines
22 KiB
JavaScript

import { app } from "../../../scripts/app.js";
import { api } from "../../../scripts/api.js";
import { $el } from "../../../scripts/ui.js";
export class SettingUtils {
static API_PREFIX = "/sn0w";
static cachedLoggingLevel = null;
static observingContextMenu = false;
static contextMenuList = [];
static async getSetting(url, defaultValue = null) {
try {
const settingUrl = `/settings/${url}`;
const response = await fetch(settingUrl);
if (!response.ok) {
return defaultValue;
}
const data = await response.json();
if (data == null) {
return defaultValue;
}
SettingUtils.logSn0w(`Got setting: ${url}`, "debug", "setting", data);
return data;
} catch (error) {
console.error("There was a problem with the fetch operation:", error);
return defaultValue;
}
}
static async getMultipleSettings(settingsArray) {
// Create an array of promises using getSetting with the corresponding default values
const promises = settingsArray.map(({ url, defaultValue }) => this.getSetting(url, defaultValue));
try {
const results = await Promise.all(promises);
// Convert the results to a Map or a plain object where each URL is the key
const settingsMap = {};
settingsArray.forEach(({ url }, index) => {
settingsMap[url] = results[index];
});
return settingsMap;
} catch (error) {
console.error("There was a problem with one of the fetch operations:", error);
return null;
}
}
static async fetchApi(route, options) {
if (!options) {
options = {};
}
if (!options.headers) {
options.headers = {};
}
options.cache = "no-store";
try {
const response = await fetch(route, options);
const data = await response.json();
return data;
} catch (error) {
console.error("There was a problem with the fetch operation:", error);
return null;
}
}
// Callback function to add or remove the filename from the settings list
static async toggleFavourite(existingList, filename, setting) {
try {
// Check if the filename is already in the list
const index = existingList.indexOf(filename);
if (index === -1) {
// Add the new filename to the list
existingList.push(filename);
} else {
// Remove the filename from the list
existingList.splice(index, 1);
}
// Store the updated list back in the settings
await api.storeSetting(setting, existingList);
} catch (error) {
console.error("Error updating settings:", error);
}
}
static registerSetting = (settingDefinition) => {
const extension = {
name: settingDefinition.id,
init() {
const setting = app.ui.settings.addSetting({
...settingDefinition, // Spread all properties from settingDefinition
});
},
};
app.registerExtension(extension);
};
static createMultilineSetting(name, setter, value, attrs) {
// Ensure that tooltip has a default value if not provided
const htmlID = `${name.replaceAll(" ", "").replaceAll("[", "").replaceAll("]", "-")}`;
// Create the textarea element
const textarea = $el("textarea", {
value,
id: htmlID,
oninput: (e) => {
adjustHeight();
},
className: "p-inputtext",
style: {
width: "100%",
resize: "none",
},
...attrs,
});
const maxLines = 10;
const adjustHeight = () => {
requestAnimationFrame(() => {
const parentDiv = textarea.parentElement;
if (parentDiv != null) {
parentDiv.style.width = "100%";
const id = parentDiv.id;
if (id != null) {
api.storeSetting(id, textarea.value);
}
}
textarea.style.height = ""; // Allow to shrink
const lines = textarea.value.split("\n").length;
const scrollHeight = textarea.scrollHeight + 3;
if (lines > maxLines) {
const height = (scrollHeight / lines) * maxLines;
textarea.setAttribute("style", `width: 100%; height: ${height}px; resize: none;`);
return;
}
textarea.setAttribute("style", `width: 100%; height: ${scrollHeight}px; resize: none;`);
});
};
adjustHeight();
return textarea;
}
static createCheckboxSetting(name, setter, value, attrs) {
// Define the threshold for showing the "Check All" checkbox
const threshold = 10;
const hideThreshold = 5;
const tr = document.createElement("tr");
const tdInput = document.createElement("td");
// Generate a unique ID for the setting container
const uniqueId = `${name.replaceAll(" ", "").replaceAll("[", "").replaceAll("]", "-")}`;
// Checkbox container
const checkboxContainer = document.createElement("div");
checkboxContainer.id = uniqueId;
//checkboxContainer.style.padding = "5px";
checkboxContainer.style.display = "block"; // Ensure it is initially visible
// Hide button
if (attrs.options.length > hideThreshold) {
const hideButton = document.createElement("button");
hideButton.textContent = "Show";
checkboxContainer.style.display = "none";
hideButton.onclick = () => {
checkboxContainer.style.display = checkboxContainer.style.display === "none" ? "block" : "none";
hideButton.textContent = checkboxContainer.style.display === "none" ? "Show" : "Hide";
hideButton.style.marginBottom = checkboxContainer.style.display === "none" ? "0px" : "5px";
};
hideButton.style.width = "100%";
// Append the hide button before the checkboxes
tdInput.appendChild(hideButton);
}
// Initialize values array based on the default value
let values = Array.isArray(value) ? value : [value];
// Check if the number of options exceeds the threshold
if (attrs.options.length > threshold) {
// Create search textbox
const searchContainer = document.createElement("div");
const searchInput = document.createElement("input");
searchInput.type = "text";
searchInput.placeholder = "Search...";
searchInput.style.width = "100%";
searchContainer.appendChild(searchInput);
checkboxContainer.appendChild(searchContainer);
// Create "Check All" checkbox
const checkAllContainer = document.createElement("div");
const checkAllCheckbox = document.createElement("input");
const checkAllLabel = document.createElement("label");
const checkAllId = `${uniqueId}-check-all`;
checkAllCheckbox.type = "checkbox";
checkAllCheckbox.id = checkAllId;
checkAllCheckbox.onchange = () => {
const allCheckboxes = checkboxContainer.querySelectorAll('input[type="checkbox"]');
allCheckboxes.forEach((checkbox) => {
if (checkbox !== checkAllCheckbox) {
checkbox.checked = checkAllCheckbox.checked;
const optionValue = checkbox.id.split("-").pop();
if (checkAllCheckbox.checked && !values.includes(optionValue)) {
values.push(optionValue);
} else if (!checkAllCheckbox.checked && values.includes(optionValue)) {
values = values.filter((v) => v !== optionValue);
}
}
});
setter(values); // Update the setting with the current array of selected values
};
checkAllLabel.setAttribute("for", checkAllId);
checkAllLabel.textContent = "Check All";
checkAllLabel.style.marginRight = "10px";
checkAllContainer.style.borderBottom = "1px solid var(--border-color)";
checkAllContainer.style.paddingBottom = "4px";
checkAllContainer.style.marginBottom = "5px";
checkAllContainer.appendChild(checkAllCheckbox);
checkAllContainer.appendChild(checkAllLabel);
checkboxContainer.appendChild(checkAllContainer);
// Filter checkboxes based on search input
searchInput.addEventListener("input", () => {
const searchText = searchInput.value.toLowerCase();
attrs.options.forEach((option) => {
const checkbox = document.getElementById(`${uniqueId}-${option.value}`);
const label = checkbox.nextSibling;
if (option.label.toLowerCase().includes(searchText)) {
checkbox.parentElement.style.display = "";
} else {
checkbox.parentElement.style.display = "none";
}
});
});
}
// Create checkboxes based on the options provided in attrs
attrs.options.forEach((option) => {
const checkbox = document.createElement("input");
const checkboxLabel = document.createElement("label");
const checkboxId = `${uniqueId}-${option.value}`;
checkbox.type = "checkbox";
checkbox.id = checkboxId;
checkbox.checked = values.includes(option.value);
checkbox.onchange = () => {
if (checkbox.checked && !values.includes(option.value)) {
values.push(option.value);
} else if (!checkbox.checked && values.includes(option.value)) {
values = values.filter((v) => v !== option.value);
}
setter(values); // Update the setting with the current array of selected values
// Update the "Check All" checkbox if it exists
const checkAllCheckbox = document.getElementById(`${uniqueId}-check-all`);
if (checkAllCheckbox) {
checkAllCheckbox.checked = values.length === attrs.options.length;
}
};
checkboxLabel.setAttribute("for", checkboxId);
checkboxLabel.textContent = option.label;
checkboxLabel.style.marginRight = "10px";
const wrapper = document.createElement("div");
wrapper.appendChild(checkbox);
wrapper.appendChild(checkboxLabel);
checkboxContainer.appendChild(wrapper);
});
tdInput.appendChild(checkboxContainer);
tr.appendChild(tdInput);
const tooltip = attrs.tooltip;
// Set tooltip if provided
if (tooltip) {
tr.title = tooltip;
label.className = "comfy-tooltip-indicator";
}
return tr;
}
// WIDGETS
static hideWidget(node, widget, suffix = "") {
const CONVERTED_TYPE = "converted-widget";
if (widget.type?.startsWith(CONVERTED_TYPE)) return;
widget.origType = widget.type;
widget.origComputeSize = widget.computeSize;
widget.origSerializeValue = widget.serializeValue;
widget.computeSize = () => [0, -4]; // -4 is due to the gap litegraph adds between widgets automatically
widget.type = CONVERTED_TYPE + suffix;
widget.serializeValue = () => {
// Prevent serializing the widget if we have no input linked
if (!node.inputs) {
return undefined;
}
let node_input = node.inputs.find((i) => i.widget?.name === widget.name);
if (!node_input || !node_input.link) {
return undefined;
}
return widget.origSerializeValue ? widget.origSerializeValue() : widget.value;
};
// Hide any linked widgets, e.g. seed+seedControl
if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) {
hideWidget(node, w, ":" + widget.name);
}
}
}
static showWidget(widget) {
widget.type = widget.origType;
widget.computeSize = widget.origComputeSize;
widget.serializeValue = widget.origSerializeValue;
delete widget.origType;
delete widget.origComputeSize;
delete widget.origSerializeValue;
// Hide any linked widgets, e.g. seed+seedControl
if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) {
showWidget(w);
}
}
}
async #invokeExtensionsAsync(method, graphCanvas, ...args) {
return await Promise.all(
graphCanvas.extensions.map(async (ext) => {
if (method in ext) {
try {
return await ext[method](...args, graphCanvas);
} catch (error) {
console.error(`Error calling extension '${ext.name}' method '${method}'`, { error }, { extension: ext }, { args });
}
}
})
);
}
async refreshComboInSingleNode(graphCanvas, nodeName) {
const defs = await api.getNodeDefs();
// Find the node definition by name
let nodeDef = null;
for (const def of Object.values(defs)) {
if (def.name === nodeName) {
nodeDef = def;
break;
}
}
if (!nodeDef) {
console.error(`Node definition with name ${nodeName} not found`);
return;
}
// Find the specific node in the graph by name
let node = null;
for (const graphNode of Object.values(graphCanvas.graph._nodes)) {
if (graphNode.type === nodeDef.name) {
node = graphNode;
break;
}
}
if (!node) {
console.warn(`Node with name ${nodeName} not found in the graph`);
return;
}
for (let nodeNum in graphCanvas.graph._nodes) {
const node = graphCanvas.graph._nodes[nodeNum];
if (node.title == nodeName) {
const def = defs[node.type];
// Allow primitive nodes to handle refresh
node.refreshComboInNode?.(defs);
if (!def) continue;
for (const widgetNum in node.widgets) {
const widget = node.widgets[widgetNum];
if (widget.type == "combo" && def["input"]["required"][widget.name] !== undefined) {
widget.options.values = def["input"]["required"][widget.name][0];
if (widget.name != "image" && !widget.options.values.includes(widget.value)) {
widget.value = widget.options.values[0];
widget.callback(widget.value);
}
}
}
}
}
await this.#invokeExtensionsAsync("refreshComboInSingleNodeByName", graphCanvas, defs);
}
// MISC
static debounce(func, delay) {
let debounceTimer;
return function () {
const context = this;
const args = arguments;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => func.apply(context, args), delay);
};
}
static leadingEdgeDebounce(func, wait) {
let timeout;
let lastCallTime = 0;
return function (...args) {
const now = Date.now();
// If the last call was longer ago than the wait period, reset the timeout
if (now - lastCallTime > wait) {
lastCallTime = now;
func.apply(this, args); // Call the function immediately
}
clearTimeout(timeout); // Clear any previous timeout
// Set a new timeout that will reset `lastCallTime` after the wait period
timeout = setTimeout(() => {
lastCallTime = 0;
}, wait);
};
}
static getLuminance(hexColor) {
// Convert hex to RGB
const rgb = parseInt(hexColor.slice(1), 16); // Remove "#" and convert to integer
const r = (rgb >> 16) & 0xff;
const g = (rgb >> 8) & 0xff;
const b = (rgb >> 0) & 0xff;
// Convert RGB to relative luminance
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance;
}
// Function to choose black or white text color based on luminance
static getTextColorBasedOnBg(bgColor) {
const luminance = SettingUtils.getLuminance(bgColor);
return luminance > 0.5 ? "#000000" : "#FFFFFF"; // Black text for bright bg, white text for dark bg
}
static setLoggingLevel(loggingLevel) {
this.cachedLoggingLevel = loggingLevel;
}
static async logSn0w(message, type, category = "", detailMessage = "") {
let loggingLevels = this.cachedLoggingLevel;
if (loggingLevels == null) {
loggingLevels = await SettingUtils.getSetting("sn0w.LoggingLevel");
}
const alwaysLog = ["emergency", "alert", "critical", "error"];
const logLevel = type.toLowerCase();
if (!alwaysLog.includes(logLevel) && !loggingLevels.includes(type.toUpperCase())) {
return;
}
const prefix = "sn0w";
const generalCss = "color: white; padding: 2px 6px 2px 4px; font-weight: lighter;";
let customCSS = generalCss + " border-radius: 3px;";
const logColors = {
error: "#c50f1f",
warning: "#c19c00",
};
const categoryColors = {
api: "#c19c00",
lora: "#3a96dd",
node: "#c19c00",
};
const color = logColors[logLevel] || "#881798";
const textColor = SettingUtils.getTextColorBasedOnBg(color);
customCSS += `color: ${textColor};`;
const categoryCSS = category ? `background-color: ${categoryColors[category.toLowerCase()] || "#13a10e"}; color: ${SettingUtils.getTextColorBasedOnBg(categoryColors[category.toLowerCase()] || "#13a10e")}; border-radius: 0 3px 3px 0; margin-left: -5px;` : "";
const logMessage = category
? [`%c${prefix}%c${category}`, `${customCSS.replace("6px", "10px")} background-color: ${color};`, `${generalCss} ${categoryCSS}`, message] // Category
: [`%c${prefix}`, `${customCSS} background-color: ${color};`, message]; // No Category
console.groupCollapsed(...logMessage);
if (detailMessage != "") {
console.log(detailMessage);
}
console.trace();
console.groupEnd();
}
static drawSigmas(sigmas) {
// Define the size of the canvas
const width = 800;
const height = 400;
// Create a canvas element
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
// Background
ctx.fillStyle = "#333"; // Dark background
ctx.fillRect(0, 0, width, height);
// Normalize sigma values to fit within the canvas height
const maxSigma = Math.max(...sigmas.map((s) => s[0]));
const minSigma = Math.min(...sigmas.map((s) => s[0]));
const range = maxSigma - minSigma;
// Function to scale a sigma value to the canvas coordinates
const scaleY = (sigma) => height - 20 - ((sigma - minSigma) / range) * (height - 40);
const scaleX = (index) => (index / (sigmas.length - 1)) * width;
// Draw horizontal grid lines and labels
const gridColor = "#555";
const textColor = "#ccc";
const numGridLines = 10;
const gridSpacing = (height - 40) / numGridLines;
ctx.strokeStyle = gridColor;
ctx.lineWidth = 1;
ctx.font = "10px Arial";
ctx.fillStyle = textColor;
for (let i = 0; i <= numGridLines; i++) {
let y = 20 + i * gridSpacing;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(width, y);
ctx.stroke();
// Add text labels for grid lines
let gridValue = maxSigma - (i / numGridLines) * range;
ctx.fillText(gridValue.toFixed(2), 5, y - 2);
}
// Draw the graph line
ctx.strokeStyle = "white"; // Line color
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(scaleX(0), scaleY(sigmas[0][0]));
sigmas.forEach((sigma, index) => {
ctx.lineTo(scaleX(index), scaleY(sigma[0]));
});
ctx.stroke();
// Draw a dot at each sigma point
ctx.fillStyle = "red"; // Dot color
const dotRadius = 4;
sigmas.forEach((sigma, index) => {
ctx.beginPath();
ctx.arc(scaleX(index), scaleY(sigma[0]), dotRadius, 0, 2 * Math.PI);
ctx.fill();
});
// Return the canvas content as a Base64 encoded image
return canvas.toDataURL("image/png");
}
static hexToRgb(hex) {
// Remove the hash at the start if it's there
hex = hex.replace(/^#/, "");
// Parse the r, g, b values
let bigint = parseInt(hex, 16);
let r = (bigint >> 16) & 255;
let g = (bigint >> 8) & 255;
let b = bigint & 255;
return `rgb(${r}, ${g}, ${b})`;
}
}