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})`; } }