├── .github └── workflows │ └── publish.yml ├── .gitignore ├── README.md ├── __init__.py ├── docs └── pictures │ ├── settings-dialog.png │ └── settings-location.png ├── pyproject.toml └── src ├── constants.js ├── defaults-manager.js ├── main.js ├── settings-dialog.js └── widget-builder.js /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Comfy registry 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - "pyproject.toml" 9 | 10 | jobs: 11 | publish-node: 12 | name: Publish Custom Node to registry 13 | runs-on: ubuntu-latest 14 | # if this is a forked repository. Skipping the workflow. 15 | if: github.event.repository.fork == false 16 | steps: 17 | - name: Check out code 18 | uses: actions/checkout@v4 19 | - name: Publish Custom Node 20 | uses: Comfy-Org/publish-node-action@main 21 | with: 22 | ## Add your own personal access token to your Github Repository secrets and reference it here. 23 | personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/*__pycache__/* 2 | **/*.pyc 3 | __pycache__/ 4 | *.pyc 5 | todo.md 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | > #### Adds a row to the settings page: 3 | 4 | ![alt text](docs/pictures/settings-location.png) 5 | 6 | 7 | 8 | > #### Clicking the `Edit Custom Defaults` button brings up this screen: 9 | 10 | 11 | 12 | ![alt text](docs/pictures/settings-dialog.png) 13 | 14 | The custom defaults will only be applied when adding a node manually. It won't alter nodes loaded from workflows/refreshes or nodes that are copy and pasted. -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | NODE_CLASS_MAPPINGS = {} 2 | WEB_DIRECTORY = "./src" -------------------------------------------------------------------------------- /docs/pictures/settings-dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christian-byrne/comfyui-default-values-manager/74d3ebf89e1fc9777f2a486010738520325b9559/docs/pictures/settings-dialog.png -------------------------------------------------------------------------------- /docs/pictures/settings-location.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christian-byrne/comfyui-default-values-manager/74d3ebf89e1fc9777f2a486010738520325b9559/docs/pictures/settings-location.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "comfyui-default-values-manager" 3 | description = "Adds a setting that lets you configure the default input values that nodes will load with" 4 | version = "1.0.0" 5 | license = "LICENSE" 6 | 7 | [project.urls] 8 | Repository = "https://github.com/christian-byrne/comfyui-default-values-manager" 9 | # Used by Comfy Registry https://comfyregistry.org 10 | 11 | [tool.comfy] 12 | PublisherId = "christian-byrne" 13 | DisplayName = "comfyui-default-values-manager" 14 | Icon = "" 15 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const EXTENSION_KEY = "default_values_extension"; 2 | export const TARGETED_WIDGET_TYPES = [ 3 | "customtext", 4 | "text", 5 | "number", 6 | "combo", 7 | "slider", 8 | "toggle", 9 | ]; 10 | export const MOD_KEYS = ["Control", "Meta", "Alt", "Shift", "Tab"]; 11 | -------------------------------------------------------------------------------- /src/defaults-manager.js: -------------------------------------------------------------------------------- 1 | import { app } from "../../scripts/app.js"; 2 | import { $el } from "../../scripts/ui.js"; 3 | import { EXTENSION_KEY, TARGETED_WIDGET_TYPES } from "./constants.js"; 4 | import { DefaultSettingsDialog } from "./settings-dialog.js"; 5 | import { WidgetRecordBuilder } from "./widget-builder.js"; 6 | 7 | export class DefaultValuesManager { 8 | constructor() { 9 | this.enabled = app.ui.settings.settingsValues[EXTENSION_KEY] || false; 10 | this.widgetsMap = {}; 11 | this.dialog = new DefaultSettingsDialog(this); 12 | this.widgetRecordBuilder = new WidgetRecordBuilder(); 13 | this.normalRowBorder = "2px solid var(--bg-color)"; 14 | this.errorRowBorder = "4px solid var(--error-text"; 15 | } 16 | 17 | addNodeToWidgetsMap(node) { 18 | const nodeType = node.type; 19 | // Skip if node already in map 20 | if (!nodeType || this.widgetsMap[nodeType]) { 21 | return; 22 | } 23 | 24 | // Skip node if it doesn't have any widgets or none of its widgets are relevant 25 | if ( 26 | !node.widgets || 27 | node.widgets.every( 28 | (widget) => !TARGETED_WIDGET_TYPES.includes(widget.type) 29 | ) 30 | ) { 31 | return; 32 | } 33 | 34 | this.widgetsMap[nodeType] = {}; 35 | for (const [widgetIndex, widget] of Object.entries(node.widgets)) { 36 | if (!TARGETED_WIDGET_TYPES.includes(widget.type)) { 37 | continue; 38 | } 39 | this.widgetsMap[nodeType][widgetIndex] = this.widgetRecordBuilder 40 | .from(widget) 41 | .inferType() 42 | .addAllowedVals() 43 | .addSelectedProps() 44 | .addDefaultVal( 45 | this.getDefaults().nodeType?.[widgetIndex]?.userDefaultVal 46 | ) 47 | .build(); 48 | } 49 | } 50 | 51 | createDialog() { 52 | return $el( 53 | "div", 54 | { 55 | style: { 56 | padding: "4px", 57 | display: "flex", 58 | flexDirection: "column", 59 | }, 60 | }, 61 | [this.createTable()] 62 | ); 63 | } 64 | 65 | createRow(widget, widgetIndex, nodeName) { 66 | const widgetDefault = this.getDefaults()[nodeName]?.[widgetIndex]; 67 | const tr = $el("tr", [ 68 | $el("td", { textContent: nodeName }), 69 | $el("td", { textContent: widget.name }), 70 | $el("td", { textContent: widget.value }), 71 | $el("td", { 72 | textContent: widgetDefault ?? "Unset", 73 | contentEditable: true, 74 | id: `defaulsRow-${nodeName}-${widgetIndex}`, 75 | style: { 76 | color: widgetDefault ? "var(--bg-color)" : "var(--descrip-text)", 77 | border: this.normalRowBorder, 78 | textShadow: "none", 79 | boxShadow: "none", 80 | backgroundColor: "var(--fg-color)", 81 | }, 82 | onclick: (event) => { 83 | if (event.target.textContent === "Unset") { 84 | event.target.textContent = ""; 85 | } 86 | }, 87 | oninput: (event) => { 88 | const expectedType = this.widgetsMap[nodeName][widgetIndex].valueType; 89 | const allowedValues = 90 | this.widgetsMap[nodeName][widgetIndex]?.["allowed values"]; 91 | console.log(allowedValues); 92 | if ( 93 | allowedValues && 94 | !allowedValues.includes(event.target.textContent) 95 | ) { 96 | console.log("Invalid value"); 97 | document.getElementById( 98 | `defaulsRow-${nodeName}-${widgetIndex}` 99 | ).style.border = this.errorRowBorder; 100 | return; 101 | } 102 | const newValue = event.target.textContent; 103 | let parsedValue; 104 | switch (expectedType) { 105 | case "int": 106 | parsedValue = parseInt(newValue); 107 | break; 108 | case "float": 109 | parsedValue = parseFloat(newValue); 110 | break; 111 | case "boolean": 112 | parsedValue = 113 | newValue.toLowerCase() === "true" || newValue == "1"; 114 | break; 115 | default: 116 | parsedValue = newValue; 117 | break; 118 | } 119 | this.widgetsMap[nodeName][widgetIndex].userDefaultVal = parsedValue; 120 | this.addDefaultVal(nodeName, widgetIndex, parsedValue); 121 | document.getElementById( 122 | `defaulsRow-${nodeName}-${widgetIndex}` 123 | ).style.color = "var(--bg-color)"; 124 | document.getElementById( 125 | `defaulsRow-${nodeName}-${widgetIndex}` 126 | ).style.border = this.normalRowBorder; 127 | }, 128 | }), 129 | $el( 130 | "td", 131 | { 132 | style: { 133 | display: "flex", 134 | flexDirection: "row", 135 | justifyContent: "center", 136 | alignItems: "center", 137 | }, 138 | }, 139 | [ 140 | $el("button.comfyui-button.primary", { 141 | textContent: "Reset", 142 | style: { 143 | flexDirection: "column", 144 | fontSize: "unset", 145 | }, 146 | onclick: () => { 147 | console.log("resetting value"); 148 | this.removeDefaultVal(nodeName, widgetIndex); 149 | this.widgetsMap[nodeName][widgetIndex].userDefaultVal = null; 150 | this.show(); 151 | }, 152 | }), 153 | ] 154 | ), 155 | ]); 156 | 157 | return tr; 158 | } 159 | 160 | createTable() { 161 | const table = $el( 162 | "table.comfy-table", 163 | { 164 | style: { 165 | width: "100%", 166 | overflow: "hidden", 167 | }, 168 | }, 169 | [ 170 | $el("tr", [ 171 | $el("th", { textContent: "Node" }), 172 | $el("th", { textContent: "Widget" }), 173 | $el("th", { textContent: "Recently Used Value" }), 174 | $el("th", { textContent: "Default Value" }), 175 | ]), 176 | ] 177 | ); 178 | 179 | for (let nodeType in this.widgetsMap) { 180 | for (let widgetIndex in this.widgetsMap[nodeType]) { 181 | const row = this.createRow( 182 | this.widgetsMap[nodeType][widgetIndex], 183 | widgetIndex, 184 | nodeType 185 | ); 186 | table.appendChild(row); 187 | } 188 | } 189 | 190 | return table; 191 | } 192 | 193 | insertDefaults(node) { 194 | const nodeType = node.type; 195 | if (!this.enabled || !nodeType || !this.widgetsMap[nodeType]) { 196 | return node; 197 | } 198 | 199 | const nodeWidgets = node.widgets; 200 | for (const [widgetIndex, widget] of Object.entries(nodeWidgets)) { 201 | if (!TARGETED_WIDGET_TYPES.includes(widget.type)) { 202 | continue; 203 | } 204 | 205 | const widgetDefault = this.getDefaults()[nodeType]?.[widgetIndex]; 206 | if (widgetDefault) { 207 | node.widgets[widgetIndex].value = widgetDefault; 208 | } 209 | } 210 | 211 | return node; 212 | } 213 | 214 | removeDefaultVal(nodeType, widgetIndex) { 215 | const existingDefaults = this.getDefaults(); 216 | if (existingDefaults[nodeType]) { 217 | delete existingDefaults[nodeType][widgetIndex]; 218 | localStorage.setItem( 219 | EXTENSION_KEY + "_defaults", 220 | JSON.stringify(existingDefaults) 221 | ); 222 | } 223 | } 224 | 225 | addDefaultVal(nodeType, widgetIndex, value) { 226 | const existingDefaults = this.getDefaults(); 227 | if (!existingDefaults[nodeType]) { 228 | existingDefaults[nodeType] = {}; 229 | } 230 | existingDefaults[nodeType][widgetIndex] = value; 231 | localStorage.setItem( 232 | EXTENSION_KEY + "_defaults", 233 | JSON.stringify(existingDefaults) 234 | ); 235 | } 236 | 237 | getDefaults() { 238 | const storage = localStorage.getItem(EXTENSION_KEY + "_defaults"); 239 | if (!storage) { 240 | return {}; 241 | } 242 | return JSON.parse(storage); 243 | } 244 | 245 | show() { 246 | this.dialog.show(this.createDialog()); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { app } from "../../scripts/app.js"; 2 | import { $el } from "../../scripts/ui.js"; 3 | import { MOD_KEYS } from "./constants.js"; 4 | import { DefaultValuesManager } from "./defaults-manager.js"; 5 | 6 | const defaultsManager = new DefaultValuesManager(); 7 | let modifierKeyDown = false; 8 | 9 | document.addEventListener("keydown", (event) => { 10 | if (MOD_KEYS.includes(event.key)) { 11 | modifierKeyDown = true; 12 | } 13 | }); 14 | document.addEventListener("keyup", (event) => { 15 | if (MOD_KEYS.includes(event.key)) { 16 | modifierKeyDown = false; 17 | } 18 | }); 19 | 20 | app.registerExtension({ 21 | name: "DefaultValuesExtension", 22 | beforeConfigureGraph: (args) => { 23 | const onNodeAdded = app.graph.onNodeAdded; 24 | app.graph.onNodeAdded = function (...args) { 25 | defaultsManager.addNodeToWidgetsMap(args[0]); 26 | if (!app.configuringGraph && !modifierKeyDown) { 27 | const node = defaultsManager.insertDefaults(args[0]); 28 | return onNodeAdded?.apply(this, [node, ...args.slice(1)]); 29 | } else { 30 | return onNodeAdded?.apply(this, args); 31 | } 32 | }; 33 | }, 34 | setup: (args) => { 35 | app.ui.settings.addSetting({ 36 | id: "default_values_extension", 37 | name: "Set Custom Default Values", 38 | // Copied from: https://github.com/comfyanonymous/ComfyUI/blob/628f0b8ebc2c9a51205e5e5a9973f8db348a310f/web/scripts/logging.js#L294 39 | type: (name, setter, value) => { 40 | return $el("tr", [ 41 | $el("td", [ 42 | $el("label", { 43 | textContent: "Custom Default Values", 44 | for: "default_values_extension_checkbox", 45 | }), 46 | ]), 47 | $el("td", [ 48 | $el("input", { 49 | id: "default_values_extension_checkbox", 50 | type: "checkbox", 51 | checked: value, 52 | onchange: (event) => { 53 | setter(event.target.checked); 54 | }, 55 | }), 56 | $el("button", { 57 | textContent: "Edit Custom Defaults", 58 | onclick: () => { 59 | app.ui.settings.element.close(); 60 | defaultsManager.show(); 61 | }, 62 | style: { 63 | fontSize: "14px", 64 | display: "block", 65 | marginTop: "5px", 66 | }, 67 | }), 68 | ]), 69 | ]); 70 | }, 71 | }); 72 | }, 73 | }); 74 | -------------------------------------------------------------------------------- /src/settings-dialog.js: -------------------------------------------------------------------------------- 1 | import { $el } from "../../scripts/ui.js"; 2 | import { ComfyDialog } from "../../scripts/ui/dialog.js"; 3 | 4 | export class DefaultSettingsDialog extends ComfyDialog { 5 | constructor(extension) { 6 | super(); 7 | this.extension = extension; 8 | this.buttons = null; 9 | this.element = $el( 10 | "div.comfy-modal", 11 | { 12 | parent: document.body, 13 | style: { 14 | width: "78vw", 15 | padding: "1.25rem", 16 | }, 17 | }, 18 | [ 19 | $el( 20 | "div.comfy-modal-content", 21 | { 22 | style: { 23 | width: "100%", 24 | }, 25 | }, 26 | [ 27 | $el("h1", { 28 | textContent: "Node Default Values", 29 | style: { 30 | textAlign: "center", 31 | color: "var(--content-fg)", 32 | }, 33 | }), 34 | $el("div", { 35 | textContent: "Set custom default values for nodes.", 36 | style: { 37 | textAlign: "center", 38 | color: "var(--descrip-text)", 39 | }, 40 | }), 41 | $el("div", { 42 | textContent: "Add a node to the graph to see it in this table.", 43 | style: { 44 | textAlign: "center", 45 | color: "var(--descrip-text)", 46 | }, 47 | }), 48 | $el("div", { 49 | textContent: 50 | "This only affects new nodes added manually — Not nodes loaded from workflows.", 51 | style: { 52 | textAlign: "center", 53 | color: "var(--drag-text)", 54 | opacity: ".64", 55 | }, 56 | }), 57 | $el("hr", { 58 | style: { 59 | width: "100%", 60 | color: "var(--content-fg)", 61 | marginTop: "1.25rem", 62 | }, 63 | }), 64 | $el("div", { 65 | $: (div) => (this.textElement = div), 66 | style: { overflowY: "scroll" }, 67 | }), 68 | ...this.createButtons(), 69 | ] 70 | ), 71 | ] 72 | ); 73 | } 74 | 75 | createButtons() { 76 | return ( 77 | this.buttons ?? [ 78 | $el("button.comfyui-button.primary", { 79 | type: "button", 80 | textContent: "Close", 81 | style: { 82 | margin: "0.5rem 0 0.5rem 0", 83 | flexDirection: "column", 84 | }, 85 | onclick: () => this.close(), 86 | }), 87 | ] 88 | ); 89 | } 90 | 91 | close() { 92 | this.element.style.display = "none"; 93 | } 94 | 95 | show(html) { 96 | if (typeof html === "string") { 97 | this.textElement.innerHTML = html; 98 | } else { 99 | this.textElement.replaceChildren( 100 | ...(html instanceof Array ? html : [html]) 101 | ); 102 | } 103 | this.element.style.display = "flex"; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/widget-builder.js: -------------------------------------------------------------------------------- 1 | export class WidgetRecordBuilder { 2 | constructor() { 3 | this.widgetData = {}; 4 | this.widgetRecord = {}; 5 | this.selectedProps = ["label", "name", "type", "value"]; 6 | } 7 | 8 | from(widgetData) { 9 | this.widgetData = widgetData; 10 | this.widgetRecord = {}; 11 | return this; 12 | } 13 | 14 | inferType() { 15 | let widgetValueType = typeof this.widgetData.value; 16 | if (widgetValueType == "number" || widgetValueType == "slider") { 17 | if (this.widgetData.value % 1 !== 0) { 18 | widgetValueType = "float"; 19 | } else { 20 | widgetValueType = "int"; 21 | } 22 | } 23 | this.widgetRecord.valueType = widgetValueType; 24 | return this; 25 | } 26 | 27 | addAllowedVals() { 28 | if (this.widgetData?.options?.values) { 29 | this.widgetRecord["allowed values"] = this.widgetData.options.values; 30 | } 31 | return this; 32 | } 33 | 34 | addSelectedProps() { 35 | for (let prop of this.selectedProps) { 36 | if (this.widgetData[prop]) { 37 | this.widgetRecord[prop] = this.widgetData[prop]; 38 | } 39 | } 40 | return this; 41 | } 42 | 43 | addDefaultVal(value) { 44 | if (value) { 45 | this.widgetRecord["userDefaulVal"] = value; 46 | } 47 | return this; 48 | } 49 | 50 | build() { 51 | const temp = this.widgetRecord; 52 | delete this.widgetRecord; 53 | return temp; 54 | } 55 | } 56 | --------------------------------------------------------------------------------