591 lines
22 KiB
JavaScript
591 lines
22 KiB
JavaScript
import { api } from '../../../scripts/api.js';
|
|
|
|
export class SettingUtils {
|
|
static API_PREFIX = '/sn0w';
|
|
|
|
// SETTINGS
|
|
static async getSetting(url) {
|
|
try {
|
|
const settingUrl = `/settings/${url}`;
|
|
const response = await fetch(settingUrl);
|
|
if (!response.ok) {
|
|
throw new Error('Network response was not ok');
|
|
}
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (error) {
|
|
console.error('There was a problem with the fetch operation:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static async fetchApi(route, options) {
|
|
if (!options) {
|
|
options = {};
|
|
}
|
|
if (!options.headers) {
|
|
options.headers = {};
|
|
}
|
|
const url = `${location.protocol}//${location.host}${route}`;
|
|
try {
|
|
const response = await fetch(url, options);
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (error) {
|
|
console.error('There was a problem with the fetch operation:', error);
|
|
}
|
|
}
|
|
|
|
// 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 createMultilineSetting(name, setter, value, attrs) {
|
|
const tr = document.createElement('tr');
|
|
const tdLabel = document.createElement('td');
|
|
const tdInput = document.createElement('td');
|
|
const label = document.createElement('label');
|
|
const textarea = document.createElement('textarea');
|
|
|
|
// Generate a unique ID for associating the label with the textarea
|
|
const uniqueId = `${name.replaceAll(' ', '').replaceAll('[', '').replaceAll(']', '-')}`;
|
|
label.setAttribute('for', uniqueId);
|
|
label.textContent = name;
|
|
|
|
textarea.id = uniqueId;
|
|
textarea.value = value;
|
|
const maxLines = 10;
|
|
// Function to set the height based on the number of lines
|
|
const adjustHeight = () => {
|
|
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();
|
|
textarea.onchange = () => setter(textarea.value);
|
|
textarea.addEventListener('input', adjustHeight);
|
|
|
|
// Apply additional attributes
|
|
for (const [key, val] of Object.entries(attrs)) {
|
|
textarea.setAttribute(key, val);
|
|
}
|
|
|
|
tdLabel.appendChild(label);
|
|
tdInput.appendChild(textarea);
|
|
tr.appendChild(tdLabel);
|
|
tr.appendChild(tdInput);
|
|
|
|
const tooltip = attrs['tooltip'];
|
|
|
|
// Setting tooltip if provided
|
|
if (tooltip != '' && tooltip != undefined) {
|
|
tr.title = tooltip;
|
|
label.className = 'comfy-tooltip-indicator';
|
|
}
|
|
|
|
// Update height when settings are opened
|
|
const dialog = document.getElementById('comfy-settings-dialog');
|
|
const observer = new MutationObserver(mutations => {
|
|
mutations.forEach(mutation => {
|
|
if (mutation.type === 'attributes' && mutation.attributeName === 'open') {
|
|
adjustHeight();
|
|
}
|
|
});
|
|
});
|
|
observer.observe(dialog, { attributes: true });
|
|
|
|
return tr;
|
|
}
|
|
|
|
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 tdLabel = document.createElement('td');
|
|
const tdInput = document.createElement('td');
|
|
const label = document.createElement('label');
|
|
|
|
// Generate a unique ID for the setting container
|
|
const uniqueId = `${name.replaceAll(' ', '').replaceAll('[', '').replaceAll(']', '-')}`;
|
|
label.setAttribute('for', uniqueId);
|
|
label.textContent = name;
|
|
|
|
// 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);
|
|
|
|
tdLabel.appendChild(label);
|
|
tr.appendChild(tdLabel);
|
|
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);
|
|
}
|
|
|
|
static async addStarsToFavourited(existingList) {
|
|
const menuEntries = document.querySelectorAll('.litemenu-entry');
|
|
const highlightLora = await SettingUtils.getSetting('sn0w.HighlightFavourite');
|
|
|
|
const root = document.documentElement;
|
|
const comfyMenuBgColor = SettingUtils.hexToRgb(
|
|
getComputedStyle(root).getPropertyValue('--comfy-menu-bg').trim()
|
|
);
|
|
|
|
// Function to check and update the background color
|
|
const checkAndUpdateBackgroundColor = (entry, currentBgColor) => {
|
|
if (highlightLora && currentBgColor == comfyMenuBgColor) {
|
|
entry.setAttribute('style', 'background-color: green !important');
|
|
// Ensure the entry uses flexbox for alignment
|
|
entry.style.display = 'flex';
|
|
entry.style.alignItems = 'center';
|
|
}
|
|
};
|
|
|
|
const observerConfig = { attributes: true, attributeFilter: ['style'] };
|
|
|
|
// Create the observer
|
|
const observer = new MutationObserver((mutations) => {
|
|
mutations.forEach((mutation) => {
|
|
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
|
|
const entry = mutation.target;
|
|
const currentBgColor = window.getComputedStyle(entry).backgroundColor;
|
|
checkAndUpdateBackgroundColor(entry, currentBgColor);
|
|
}
|
|
});
|
|
});
|
|
|
|
menuEntries.forEach((entry) => {
|
|
const value = entry.getAttribute('data-value');
|
|
let filename = value;
|
|
if (value !== null && value.includes('\\')) {
|
|
const pathArray = value.split('\\');
|
|
filename = pathArray[pathArray.length - 1];
|
|
}
|
|
|
|
if (existingList.includes(filename)) {
|
|
// Create star element
|
|
const star = document.createElement('span');
|
|
star.innerHTML = '★';
|
|
star.style.marginLeft = 'auto'; // Push the star to the right
|
|
star.style.alignSelf = 'center'; // Center the star vertically
|
|
|
|
// Ensure the entry uses flexbox for alignment
|
|
entry.style.display = 'flex';
|
|
entry.style.alignItems = 'center';
|
|
|
|
// Check if star is already added to avoid duplication
|
|
if (!entry.querySelector('span')) {
|
|
entry.appendChild(star);
|
|
}
|
|
|
|
// Initial background color check
|
|
const currentBgColor = window.getComputedStyle(entry).backgroundColor;
|
|
checkAndUpdateBackgroundColor(entry, currentBgColor);
|
|
|
|
// Attach the observer to the entry
|
|
observer.observe(entry, observerConfig);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 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 async logSn0w(message, type) {
|
|
const loggingLevels = await SettingUtils.getSetting("sn0w.LoggingLevel");
|
|
|
|
const logColors = {
|
|
"error": "red",
|
|
"warning": "yellow",
|
|
};
|
|
|
|
const alwaysLog = ["emergency", "alert", "critical", "error"]
|
|
const logLevel = type.toLowerCase();
|
|
const color = logColors[logLevel] || "rgb(136, 23, 152)";
|
|
|
|
if (alwaysLog.includes(logLevel) || loggingLevels.includes(type.toUpperCase())) {
|
|
console.log(`%c[sn0w]`, `color: ${color};`, message);
|
|
}
|
|
}
|
|
|
|
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})`;
|
|
}
|
|
}
|