mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 18:22:40 +00:00
Format all code / Add pre-commit format hook (#81)
* Add format-guard * Format code
This commit is contained in:
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
npx lint-staged
|
||||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
||||||
664
package-lock.json
generated
664
package-lock.json
generated
@@ -20,9 +20,12 @@
|
|||||||
"@types/node": "^20.14.8",
|
"@types/node": "^20.14.8",
|
||||||
"babel-plugin-transform-import-meta": "^2.2.1",
|
"babel-plugin-transform-import-meta": "^2.2.1",
|
||||||
"babel-plugin-transform-rename-import": "^2.3.0",
|
"babel-plugin-transform-rename-import": "^2.3.0",
|
||||||
|
"husky": "^9.0.11",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
|
"lint-staged": "^15.2.7",
|
||||||
|
"prettier": "^3.3.2",
|
||||||
"ts-jest": "^29.1.4",
|
"ts-jest": "^29.1.4",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.15.6",
|
"tsx": "^4.15.6",
|
||||||
@@ -3884,6 +3887,87 @@
|
|||||||
"integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==",
|
"integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/cli-cursor": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"restore-cursor": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cli-truncate": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"slice-ansi": "^5.0.0",
|
||||||
|
"string-width": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cli-truncate/node_modules/ansi-regex": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cli-truncate/node_modules/emoji-regex": {
|
||||||
|
"version": "10.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
|
||||||
|
"integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/cli-truncate/node_modules/string-width": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^10.3.0",
|
||||||
|
"get-east-asian-width": "^1.0.0",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cli-truncate/node_modules/strip-ansi": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cliui": {
|
"node_modules/cliui": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
@@ -3929,6 +4013,12 @@
|
|||||||
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
|
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/colorette": {
|
||||||
|
"version": "2.0.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
|
||||||
|
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/combined-stream": {
|
"node_modules/combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
@@ -3941,6 +4031,15 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "12.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||||
|
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -4380,6 +4479,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/execa": {
|
"node_modules/execa": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
||||||
@@ -4577,6 +4682,18 @@
|
|||||||
"node": "6.* || 8.* || >= 10.*"
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-east-asian-width": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-package-type": {
|
"node_modules/get-package-type": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
|
||||||
@@ -4739,6 +4856,21 @@
|
|||||||
"node": ">=10.17.0"
|
"node": ">=10.17.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/husky": {
|
||||||
|
"version": "9.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz",
|
||||||
|
"integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"husky": "bin.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/typicode"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
@@ -6758,12 +6890,293 @@
|
|||||||
"immediate": "~3.0.5"
|
"immediate": "~3.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lilconfig": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antonk52"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lines-and-columns": {
|
"node_modules/lines-and-columns": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/lint-staged": {
|
||||||
|
"version": "15.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.7.tgz",
|
||||||
|
"integrity": "sha512-+FdVbbCZ+yoh7E/RosSdqKJyUM2OEjTciH0TFNkawKgvFp1zbGlEC39RADg+xKBG1R4mhoH2j85myBQZ5wR+lw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "~5.3.0",
|
||||||
|
"commander": "~12.1.0",
|
||||||
|
"debug": "~4.3.4",
|
||||||
|
"execa": "~8.0.1",
|
||||||
|
"lilconfig": "~3.1.1",
|
||||||
|
"listr2": "~8.2.1",
|
||||||
|
"micromatch": "~4.0.7",
|
||||||
|
"pidtree": "~0.6.0",
|
||||||
|
"string-argv": "~0.3.2",
|
||||||
|
"yaml": "~2.4.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"lint-staged": "bin/lint-staged.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.12.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/lint-staged"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lint-staged/node_modules/chalk": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lint-staged/node_modules/execa": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"cross-spawn": "^7.0.3",
|
||||||
|
"get-stream": "^8.0.1",
|
||||||
|
"human-signals": "^5.0.0",
|
||||||
|
"is-stream": "^3.0.0",
|
||||||
|
"merge-stream": "^2.0.0",
|
||||||
|
"npm-run-path": "^5.1.0",
|
||||||
|
"onetime": "^6.0.0",
|
||||||
|
"signal-exit": "^4.1.0",
|
||||||
|
"strip-final-newline": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.17"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lint-staged/node_modules/get-stream": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lint-staged/node_modules/human-signals": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lint-staged/node_modules/is-stream": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lint-staged/node_modules/mimic-fn": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lint-staged/node_modules/npm-run-path": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"path-key": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lint-staged/node_modules/onetime": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-fn": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lint-staged/node_modules/path-key": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lint-staged/node_modules/signal-exit": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lint-staged/node_modules/strip-final-newline": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/listr2": {
|
||||||
|
"version": "8.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.3.tgz",
|
||||||
|
"integrity": "sha512-Lllokma2mtoniUOS94CcOErHWAug5iu7HOmDrvWgpw8jyQH2fomgB+7lZS4HWZxytUuQwkGOwe49FvwVaA85Xw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"cli-truncate": "^4.0.0",
|
||||||
|
"colorette": "^2.0.20",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
|
"log-update": "^6.0.0",
|
||||||
|
"rfdc": "^1.4.1",
|
||||||
|
"wrap-ansi": "^9.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/listr2/node_modules/ansi-regex": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/listr2/node_modules/ansi-styles": {
|
||||||
|
"version": "6.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||||
|
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/listr2/node_modules/emoji-regex": {
|
||||||
|
"version": "10.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
|
||||||
|
"integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/listr2/node_modules/string-width": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^10.3.0",
|
||||||
|
"get-east-asian-width": "^1.0.0",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/listr2/node_modules/strip-ansi": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/listr2/node_modules/wrap-ansi": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
|
||||||
|
"integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^6.2.1",
|
||||||
|
"string-width": "^7.0.0",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||||
@@ -6788,6 +7201,147 @@
|
|||||||
"integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
|
"integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/log-update": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/log-update/-/log-update-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-escapes": "^6.2.0",
|
||||||
|
"cli-cursor": "^4.0.0",
|
||||||
|
"slice-ansi": "^7.0.0",
|
||||||
|
"strip-ansi": "^7.1.0",
|
||||||
|
"wrap-ansi": "^9.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-update/node_modules/ansi-escapes": {
|
||||||
|
"version": "6.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz",
|
||||||
|
"integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-update/node_modules/ansi-regex": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-update/node_modules/ansi-styles": {
|
||||||
|
"version": "6.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||||
|
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-update/node_modules/emoji-regex": {
|
||||||
|
"version": "10.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
|
||||||
|
"integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/log-update/node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"get-east-asian-width": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-update/node_modules/slice-ansi": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^6.2.1",
|
||||||
|
"is-fullwidth-code-point": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-update/node_modules/string-width": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^10.3.0",
|
||||||
|
"get-east-asian-width": "^1.0.0",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-update/node_modules/strip-ansi": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-update/node_modules/wrap-ansi": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
|
||||||
|
"integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^6.2.1",
|
||||||
|
"string-width": "^7.0.0",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
@@ -7140,6 +7694,18 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pidtree": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"pidtree": "bin/pidtree.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pirates": {
|
"node_modules/pirates": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
|
||||||
@@ -7233,6 +7799,21 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pretty-format": {
|
"node_modules/pretty-format": {
|
||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
|
||||||
@@ -7510,6 +8091,22 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/restore-cursor": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"onetime": "^5.1.0",
|
||||||
|
"signal-exit": "^3.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reusify": {
|
"node_modules/reusify": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||||
@@ -7520,6 +8117,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rfdc": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.18.0",
|
"version": "4.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz",
|
||||||
@@ -7659,6 +8262,46 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/slice-ansi": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^6.0.0",
|
||||||
|
"is-fullwidth-code-point": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/slice-ansi/node_modules/ansi-styles": {
|
||||||
|
"version": "6.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||||
|
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
@@ -7723,6 +8366,15 @@
|
|||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string-argv": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6.19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string-length": {
|
"node_modules/string-length": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
|
||||||
@@ -8850,6 +9502,18 @@
|
|||||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/yaml": {
|
||||||
|
"version": "2.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz",
|
||||||
|
"integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"yaml": "bin.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yargs": {
|
"node_modules/yargs": {
|
||||||
"version": "17.7.2",
|
"version": "17.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -8,6 +8,7 @@
|
|||||||
"build": "npm run typecheck && vite build",
|
"build": "npm run typecheck && vite build",
|
||||||
"zipdist": "node scripts/zipdist.js",
|
"zipdist": "node scripts/zipdist.js",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
|
"format": "prettier --write 'src/**/*.{js,ts,tsx}'",
|
||||||
"test": "npm run build && jest",
|
"test": "npm run build && jest",
|
||||||
"test:generate": "npx tsx tests-ui/setup",
|
"test:generate": "npx tsx tests-ui/setup",
|
||||||
"test:browser": "npx playwright test",
|
"test:browser": "npx playwright test",
|
||||||
@@ -21,9 +22,12 @@
|
|||||||
"@types/node": "^20.14.8",
|
"@types/node": "^20.14.8",
|
||||||
"babel-plugin-transform-import-meta": "^2.2.1",
|
"babel-plugin-transform-import-meta": "^2.2.1",
|
||||||
"babel-plugin-transform-rename-import": "^2.3.0",
|
"babel-plugin-transform-rename-import": "^2.3.0",
|
||||||
|
"husky": "^9.0.11",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
|
"lint-staged": "^15.2.7",
|
||||||
|
"prettier": "^3.3.2",
|
||||||
"ts-jest": "^29.1.4",
|
"ts-jest": "^29.1.4",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsx": "^4.15.6",
|
"tsx": "^4.15.6",
|
||||||
@@ -36,5 +40,11 @@
|
|||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
"zod-validation-error": "^3.3.0"
|
"zod-validation-error": "^3.3.0"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"src/**/*.{js,ts,tsx}": [
|
||||||
|
"prettier --write",
|
||||||
|
"git add"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,164 +3,182 @@ import { ComfyDialog, $el } from "../../scripts/ui";
|
|||||||
import { ComfyApp } from "../../scripts/app";
|
import { ComfyApp } from "../../scripts/app";
|
||||||
|
|
||||||
export class ClipspaceDialog extends ComfyDialog {
|
export class ClipspaceDialog extends ComfyDialog {
|
||||||
static items = [];
|
static items = [];
|
||||||
static instance = null;
|
static instance = null;
|
||||||
|
|
||||||
static registerButton(name, contextPredicate, callback) {
|
static registerButton(name, contextPredicate, callback) {
|
||||||
const item =
|
const item = $el("button", {
|
||||||
$el("button", {
|
type: "button",
|
||||||
type: "button",
|
textContent: name,
|
||||||
textContent: name,
|
contextPredicate: contextPredicate,
|
||||||
contextPredicate: contextPredicate,
|
onclick: callback,
|
||||||
onclick: callback
|
});
|
||||||
})
|
|
||||||
|
|
||||||
ClipspaceDialog.items.push(item);
|
ClipspaceDialog.items.push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
static invalidatePreview() {
|
static invalidatePreview() {
|
||||||
if(ComfyApp.clipspace && ComfyApp.clipspace.imgs && ComfyApp.clipspace.imgs.length > 0) {
|
if (
|
||||||
const img_preview = document.getElementById("clipspace_preview") as HTMLImageElement;
|
ComfyApp.clipspace &&
|
||||||
if(img_preview) {
|
ComfyApp.clipspace.imgs &&
|
||||||
img_preview.src = ComfyApp.clipspace.imgs[ComfyApp.clipspace['selectedIndex']].src;
|
ComfyApp.clipspace.imgs.length > 0
|
||||||
img_preview.style.maxHeight = "100%";
|
) {
|
||||||
img_preview.style.maxWidth = "100%";
|
const img_preview = document.getElementById(
|
||||||
}
|
"clipspace_preview"
|
||||||
}
|
) as HTMLImageElement;
|
||||||
}
|
if (img_preview) {
|
||||||
|
img_preview.src =
|
||||||
|
ComfyApp.clipspace.imgs[ComfyApp.clipspace["selectedIndex"]].src;
|
||||||
|
img_preview.style.maxHeight = "100%";
|
||||||
|
img_preview.style.maxWidth = "100%";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static invalidate() {
|
static invalidate() {
|
||||||
if(ClipspaceDialog.instance) {
|
if (ClipspaceDialog.instance) {
|
||||||
const self = ClipspaceDialog.instance;
|
const self = ClipspaceDialog.instance;
|
||||||
// allow reconstruct controls when copying from non-image to image content.
|
// allow reconstruct controls when copying from non-image to image content.
|
||||||
const children = $el("div.comfy-modal-content", [ self.createImgSettings(), ...self.createButtons() ]);
|
const children = $el("div.comfy-modal-content", [
|
||||||
|
self.createImgSettings(),
|
||||||
|
...self.createButtons(),
|
||||||
|
]);
|
||||||
|
|
||||||
if(self.element) {
|
if (self.element) {
|
||||||
// update
|
// update
|
||||||
self.element.removeChild(self.element.firstChild);
|
self.element.removeChild(self.element.firstChild);
|
||||||
self.element.appendChild(children);
|
self.element.appendChild(children);
|
||||||
}
|
} else {
|
||||||
else {
|
// new
|
||||||
// new
|
self.element = $el("div.comfy-modal", { parent: document.body }, [
|
||||||
self.element = $el("div.comfy-modal", { parent: document.body }, [children,]);
|
children,
|
||||||
}
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
if(self.element.children[0].children.length <= 1) {
|
if (self.element.children[0].children.length <= 1) {
|
||||||
self.element.children[0].appendChild($el("p", {}, ["Unable to find the features to edit content of a format stored in the current Clipspace."]));
|
self.element.children[0].appendChild(
|
||||||
}
|
$el("p", {}, [
|
||||||
|
"Unable to find the features to edit content of a format stored in the current Clipspace.",
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
ClipspaceDialog.invalidatePreview();
|
ClipspaceDialog.invalidatePreview();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
createButtons() {
|
createButtons() {
|
||||||
const buttons = [];
|
const buttons = [];
|
||||||
|
|
||||||
for(let idx in ClipspaceDialog.items) {
|
for (let idx in ClipspaceDialog.items) {
|
||||||
const item = ClipspaceDialog.items[idx];
|
const item = ClipspaceDialog.items[idx];
|
||||||
if(!item.contextPredicate || item.contextPredicate())
|
if (!item.contextPredicate || item.contextPredicate())
|
||||||
buttons.push(ClipspaceDialog.items[idx]);
|
buttons.push(ClipspaceDialog.items[idx]);
|
||||||
}
|
}
|
||||||
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
$el("button", {
|
$el("button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Close",
|
textContent: "Close",
|
||||||
onclick: () => { this.close(); }
|
onclick: () => {
|
||||||
})
|
this.close();
|
||||||
);
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return buttons;
|
return buttons;
|
||||||
}
|
}
|
||||||
|
|
||||||
createImgSettings() {
|
createImgSettings() {
|
||||||
if(ComfyApp.clipspace.imgs) {
|
if (ComfyApp.clipspace.imgs) {
|
||||||
const combo_items = [];
|
const combo_items = [];
|
||||||
const imgs = ComfyApp.clipspace.imgs;
|
const imgs = ComfyApp.clipspace.imgs;
|
||||||
|
|
||||||
for(let i=0; i < imgs.length; i++) {
|
for (let i = 0; i < imgs.length; i++) {
|
||||||
combo_items.push($el("option", {value:i}, [`${i}`]));
|
combo_items.push($el("option", { value: i }, [`${i}`]));
|
||||||
}
|
}
|
||||||
|
|
||||||
const combo1 = $el("select",
|
const combo1 = $el(
|
||||||
{id:"clipspace_img_selector", onchange:(event) => {
|
"select",
|
||||||
ComfyApp.clipspace['selectedIndex'] = event.target.selectedIndex;
|
{
|
||||||
ClipspaceDialog.invalidatePreview();
|
id: "clipspace_img_selector",
|
||||||
} }, combo_items);
|
onchange: (event) => {
|
||||||
|
ComfyApp.clipspace["selectedIndex"] = event.target.selectedIndex;
|
||||||
|
ClipspaceDialog.invalidatePreview();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
combo_items
|
||||||
|
);
|
||||||
|
|
||||||
const row1 =
|
const row1 = $el("tr", {}, [
|
||||||
$el("tr", {},
|
$el("td", {}, [$el("font", { color: "white" }, ["Select Image"])]),
|
||||||
[
|
$el("td", {}, [combo1]),
|
||||||
$el("td", {}, [$el("font", {color:"white"}, ["Select Image"])]),
|
]);
|
||||||
$el("td", {}, [combo1])
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
const combo2 = $el(
|
||||||
|
"select",
|
||||||
|
{
|
||||||
|
id: "clipspace_img_paste_mode",
|
||||||
|
onchange: (event) => {
|
||||||
|
ComfyApp.clipspace["img_paste_mode"] = event.target.value;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[
|
||||||
|
$el("option", { value: "selected" }, "selected"),
|
||||||
|
$el("option", { value: "all" }, "all"),
|
||||||
|
]
|
||||||
|
) as HTMLSelectElement;
|
||||||
|
combo2.value = ComfyApp.clipspace["img_paste_mode"];
|
||||||
|
|
||||||
const combo2 = $el("select",
|
const row2 = $el("tr", {}, [
|
||||||
{id:"clipspace_img_paste_mode", onchange:(event) => {
|
$el("td", {}, [$el("font", { color: "white" }, ["Paste Mode"])]),
|
||||||
ComfyApp.clipspace['img_paste_mode'] = event.target.value;
|
$el("td", {}, [combo2]),
|
||||||
} },
|
]);
|
||||||
[
|
|
||||||
$el("option", {value:'selected'}, 'selected'),
|
|
||||||
$el("option", {value:'all'}, 'all')
|
|
||||||
]) as HTMLSelectElement;
|
|
||||||
combo2.value = ComfyApp.clipspace['img_paste_mode'];
|
|
||||||
|
|
||||||
const row2 =
|
const td = $el(
|
||||||
$el("tr", {},
|
"td",
|
||||||
[
|
{ align: "center", width: "100px", height: "100px", colSpan: "2" },
|
||||||
$el("td", {}, [$el("font", {color:"white"}, ["Paste Mode"])]),
|
[$el("img", { id: "clipspace_preview", ondragstart: () => false }, [])]
|
||||||
$el("td", {}, [combo2])
|
);
|
||||||
]);
|
|
||||||
|
|
||||||
const td = $el("td", {align:'center', width:'100px', height:'100px', colSpan:'2'},
|
const row3 = $el("tr", {}, [td]);
|
||||||
[ $el("img",{id:"clipspace_preview", ondragstart:() => false},[]) ]);
|
|
||||||
|
|
||||||
const row3 =
|
return $el("table", {}, [row1, row2, row3]);
|
||||||
$el("tr", {}, [td]);
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $el("table", {}, [row1, row2, row3]);
|
createImgPreview() {
|
||||||
}
|
if (ComfyApp.clipspace.imgs) {
|
||||||
else {
|
return $el("img", { id: "clipspace_preview", ondragstart: () => false });
|
||||||
return [];
|
} else return [];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
createImgPreview() {
|
show() {
|
||||||
if(ComfyApp.clipspace.imgs) {
|
const img_preview = document.getElementById("clipspace_preview");
|
||||||
return $el("img",{id:"clipspace_preview", ondragstart:() => false});
|
ClipspaceDialog.invalidate();
|
||||||
}
|
|
||||||
else
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
show() {
|
this.element.style.display = "block";
|
||||||
const img_preview = document.getElementById("clipspace_preview");
|
}
|
||||||
ClipspaceDialog.invalidate();
|
|
||||||
|
|
||||||
this.element.style.display = "block";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "Comfy.Clipspace",
|
name: "Comfy.Clipspace",
|
||||||
init(app) {
|
init(app) {
|
||||||
app.openClipspace =
|
app.openClipspace = function () {
|
||||||
function () {
|
if (!ClipspaceDialog.instance) {
|
||||||
if(!ClipspaceDialog.instance) {
|
ClipspaceDialog.instance = new ClipspaceDialog();
|
||||||
ClipspaceDialog.instance = new ClipspaceDialog();
|
ComfyApp.clipspace_invalidate_handler = ClipspaceDialog.invalidate;
|
||||||
ComfyApp.clipspace_invalidate_handler = ClipspaceDialog.invalidate;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if(ComfyApp.clipspace) {
|
if (ComfyApp.clipspace) {
|
||||||
ClipspaceDialog.instance.show();
|
ClipspaceDialog.instance.show();
|
||||||
}
|
} else app.ui.dialog.show("Clipspace is Empty!");
|
||||||
else
|
};
|
||||||
app.ui.dialog.show("Clipspace is Empty!");
|
},
|
||||||
};
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,150 +1,171 @@
|
|||||||
import {app} from "../../scripts/app";
|
import { app } from "../../scripts/app";
|
||||||
|
|
||||||
// Adds filtering to combo context menus
|
// Adds filtering to combo context menus
|
||||||
|
|
||||||
const ext = {
|
const ext = {
|
||||||
name: "Comfy.ContextMenuFilter",
|
name: "Comfy.ContextMenuFilter",
|
||||||
init() {
|
init() {
|
||||||
const ctxMenu = LiteGraph.ContextMenu;
|
const ctxMenu = LiteGraph.ContextMenu;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// TODO Very hacky way to modify Litegraph behaviour. Fix this later.
|
// TODO Very hacky way to modify Litegraph behaviour. Fix this later.
|
||||||
LiteGraph.ContextMenu = function (values, options) {
|
LiteGraph.ContextMenu = function (values, options) {
|
||||||
const ctx = ctxMenu.call(this, values, options);
|
const ctx = ctxMenu.call(this, values, options);
|
||||||
|
|
||||||
// If we are a dark menu (only used for combo boxes) then add a filter input
|
// If we are a dark menu (only used for combo boxes) then add a filter input
|
||||||
if (options?.className === "dark" && values?.length > 10) {
|
if (options?.className === "dark" && values?.length > 10) {
|
||||||
const filter = document.createElement("input");
|
const filter = document.createElement("input");
|
||||||
filter.classList.add("comfy-context-menu-filter");
|
filter.classList.add("comfy-context-menu-filter");
|
||||||
filter.placeholder = "Filter list";
|
filter.placeholder = "Filter list";
|
||||||
this.root.prepend(filter);
|
this.root.prepend(filter);
|
||||||
|
|
||||||
const items = Array.from(this.root.querySelectorAll(".litemenu-entry")) as HTMLElement[];
|
const items = Array.from(
|
||||||
let displayedItems = [...items];
|
this.root.querySelectorAll(".litemenu-entry")
|
||||||
let itemCount = displayedItems.length;
|
) as HTMLElement[];
|
||||||
|
let displayedItems = [...items];
|
||||||
|
let itemCount = displayedItems.length;
|
||||||
|
|
||||||
// We must request an animation frame for the current node of the active canvas to update.
|
// We must request an animation frame for the current node of the active canvas to update.
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const currentNode = LGraphCanvas.active_canvas.current_node;
|
const currentNode = LGraphCanvas.active_canvas.current_node;
|
||||||
const clickedComboValue = currentNode.widgets
|
const clickedComboValue = currentNode.widgets
|
||||||
?.filter(w => w.type === "combo" && w.options.values.length === values.length)
|
?.filter(
|
||||||
.find(w => w.options.values.every((v, i) => v === values[i]))
|
(w) =>
|
||||||
?.value;
|
w.type === "combo" && w.options.values.length === values.length
|
||||||
|
)
|
||||||
|
.find((w) =>
|
||||||
|
w.options.values.every((v, i) => v === values[i])
|
||||||
|
)?.value;
|
||||||
|
|
||||||
let selectedIndex = clickedComboValue ? values.findIndex(v => v === clickedComboValue) : 0;
|
let selectedIndex = clickedComboValue
|
||||||
if (selectedIndex < 0) {
|
? values.findIndex((v) => v === clickedComboValue)
|
||||||
selectedIndex = 0;
|
: 0;
|
||||||
}
|
if (selectedIndex < 0) {
|
||||||
let selectedItem = displayedItems[selectedIndex];
|
selectedIndex = 0;
|
||||||
updateSelected();
|
}
|
||||||
|
let selectedItem = displayedItems[selectedIndex];
|
||||||
|
updateSelected();
|
||||||
|
|
||||||
// Apply highlighting to the selected item
|
// Apply highlighting to the selected item
|
||||||
function updateSelected() {
|
function updateSelected() {
|
||||||
selectedItem?.style.setProperty("background-color", "");
|
selectedItem?.style.setProperty("background-color", "");
|
||||||
selectedItem?.style.setProperty("color", "");
|
selectedItem?.style.setProperty("color", "");
|
||||||
selectedItem = displayedItems[selectedIndex];
|
selectedItem = displayedItems[selectedIndex];
|
||||||
selectedItem?.style.setProperty("background-color", "#ccc", "important");
|
selectedItem?.style.setProperty(
|
||||||
selectedItem?.style.setProperty("color", "#000", "important");
|
"background-color",
|
||||||
}
|
"#ccc",
|
||||||
|
"important"
|
||||||
|
);
|
||||||
|
selectedItem?.style.setProperty("color", "#000", "important");
|
||||||
|
}
|
||||||
|
|
||||||
const positionList = () => {
|
const positionList = () => {
|
||||||
const rect = this.root.getBoundingClientRect();
|
const rect = this.root.getBoundingClientRect();
|
||||||
|
|
||||||
// If the top is off-screen then shift the element with scaling applied
|
// If the top is off-screen then shift the element with scaling applied
|
||||||
if (rect.top < 0) {
|
if (rect.top < 0) {
|
||||||
const scale = 1 - this.root.getBoundingClientRect().height / this.root.clientHeight;
|
const scale =
|
||||||
const shift = (this.root.clientHeight * scale) / 2;
|
1 -
|
||||||
this.root.style.top = -shift + "px";
|
this.root.getBoundingClientRect().height /
|
||||||
}
|
this.root.clientHeight;
|
||||||
}
|
const shift = (this.root.clientHeight * scale) / 2;
|
||||||
|
this.root.style.top = -shift + "px";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Arrow up/down to select items
|
// Arrow up/down to select items
|
||||||
filter.addEventListener("keydown", (event) => {
|
filter.addEventListener("keydown", (event) => {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case "ArrowUp":
|
case "ArrowUp":
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (selectedIndex === 0) {
|
if (selectedIndex === 0) {
|
||||||
selectedIndex = itemCount - 1;
|
selectedIndex = itemCount - 1;
|
||||||
} else {
|
} else {
|
||||||
selectedIndex--;
|
selectedIndex--;
|
||||||
}
|
}
|
||||||
updateSelected();
|
updateSelected();
|
||||||
break;
|
break;
|
||||||
case "ArrowRight":
|
case "ArrowRight":
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
selectedIndex = itemCount - 1;
|
selectedIndex = itemCount - 1;
|
||||||
updateSelected();
|
updateSelected();
|
||||||
break;
|
break;
|
||||||
case "ArrowDown":
|
case "ArrowDown":
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (selectedIndex === itemCount - 1) {
|
if (selectedIndex === itemCount - 1) {
|
||||||
selectedIndex = 0;
|
selectedIndex = 0;
|
||||||
} else {
|
} else {
|
||||||
selectedIndex++;
|
selectedIndex++;
|
||||||
}
|
}
|
||||||
updateSelected();
|
updateSelected();
|
||||||
break;
|
break;
|
||||||
case "ArrowLeft":
|
case "ArrowLeft":
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
selectedIndex = 0;
|
selectedIndex = 0;
|
||||||
updateSelected();
|
updateSelected();
|
||||||
break;
|
break;
|
||||||
case "Enter":
|
case "Enter":
|
||||||
selectedItem?.click();
|
selectedItem?.click();
|
||||||
break;
|
break;
|
||||||
case "Escape":
|
case "Escape":
|
||||||
this.close();
|
this.close();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
filter.addEventListener("input", () => {
|
filter.addEventListener("input", () => {
|
||||||
// Hide all items that don't match our filter
|
// Hide all items that don't match our filter
|
||||||
const term = filter.value.toLocaleLowerCase();
|
const term = filter.value.toLocaleLowerCase();
|
||||||
// When filtering, recompute which items are visible for arrow up/down and maintain selection.
|
// When filtering, recompute which items are visible for arrow up/down and maintain selection.
|
||||||
displayedItems = items.filter(item => {
|
displayedItems = items.filter((item) => {
|
||||||
const isVisible = !term || item.textContent.toLocaleLowerCase().includes(term);
|
const isVisible =
|
||||||
item.style.display = isVisible ? "block" : "none";
|
!term || item.textContent.toLocaleLowerCase().includes(term);
|
||||||
return isVisible;
|
item.style.display = isVisible ? "block" : "none";
|
||||||
});
|
return isVisible;
|
||||||
|
});
|
||||||
|
|
||||||
selectedIndex = 0;
|
selectedIndex = 0;
|
||||||
if (displayedItems.includes(selectedItem)) {
|
if (displayedItems.includes(selectedItem)) {
|
||||||
selectedIndex = displayedItems.findIndex(d => d === selectedItem);
|
selectedIndex = displayedItems.findIndex(
|
||||||
}
|
(d) => d === selectedItem
|
||||||
itemCount = displayedItems.length;
|
);
|
||||||
|
}
|
||||||
|
itemCount = displayedItems.length;
|
||||||
|
|
||||||
updateSelected();
|
updateSelected();
|
||||||
|
|
||||||
// If we have an event then we can try and position the list under the source
|
// If we have an event then we can try and position the list under the source
|
||||||
if (options.event) {
|
if (options.event) {
|
||||||
let top = options.event.clientY - 10;
|
let top = options.event.clientY - 10;
|
||||||
|
|
||||||
const bodyRect = document.body.getBoundingClientRect();
|
const bodyRect = document.body.getBoundingClientRect();
|
||||||
const rootRect = this.root.getBoundingClientRect();
|
const rootRect = this.root.getBoundingClientRect();
|
||||||
if (bodyRect.height && top > bodyRect.height - rootRect.height - 10) {
|
if (
|
||||||
top = Math.max(0, bodyRect.height - rootRect.height - 10);
|
bodyRect.height &&
|
||||||
}
|
top > bodyRect.height - rootRect.height - 10
|
||||||
|
) {
|
||||||
|
top = Math.max(0, bodyRect.height - rootRect.height - 10);
|
||||||
|
}
|
||||||
|
|
||||||
this.root.style.top = top + "px";
|
this.root.style.top = top + "px";
|
||||||
positionList();
|
positionList();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
// Focus the filter box when opening
|
// Focus the filter box when opening
|
||||||
filter.focus();
|
filter.focus();
|
||||||
|
|
||||||
positionList();
|
positionList();
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx;
|
return ctx;
|
||||||
};
|
};
|
||||||
|
|
||||||
LiteGraph.ContextMenu.prototype = ctxMenu.prototype;
|
LiteGraph.ContextMenu.prototype = ctxMenu.prototype;
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
app.registerExtension(ext);
|
app.registerExtension(ext);
|
||||||
|
|||||||
@@ -7,42 +7,46 @@ import { app } from "../../scripts/app";
|
|||||||
* Strips C-style line and block comments from a string
|
* Strips C-style line and block comments from a string
|
||||||
*/
|
*/
|
||||||
function stripComments(str) {
|
function stripComments(str) {
|
||||||
return str.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g,'');
|
return str.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "Comfy.DynamicPrompts",
|
name: "Comfy.DynamicPrompts",
|
||||||
nodeCreated(node) {
|
nodeCreated(node) {
|
||||||
if (node.widgets) {
|
if (node.widgets) {
|
||||||
// Locate dynamic prompt text widgets
|
// Locate dynamic prompt text widgets
|
||||||
// Include any widgets with dynamicPrompts set to true, and customtext
|
// Include any widgets with dynamicPrompts set to true, and customtext
|
||||||
const widgets = node.widgets.filter(
|
const widgets = node.widgets.filter((n) => n.dynamicPrompts);
|
||||||
(n) => n.dynamicPrompts
|
for (const widget of widgets) {
|
||||||
);
|
// Override the serialization of the value to resolve dynamic prompts for all widgets supporting it in this node
|
||||||
for (const widget of widgets) {
|
widget.serializeValue = (workflowNode, widgetIndex) => {
|
||||||
// Override the serialization of the value to resolve dynamic prompts for all widgets supporting it in this node
|
let prompt = stripComments(widget.value);
|
||||||
widget.serializeValue = (workflowNode, widgetIndex) => {
|
while (
|
||||||
let prompt = stripComments(widget.value);
|
prompt.replace("\\{", "").includes("{") &&
|
||||||
while (prompt.replace("\\{", "").includes("{") && prompt.replace("\\}", "").includes("}")) {
|
prompt.replace("\\}", "").includes("}")
|
||||||
const startIndex = prompt.replace("\\{", "00").indexOf("{");
|
) {
|
||||||
const endIndex = prompt.replace("\\}", "00").indexOf("}");
|
const startIndex = prompt.replace("\\{", "00").indexOf("{");
|
||||||
|
const endIndex = prompt.replace("\\}", "00").indexOf("}");
|
||||||
|
|
||||||
const optionsString = prompt.substring(startIndex + 1, endIndex);
|
const optionsString = prompt.substring(startIndex + 1, endIndex);
|
||||||
const options = optionsString.split("|");
|
const options = optionsString.split("|");
|
||||||
|
|
||||||
const randomIndex = Math.floor(Math.random() * options.length);
|
const randomIndex = Math.floor(Math.random() * options.length);
|
||||||
const randomOption = options[randomIndex];
|
const randomOption = options[randomIndex];
|
||||||
|
|
||||||
prompt = prompt.substring(0, startIndex) + randomOption + prompt.substring(endIndex + 1);
|
prompt =
|
||||||
}
|
prompt.substring(0, startIndex) +
|
||||||
|
randomOption +
|
||||||
|
prompt.substring(endIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
// Overwrite the value in the serialized workflow pnginfo
|
// Overwrite the value in the serialized workflow pnginfo
|
||||||
if (workflowNode?.widgets_values)
|
if (workflowNode?.widgets_values)
|
||||||
workflowNode.widgets_values[widgetIndex] = prompt;
|
workflowNode.widgets_values[widgetIndex] = prompt;
|
||||||
|
|
||||||
return prompt;
|
return prompt;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,142 +3,159 @@ import { app } from "../../scripts/app";
|
|||||||
// Allows you to edit the attention weight by holding ctrl (or cmd) and using the up/down arrow keys
|
// Allows you to edit the attention weight by holding ctrl (or cmd) and using the up/down arrow keys
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "Comfy.EditAttention",
|
name: "Comfy.EditAttention",
|
||||||
init() {
|
init() {
|
||||||
const editAttentionDelta = app.ui.settings.addSetting({
|
const editAttentionDelta = app.ui.settings.addSetting({
|
||||||
id: "Comfy.EditAttention.Delta",
|
id: "Comfy.EditAttention.Delta",
|
||||||
name: "Ctrl+up/down precision",
|
name: "Ctrl+up/down precision",
|
||||||
type: "slider",
|
type: "slider",
|
||||||
attrs: {
|
attrs: {
|
||||||
min: 0.01,
|
min: 0.01,
|
||||||
max: 0.5,
|
max: 0.5,
|
||||||
step: 0.01,
|
step: 0.01,
|
||||||
},
|
},
|
||||||
defaultValue: 0.05,
|
defaultValue: 0.05,
|
||||||
});
|
});
|
||||||
|
|
||||||
function incrementWeight(weight, delta) {
|
function incrementWeight(weight, delta) {
|
||||||
const floatWeight = parseFloat(weight);
|
const floatWeight = parseFloat(weight);
|
||||||
if (isNaN(floatWeight)) return weight;
|
if (isNaN(floatWeight)) return weight;
|
||||||
const newWeight = floatWeight + delta;
|
const newWeight = floatWeight + delta;
|
||||||
if (newWeight < 0) return "0";
|
if (newWeight < 0) return "0";
|
||||||
return String(Number(newWeight.toFixed(10)));
|
return String(Number(newWeight.toFixed(10)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNearestEnclosure(text, cursorPos) {
|
||||||
|
let start = cursorPos,
|
||||||
|
end = cursorPos;
|
||||||
|
let openCount = 0,
|
||||||
|
closeCount = 0;
|
||||||
|
|
||||||
|
// Find opening parenthesis before cursor
|
||||||
|
while (start >= 0) {
|
||||||
|
start--;
|
||||||
|
if (text[start] === "(" && openCount === closeCount) break;
|
||||||
|
if (text[start] === "(") openCount++;
|
||||||
|
if (text[start] === ")") closeCount++;
|
||||||
|
}
|
||||||
|
if (start < 0) return false;
|
||||||
|
|
||||||
|
openCount = 0;
|
||||||
|
closeCount = 0;
|
||||||
|
|
||||||
|
// Find closing parenthesis after cursor
|
||||||
|
while (end < text.length) {
|
||||||
|
if (text[end] === ")" && openCount === closeCount) break;
|
||||||
|
if (text[end] === "(") openCount++;
|
||||||
|
if (text[end] === ")") closeCount++;
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
if (end === text.length) return false;
|
||||||
|
|
||||||
|
return { start: start + 1, end: end };
|
||||||
|
}
|
||||||
|
|
||||||
|
function addWeightToParentheses(text) {
|
||||||
|
const parenRegex = /^\((.*)\)$/;
|
||||||
|
const parenMatch = text.match(parenRegex);
|
||||||
|
|
||||||
|
const floatRegex = /:([+-]?(\d*\.)?\d+([eE][+-]?\d+)?)/;
|
||||||
|
const floatMatch = text.match(floatRegex);
|
||||||
|
|
||||||
|
if (parenMatch && !floatMatch) {
|
||||||
|
return `(${parenMatch[1]}:1.0)`;
|
||||||
|
} else {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function editAttention(event) {
|
||||||
|
const inputField = event.composedPath()[0];
|
||||||
|
const delta = parseFloat(editAttentionDelta.value);
|
||||||
|
|
||||||
|
if (inputField.tagName !== "TEXTAREA") return;
|
||||||
|
if (!(event.key === "ArrowUp" || event.key === "ArrowDown")) return;
|
||||||
|
if (!event.ctrlKey && !event.metaKey) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
let start = inputField.selectionStart;
|
||||||
|
let end = inputField.selectionEnd;
|
||||||
|
let selectedText = inputField.value.substring(start, end);
|
||||||
|
|
||||||
|
// If there is no selection, attempt to find the nearest enclosure, or select the current word
|
||||||
|
if (!selectedText) {
|
||||||
|
const nearestEnclosure = findNearestEnclosure(inputField.value, start);
|
||||||
|
if (nearestEnclosure) {
|
||||||
|
start = nearestEnclosure.start;
|
||||||
|
end = nearestEnclosure.end;
|
||||||
|
selectedText = inputField.value.substring(start, end);
|
||||||
|
} else {
|
||||||
|
// Select the current word, find the start and end of the word
|
||||||
|
const delimiters = " .,\\/!?%^*;:{}=-_`~()\r\n\t";
|
||||||
|
|
||||||
|
while (
|
||||||
|
!delimiters.includes(inputField.value[start - 1]) &&
|
||||||
|
start > 0
|
||||||
|
) {
|
||||||
|
start--;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (
|
||||||
|
!delimiters.includes(inputField.value[end]) &&
|
||||||
|
end < inputField.value.length
|
||||||
|
) {
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedText = inputField.value.substring(start, end);
|
||||||
|
if (!selectedText) return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function findNearestEnclosure(text, cursorPos) {
|
// If the selection ends with a space, remove it
|
||||||
let start = cursorPos, end = cursorPos;
|
if (selectedText[selectedText.length - 1] === " ") {
|
||||||
let openCount = 0, closeCount = 0;
|
selectedText = selectedText.substring(0, selectedText.length - 1);
|
||||||
|
end -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Find opening parenthesis before cursor
|
// If there are parentheses left and right of the selection, select them
|
||||||
while (start >= 0) {
|
if (
|
||||||
start--;
|
inputField.value[start - 1] === "(" &&
|
||||||
if (text[start] === "(" && openCount === closeCount) break;
|
inputField.value[end] === ")"
|
||||||
if (text[start] === "(") openCount++;
|
) {
|
||||||
if (text[start] === ")") closeCount++;
|
start -= 1;
|
||||||
}
|
end += 1;
|
||||||
if (start < 0) return false;
|
selectedText = inputField.value.substring(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
openCount = 0;
|
// If the selection is not enclosed in parentheses, add them
|
||||||
closeCount = 0;
|
if (
|
||||||
|
selectedText[0] !== "(" ||
|
||||||
|
selectedText[selectedText.length - 1] !== ")"
|
||||||
|
) {
|
||||||
|
selectedText = `(${selectedText})`;
|
||||||
|
}
|
||||||
|
|
||||||
// Find closing parenthesis after cursor
|
// If the selection does not have a weight, add a weight of 1.0
|
||||||
while (end < text.length) {
|
selectedText = addWeightToParentheses(selectedText);
|
||||||
if (text[end] === ")" && openCount === closeCount) break;
|
|
||||||
if (text[end] === "(") openCount++;
|
|
||||||
if (text[end] === ")") closeCount++;
|
|
||||||
end++;
|
|
||||||
}
|
|
||||||
if (end === text.length) return false;
|
|
||||||
|
|
||||||
return { start: start + 1, end: end };
|
// Increment the weight
|
||||||
|
const weightDelta = event.key === "ArrowUp" ? delta : -delta;
|
||||||
|
const updatedText = selectedText.replace(
|
||||||
|
/\((.*):(\d+(?:\.\d+)?)\)/,
|
||||||
|
(match, text, weight) => {
|
||||||
|
weight = incrementWeight(weight, weightDelta);
|
||||||
|
if (weight == 1) {
|
||||||
|
return text;
|
||||||
|
} else {
|
||||||
|
return `(${text}:${weight})`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function addWeightToParentheses(text) {
|
inputField.setRangeText(updatedText, start, end, "select");
|
||||||
const parenRegex = /^\((.*)\)$/;
|
}
|
||||||
const parenMatch = text.match(parenRegex);
|
window.addEventListener("keydown", editAttention);
|
||||||
|
},
|
||||||
const floatRegex = /:([+-]?(\d*\.)?\d+([eE][+-]?\d+)?)/;
|
|
||||||
const floatMatch = text.match(floatRegex);
|
|
||||||
|
|
||||||
if (parenMatch && !floatMatch) {
|
|
||||||
return `(${parenMatch[1]}:1.0)`;
|
|
||||||
} else {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function editAttention(event) {
|
|
||||||
const inputField = event.composedPath()[0];
|
|
||||||
const delta = parseFloat(editAttentionDelta.value);
|
|
||||||
|
|
||||||
if (inputField.tagName !== "TEXTAREA") return;
|
|
||||||
if (!(event.key === "ArrowUp" || event.key === "ArrowDown")) return;
|
|
||||||
if (!event.ctrlKey && !event.metaKey) return;
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
let start = inputField.selectionStart;
|
|
||||||
let end = inputField.selectionEnd;
|
|
||||||
let selectedText = inputField.value.substring(start, end);
|
|
||||||
|
|
||||||
// If there is no selection, attempt to find the nearest enclosure, or select the current word
|
|
||||||
if (!selectedText) {
|
|
||||||
const nearestEnclosure = findNearestEnclosure(inputField.value, start);
|
|
||||||
if (nearestEnclosure) {
|
|
||||||
start = nearestEnclosure.start;
|
|
||||||
end = nearestEnclosure.end;
|
|
||||||
selectedText = inputField.value.substring(start, end);
|
|
||||||
} else {
|
|
||||||
// Select the current word, find the start and end of the word
|
|
||||||
const delimiters = " .,\\/!?%^*;:{}=-_`~()\r\n\t";
|
|
||||||
|
|
||||||
while (!delimiters.includes(inputField.value[start - 1]) && start > 0) {
|
|
||||||
start--;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (!delimiters.includes(inputField.value[end]) && end < inputField.value.length) {
|
|
||||||
end++;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedText = inputField.value.substring(start, end);
|
|
||||||
if (!selectedText) return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the selection ends with a space, remove it
|
|
||||||
if (selectedText[selectedText.length - 1] === " ") {
|
|
||||||
selectedText = selectedText.substring(0, selectedText.length - 1);
|
|
||||||
end -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are parentheses left and right of the selection, select them
|
|
||||||
if (inputField.value[start - 1] === "(" && inputField.value[end] === ")") {
|
|
||||||
start -= 1;
|
|
||||||
end += 1;
|
|
||||||
selectedText = inputField.value.substring(start, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the selection is not enclosed in parentheses, add them
|
|
||||||
if (selectedText[0] !== "(" || selectedText[selectedText.length - 1] !== ")") {
|
|
||||||
selectedText = `(${selectedText})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the selection does not have a weight, add a weight of 1.0
|
|
||||||
selectedText = addWeightToParentheses(selectedText);
|
|
||||||
|
|
||||||
// Increment the weight
|
|
||||||
const weightDelta = event.key === "ArrowUp" ? delta : -delta;
|
|
||||||
const updatedText = selectedText.replace(/\((.*):(\d+(?:\.\d+)?)\)/, (match, text, weight) => {
|
|
||||||
weight = incrementWeight(weight, weightDelta);
|
|
||||||
if (weight == 1) {
|
|
||||||
return text;
|
|
||||||
} else {
|
|
||||||
return `(${text}:${weight})`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
inputField.setRangeText(updatedText, start, end, "select");
|
|
||||||
}
|
|
||||||
window.addEventListener("keydown", editAttention);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@ import "./groupNodeManage.css";
|
|||||||
import { app, type ComfyApp } from "../../scripts/app";
|
import { app, type ComfyApp } from "../../scripts/app";
|
||||||
import type { LGraphNode, LGraphNodeConstructor } from "/types/litegraph";
|
import type { LGraphNode, LGraphNodeConstructor } from "/types/litegraph";
|
||||||
|
|
||||||
|
|
||||||
const ORDER: symbol = Symbol();
|
const ORDER: symbol = Symbol();
|
||||||
|
|
||||||
function merge(target, source) {
|
function merge(target, source) {
|
||||||
@@ -26,11 +25,23 @@ function merge(target, source) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||||
tabs: Record<"Inputs" | "Outputs" | "Widgets", {tab: HTMLAnchorElement, page: HTMLElement}>;
|
tabs: Record<
|
||||||
|
"Inputs" | "Outputs" | "Widgets",
|
||||||
|
{ tab: HTMLAnchorElement; page: HTMLElement }
|
||||||
|
>;
|
||||||
selectedNodeIndex: number | null | undefined;
|
selectedNodeIndex: number | null | undefined;
|
||||||
selectedTab: keyof ManageGroupDialog["tabs"] = "Inputs";
|
selectedTab: keyof ManageGroupDialog["tabs"] = "Inputs";
|
||||||
selectedGroup: string | undefined;
|
selectedGroup: string | undefined;
|
||||||
modifications: Record<string, Record<string, Record<string, { name?: string | undefined, visible?: boolean | undefined }>>> = {};
|
modifications: Record<
|
||||||
|
string,
|
||||||
|
Record<
|
||||||
|
string,
|
||||||
|
Record<
|
||||||
|
string,
|
||||||
|
{ name?: string | undefined; visible?: boolean | undefined }
|
||||||
|
>
|
||||||
|
>
|
||||||
|
> = {};
|
||||||
nodeItems: any[];
|
nodeItems: any[];
|
||||||
app: ComfyApp;
|
app: ComfyApp;
|
||||||
groupNodeType: LGraphNodeConstructor<LGraphNode>;
|
groupNodeType: LGraphNodeConstructor<LGraphNode>;
|
||||||
@@ -86,7 +97,8 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getGroupData() {
|
getGroupData() {
|
||||||
this.groupNodeType = LiteGraph.registered_node_types["workflow/" + this.selectedGroup];
|
this.groupNodeType =
|
||||||
|
LiteGraph.registered_node_types["workflow/" + this.selectedGroup];
|
||||||
this.groupNodeDef = this.groupNodeType.nodeData;
|
this.groupNodeDef = this.groupNodeType.nodeData;
|
||||||
this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType);
|
this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType);
|
||||||
}
|
}
|
||||||
@@ -131,24 +143,39 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
|||||||
this.changeNode(0);
|
this.changeNode(0);
|
||||||
} else {
|
} else {
|
||||||
const items = this.draggable.getAllItems();
|
const items = this.draggable.getAllItems();
|
||||||
let index = items.findIndex(item => item.classList.contains("selected"));
|
let index = items.findIndex((item) =>
|
||||||
if(index === -1) index = this.selectedNodeIndex;
|
item.classList.contains("selected")
|
||||||
|
);
|
||||||
|
if (index === -1) index = this.selectedNodeIndex;
|
||||||
this.changeNode(index, true);
|
this.changeNode(index, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ordered = [...nodes];
|
const ordered = [...nodes];
|
||||||
this.draggable?.dispose();
|
this.draggable?.dispose();
|
||||||
this.draggable = new DraggableList(this.innerNodesList, "li");
|
this.draggable = new DraggableList(this.innerNodesList, "li");
|
||||||
this.draggable.addEventListener("dragend", ({ detail: { oldPosition, newPosition } }) => {
|
this.draggable.addEventListener(
|
||||||
if (oldPosition === newPosition) return;
|
"dragend",
|
||||||
ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0]);
|
({ detail: { oldPosition, newPosition } }) => {
|
||||||
for (let i = 0; i < ordered.length; i++) {
|
if (oldPosition === newPosition) return;
|
||||||
this.storeModification({ nodeIndex: ordered[i].index, section: ORDER, prop: "order", value: i });
|
ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0]);
|
||||||
|
for (let i = 0; i < ordered.length; i++) {
|
||||||
|
this.storeModification({
|
||||||
|
nodeIndex: ordered[i].index,
|
||||||
|
section: ORDER,
|
||||||
|
prop: "order",
|
||||||
|
value: i,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
storeModification(props: { nodeIndex?: number; section: symbol; prop: string; value: any }) {
|
storeModification(props: {
|
||||||
|
nodeIndex?: number;
|
||||||
|
section: symbol;
|
||||||
|
prop: string;
|
||||||
|
value: any;
|
||||||
|
}) {
|
||||||
const { nodeIndex, section, prop, value } = props;
|
const { nodeIndex, section, prop, value } = props;
|
||||||
const groupMod = (this.modifications[this.selectedGroup] ??= {});
|
const groupMod = (this.modifications[this.selectedGroup] ??= {});
|
||||||
const nodesMod = (groupMod.nodes ??= {});
|
const nodesMod = (groupMod.nodes ??= {});
|
||||||
@@ -165,7 +192,10 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
|||||||
getEditElement(section, prop, value, placeholder, checked, checkable = true) {
|
getEditElement(section, prop, value, placeholder, checked, checkable = true) {
|
||||||
if (value === placeholder) value = "";
|
if (value === placeholder) value = "";
|
||||||
|
|
||||||
const mods = this.modifications[this.selectedGroup]?.nodes?.[this.selectedNodeInnerIndex]?.[section]?.[prop];
|
const mods =
|
||||||
|
this.modifications[this.selectedGroup]?.nodes?.[
|
||||||
|
this.selectedNodeInnerIndex
|
||||||
|
]?.[section]?.[prop];
|
||||||
if (mods) {
|
if (mods) {
|
||||||
if (mods.name != null) {
|
if (mods.name != null) {
|
||||||
value = mods.name;
|
value = mods.name;
|
||||||
@@ -181,7 +211,11 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
|||||||
placeholder,
|
placeholder,
|
||||||
type: "text",
|
type: "text",
|
||||||
onchange: (e) => {
|
onchange: (e) => {
|
||||||
this.storeModification({ section, prop, value: { name: e.target.value } });
|
this.storeModification({
|
||||||
|
section,
|
||||||
|
prop,
|
||||||
|
value: { name: e.target.value },
|
||||||
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
$el("label", { textContent: "Visible" }, [
|
$el("label", { textContent: "Visible" }, [
|
||||||
@@ -190,7 +224,11 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
|||||||
checked,
|
checked,
|
||||||
disabled: !checkable,
|
disabled: !checkable,
|
||||||
onchange: (e) => {
|
onchange: (e) => {
|
||||||
this.storeModification({ section, prop, value: { visible: !!e.target.checked } });
|
this.storeModification({
|
||||||
|
section,
|
||||||
|
prop,
|
||||||
|
value: { visible: !!e.target.checked },
|
||||||
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
@@ -198,13 +236,20 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildWidgetsPage() {
|
buildWidgetsPage() {
|
||||||
const widgets = this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex];
|
const widgets =
|
||||||
|
this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex];
|
||||||
const items = Object.keys(widgets ?? {});
|
const items = Object.keys(widgets ?? {});
|
||||||
const type = app.graph.extra.groupNodes[this.selectedGroup];
|
const type = app.graph.extra.groupNodes[this.selectedGroup];
|
||||||
const config = type.config?.[this.selectedNodeInnerIndex]?.input;
|
const config = type.config?.[this.selectedNodeInnerIndex]?.input;
|
||||||
this.widgetsPage.replaceChildren(
|
this.widgetsPage.replaceChildren(
|
||||||
...items.map((oldName) => {
|
...items.map((oldName) => {
|
||||||
return this.getEditElement("input", oldName, widgets[oldName], oldName, config?.[oldName]?.visible !== false);
|
return this.getEditElement(
|
||||||
|
"input",
|
||||||
|
oldName,
|
||||||
|
widgets[oldName],
|
||||||
|
oldName,
|
||||||
|
config?.[oldName]?.visible !== false
|
||||||
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return !!items.length;
|
return !!items.length;
|
||||||
@@ -223,7 +268,13 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getEditElement("input", oldName, value, oldName, config?.[oldName]?.visible !== false);
|
return this.getEditElement(
|
||||||
|
"input",
|
||||||
|
oldName,
|
||||||
|
value,
|
||||||
|
oldName,
|
||||||
|
config?.[oldName]?.visible !== false
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
);
|
);
|
||||||
@@ -232,9 +283,12 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
|||||||
|
|
||||||
buildOutputsPage() {
|
buildOutputsPage() {
|
||||||
const nodes = this.groupData.nodeData.nodes;
|
const nodes = this.groupData.nodeData.nodes;
|
||||||
const innerNodeDef = this.groupData.getNodeDef(nodes[this.selectedNodeInnerIndex]);
|
const innerNodeDef = this.groupData.getNodeDef(
|
||||||
|
nodes[this.selectedNodeInnerIndex]
|
||||||
|
);
|
||||||
const outputs = innerNodeDef?.output ?? [];
|
const outputs = innerNodeDef?.output ?? [];
|
||||||
const groupOutputs = this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex];
|
const groupOutputs =
|
||||||
|
this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex];
|
||||||
|
|
||||||
const type = app.graph.extra.groupNodes[this.selectedGroup];
|
const type = app.graph.extra.groupNodes[this.selectedGroup];
|
||||||
const config = type.config?.[this.selectedNodeInnerIndex]?.output;
|
const config = type.config?.[this.selectedNodeInnerIndex]?.output;
|
||||||
@@ -250,7 +304,14 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
|||||||
if (!value || value === oldName) {
|
if (!value || value === oldName) {
|
||||||
value = "";
|
value = "";
|
||||||
}
|
}
|
||||||
return this.getEditElement("output", slot, value, oldName, visible, checkable);
|
return this.getEditElement(
|
||||||
|
"output",
|
||||||
|
slot,
|
||||||
|
value,
|
||||||
|
oldName,
|
||||||
|
visible,
|
||||||
|
checkable
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
);
|
);
|
||||||
@@ -258,13 +319,21 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
show(type?) {
|
show(type?) {
|
||||||
const groupNodes = Object.keys(app.graph.extra?.groupNodes ?? {}).sort((a, b) => a.localeCompare(b));
|
const groupNodes = Object.keys(app.graph.extra?.groupNodes ?? {}).sort(
|
||||||
|
(a, b) => a.localeCompare(b)
|
||||||
|
);
|
||||||
|
|
||||||
this.innerNodesList = $el("ul.comfy-group-manage-list-items") as HTMLUListElement;
|
this.innerNodesList = $el(
|
||||||
|
"ul.comfy-group-manage-list-items"
|
||||||
|
) as HTMLUListElement;
|
||||||
this.widgetsPage = $el("section.comfy-group-manage-node-page");
|
this.widgetsPage = $el("section.comfy-group-manage-node-page");
|
||||||
this.inputsPage = $el("section.comfy-group-manage-node-page");
|
this.inputsPage = $el("section.comfy-group-manage-node-page");
|
||||||
this.outputsPage = $el("section.comfy-group-manage-node-page");
|
this.outputsPage = $el("section.comfy-group-manage-node-page");
|
||||||
const pages = $el("div", [this.widgetsPage, this.inputsPage, this.outputsPage]);
|
const pages = $el("div", [
|
||||||
|
this.widgetsPage,
|
||||||
|
this.inputsPage,
|
||||||
|
this.outputsPage,
|
||||||
|
]);
|
||||||
|
|
||||||
this.tabs = [
|
this.tabs = [
|
||||||
["Inputs", this.inputsPage],
|
["Inputs", this.inputsPage],
|
||||||
@@ -318,12 +387,20 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
|||||||
{
|
{
|
||||||
onclick: (e) => {
|
onclick: (e) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const node = app.graph._nodes.find((n) => n.type === "workflow/" + this.selectedGroup);
|
const node = app.graph._nodes.find(
|
||||||
|
(n) => n.type === "workflow/" + this.selectedGroup
|
||||||
|
);
|
||||||
if (node) {
|
if (node) {
|
||||||
alert("This group node is in use in the current workflow, please first remove these.");
|
alert(
|
||||||
|
"This group node is in use in the current workflow, please first remove these."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (confirm(`Are you sure you want to remove the node: "${this.selectedGroup}"`)) {
|
if (
|
||||||
|
confirm(
|
||||||
|
`Are you sure you want to remove the node: "${this.selectedGroup}"`
|
||||||
|
)
|
||||||
|
) {
|
||||||
delete app.graph.extra.groupNodes[this.selectedGroup];
|
delete app.graph.extra.groupNodes[this.selectedGroup];
|
||||||
LiteGraph.unregisterNodeType("workflow/" + this.selectedGroup);
|
LiteGraph.unregisterNodeType("workflow/" + this.selectedGroup);
|
||||||
}
|
}
|
||||||
@@ -416,16 +493,22 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
|||||||
},
|
},
|
||||||
"Save"
|
"Save"
|
||||||
),
|
),
|
||||||
$el("button.comfy-btn", { onclick: () => this.element.close() }, "Close"),
|
$el(
|
||||||
|
"button.comfy-btn",
|
||||||
|
{ onclick: () => this.element.close() },
|
||||||
|
"Close"
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.element.replaceChildren(outer);
|
this.element.replaceChildren(outer);
|
||||||
this.changeGroup(type ? groupNodes.find((g) => "workflow/" + g === type) : groupNodes[0]);
|
this.changeGroup(
|
||||||
|
type ? groupNodes.find((g) => "workflow/" + g === type) : groupNodes[0]
|
||||||
|
);
|
||||||
this.element.showModal();
|
this.element.showModal();
|
||||||
|
|
||||||
this.element.addEventListener("close", () => {
|
this.element.addEventListener("close", () => {
|
||||||
this.draggable?.dispose();
|
this.draggable?.dispose();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,262 +1,265 @@
|
|||||||
import {app} from "../../scripts/app";
|
import { app } from "../../scripts/app";
|
||||||
|
|
||||||
function setNodeMode(node, mode) {
|
function setNodeMode(node, mode) {
|
||||||
node.mode = mode;
|
node.mode = mode;
|
||||||
node.graph.change();
|
node.graph.change();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addNodesToGroup(group, nodes=[]) {
|
function addNodesToGroup(group, nodes = []) {
|
||||||
var x1, y1, x2, y2;
|
var x1, y1, x2, y2;
|
||||||
var nx1, ny1, nx2, ny2;
|
var nx1, ny1, nx2, ny2;
|
||||||
var node;
|
var node;
|
||||||
|
|
||||||
x1 = y1 = x2 = y2 = -1;
|
x1 = y1 = x2 = y2 = -1;
|
||||||
nx1 = ny1 = nx2 = ny2 = -1;
|
nx1 = ny1 = nx2 = ny2 = -1;
|
||||||
|
|
||||||
for (var n of [group._nodes, nodes]) {
|
for (var n of [group._nodes, nodes]) {
|
||||||
for (var i in n) {
|
for (var i in n) {
|
||||||
node = n[i]
|
node = n[i];
|
||||||
|
|
||||||
nx1 = node.pos[0]
|
nx1 = node.pos[0];
|
||||||
ny1 = node.pos[1]
|
ny1 = node.pos[1];
|
||||||
nx2 = node.pos[0] + node.size[0]
|
nx2 = node.pos[0] + node.size[0];
|
||||||
ny2 = node.pos[1] + node.size[1]
|
ny2 = node.pos[1] + node.size[1];
|
||||||
|
|
||||||
if (node.type != "Reroute") {
|
if (node.type != "Reroute") {
|
||||||
ny1 -= LiteGraph.NODE_TITLE_HEIGHT;
|
ny1 -= LiteGraph.NODE_TITLE_HEIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.flags?.collapsed) {
|
if (node.flags?.collapsed) {
|
||||||
ny2 = ny1 + LiteGraph.NODE_TITLE_HEIGHT;
|
ny2 = ny1 + LiteGraph.NODE_TITLE_HEIGHT;
|
||||||
|
|
||||||
if (node?._collapsed_width) {
|
if (node?._collapsed_width) {
|
||||||
nx2 = nx1 + Math.round(node._collapsed_width);
|
nx2 = nx1 + Math.round(node._collapsed_width);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (x1 == -1 || nx1 < x1) {
|
|
||||||
x1 = nx1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (y1 == -1 || ny1 < y1) {
|
|
||||||
y1 = ny1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (x2 == -1 || nx2 > x2) {
|
|
||||||
x2 = nx2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (y2 == -1 || ny2 > y2) {
|
|
||||||
y2 = ny2;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x1 == -1 || nx1 < x1) {
|
||||||
|
x1 = nx1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y1 == -1 || ny1 < y1) {
|
||||||
|
y1 = ny1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x2 == -1 || nx2 > x2) {
|
||||||
|
x2 = nx2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y2 == -1 || ny2 > y2) {
|
||||||
|
y2 = ny2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var padding = 10;
|
var padding = 10;
|
||||||
|
|
||||||
y1 = y1 - Math.round(group.font_size * 1.4);
|
y1 = y1 - Math.round(group.font_size * 1.4);
|
||||||
|
|
||||||
group.pos = [x1 - padding, y1 - padding];
|
group.pos = [x1 - padding, y1 - padding];
|
||||||
group.size = [x2 - x1 + padding * 2, y2 - y1 + padding * 2];
|
group.size = [x2 - x1 + padding * 2, y2 - y1 + padding * 2];
|
||||||
}
|
}
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "Comfy.GroupOptions",
|
name: "Comfy.GroupOptions",
|
||||||
setup() {
|
setup() {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
|
const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
|
||||||
// graph_mouse
|
// graph_mouse
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
|
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
|
||||||
const options = orig.apply(this, arguments);
|
const options = orig.apply(this, arguments);
|
||||||
const group = this.graph.getGroupOnPos(this.graph_mouse[0], this.graph_mouse[1]);
|
const group = this.graph.getGroupOnPos(
|
||||||
if (!group) {
|
this.graph_mouse[0],
|
||||||
options.push({
|
this.graph_mouse[1]
|
||||||
content: "Add Group For Selected Nodes",
|
);
|
||||||
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
|
if (!group) {
|
||||||
callback: () => {
|
options.push({
|
||||||
// @ts-ignore
|
content: "Add Group For Selected Nodes",
|
||||||
var group = new LiteGraph.LGraphGroup();
|
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
|
||||||
addNodesToGroup(group, this.selected_nodes)
|
callback: () => {
|
||||||
app.canvas.graph.add(group);
|
// @ts-ignore
|
||||||
this.graph.change();
|
var group = new LiteGraph.LGraphGroup();
|
||||||
}
|
addNodesToGroup(group, this.selected_nodes);
|
||||||
});
|
app.canvas.graph.add(group);
|
||||||
|
this.graph.change();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group nodes aren't recomputed until the group is moved, this ensures the nodes are up-to-date
|
// Group nodes aren't recomputed until the group is moved, this ensures the nodes are up-to-date
|
||||||
group.recomputeInsideNodes();
|
group.recomputeInsideNodes();
|
||||||
const nodesInGroup = group._nodes;
|
const nodesInGroup = group._nodes;
|
||||||
|
|
||||||
options.push({
|
options.push({
|
||||||
content: "Add Selected Nodes To Group",
|
content: "Add Selected Nodes To Group",
|
||||||
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
|
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
addNodesToGroup(group, this.selected_nodes)
|
addNodesToGroup(group, this.selected_nodes);
|
||||||
this.graph.change();
|
this.graph.change();
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// No nodes in group, return default options
|
// No nodes in group, return default options
|
||||||
if (nodesInGroup.length === 0) {
|
if (nodesInGroup.length === 0) {
|
||||||
return options;
|
return options;
|
||||||
} else {
|
} else {
|
||||||
// Add a separator between the default options and the group options
|
// Add a separator between the default options and the group options
|
||||||
options.push(null);
|
options.push(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if all nodes are the same mode
|
// Check if all nodes are the same mode
|
||||||
let allNodesAreSameMode = true;
|
let allNodesAreSameMode = true;
|
||||||
for (let i = 1; i < nodesInGroup.length; i++) {
|
for (let i = 1; i < nodesInGroup.length; i++) {
|
||||||
if (nodesInGroup[i].mode !== nodesInGroup[0].mode) {
|
if (nodesInGroup[i].mode !== nodesInGroup[0].mode) {
|
||||||
allNodesAreSameMode = false;
|
allNodesAreSameMode = false;
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
options.push({
|
|
||||||
content: "Fit Group To Nodes",
|
|
||||||
callback: () => {
|
|
||||||
addNodesToGroup(group)
|
|
||||||
this.graph.change();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
options.push({
|
|
||||||
content: "Select Nodes",
|
|
||||||
callback: () => {
|
|
||||||
this.selectNodes(nodesInGroup);
|
|
||||||
this.graph.change();
|
|
||||||
this.canvas.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Modes
|
|
||||||
// 0: Always
|
|
||||||
// 1: On Event
|
|
||||||
// 2: Never
|
|
||||||
// 3: On Trigger
|
|
||||||
// 4: Bypass
|
|
||||||
// If all nodes are the same mode, add a menu option to change the mode
|
|
||||||
if (allNodesAreSameMode) {
|
|
||||||
const mode = nodesInGroup[0].mode;
|
|
||||||
switch (mode) {
|
|
||||||
case 0:
|
|
||||||
// All nodes are always, option to disable, and bypass
|
|
||||||
options.push({
|
|
||||||
content: "Set Group Nodes to Never",
|
|
||||||
callback: () => {
|
|
||||||
for (const node of nodesInGroup) {
|
|
||||||
setNodeMode(node, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
options.push({
|
|
||||||
content: "Bypass Group Nodes",
|
|
||||||
callback: () => {
|
|
||||||
for (const node of nodesInGroup) {
|
|
||||||
setNodeMode(node, 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
// All nodes are never, option to enable, and bypass
|
|
||||||
options.push({
|
|
||||||
content: "Set Group Nodes to Always",
|
|
||||||
callback: () => {
|
|
||||||
for (const node of nodesInGroup) {
|
|
||||||
setNodeMode(node, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
options.push({
|
|
||||||
content: "Bypass Group Nodes",
|
|
||||||
callback: () => {
|
|
||||||
for (const node of nodesInGroup) {
|
|
||||||
setNodeMode(node, 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
// All nodes are bypass, option to enable, and disable
|
|
||||||
options.push({
|
|
||||||
content: "Set Group Nodes to Always",
|
|
||||||
callback: () => {
|
|
||||||
for (const node of nodesInGroup) {
|
|
||||||
setNodeMode(node, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
options.push({
|
|
||||||
content: "Set Group Nodes to Never",
|
|
||||||
callback: () => {
|
|
||||||
for (const node of nodesInGroup) {
|
|
||||||
setNodeMode(node, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// All nodes are On Trigger or On Event(Or other?), option to disable, set to always, or bypass
|
|
||||||
options.push({
|
|
||||||
content: "Set Group Nodes to Always",
|
|
||||||
callback: () => {
|
|
||||||
for (const node of nodesInGroup) {
|
|
||||||
setNodeMode(node, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
options.push({
|
|
||||||
content: "Set Group Nodes to Never",
|
|
||||||
callback: () => {
|
|
||||||
for (const node of nodesInGroup) {
|
|
||||||
setNodeMode(node, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
options.push({
|
|
||||||
content: "Bypass Group Nodes",
|
|
||||||
callback: () => {
|
|
||||||
for (const node of nodesInGroup) {
|
|
||||||
setNodeMode(node, 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Nodes are not all the same mode, add a menu option to change the mode to always, never, or bypass
|
|
||||||
options.push({
|
|
||||||
content: "Set Group Nodes to Always",
|
|
||||||
callback: () => {
|
|
||||||
for (const node of nodesInGroup) {
|
|
||||||
setNodeMode(node, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
options.push({
|
|
||||||
content: "Set Group Nodes to Never",
|
|
||||||
callback: () => {
|
|
||||||
for (const node of nodesInGroup) {
|
|
||||||
setNodeMode(node, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
options.push({
|
|
||||||
content: "Bypass Group Nodes",
|
|
||||||
callback: () => {
|
|
||||||
for (const node of nodesInGroup) {
|
|
||||||
setNodeMode(node, 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return options
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
content: "Fit Group To Nodes",
|
||||||
|
callback: () => {
|
||||||
|
addNodesToGroup(group);
|
||||||
|
this.graph.change();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
content: "Select Nodes",
|
||||||
|
callback: () => {
|
||||||
|
this.selectNodes(nodesInGroup);
|
||||||
|
this.graph.change();
|
||||||
|
this.canvas.focus();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modes
|
||||||
|
// 0: Always
|
||||||
|
// 1: On Event
|
||||||
|
// 2: Never
|
||||||
|
// 3: On Trigger
|
||||||
|
// 4: Bypass
|
||||||
|
// If all nodes are the same mode, add a menu option to change the mode
|
||||||
|
if (allNodesAreSameMode) {
|
||||||
|
const mode = nodesInGroup[0].mode;
|
||||||
|
switch (mode) {
|
||||||
|
case 0:
|
||||||
|
// All nodes are always, option to disable, and bypass
|
||||||
|
options.push({
|
||||||
|
content: "Set Group Nodes to Never",
|
||||||
|
callback: () => {
|
||||||
|
for (const node of nodesInGroup) {
|
||||||
|
setNodeMode(node, 2);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
options.push({
|
||||||
|
content: "Bypass Group Nodes",
|
||||||
|
callback: () => {
|
||||||
|
for (const node of nodesInGroup) {
|
||||||
|
setNodeMode(node, 4);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
// All nodes are never, option to enable, and bypass
|
||||||
|
options.push({
|
||||||
|
content: "Set Group Nodes to Always",
|
||||||
|
callback: () => {
|
||||||
|
for (const node of nodesInGroup) {
|
||||||
|
setNodeMode(node, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
options.push({
|
||||||
|
content: "Bypass Group Nodes",
|
||||||
|
callback: () => {
|
||||||
|
for (const node of nodesInGroup) {
|
||||||
|
setNodeMode(node, 4);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
// All nodes are bypass, option to enable, and disable
|
||||||
|
options.push({
|
||||||
|
content: "Set Group Nodes to Always",
|
||||||
|
callback: () => {
|
||||||
|
for (const node of nodesInGroup) {
|
||||||
|
setNodeMode(node, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
options.push({
|
||||||
|
content: "Set Group Nodes to Never",
|
||||||
|
callback: () => {
|
||||||
|
for (const node of nodesInGroup) {
|
||||||
|
setNodeMode(node, 2);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// All nodes are On Trigger or On Event(Or other?), option to disable, set to always, or bypass
|
||||||
|
options.push({
|
||||||
|
content: "Set Group Nodes to Always",
|
||||||
|
callback: () => {
|
||||||
|
for (const node of nodesInGroup) {
|
||||||
|
setNodeMode(node, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
options.push({
|
||||||
|
content: "Set Group Nodes to Never",
|
||||||
|
callback: () => {
|
||||||
|
for (const node of nodesInGroup) {
|
||||||
|
setNodeMode(node, 2);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
options.push({
|
||||||
|
content: "Bypass Group Nodes",
|
||||||
|
callback: () => {
|
||||||
|
for (const node of nodesInGroup) {
|
||||||
|
setNodeMode(node, 4);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Nodes are not all the same mode, add a menu option to change the mode to always, never, or bypass
|
||||||
|
options.push({
|
||||||
|
content: "Set Group Nodes to Always",
|
||||||
|
callback: () => {
|
||||||
|
for (const node of nodesInGroup) {
|
||||||
|
setNodeMode(node, 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
options.push({
|
||||||
|
content: "Set Group Nodes to Never",
|
||||||
|
callback: () => {
|
||||||
|
for (const node of nodesInGroup) {
|
||||||
|
setNodeMode(node, 2);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
options.push({
|
||||||
|
content: "Bypass Group Nodes",
|
||||||
|
callback: () => {
|
||||||
|
for (const node of nodesInGroup) {
|
||||||
|
setNodeMode(node, 4);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,34 +4,34 @@ import { app } from "../../scripts/app";
|
|||||||
|
|
||||||
const id = "Comfy.InvertMenuScrolling";
|
const id = "Comfy.InvertMenuScrolling";
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: id,
|
name: id,
|
||||||
init() {
|
init() {
|
||||||
const ctxMenu = LiteGraph.ContextMenu;
|
const ctxMenu = LiteGraph.ContextMenu;
|
||||||
const replace = () => {
|
const replace = () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
LiteGraph.ContextMenu = function (values, options) {
|
LiteGraph.ContextMenu = function (values, options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
if (options.scroll_speed) {
|
if (options.scroll_speed) {
|
||||||
options.scroll_speed *= -1;
|
options.scroll_speed *= -1;
|
||||||
} else {
|
} else {
|
||||||
options.scroll_speed = -0.1;
|
options.scroll_speed = -0.1;
|
||||||
}
|
}
|
||||||
return ctxMenu.call(this, values, options);
|
return ctxMenu.call(this, values, options);
|
||||||
};
|
};
|
||||||
LiteGraph.ContextMenu.prototype = ctxMenu.prototype;
|
LiteGraph.ContextMenu.prototype = ctxMenu.prototype;
|
||||||
};
|
};
|
||||||
app.ui.settings.addSetting({
|
app.ui.settings.addSetting({
|
||||||
id,
|
id,
|
||||||
name: "Invert Menu Scrolling",
|
name: "Invert Menu Scrolling",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
onChange(value) {
|
onChange(value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
replace();
|
replace();
|
||||||
} else {
|
} else {
|
||||||
LiteGraph.ContextMenu = ctxMenu;
|
LiteGraph.ContextMenu = ctxMenu;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,69 +1,73 @@
|
|||||||
import {app} from "../../scripts/app";
|
import { app } from "../../scripts/app";
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "Comfy.Keybinds",
|
name: "Comfy.Keybinds",
|
||||||
init() {
|
init() {
|
||||||
const keybindListener = function (event) {
|
const keybindListener = function (event) {
|
||||||
const modifierPressed = event.ctrlKey || event.metaKey;
|
const modifierPressed = event.ctrlKey || event.metaKey;
|
||||||
|
|
||||||
// Queue prompt using ctrl or command + enter
|
// Queue prompt using ctrl or command + enter
|
||||||
if (modifierPressed && event.key === "Enter") {
|
if (modifierPressed && event.key === "Enter") {
|
||||||
app.queuePrompt(event.shiftKey ? -1 : 0).then();
|
app.queuePrompt(event.shiftKey ? -1 : 0).then();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = event.composedPath()[0];
|
const target = event.composedPath()[0];
|
||||||
if (["INPUT", "TEXTAREA"].includes(target.tagName)) {
|
if (["INPUT", "TEXTAREA"].includes(target.tagName)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const modifierKeyIdMap = {
|
const modifierKeyIdMap = {
|
||||||
s: "#comfy-save-button",
|
s: "#comfy-save-button",
|
||||||
o: "#comfy-file-input",
|
o: "#comfy-file-input",
|
||||||
Backspace: "#comfy-clear-button",
|
Backspace: "#comfy-clear-button",
|
||||||
d: "#comfy-load-default-button",
|
d: "#comfy-load-default-button",
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifierKeybindId = modifierKeyIdMap[event.key];
|
const modifierKeybindId = modifierKeyIdMap[event.key];
|
||||||
if (modifierPressed && modifierKeybindId) {
|
if (modifierPressed && modifierKeybindId) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const elem = document.querySelector(modifierKeybindId);
|
const elem = document.querySelector(modifierKeybindId);
|
||||||
elem.click();
|
elem.click();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finished Handling all modifier keybinds, now handle the rest
|
// Finished Handling all modifier keybinds, now handle the rest
|
||||||
if (event.ctrlKey || event.altKey || event.metaKey) {
|
if (event.ctrlKey || event.altKey || event.metaKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close out of modals using escape
|
// Close out of modals using escape
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
const modals = document.querySelectorAll<HTMLElement>(".comfy-modal");
|
const modals = document.querySelectorAll<HTMLElement>(".comfy-modal");
|
||||||
const modal = Array.from(modals).find(modal => window.getComputedStyle(modal).getPropertyValue("display") !== "none");
|
const modal = Array.from(modals).find(
|
||||||
if (modal) {
|
(modal) =>
|
||||||
modal.style.display = "none";
|
window.getComputedStyle(modal).getPropertyValue("display") !==
|
||||||
}
|
"none"
|
||||||
|
);
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
[...document.querySelectorAll("dialog")].forEach(d => {
|
[...document.querySelectorAll("dialog")].forEach((d) => {
|
||||||
d.close();
|
d.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyIdMap = {
|
const keyIdMap = {
|
||||||
q: "#comfy-view-queue-button",
|
q: "#comfy-view-queue-button",
|
||||||
h: "#comfy-view-history-button",
|
h: "#comfy-view-history-button",
|
||||||
r: "#comfy-refresh-button",
|
r: "#comfy-refresh-button",
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonId = keyIdMap[event.key];
|
const buttonId = keyIdMap[event.key];
|
||||||
if (buttonId) {
|
if (buttonId) {
|
||||||
const button = document.querySelector(buttonId);
|
const button = document.querySelector(buttonId);
|
||||||
button.click();
|
button.click();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
window.addEventListener("keydown", keybindListener, true);
|
window.addEventListener("keydown", keybindListener, true);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,25 +2,25 @@ import { app } from "../../scripts/app";
|
|||||||
|
|
||||||
const id = "Comfy.LinkRenderMode";
|
const id = "Comfy.LinkRenderMode";
|
||||||
const ext = {
|
const ext = {
|
||||||
name: id,
|
name: id,
|
||||||
async setup(app) {
|
async setup(app) {
|
||||||
app.ui.settings.addSetting({
|
app.ui.settings.addSetting({
|
||||||
id,
|
id,
|
||||||
name: "Link Render Mode",
|
name: "Link Render Mode",
|
||||||
defaultValue: 2,
|
defaultValue: 2,
|
||||||
type: "combo",
|
type: "combo",
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
options: [...LiteGraph.LINK_RENDER_MODES, "Hidden"].map((m, i) => ({
|
options: [...LiteGraph.LINK_RENDER_MODES, "Hidden"].map((m, i) => ({
|
||||||
value: i,
|
value: i,
|
||||||
text: m,
|
text: m,
|
||||||
selected: i == app.canvas.links_render_mode,
|
selected: i == app.canvas.links_render_mode,
|
||||||
})),
|
})),
|
||||||
onChange(value) {
|
onChange(value) {
|
||||||
app.canvas.links_render_mode = +value;
|
app.canvas.links_render_mode = +value;
|
||||||
app.graph.setDirtyCanvas(true);
|
app.graph.setDirtyCanvas(true);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
app.registerExtension(ext);
|
app.registerExtension(ext);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -24,404 +24,416 @@ const id = "Comfy.NodeTemplates";
|
|||||||
const file = "comfy.templates.json";
|
const file = "comfy.templates.json";
|
||||||
|
|
||||||
class ManageTemplates extends ComfyDialog {
|
class ManageTemplates extends ComfyDialog {
|
||||||
templates: any[];
|
templates: any[];
|
||||||
draggedEl: HTMLElement | null;
|
draggedEl: HTMLElement | null;
|
||||||
saveVisualCue: number | null;
|
saveVisualCue: number | null;
|
||||||
emptyImg: HTMLImageElement;
|
emptyImg: HTMLImageElement;
|
||||||
importInput: HTMLInputElement;
|
importInput: HTMLInputElement;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.load().then((v) => {
|
this.load().then((v) => {
|
||||||
this.templates = v;
|
this.templates = v;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.element.classList.add("comfy-manage-templates");
|
this.element.classList.add("comfy-manage-templates");
|
||||||
this.draggedEl = null;
|
this.draggedEl = null;
|
||||||
this.saveVisualCue = null;
|
this.saveVisualCue = null;
|
||||||
this.emptyImg = new Image();
|
this.emptyImg = new Image();
|
||||||
this.emptyImg.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=";
|
this.emptyImg.src =
|
||||||
|
"data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=";
|
||||||
|
|
||||||
this.importInput = $el("input", {
|
this.importInput = $el("input", {
|
||||||
type: "file",
|
type: "file",
|
||||||
accept: ".json",
|
accept: ".json",
|
||||||
multiple: true,
|
multiple: true,
|
||||||
style: { display: "none" },
|
style: { display: "none" },
|
||||||
parent: document.body,
|
parent: document.body,
|
||||||
onchange: () => this.importAll(),
|
onchange: () => this.importAll(),
|
||||||
}) as HTMLInputElement;
|
}) as HTMLInputElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
createButtons() {
|
createButtons() {
|
||||||
const btns = super.createButtons();
|
const btns = super.createButtons();
|
||||||
btns[0].textContent = "Close";
|
btns[0].textContent = "Close";
|
||||||
btns[0].onclick = (e) => {
|
btns[0].onclick = (e) => {
|
||||||
clearTimeout(this.saveVisualCue);
|
clearTimeout(this.saveVisualCue);
|
||||||
this.close();
|
this.close();
|
||||||
};
|
};
|
||||||
btns.unshift(
|
btns.unshift(
|
||||||
$el("button", {
|
$el("button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Export",
|
textContent: "Export",
|
||||||
onclick: () => this.exportAll(),
|
onclick: () => this.exportAll(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
btns.unshift(
|
btns.unshift(
|
||||||
$el("button", {
|
$el("button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "Import",
|
textContent: "Import",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
this.importInput.click();
|
this.importInput.click();
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return btns;
|
return btns;
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
let templates = [];
|
let templates = [];
|
||||||
if (app.storageLocation === "server") {
|
if (app.storageLocation === "server") {
|
||||||
if (app.isNewUserSession) {
|
if (app.isNewUserSession) {
|
||||||
// New user so migrate existing templates
|
// New user so migrate existing templates
|
||||||
const json = localStorage.getItem(id);
|
const json = localStorage.getItem(id);
|
||||||
if (json) {
|
if (json) {
|
||||||
templates = JSON.parse(json);
|
templates = JSON.parse(json);
|
||||||
}
|
}
|
||||||
await api.storeUserData(file, json, { stringify: false });
|
await api.storeUserData(file, json, { stringify: false });
|
||||||
} else {
|
} else {
|
||||||
const res = await api.getUserData(file);
|
const res = await api.getUserData(file);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
try {
|
try {
|
||||||
templates = await res.json();
|
templates = await res.json();
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
}
|
} else if (res.status !== 404) {
|
||||||
} else if (res.status !== 404) {
|
console.error(res.status + " " + res.statusText);
|
||||||
console.error(res.status + " " + res.statusText);
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
} else {
|
const json = localStorage.getItem(id);
|
||||||
const json = localStorage.getItem(id);
|
if (json) {
|
||||||
if (json) {
|
templates = JSON.parse(json);
|
||||||
templates = JSON.parse(json);
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return templates ?? [];
|
return templates ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async store() {
|
async store() {
|
||||||
if(app.storageLocation === "server") {
|
if (app.storageLocation === "server") {
|
||||||
const templates = JSON.stringify(this.templates, undefined, 4);
|
const templates = JSON.stringify(this.templates, undefined, 4);
|
||||||
localStorage.setItem(id, templates); // Backwards compatibility
|
localStorage.setItem(id, templates); // Backwards compatibility
|
||||||
try {
|
try {
|
||||||
await api.storeUserData(file, templates, { stringify: false });
|
await api.storeUserData(file, templates, { stringify: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
alert(error.message);
|
alert(error.message);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem(id, JSON.stringify(this.templates));
|
localStorage.setItem(id, JSON.stringify(this.templates));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async importAll() {
|
async importAll() {
|
||||||
for (const file of this.importInput.files) {
|
for (const file of this.importInput.files) {
|
||||||
if (file.type === "application/json" || file.name.endsWith(".json")) {
|
if (file.type === "application/json" || file.name.endsWith(".json")) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = async () => {
|
reader.onload = async () => {
|
||||||
const importFile = JSON.parse(reader.result as string);
|
const importFile = JSON.parse(reader.result as string);
|
||||||
if (importFile?.templates) {
|
if (importFile?.templates) {
|
||||||
for (const template of importFile.templates) {
|
for (const template of importFile.templates) {
|
||||||
if (template?.name && template?.data) {
|
if (template?.name && template?.data) {
|
||||||
this.templates.push(template);
|
this.templates.push(template);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.store();
|
await this.store();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
await reader.readAsText(file);
|
await reader.readAsText(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.importInput.value = null;
|
this.importInput.value = null;
|
||||||
|
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
exportAll() {
|
exportAll() {
|
||||||
if (this.templates.length == 0) {
|
if (this.templates.length == 0) {
|
||||||
alert("No templates to export.");
|
alert("No templates to export.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = JSON.stringify({ templates: this.templates }, null, 2); // convert the data to a JSON string
|
const json = JSON.stringify({ templates: this.templates }, null, 2); // convert the data to a JSON string
|
||||||
const blob = new Blob([json], { type: "application/json" });
|
const blob = new Blob([json], { type: "application/json" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = $el("a", {
|
const a = $el("a", {
|
||||||
href: url,
|
href: url,
|
||||||
download: "node_templates.json",
|
download: "node_templates.json",
|
||||||
style: { display: "none" },
|
style: { display: "none" },
|
||||||
parent: document.body,
|
parent: document.body,
|
||||||
});
|
});
|
||||||
a.click();
|
a.click();
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
a.remove();
|
a.remove();
|
||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
// Show list of template names + delete button
|
// Show list of template names + delete button
|
||||||
super.show(
|
super.show(
|
||||||
$el(
|
$el(
|
||||||
"div",
|
"div",
|
||||||
{},
|
{},
|
||||||
this.templates.flatMap((t,i) => {
|
this.templates.flatMap((t, i) => {
|
||||||
let nameInput;
|
let nameInput;
|
||||||
return [
|
return [
|
||||||
$el(
|
$el(
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
dataset: { id: i.toString() },
|
dataset: { id: i.toString() },
|
||||||
className: "tempateManagerRow",
|
className: "tempateManagerRow",
|
||||||
style: {
|
style: {
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "1fr auto",
|
gridTemplateColumns: "1fr auto",
|
||||||
border: "1px dashed transparent",
|
border: "1px dashed transparent",
|
||||||
gap: "5px",
|
gap: "5px",
|
||||||
backgroundColor: "var(--comfy-menu-bg)"
|
backgroundColor: "var(--comfy-menu-bg)",
|
||||||
},
|
},
|
||||||
ondragstart: (e) => {
|
ondragstart: (e) => {
|
||||||
this.draggedEl = e.currentTarget;
|
this.draggedEl = e.currentTarget;
|
||||||
e.currentTarget.style.opacity = "0.6";
|
e.currentTarget.style.opacity = "0.6";
|
||||||
e.currentTarget.style.border = "1px dashed yellow";
|
e.currentTarget.style.border = "1px dashed yellow";
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
e.dataTransfer.effectAllowed = "move";
|
||||||
e.dataTransfer.setDragImage(this.emptyImg, 0, 0);
|
e.dataTransfer.setDragImage(this.emptyImg, 0, 0);
|
||||||
},
|
},
|
||||||
ondragend: (e) => {
|
ondragend: (e) => {
|
||||||
e.target.style.opacity = "1";
|
e.target.style.opacity = "1";
|
||||||
e.currentTarget.style.border = "1px dashed transparent";
|
e.currentTarget.style.border = "1px dashed transparent";
|
||||||
e.currentTarget.removeAttribute("draggable");
|
e.currentTarget.removeAttribute("draggable");
|
||||||
|
|
||||||
// rearrange the elements
|
// rearrange the elements
|
||||||
this.element.querySelectorAll('.tempateManagerRow').forEach((el: HTMLElement,i) => {
|
this.element
|
||||||
var prev_i = Number.parseInt(el.dataset.id);
|
.querySelectorAll(".tempateManagerRow")
|
||||||
|
.forEach((el: HTMLElement, i) => {
|
||||||
|
var prev_i = Number.parseInt(el.dataset.id);
|
||||||
|
|
||||||
if ( el == this.draggedEl && prev_i != i ) {
|
if (el == this.draggedEl && prev_i != i) {
|
||||||
this.templates.splice(i, 0, this.templates.splice(prev_i, 1)[0]);
|
this.templates.splice(
|
||||||
}
|
i,
|
||||||
el.dataset.id = i.toString();
|
0,
|
||||||
});
|
this.templates.splice(prev_i, 1)[0]
|
||||||
this.store();
|
);
|
||||||
},
|
}
|
||||||
ondragover: (e) => {
|
el.dataset.id = i.toString();
|
||||||
e.preventDefault();
|
});
|
||||||
if ( e.currentTarget == this.draggedEl )
|
this.store();
|
||||||
return;
|
},
|
||||||
|
ondragover: (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.currentTarget == this.draggedEl) return;
|
||||||
|
|
||||||
let rect = e.currentTarget.getBoundingClientRect();
|
let rect = e.currentTarget.getBoundingClientRect();
|
||||||
if (e.clientY > rect.top + rect.height / 2) {
|
if (e.clientY > rect.top + rect.height / 2) {
|
||||||
e.currentTarget.parentNode.insertBefore(this.draggedEl, e.currentTarget.nextSibling);
|
e.currentTarget.parentNode.insertBefore(
|
||||||
} else {
|
this.draggedEl,
|
||||||
e.currentTarget.parentNode.insertBefore(this.draggedEl, e.currentTarget);
|
e.currentTarget.nextSibling
|
||||||
}
|
);
|
||||||
}
|
} else {
|
||||||
},
|
e.currentTarget.parentNode.insertBefore(
|
||||||
[
|
this.draggedEl,
|
||||||
$el(
|
e.currentTarget
|
||||||
"label",
|
);
|
||||||
{
|
}
|
||||||
textContent: "Name: ",
|
},
|
||||||
style: {
|
},
|
||||||
cursor: "grab",
|
[
|
||||||
},
|
$el(
|
||||||
onmousedown: (e) => {
|
"label",
|
||||||
// enable dragging only from the label
|
{
|
||||||
if (e.target.localName == 'label')
|
textContent: "Name: ",
|
||||||
e.currentTarget.parentNode.draggable = 'true';
|
style: {
|
||||||
}
|
cursor: "grab",
|
||||||
},
|
},
|
||||||
[
|
onmousedown: (e) => {
|
||||||
$el("input", {
|
// enable dragging only from the label
|
||||||
value: t.name,
|
if (e.target.localName == "label")
|
||||||
dataset: { name: t.name },
|
e.currentTarget.parentNode.draggable = "true";
|
||||||
style: {
|
},
|
||||||
transitionProperty: 'background-color',
|
},
|
||||||
transitionDuration: '0s',
|
[
|
||||||
},
|
$el("input", {
|
||||||
onchange: (e) => {
|
value: t.name,
|
||||||
clearTimeout(this.saveVisualCue);
|
dataset: { name: t.name },
|
||||||
var el = e.target;
|
style: {
|
||||||
var row = el.parentNode.parentNode;
|
transitionProperty: "background-color",
|
||||||
this.templates[row.dataset.id].name = el.value.trim() || 'untitled';
|
transitionDuration: "0s",
|
||||||
this.store();
|
},
|
||||||
el.style.backgroundColor = 'rgb(40, 95, 40)';
|
onchange: (e) => {
|
||||||
el.style.transitionDuration = '0s';
|
clearTimeout(this.saveVisualCue);
|
||||||
// @ts-expect-error
|
var el = e.target;
|
||||||
// In browser env the return value is number.
|
var row = el.parentNode.parentNode;
|
||||||
this.saveVisualCue = setTimeout(function () {
|
this.templates[row.dataset.id].name =
|
||||||
el.style.transitionDuration = '.7s';
|
el.value.trim() || "untitled";
|
||||||
el.style.backgroundColor = 'var(--comfy-input-bg)';
|
this.store();
|
||||||
}, 15);
|
el.style.backgroundColor = "rgb(40, 95, 40)";
|
||||||
},
|
el.style.transitionDuration = "0s";
|
||||||
onkeypress: (e) => {
|
// @ts-expect-error
|
||||||
var el = e.target;
|
// In browser env the return value is number.
|
||||||
clearTimeout(this.saveVisualCue);
|
this.saveVisualCue = setTimeout(function () {
|
||||||
el.style.transitionDuration = '0s';
|
el.style.transitionDuration = ".7s";
|
||||||
el.style.backgroundColor = 'var(--comfy-input-bg)';
|
el.style.backgroundColor = "var(--comfy-input-bg)";
|
||||||
},
|
}, 15);
|
||||||
$: (el) => (nameInput = el),
|
},
|
||||||
})
|
onkeypress: (e) => {
|
||||||
]
|
var el = e.target;
|
||||||
),
|
clearTimeout(this.saveVisualCue);
|
||||||
$el(
|
el.style.transitionDuration = "0s";
|
||||||
"div",
|
el.style.backgroundColor = "var(--comfy-input-bg)";
|
||||||
{},
|
},
|
||||||
[
|
$: (el) => (nameInput = el),
|
||||||
$el("button", {
|
}),
|
||||||
textContent: "Export",
|
]
|
||||||
style: {
|
),
|
||||||
fontSize: "12px",
|
$el("div", {}, [
|
||||||
fontWeight: "normal",
|
$el("button", {
|
||||||
},
|
textContent: "Export",
|
||||||
onclick: (e) => {
|
style: {
|
||||||
const json = JSON.stringify({templates: [t]}, null, 2); // convert the data to a JSON string
|
fontSize: "12px",
|
||||||
const blob = new Blob([json], {type: "application/json"});
|
fontWeight: "normal",
|
||||||
const url = URL.createObjectURL(blob);
|
},
|
||||||
const a = $el("a", {
|
onclick: (e) => {
|
||||||
href: url,
|
const json = JSON.stringify({ templates: [t] }, null, 2); // convert the data to a JSON string
|
||||||
download: (nameInput.value || t.name) + ".json",
|
const blob = new Blob([json], {
|
||||||
style: {display: "none"},
|
type: "application/json",
|
||||||
parent: document.body,
|
});
|
||||||
});
|
const url = URL.createObjectURL(blob);
|
||||||
a.click();
|
const a = $el("a", {
|
||||||
setTimeout(function () {
|
href: url,
|
||||||
a.remove();
|
download: (nameInput.value || t.name) + ".json",
|
||||||
window.URL.revokeObjectURL(url);
|
style: { display: "none" },
|
||||||
}, 0);
|
parent: document.body,
|
||||||
},
|
});
|
||||||
}),
|
a.click();
|
||||||
$el("button", {
|
setTimeout(function () {
|
||||||
textContent: "Delete",
|
a.remove();
|
||||||
style: {
|
window.URL.revokeObjectURL(url);
|
||||||
fontSize: "12px",
|
}, 0);
|
||||||
color: "red",
|
},
|
||||||
fontWeight: "normal",
|
}),
|
||||||
},
|
$el("button", {
|
||||||
onclick: (e) => {
|
textContent: "Delete",
|
||||||
const item = e.target.parentNode.parentNode;
|
style: {
|
||||||
item.parentNode.removeChild(item);
|
fontSize: "12px",
|
||||||
this.templates.splice(item.dataset.id*1, 1);
|
color: "red",
|
||||||
this.store();
|
fontWeight: "normal",
|
||||||
// update the rows index, setTimeout ensures that the list is updated
|
},
|
||||||
var that = this;
|
onclick: (e) => {
|
||||||
setTimeout(function (){
|
const item = e.target.parentNode.parentNode;
|
||||||
that.element.querySelectorAll('.tempateManagerRow').forEach((el: HTMLElement,i) => {
|
item.parentNode.removeChild(item);
|
||||||
el.dataset.id = i.toString();
|
this.templates.splice(item.dataset.id * 1, 1);
|
||||||
});
|
this.store();
|
||||||
}, 0);
|
// update the rows index, setTimeout ensures that the list is updated
|
||||||
},
|
var that = this;
|
||||||
}),
|
setTimeout(function () {
|
||||||
]
|
that.element
|
||||||
),
|
.querySelectorAll(".tempateManagerRow")
|
||||||
]
|
.forEach((el: HTMLElement, i) => {
|
||||||
)
|
el.dataset.id = i.toString();
|
||||||
];
|
});
|
||||||
})
|
}, 0);
|
||||||
)
|
},
|
||||||
);
|
}),
|
||||||
}
|
]),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: id,
|
name: id,
|
||||||
setup() {
|
setup() {
|
||||||
const manage = new ManageTemplates();
|
const manage = new ManageTemplates();
|
||||||
|
|
||||||
const clipboardAction = async (cb) => {
|
const clipboardAction = async (cb) => {
|
||||||
// We use the clipboard functions but dont want to overwrite the current user clipboard
|
// We use the clipboard functions but dont want to overwrite the current user clipboard
|
||||||
// Restore it after we've run our callback
|
// Restore it after we've run our callback
|
||||||
const old = localStorage.getItem("litegrapheditor_clipboard");
|
const old = localStorage.getItem("litegrapheditor_clipboard");
|
||||||
await cb();
|
await cb();
|
||||||
localStorage.setItem("litegrapheditor_clipboard", old);
|
localStorage.setItem("litegrapheditor_clipboard", old);
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
|
const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
|
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
|
||||||
const options = orig.apply(this, arguments);
|
const options = orig.apply(this, arguments);
|
||||||
|
|
||||||
options.push(null);
|
options.push(null);
|
||||||
options.push({
|
options.push({
|
||||||
content: `Save Selected as Template`,
|
content: `Save Selected as Template`,
|
||||||
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
|
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
const name = prompt("Enter name");
|
const name = prompt("Enter name");
|
||||||
if (!name?.trim()) return;
|
if (!name?.trim()) return;
|
||||||
|
|
||||||
clipboardAction(() => {
|
clipboardAction(() => {
|
||||||
app.canvas.copyToClipboard();
|
app.canvas.copyToClipboard();
|
||||||
let data = localStorage.getItem("litegrapheditor_clipboard");
|
let data = localStorage.getItem("litegrapheditor_clipboard");
|
||||||
data = JSON.parse(data);
|
data = JSON.parse(data);
|
||||||
const nodeIds = Object.keys(app.canvas.selected_nodes);
|
const nodeIds = Object.keys(app.canvas.selected_nodes);
|
||||||
for (let i = 0; i < nodeIds.length; i++) {
|
for (let i = 0; i < nodeIds.length; i++) {
|
||||||
const node = app.graph.getNodeById(Number.parseInt(nodeIds[i]));
|
const node = app.graph.getNodeById(Number.parseInt(nodeIds[i]));
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const nodeData = node?.constructor.nodeData;
|
const nodeData = node?.constructor.nodeData;
|
||||||
|
|
||||||
let groupData = GroupNodeHandler.getGroupData(node);
|
let groupData = GroupNodeHandler.getGroupData(node);
|
||||||
if (groupData) {
|
if (groupData) {
|
||||||
groupData = groupData.nodeData;
|
groupData = groupData.nodeData;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (!data.groupNodes) {
|
if (!data.groupNodes) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
data.groupNodes = {};
|
data.groupNodes = {};
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
data.groupNodes[nodeData.name] = groupData;
|
data.groupNodes[nodeData.name] = groupData;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
data.nodes[i].type = nodeData.name;
|
data.nodes[i].type = nodeData.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
manage.templates.push({
|
manage.templates.push({
|
||||||
name,
|
name,
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
manage.store();
|
manage.store();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Map each template to a menu item
|
// Map each template to a menu item
|
||||||
const subItems = manage.templates.map((t) => {
|
const subItems = manage.templates.map((t) => {
|
||||||
return {
|
return {
|
||||||
content: t.name,
|
content: t.name,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
clipboardAction(async () => {
|
clipboardAction(async () => {
|
||||||
const data = JSON.parse(t.data);
|
const data = JSON.parse(t.data);
|
||||||
await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {});
|
await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {});
|
||||||
localStorage.setItem("litegrapheditor_clipboard", t.data);
|
localStorage.setItem("litegrapheditor_clipboard", t.data);
|
||||||
app.canvas.pasteFromClipboard();
|
app.canvas.pasteFromClipboard();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
subItems.push(null, {
|
subItems.push(null, {
|
||||||
content: "Manage",
|
content: "Manage",
|
||||||
callback: () => manage.show(),
|
callback: () => manage.show(),
|
||||||
});
|
});
|
||||||
|
|
||||||
options.push({
|
options.push({
|
||||||
content: "Node Templates",
|
content: "Node Templates",
|
||||||
submenu: {
|
submenu: {
|
||||||
options: subItems,
|
options: subItems,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,53 +1,55 @@
|
|||||||
import {app} from "../../scripts/app";
|
import { app } from "../../scripts/app";
|
||||||
import {ComfyWidgets} from "../../scripts/widgets";
|
import { ComfyWidgets } from "../../scripts/widgets";
|
||||||
// Node that add notes to your project
|
// Node that add notes to your project
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "Comfy.NoteNode",
|
name: "Comfy.NoteNode",
|
||||||
registerCustomNodes() {
|
registerCustomNodes() {
|
||||||
class NoteNode {
|
class NoteNode {
|
||||||
static category: string;
|
static category: string;
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
color=LGraphCanvas.node_colors.yellow.color;
|
|
||||||
// @ts-ignore
|
|
||||||
bgcolor=LGraphCanvas.node_colors.yellow.bgcolor;
|
|
||||||
// @ts-ignore
|
|
||||||
groupcolor = LGraphCanvas.node_colors.yellow.groupcolor;
|
|
||||||
properties: { text: string };
|
|
||||||
serialize_widgets: boolean;
|
|
||||||
isVirtualNode: boolean;
|
|
||||||
collapsable: boolean;
|
|
||||||
title_mode: number;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
if (!this.properties) {
|
|
||||||
this.properties = { text: "" };
|
|
||||||
}
|
|
||||||
// @ts-ignore
|
|
||||||
// Should we extends LGraphNode?
|
|
||||||
ComfyWidgets.STRING(this, "", ["", {default:this.properties.text, multiline: true}], app)
|
|
||||||
|
|
||||||
this.serialize_widgets = true;
|
|
||||||
this.isVirtualNode = true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
color = LGraphCanvas.node_colors.yellow.color;
|
||||||
|
// @ts-ignore
|
||||||
|
bgcolor = LGraphCanvas.node_colors.yellow.bgcolor;
|
||||||
|
// @ts-ignore
|
||||||
|
groupcolor = LGraphCanvas.node_colors.yellow.groupcolor;
|
||||||
|
properties: { text: string };
|
||||||
|
serialize_widgets: boolean;
|
||||||
|
isVirtualNode: boolean;
|
||||||
|
collapsable: boolean;
|
||||||
|
title_mode: number;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
if (!this.properties) {
|
||||||
|
this.properties = { text: "" };
|
||||||
}
|
}
|
||||||
|
ComfyWidgets.STRING(
|
||||||
// Load default visibility
|
// @ts-ignore
|
||||||
|
// Should we extends LGraphNode?
|
||||||
LiteGraph.registerNodeType(
|
this,
|
||||||
"Note",
|
"",
|
||||||
// @ts-ignore
|
["", { default: this.properties.text, multiline: true }],
|
||||||
Object.assign(NoteNode, {
|
app
|
||||||
title_mode: LiteGraph.NORMAL_TITLE,
|
|
||||||
title: "Note",
|
|
||||||
collapsable: true,
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
NoteNode.category = "utils";
|
this.serialize_widgets = true;
|
||||||
},
|
this.isVirtualNode = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load default visibility
|
||||||
|
|
||||||
|
LiteGraph.registerNodeType(
|
||||||
|
"Note",
|
||||||
|
// @ts-ignore
|
||||||
|
Object.assign(NoteNode, {
|
||||||
|
title_mode: LiteGraph.NORMAL_TITLE,
|
||||||
|
title: "Note",
|
||||||
|
collapsable: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
NoteNode.category = "utils";
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,271 +4,311 @@ import { mergeIfValid, getWidgetConfig, setWidgetConfig } from "./widgetInputs";
|
|||||||
// Node that allows you to redirect connections for cleaner graphs
|
// Node that allows you to redirect connections for cleaner graphs
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "Comfy.RerouteNode",
|
name: "Comfy.RerouteNode",
|
||||||
registerCustomNodes(app) {
|
registerCustomNodes(app) {
|
||||||
class RerouteNode {
|
class RerouteNode {
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!this.properties) {
|
if (!this.properties) {
|
||||||
this.properties = {};
|
this.properties = {};
|
||||||
}
|
}
|
||||||
this.properties.showOutputText = RerouteNode.defaultVisibility;
|
this.properties.showOutputText = RerouteNode.defaultVisibility;
|
||||||
this.properties.horizontal = false;
|
this.properties.horizontal = false;
|
||||||
|
|
||||||
this.addInput("", "*");
|
this.addInput("", "*");
|
||||||
this.addOutput(this.properties.showOutputText ? "*" : "", "*");
|
this.addOutput(this.properties.showOutputText ? "*" : "", "*");
|
||||||
|
|
||||||
this.onAfterGraphConfigured = function () {
|
this.onAfterGraphConfigured = function () {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
this.onConnectionsChange(LiteGraph.INPUT, null, true, null);
|
this.onConnectionsChange(LiteGraph.INPUT, null, true, null);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onConnectionsChange = function (type, index, connected, link_info) {
|
this.onConnectionsChange = function (
|
||||||
this.applyOrientation();
|
type,
|
||||||
|
index,
|
||||||
|
connected,
|
||||||
|
link_info
|
||||||
|
) {
|
||||||
|
this.applyOrientation();
|
||||||
|
|
||||||
// Prevent multiple connections to different types when we have no input
|
// Prevent multiple connections to different types when we have no input
|
||||||
if (connected && type === LiteGraph.OUTPUT) {
|
if (connected && type === LiteGraph.OUTPUT) {
|
||||||
// Ignore wildcard nodes as these will be updated to real types
|
// Ignore wildcard nodes as these will be updated to real types
|
||||||
const types = new Set(this.outputs[0].links.map((l) => app.graph.links[l].type).filter((t) => t !== "*"));
|
const types = new Set(
|
||||||
if (types.size > 1) {
|
this.outputs[0].links
|
||||||
const linksToDisconnect = [];
|
.map((l) => app.graph.links[l].type)
|
||||||
for (let i = 0; i < this.outputs[0].links.length - 1; i++) {
|
.filter((t) => t !== "*")
|
||||||
const linkId = this.outputs[0].links[i];
|
);
|
||||||
const link = app.graph.links[linkId];
|
if (types.size > 1) {
|
||||||
linksToDisconnect.push(link);
|
const linksToDisconnect = [];
|
||||||
}
|
for (let i = 0; i < this.outputs[0].links.length - 1; i++) {
|
||||||
for (const link of linksToDisconnect) {
|
const linkId = this.outputs[0].links[i];
|
||||||
const node = app.graph.getNodeById(link.target_id);
|
const link = app.graph.links[linkId];
|
||||||
node.disconnectInput(link.target_slot);
|
linksToDisconnect.push(link);
|
||||||
}
|
}
|
||||||
}
|
for (const link of linksToDisconnect) {
|
||||||
}
|
const node = app.graph.getNodeById(link.target_id);
|
||||||
|
node.disconnectInput(link.target_slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Find root input
|
// Find root input
|
||||||
let currentNode = this;
|
let currentNode = this;
|
||||||
let updateNodes = [];
|
let updateNodes = [];
|
||||||
let inputType = null;
|
let inputType = null;
|
||||||
let inputNode = null;
|
let inputNode = null;
|
||||||
while (currentNode) {
|
while (currentNode) {
|
||||||
updateNodes.unshift(currentNode);
|
updateNodes.unshift(currentNode);
|
||||||
const linkId = currentNode.inputs[0].link;
|
const linkId = currentNode.inputs[0].link;
|
||||||
if (linkId !== null) {
|
if (linkId !== null) {
|
||||||
const link = app.graph.links[linkId];
|
const link = app.graph.links[linkId];
|
||||||
if (!link) return;
|
if (!link) return;
|
||||||
const node = app.graph.getNodeById(link.origin_id);
|
const node = app.graph.getNodeById(link.origin_id);
|
||||||
const type = node.constructor.type;
|
const type = node.constructor.type;
|
||||||
if (type === "Reroute") {
|
if (type === "Reroute") {
|
||||||
if (node === this) {
|
if (node === this) {
|
||||||
// We've found a circle
|
// We've found a circle
|
||||||
currentNode.disconnectInput(link.target_slot);
|
currentNode.disconnectInput(link.target_slot);
|
||||||
currentNode = null;
|
currentNode = null;
|
||||||
} else {
|
} else {
|
||||||
// Move the previous node
|
// Move the previous node
|
||||||
currentNode = node;
|
currentNode = node;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// We've found the end
|
// We've found the end
|
||||||
inputNode = currentNode;
|
inputNode = currentNode;
|
||||||
inputType = node.outputs[link.origin_slot]?.type ?? null;
|
inputType = node.outputs[link.origin_slot]?.type ?? null;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// This path has no input node
|
// This path has no input node
|
||||||
currentNode = null;
|
currentNode = null;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all outputs
|
// Find all outputs
|
||||||
const nodes = [this];
|
const nodes = [this];
|
||||||
let outputType = null;
|
let outputType = null;
|
||||||
while (nodes.length) {
|
while (nodes.length) {
|
||||||
currentNode = nodes.pop();
|
currentNode = nodes.pop();
|
||||||
const outputs = (currentNode.outputs ? currentNode.outputs[0].links : []) || [];
|
const outputs =
|
||||||
if (outputs.length) {
|
(currentNode.outputs ? currentNode.outputs[0].links : []) || [];
|
||||||
for (const linkId of outputs) {
|
if (outputs.length) {
|
||||||
const link = app.graph.links[linkId];
|
for (const linkId of outputs) {
|
||||||
|
const link = app.graph.links[linkId];
|
||||||
|
|
||||||
// When disconnecting sometimes the link is still registered
|
// When disconnecting sometimes the link is still registered
|
||||||
if (!link) continue;
|
if (!link) continue;
|
||||||
|
|
||||||
const node = app.graph.getNodeById(link.target_id);
|
const node = app.graph.getNodeById(link.target_id);
|
||||||
const type = node.constructor.type;
|
const type = node.constructor.type;
|
||||||
|
|
||||||
if (type === "Reroute") {
|
if (type === "Reroute") {
|
||||||
// Follow reroute nodes
|
// Follow reroute nodes
|
||||||
nodes.push(node);
|
nodes.push(node);
|
||||||
updateNodes.push(node);
|
updateNodes.push(node);
|
||||||
} else {
|
} else {
|
||||||
// We've found an output
|
// We've found an output
|
||||||
const nodeOutType =
|
const nodeOutType =
|
||||||
node.inputs && node.inputs[link?.target_slot] && node.inputs[link.target_slot].type
|
node.inputs &&
|
||||||
? node.inputs[link.target_slot].type
|
node.inputs[link?.target_slot] &&
|
||||||
: null;
|
node.inputs[link.target_slot].type
|
||||||
if (inputType && inputType !== "*" && nodeOutType !== inputType) {
|
? node.inputs[link.target_slot].type
|
||||||
// The output doesnt match our input so disconnect it
|
: null;
|
||||||
node.disconnectInput(link.target_slot);
|
if (
|
||||||
} else {
|
inputType &&
|
||||||
outputType = nodeOutType;
|
inputType !== "*" &&
|
||||||
}
|
nodeOutType !== inputType
|
||||||
}
|
) {
|
||||||
}
|
// The output doesnt match our input so disconnect it
|
||||||
} else {
|
node.disconnectInput(link.target_slot);
|
||||||
// No more outputs for this path
|
} else {
|
||||||
}
|
outputType = nodeOutType;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No more outputs for this path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const displayType = inputType || outputType || "*";
|
const displayType = inputType || outputType || "*";
|
||||||
const color = LGraphCanvas.link_type_colors[displayType];
|
const color = LGraphCanvas.link_type_colors[displayType];
|
||||||
|
|
||||||
let widgetConfig;
|
let widgetConfig;
|
||||||
let targetWidget;
|
let targetWidget;
|
||||||
let widgetType;
|
let widgetType;
|
||||||
// Update the types of each node
|
// Update the types of each node
|
||||||
for (const node of updateNodes) {
|
for (const node of updateNodes) {
|
||||||
// If we dont have an input type we are always wildcard but we'll show the output type
|
// If we dont have an input type we are always wildcard but we'll show the output type
|
||||||
// This lets you change the output link to a different type and all nodes will update
|
// This lets you change the output link to a different type and all nodes will update
|
||||||
node.outputs[0].type = inputType || "*";
|
node.outputs[0].type = inputType || "*";
|
||||||
node.__outputType = displayType;
|
node.__outputType = displayType;
|
||||||
node.outputs[0].name = node.properties.showOutputText ? displayType : "";
|
node.outputs[0].name = node.properties.showOutputText
|
||||||
node.size = node.computeSize();
|
? displayType
|
||||||
node.applyOrientation();
|
: "";
|
||||||
|
node.size = node.computeSize();
|
||||||
|
node.applyOrientation();
|
||||||
|
|
||||||
for (const l of node.outputs[0].links || []) {
|
for (const l of node.outputs[0].links || []) {
|
||||||
const link = app.graph.links[l];
|
const link = app.graph.links[l];
|
||||||
if (link) {
|
if (link) {
|
||||||
link.color = color;
|
link.color = color;
|
||||||
|
|
||||||
if (app.configuringGraph) continue;
|
if (app.configuringGraph) continue;
|
||||||
const targetNode = app.graph.getNodeById(link.target_id);
|
const targetNode = app.graph.getNodeById(link.target_id);
|
||||||
const targetInput = targetNode.inputs?.[link.target_slot];
|
const targetInput = targetNode.inputs?.[link.target_slot];
|
||||||
if (targetInput?.widget) {
|
if (targetInput?.widget) {
|
||||||
const config = getWidgetConfig(targetInput);
|
const config = getWidgetConfig(targetInput);
|
||||||
if (!widgetConfig) {
|
if (!widgetConfig) {
|
||||||
widgetConfig = config[1] ?? {};
|
widgetConfig = config[1] ?? {};
|
||||||
widgetType = config[0];
|
widgetType = config[0];
|
||||||
}
|
}
|
||||||
if (!targetWidget) {
|
if (!targetWidget) {
|
||||||
targetWidget = targetNode.widgets?.find((w) => w.name === targetInput.widget.name);
|
targetWidget = targetNode.widgets?.find(
|
||||||
}
|
(w) => w.name === targetInput.widget.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const merged = mergeIfValid(targetInput, [config[0], widgetConfig]);
|
const merged = mergeIfValid(targetInput, [
|
||||||
if (merged.customConfig) {
|
config[0],
|
||||||
widgetConfig = merged.customConfig;
|
widgetConfig,
|
||||||
}
|
]);
|
||||||
}
|
if (merged.customConfig) {
|
||||||
}
|
widgetConfig = merged.customConfig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const node of updateNodes) {
|
for (const node of updateNodes) {
|
||||||
if (widgetConfig && outputType) {
|
if (widgetConfig && outputType) {
|
||||||
node.inputs[0].widget = { name: "value" };
|
node.inputs[0].widget = { name: "value" };
|
||||||
setWidgetConfig(node.inputs[0], [widgetType ?? displayType, widgetConfig], targetWidget);
|
setWidgetConfig(
|
||||||
} else {
|
node.inputs[0],
|
||||||
setWidgetConfig(node.inputs[0], null);
|
[widgetType ?? displayType, widgetConfig],
|
||||||
}
|
targetWidget
|
||||||
}
|
);
|
||||||
|
} else {
|
||||||
|
setWidgetConfig(node.inputs[0], null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (inputNode) {
|
if (inputNode) {
|
||||||
const link = app.graph.links[inputNode.inputs[0].link];
|
const link = app.graph.links[inputNode.inputs[0].link];
|
||||||
if (link) {
|
if (link) {
|
||||||
link.color = color;
|
link.color = color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.clone = function () {
|
this.clone = function () {
|
||||||
const cloned = RerouteNode.prototype.clone.apply(this);
|
const cloned = RerouteNode.prototype.clone.apply(this);
|
||||||
cloned.removeOutput(0);
|
cloned.removeOutput(0);
|
||||||
cloned.addOutput(this.properties.showOutputText ? "*" : "", "*");
|
cloned.addOutput(this.properties.showOutputText ? "*" : "", "*");
|
||||||
cloned.size = cloned.computeSize();
|
cloned.size = cloned.computeSize();
|
||||||
return cloned;
|
return cloned;
|
||||||
};
|
};
|
||||||
|
|
||||||
// This node is purely frontend and does not impact the resulting prompt so should not be serialized
|
// This node is purely frontend and does not impact the resulting prompt so should not be serialized
|
||||||
this.isVirtualNode = true;
|
this.isVirtualNode = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
getExtraMenuOptions(_, options) {
|
getExtraMenuOptions(_, options) {
|
||||||
options.unshift(
|
options.unshift(
|
||||||
{
|
{
|
||||||
content: (this.properties.showOutputText ? "Hide" : "Show") + " Type",
|
content:
|
||||||
callback: () => {
|
(this.properties.showOutputText ? "Hide" : "Show") + " Type",
|
||||||
this.properties.showOutputText = !this.properties.showOutputText;
|
callback: () => {
|
||||||
if (this.properties.showOutputText) {
|
this.properties.showOutputText = !this.properties.showOutputText;
|
||||||
this.outputs[0].name = this.__outputType || this.outputs[0].type;
|
if (this.properties.showOutputText) {
|
||||||
} else {
|
this.outputs[0].name =
|
||||||
this.outputs[0].name = "";
|
this.__outputType || this.outputs[0].type;
|
||||||
}
|
} else {
|
||||||
this.size = this.computeSize();
|
this.outputs[0].name = "";
|
||||||
this.applyOrientation();
|
}
|
||||||
app.graph.setDirtyCanvas(true, true);
|
this.size = this.computeSize();
|
||||||
},
|
this.applyOrientation();
|
||||||
},
|
app.graph.setDirtyCanvas(true, true);
|
||||||
{
|
},
|
||||||
content: (RerouteNode.defaultVisibility ? "Hide" : "Show") + " Type By Default",
|
},
|
||||||
callback: () => {
|
{
|
||||||
RerouteNode.setDefaultTextVisibility(!RerouteNode.defaultVisibility);
|
content:
|
||||||
},
|
(RerouteNode.defaultVisibility ? "Hide" : "Show") +
|
||||||
},
|
" Type By Default",
|
||||||
{
|
callback: () => {
|
||||||
// naming is inverted with respect to LiteGraphNode.horizontal
|
RerouteNode.setDefaultTextVisibility(
|
||||||
// LiteGraphNode.horizontal == true means that
|
!RerouteNode.defaultVisibility
|
||||||
// each slot in the inputs and outputs are layed out horizontally,
|
);
|
||||||
// which is the opposite of the visual orientation of the inputs and outputs as a node
|
},
|
||||||
content: "Set " + (this.properties.horizontal ? "Horizontal" : "Vertical"),
|
},
|
||||||
callback: () => {
|
{
|
||||||
this.properties.horizontal = !this.properties.horizontal;
|
// naming is inverted with respect to LiteGraphNode.horizontal
|
||||||
this.applyOrientation();
|
// LiteGraphNode.horizontal == true means that
|
||||||
},
|
// each slot in the inputs and outputs are layed out horizontally,
|
||||||
}
|
// which is the opposite of the visual orientation of the inputs and outputs as a node
|
||||||
);
|
content:
|
||||||
}
|
"Set " + (this.properties.horizontal ? "Horizontal" : "Vertical"),
|
||||||
applyOrientation() {
|
callback: () => {
|
||||||
this.horizontal = this.properties.horizontal;
|
this.properties.horizontal = !this.properties.horizontal;
|
||||||
if (this.horizontal) {
|
this.applyOrientation();
|
||||||
// we correct the input position, because LiteGraphNode.horizontal
|
},
|
||||||
// doesn't account for title presence
|
}
|
||||||
// which reroute nodes don't have
|
);
|
||||||
this.inputs[0].pos = [this.size[0] / 2, 0];
|
}
|
||||||
} else {
|
applyOrientation() {
|
||||||
delete this.inputs[0].pos;
|
this.horizontal = this.properties.horizontal;
|
||||||
}
|
if (this.horizontal) {
|
||||||
app.graph.setDirtyCanvas(true, true);
|
// we correct the input position, because LiteGraphNode.horizontal
|
||||||
}
|
// doesn't account for title presence
|
||||||
|
// which reroute nodes don't have
|
||||||
|
this.inputs[0].pos = [this.size[0] / 2, 0];
|
||||||
|
} else {
|
||||||
|
delete this.inputs[0].pos;
|
||||||
|
}
|
||||||
|
app.graph.setDirtyCanvas(true, true);
|
||||||
|
}
|
||||||
|
|
||||||
computeSize() {
|
computeSize() {
|
||||||
return [
|
return [
|
||||||
this.properties.showOutputText && this.outputs && this.outputs.length
|
this.properties.showOutputText && this.outputs && this.outputs.length
|
||||||
? Math.max(75, LiteGraph.NODE_TEXT_SIZE * this.outputs[0].name.length * 0.6 + 40)
|
? Math.max(
|
||||||
: 75,
|
75,
|
||||||
26,
|
LiteGraph.NODE_TEXT_SIZE * this.outputs[0].name.length * 0.6 +
|
||||||
];
|
40
|
||||||
}
|
)
|
||||||
|
: 75,
|
||||||
|
26,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
static setDefaultTextVisibility(visible) {
|
static setDefaultTextVisibility(visible) {
|
||||||
RerouteNode.defaultVisibility = visible;
|
RerouteNode.defaultVisibility = visible;
|
||||||
if (visible) {
|
if (visible) {
|
||||||
localStorage["Comfy.RerouteNode.DefaultVisibility"] = "true";
|
localStorage["Comfy.RerouteNode.DefaultVisibility"] = "true";
|
||||||
} else {
|
} else {
|
||||||
delete localStorage["Comfy.RerouteNode.DefaultVisibility"];
|
delete localStorage["Comfy.RerouteNode.DefaultVisibility"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load default visibility
|
// Load default visibility
|
||||||
RerouteNode.setDefaultTextVisibility(!!localStorage["Comfy.RerouteNode.DefaultVisibility"]);
|
RerouteNode.setDefaultTextVisibility(
|
||||||
|
!!localStorage["Comfy.RerouteNode.DefaultVisibility"]
|
||||||
|
);
|
||||||
|
|
||||||
LiteGraph.registerNodeType(
|
LiteGraph.registerNodeType(
|
||||||
"Reroute",
|
"Reroute",
|
||||||
Object.assign(RerouteNode, {
|
Object.assign(RerouteNode, {
|
||||||
title_mode: LiteGraph.NO_TITLE,
|
title_mode: LiteGraph.NO_TITLE,
|
||||||
title: "Reroute",
|
title: "Reroute",
|
||||||
collapsable: false,
|
collapsable: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
RerouteNode.category = "utils";
|
RerouteNode.category = "utils";
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,33 +3,41 @@ import { applyTextReplacements } from "../../scripts/utils";
|
|||||||
// Use widget values and dates in output filenames
|
// Use widget values and dates in output filenames
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "Comfy.SaveImageExtraOutput",
|
name: "Comfy.SaveImageExtraOutput",
|
||||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||||
if (nodeData.name === "SaveImage") {
|
if (nodeData.name === "SaveImage") {
|
||||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||||
// When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R
|
// When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R
|
||||||
nodeType.prototype.onNodeCreated = function () {
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
const r = onNodeCreated
|
||||||
|
? onNodeCreated.apply(this, arguments)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const widget = this.widgets.find((w) => w.name === "filename_prefix");
|
const widget = this.widgets.find((w) => w.name === "filename_prefix");
|
||||||
widget.serializeValue = () => {
|
widget.serializeValue = () => {
|
||||||
return applyTextReplacements(app, widget.value);
|
return applyTextReplacements(app, widget.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
return r;
|
return r;
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// When any other node is created add a property to alias the node
|
// When any other node is created add a property to alias the node
|
||||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||||
nodeType.prototype.onNodeCreated = function () {
|
nodeType.prototype.onNodeCreated = function () {
|
||||||
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
const r = onNodeCreated
|
||||||
|
? onNodeCreated.apply(this, arguments)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (!this.properties || !("Node name for S&R" in this.properties)) {
|
if (!this.properties || !("Node name for S&R" in this.properties)) {
|
||||||
this.addProperty("Node name for S&R", this.constructor.type, "string");
|
this.addProperty(
|
||||||
}
|
"Node name for S&R",
|
||||||
|
this.constructor.type,
|
||||||
|
"string"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return r;
|
return r;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,108 +4,111 @@ let touchZooming;
|
|||||||
let touchCount = 0;
|
let touchCount = 0;
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "Comfy.SimpleTouchSupport",
|
name: "Comfy.SimpleTouchSupport",
|
||||||
setup() {
|
setup() {
|
||||||
let zoomPos;
|
let zoomPos;
|
||||||
let touchTime;
|
let touchTime;
|
||||||
let lastTouch;
|
let lastTouch;
|
||||||
|
|
||||||
function getMultiTouchPos(e) {
|
function getMultiTouchPos(e) {
|
||||||
return Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
|
return Math.hypot(
|
||||||
}
|
e.touches[0].clientX - e.touches[1].clientX,
|
||||||
|
e.touches[0].clientY - e.touches[1].clientY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
app.canvasEl.addEventListener(
|
app.canvasEl.addEventListener(
|
||||||
"touchstart",
|
"touchstart",
|
||||||
(e) => {
|
(e) => {
|
||||||
touchCount++;
|
touchCount++;
|
||||||
lastTouch = null;
|
lastTouch = null;
|
||||||
if (e.touches?.length === 1) {
|
if (e.touches?.length === 1) {
|
||||||
// Store start time for press+hold for context menu
|
// Store start time for press+hold for context menu
|
||||||
touchTime = new Date();
|
touchTime = new Date();
|
||||||
lastTouch = e.touches[0];
|
lastTouch = e.touches[0];
|
||||||
} else {
|
} else {
|
||||||
touchTime = null;
|
touchTime = null;
|
||||||
if (e.touches?.length === 2) {
|
if (e.touches?.length === 2) {
|
||||||
// Store center pos for zoom
|
// Store center pos for zoom
|
||||||
zoomPos = getMultiTouchPos(e);
|
zoomPos = getMultiTouchPos(e);
|
||||||
app.canvas.pointer_is_down = false;
|
app.canvas.pointer_is_down = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
app.canvasEl.addEventListener("touchend", (e: TouchEvent) => {
|
app.canvasEl.addEventListener("touchend", (e: TouchEvent) => {
|
||||||
touchZooming = false;
|
touchZooming = false;
|
||||||
touchCount = e.touches?.length ?? touchCount - 1;
|
touchCount = e.touches?.length ?? touchCount - 1;
|
||||||
if (touchTime && !e.touches?.length) {
|
if (touchTime && !e.touches?.length) {
|
||||||
if ((new Date()).getTime() - touchTime > 600) {
|
if (new Date().getTime() - touchTime > 600) {
|
||||||
try {
|
try {
|
||||||
// hack to get litegraph to use this event
|
// hack to get litegraph to use this event
|
||||||
e.constructor = CustomEvent;
|
e.constructor = CustomEvent;
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
e.clientX = lastTouch.clientX;
|
e.clientX = lastTouch.clientX;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
e.clientY = lastTouch.clientY;
|
e.clientY = lastTouch.clientY;
|
||||||
|
|
||||||
app.canvas.pointer_is_down = true;
|
app.canvas.pointer_is_down = true;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
app.canvas._mousedown_callback(e);
|
app.canvas._mousedown_callback(e);
|
||||||
}
|
}
|
||||||
touchTime = null;
|
touchTime = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.canvasEl.addEventListener(
|
app.canvasEl.addEventListener(
|
||||||
"touchmove",
|
"touchmove",
|
||||||
(e) => {
|
(e) => {
|
||||||
touchTime = null;
|
touchTime = null;
|
||||||
if (e.touches?.length === 2) {
|
if (e.touches?.length === 2) {
|
||||||
app.canvas.pointer_is_down = false;
|
app.canvas.pointer_is_down = false;
|
||||||
touchZooming = true;
|
touchZooming = true;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
LiteGraph.closeAllContextMenus();
|
LiteGraph.closeAllContextMenus();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
app.canvas.search_box?.close();
|
app.canvas.search_box?.close();
|
||||||
const newZoomPos = getMultiTouchPos(e);
|
const newZoomPos = getMultiTouchPos(e);
|
||||||
|
|
||||||
const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
|
const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
|
||||||
const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
|
const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
|
||||||
|
|
||||||
let scale = app.canvas.ds.scale;
|
let scale = app.canvas.ds.scale;
|
||||||
const diff = zoomPos - newZoomPos;
|
const diff = zoomPos - newZoomPos;
|
||||||
if (diff > 0.5) {
|
if (diff > 0.5) {
|
||||||
scale *= 1 / 1.07;
|
scale *= 1 / 1.07;
|
||||||
} else if (diff < -0.5) {
|
} else if (diff < -0.5) {
|
||||||
scale *= 1.07;
|
scale *= 1.07;
|
||||||
}
|
}
|
||||||
app.canvas.ds.changeScale(scale, [midX, midY]);
|
app.canvas.ds.changeScale(scale, [midX, midY]);
|
||||||
app.canvas.setDirty(true, true);
|
app.canvas.setDirty(true, true);
|
||||||
zoomPos = newZoomPos;
|
zoomPos = newZoomPos;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const processMouseDown = LGraphCanvas.prototype.processMouseDown;
|
const processMouseDown = LGraphCanvas.prototype.processMouseDown;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
LGraphCanvas.prototype.processMouseDown = function (e) {
|
LGraphCanvas.prototype.processMouseDown = function (e) {
|
||||||
if (touchZooming || touchCount) {
|
if (touchZooming || touchCount) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return processMouseDown.apply(this, arguments);
|
return processMouseDown.apply(this, arguments);
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const processMouseMove = LGraphCanvas.prototype.processMouseMove;
|
const processMouseMove = LGraphCanvas.prototype.processMouseMove;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
LGraphCanvas.prototype.processMouseMove = function (e) {
|
LGraphCanvas.prototype.processMouseMove = function (e) {
|
||||||
if (touchZooming || touchCount > 1) {
|
if (touchZooming || touchCount > 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return processMouseMove.apply(this, arguments);
|
return processMouseMove.apply(this, arguments);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,89 +3,94 @@ import { ComfyWidgets } from "../../scripts/widgets";
|
|||||||
// Adds defaults for quickly adding nodes with middle click on the input/output
|
// Adds defaults for quickly adding nodes with middle click on the input/output
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "Comfy.SlotDefaults",
|
name: "Comfy.SlotDefaults",
|
||||||
suggestionsNumber: null,
|
suggestionsNumber: null,
|
||||||
init() {
|
init() {
|
||||||
LiteGraph.search_filter_enabled = true;
|
LiteGraph.search_filter_enabled = true;
|
||||||
LiteGraph.middle_click_slot_add_default_node = true;
|
LiteGraph.middle_click_slot_add_default_node = true;
|
||||||
this.suggestionsNumber = app.ui.settings.addSetting({
|
this.suggestionsNumber = app.ui.settings.addSetting({
|
||||||
id: "Comfy.NodeSuggestions.number",
|
id: "Comfy.NodeSuggestions.number",
|
||||||
name: "Number of nodes suggestions",
|
name: "Number of nodes suggestions",
|
||||||
type: "slider",
|
type: "slider",
|
||||||
attrs: {
|
attrs: {
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 100,
|
max: 100,
|
||||||
step: 1,
|
step: 1,
|
||||||
},
|
},
|
||||||
defaultValue: 5,
|
defaultValue: 5,
|
||||||
onChange: (newVal, oldVal) => {
|
onChange: (newVal, oldVal) => {
|
||||||
this.setDefaults(newVal);
|
this.setDefaults(newVal);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
slot_types_default_out: {},
|
slot_types_default_out: {},
|
||||||
slot_types_default_in: {},
|
slot_types_default_in: {},
|
||||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||||
var nodeId = nodeData.name;
|
var nodeId = nodeData.name;
|
||||||
var inputs = [];
|
var inputs = [];
|
||||||
inputs = nodeData["input"]["required"]; //only show required inputs to reduce the mess also not logical to create node with optional inputs
|
inputs = nodeData["input"]["required"]; //only show required inputs to reduce the mess also not logical to create node with optional inputs
|
||||||
for (const inputKey in inputs) {
|
for (const inputKey in inputs) {
|
||||||
var input = (inputs[inputKey]);
|
var input = inputs[inputKey];
|
||||||
if (typeof input[0] !== "string") continue;
|
if (typeof input[0] !== "string") continue;
|
||||||
|
|
||||||
var type = input[0]
|
var type = input[0];
|
||||||
if (type in ComfyWidgets) {
|
if (type in ComfyWidgets) {
|
||||||
var customProperties = input[1]
|
var customProperties = input[1];
|
||||||
if (!(customProperties?.forceInput)) continue; //ignore widgets that don't force input
|
if (!customProperties?.forceInput) continue; //ignore widgets that don't force input
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(type in this.slot_types_default_out)) {
|
if (!(type in this.slot_types_default_out)) {
|
||||||
this.slot_types_default_out[type] = ["Reroute"];
|
this.slot_types_default_out[type] = ["Reroute"];
|
||||||
}
|
}
|
||||||
if (this.slot_types_default_out[type].includes(nodeId)) continue;
|
if (this.slot_types_default_out[type].includes(nodeId)) continue;
|
||||||
this.slot_types_default_out[type].push(nodeId);
|
this.slot_types_default_out[type].push(nodeId);
|
||||||
|
|
||||||
// Input types have to be stored as lower case
|
// Input types have to be stored as lower case
|
||||||
// Store each node that can handle this input type
|
// Store each node that can handle this input type
|
||||||
const lowerType = type.toLocaleLowerCase();
|
const lowerType = type.toLocaleLowerCase();
|
||||||
if (!(lowerType in LiteGraph.registered_slot_in_types)) {
|
if (!(lowerType in LiteGraph.registered_slot_in_types)) {
|
||||||
LiteGraph.registered_slot_in_types[lowerType] = { nodes: [] };
|
LiteGraph.registered_slot_in_types[lowerType] = { nodes: [] };
|
||||||
}
|
}
|
||||||
LiteGraph.registered_slot_in_types[lowerType].nodes.push(nodeType.comfyClass);
|
LiteGraph.registered_slot_in_types[lowerType].nodes.push(
|
||||||
}
|
nodeType.comfyClass
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
var outputs = nodeData["output"];
|
var outputs = nodeData["output"];
|
||||||
for (const key in outputs) {
|
for (const key in outputs) {
|
||||||
var type = outputs[key];
|
var type = outputs[key];
|
||||||
if (!(type in this.slot_types_default_in)) {
|
if (!(type in this.slot_types_default_in)) {
|
||||||
this.slot_types_default_in[type] = ["Reroute"];// ["Reroute", "Primitive"]; primitive doesn't always work :'()
|
this.slot_types_default_in[type] = ["Reroute"]; // ["Reroute", "Primitive"]; primitive doesn't always work :'()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.slot_types_default_in[type].push(nodeId);
|
this.slot_types_default_in[type].push(nodeId);
|
||||||
|
|
||||||
// Store each node that can handle this output type
|
// Store each node that can handle this output type
|
||||||
if (!(type in LiteGraph.registered_slot_out_types)) {
|
if (!(type in LiteGraph.registered_slot_out_types)) {
|
||||||
LiteGraph.registered_slot_out_types[type] = { nodes: [] };
|
LiteGraph.registered_slot_out_types[type] = { nodes: [] };
|
||||||
}
|
}
|
||||||
LiteGraph.registered_slot_out_types[type].nodes.push(nodeType.comfyClass);
|
LiteGraph.registered_slot_out_types[type].nodes.push(nodeType.comfyClass);
|
||||||
|
|
||||||
if(!LiteGraph.slot_types_out.includes(type)) {
|
if (!LiteGraph.slot_types_out.includes(type)) {
|
||||||
LiteGraph.slot_types_out.push(type);
|
LiteGraph.slot_types_out.push(type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var maxNum = this.suggestionsNumber.value;
|
var maxNum = this.suggestionsNumber.value;
|
||||||
this.setDefaults(maxNum);
|
this.setDefaults(maxNum);
|
||||||
},
|
},
|
||||||
setDefaults(maxNum) {
|
setDefaults(maxNum) {
|
||||||
|
LiteGraph.slot_types_default_out = {};
|
||||||
|
LiteGraph.slot_types_default_in = {};
|
||||||
|
|
||||||
LiteGraph.slot_types_default_out = {};
|
for (const type in this.slot_types_default_out) {
|
||||||
LiteGraph.slot_types_default_in = {};
|
LiteGraph.slot_types_default_out[type] = this.slot_types_default_out[
|
||||||
|
type
|
||||||
for (const type in this.slot_types_default_out) {
|
].slice(0, maxNum);
|
||||||
LiteGraph.slot_types_default_out[type] = this.slot_types_default_out[type].slice(0, maxNum);
|
}
|
||||||
}
|
for (const type in this.slot_types_default_in) {
|
||||||
for (const type in this.slot_types_default_in) {
|
LiteGraph.slot_types_default_in[type] = this.slot_types_default_in[
|
||||||
LiteGraph.slot_types_default_in[type] = this.slot_types_default_in[type].slice(0, maxNum);
|
type
|
||||||
}
|
].slice(0, maxNum);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,178 +4,192 @@ import { app } from "../../scripts/app";
|
|||||||
|
|
||||||
/** Rounds a Vector2 in-place to the current CANVAS_GRID_SIZE. */
|
/** Rounds a Vector2 in-place to the current CANVAS_GRID_SIZE. */
|
||||||
function roundVectorToGrid(vec) {
|
function roundVectorToGrid(vec) {
|
||||||
vec[0] = LiteGraph.CANVAS_GRID_SIZE * Math.round(vec[0] / LiteGraph.CANVAS_GRID_SIZE);
|
vec[0] =
|
||||||
vec[1] = LiteGraph.CANVAS_GRID_SIZE * Math.round(vec[1] / LiteGraph.CANVAS_GRID_SIZE);
|
LiteGraph.CANVAS_GRID_SIZE *
|
||||||
return vec;
|
Math.round(vec[0] / LiteGraph.CANVAS_GRID_SIZE);
|
||||||
|
vec[1] =
|
||||||
|
LiteGraph.CANVAS_GRID_SIZE *
|
||||||
|
Math.round(vec[1] / LiteGraph.CANVAS_GRID_SIZE);
|
||||||
|
return vec;
|
||||||
}
|
}
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "Comfy.SnapToGrid",
|
name: "Comfy.SnapToGrid",
|
||||||
init() {
|
init() {
|
||||||
// Add setting to control grid size
|
// Add setting to control grid size
|
||||||
app.ui.settings.addSetting({
|
app.ui.settings.addSetting({
|
||||||
id: "Comfy.SnapToGrid.GridSize",
|
id: "Comfy.SnapToGrid.GridSize",
|
||||||
name: "Grid Size",
|
name: "Grid Size",
|
||||||
type: "slider",
|
type: "slider",
|
||||||
attrs: {
|
attrs: {
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 500,
|
max: 500,
|
||||||
},
|
},
|
||||||
tooltip:
|
tooltip:
|
||||||
"When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid.",
|
"When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid.",
|
||||||
defaultValue: LiteGraph.CANVAS_GRID_SIZE,
|
defaultValue: LiteGraph.CANVAS_GRID_SIZE,
|
||||||
onChange(value) {
|
onChange(value) {
|
||||||
LiteGraph.CANVAS_GRID_SIZE = +value;
|
LiteGraph.CANVAS_GRID_SIZE = +value;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// After moving a node, if the shift key is down align it to grid
|
// After moving a node, if the shift key is down align it to grid
|
||||||
const onNodeMoved = app.canvas.onNodeMoved;
|
const onNodeMoved = app.canvas.onNodeMoved;
|
||||||
app.canvas.onNodeMoved = function (node) {
|
app.canvas.onNodeMoved = function (node) {
|
||||||
const r = onNodeMoved?.apply(this, arguments);
|
const r = onNodeMoved?.apply(this, arguments);
|
||||||
|
|
||||||
if (app.shiftDown) {
|
if (app.shiftDown) {
|
||||||
// Ensure all selected nodes are realigned
|
// Ensure all selected nodes are realigned
|
||||||
for (const id in this.selected_nodes) {
|
for (const id in this.selected_nodes) {
|
||||||
this.selected_nodes[id].alignToGrid();
|
this.selected_nodes[id].alignToGrid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return r;
|
return r;
|
||||||
};
|
};
|
||||||
|
|
||||||
// When a node is added, add a resize handler to it so we can fix align the size with the grid
|
// When a node is added, add a resize handler to it so we can fix align the size with the grid
|
||||||
const onNodeAdded = app.graph.onNodeAdded;
|
const onNodeAdded = app.graph.onNodeAdded;
|
||||||
app.graph.onNodeAdded = function (node) {
|
app.graph.onNodeAdded = function (node) {
|
||||||
const onResize = node.onResize;
|
const onResize = node.onResize;
|
||||||
node.onResize = function () {
|
node.onResize = function () {
|
||||||
if (app.shiftDown) {
|
if (app.shiftDown) {
|
||||||
roundVectorToGrid(node.size);
|
roundVectorToGrid(node.size);
|
||||||
}
|
}
|
||||||
return onResize?.apply(this, arguments);
|
return onResize?.apply(this, arguments);
|
||||||
};
|
};
|
||||||
return onNodeAdded?.apply(this, arguments);
|
return onNodeAdded?.apply(this, arguments);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Draw a preview of where the node will go if holding shift and the node is selected
|
// Draw a preview of where the node will go if holding shift and the node is selected
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const origDrawNode = LGraphCanvas.prototype.drawNode;
|
const origDrawNode = LGraphCanvas.prototype.drawNode;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
LGraphCanvas.prototype.drawNode = function (node, ctx) {
|
LGraphCanvas.prototype.drawNode = function (node, ctx) {
|
||||||
if (app.shiftDown && this.node_dragged && node.id in this.selected_nodes) {
|
if (
|
||||||
const [x, y] = roundVectorToGrid([...node.pos]);
|
app.shiftDown &&
|
||||||
const shiftX = x - node.pos[0];
|
this.node_dragged &&
|
||||||
let shiftY = y - node.pos[1];
|
node.id in this.selected_nodes
|
||||||
|
) {
|
||||||
|
const [x, y] = roundVectorToGrid([...node.pos]);
|
||||||
|
const shiftX = x - node.pos[0];
|
||||||
|
let shiftY = y - node.pos[1];
|
||||||
|
|
||||||
let w, h;
|
let w, h;
|
||||||
if (node.flags.collapsed) {
|
if (node.flags.collapsed) {
|
||||||
w = node._collapsed_width;
|
w = node._collapsed_width;
|
||||||
h = LiteGraph.NODE_TITLE_HEIGHT;
|
h = LiteGraph.NODE_TITLE_HEIGHT;
|
||||||
shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
|
shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
|
||||||
} else {
|
} else {
|
||||||
w = node.size[0];
|
w = node.size[0];
|
||||||
h = node.size[1];
|
h = node.size[1];
|
||||||
let titleMode = node.constructor.title_mode;
|
let titleMode = node.constructor.title_mode;
|
||||||
if (titleMode !== LiteGraph.TRANSPARENT_TITLE && titleMode !== LiteGraph.NO_TITLE) {
|
if (
|
||||||
h += LiteGraph.NODE_TITLE_HEIGHT;
|
titleMode !== LiteGraph.TRANSPARENT_TITLE &&
|
||||||
shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
|
titleMode !== LiteGraph.NO_TITLE
|
||||||
}
|
) {
|
||||||
}
|
h += LiteGraph.NODE_TITLE_HEIGHT;
|
||||||
const f = ctx.fillStyle;
|
shiftY -= LiteGraph.NODE_TITLE_HEIGHT;
|
||||||
ctx.fillStyle = "rgba(100, 100, 100, 0.5)";
|
}
|
||||||
ctx.fillRect(shiftX, shiftY, w, h);
|
}
|
||||||
ctx.fillStyle = f;
|
const f = ctx.fillStyle;
|
||||||
}
|
ctx.fillStyle = "rgba(100, 100, 100, 0.5)";
|
||||||
|
ctx.fillRect(shiftX, shiftY, w, h);
|
||||||
|
ctx.fillStyle = f;
|
||||||
|
}
|
||||||
|
|
||||||
return origDrawNode.apply(this, arguments);
|
return origDrawNode.apply(this, arguments);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The currently moving, selected group only. Set after the `selected_group` has actually started
|
* The currently moving, selected group only. Set after the `selected_group` has actually started
|
||||||
* moving.
|
* moving.
|
||||||
*/
|
*/
|
||||||
let selectedAndMovingGroup = null;
|
let selectedAndMovingGroup = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles moving a group; tracking when a group has been moved (to show the ghost in `drawGroups`
|
* Handles moving a group; tracking when a group has been moved (to show the ghost in `drawGroups`
|
||||||
* below) as well as handle the last move call from LiteGraph's `processMouseUp`.
|
* below) as well as handle the last move call from LiteGraph's `processMouseUp`.
|
||||||
*/
|
*/
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const groupMove = LGraphGroup.prototype.move;
|
const groupMove = LGraphGroup.prototype.move;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
LGraphGroup.prototype.move = function(deltax, deltay, ignore_nodes) {
|
LGraphGroup.prototype.move = function (deltax, deltay, ignore_nodes) {
|
||||||
const v = groupMove.apply(this, arguments);
|
const v = groupMove.apply(this, arguments);
|
||||||
// When we've started moving, set `selectedAndMovingGroup` as LiteGraph sets `selected_group`
|
// When we've started moving, set `selectedAndMovingGroup` as LiteGraph sets `selected_group`
|
||||||
// too eagerly and we don't want to behave like we're moving until we get a delta.
|
// too eagerly and we don't want to behave like we're moving until we get a delta.
|
||||||
if (!selectedAndMovingGroup && app.canvas.selected_group === this && (deltax || deltay)) {
|
if (
|
||||||
selectedAndMovingGroup = this;
|
!selectedAndMovingGroup &&
|
||||||
}
|
app.canvas.selected_group === this &&
|
||||||
|
(deltax || deltay)
|
||||||
|
) {
|
||||||
|
selectedAndMovingGroup = this;
|
||||||
|
}
|
||||||
|
|
||||||
// LiteGraph will call group.move both on mouse-move as well as mouse-up though we only want
|
// LiteGraph will call group.move both on mouse-move as well as mouse-up though we only want
|
||||||
// to snap on a mouse-up which we can determine by checking if `app.canvas.last_mouse_dragging`
|
// to snap on a mouse-up which we can determine by checking if `app.canvas.last_mouse_dragging`
|
||||||
// has been set to `false`. Essentially, this check here is the equivilant to calling an
|
// has been set to `false`. Essentially, this check here is the equivilant to calling an
|
||||||
// `LGraphGroup.prototype.onNodeMoved` if it had existed.
|
// `LGraphGroup.prototype.onNodeMoved` if it had existed.
|
||||||
if (app.canvas.last_mouse_dragging === false && app.shiftDown) {
|
if (app.canvas.last_mouse_dragging === false && app.shiftDown) {
|
||||||
// After moving a group (while app.shiftDown), snap all the child nodes and, finally,
|
// After moving a group (while app.shiftDown), snap all the child nodes and, finally,
|
||||||
// align the group itself.
|
// align the group itself.
|
||||||
this.recomputeInsideNodes();
|
this.recomputeInsideNodes();
|
||||||
for (const node of this._nodes) {
|
for (const node of this._nodes) {
|
||||||
node.alignToGrid();
|
node.alignToGrid();
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
LGraphNode.prototype.alignToGrid.apply(this);
|
LGraphNode.prototype.alignToGrid.apply(this);
|
||||||
}
|
}
|
||||||
return v;
|
return v;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles drawing a group when, snapping the size when one is actively being resized tracking and/or
|
* Handles drawing a group when, snapping the size when one is actively being resized tracking and/or
|
||||||
* drawing a ghost box when one is actively being moved. This mimics the node snapping behavior for
|
* drawing a ghost box when one is actively being moved. This mimics the node snapping behavior for
|
||||||
* both.
|
* both.
|
||||||
*/
|
*/
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const drawGroups = LGraphCanvas.prototype.drawGroups;
|
const drawGroups = LGraphCanvas.prototype.drawGroups;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
LGraphCanvas.prototype.drawGroups = function (canvas, ctx) {
|
LGraphCanvas.prototype.drawGroups = function (canvas, ctx) {
|
||||||
if (this.selected_group && app.shiftDown) {
|
if (this.selected_group && app.shiftDown) {
|
||||||
if (this.selected_group_resizing) {
|
if (this.selected_group_resizing) {
|
||||||
roundVectorToGrid(this.selected_group.size);
|
roundVectorToGrid(this.selected_group.size);
|
||||||
} else if (selectedAndMovingGroup) {
|
} else if (selectedAndMovingGroup) {
|
||||||
const [x, y] = roundVectorToGrid([...selectedAndMovingGroup.pos]);
|
const [x, y] = roundVectorToGrid([...selectedAndMovingGroup.pos]);
|
||||||
const f = ctx.fillStyle;
|
const f = ctx.fillStyle;
|
||||||
const s = ctx.strokeStyle;
|
const s = ctx.strokeStyle;
|
||||||
ctx.fillStyle = "rgba(100, 100, 100, 0.33)";
|
ctx.fillStyle = "rgba(100, 100, 100, 0.33)";
|
||||||
ctx.strokeStyle = "rgba(100, 100, 100, 0.66)";
|
ctx.strokeStyle = "rgba(100, 100, 100, 0.66)";
|
||||||
ctx.rect(x, y, ...selectedAndMovingGroup.size);
|
ctx.rect(x, y, ...selectedAndMovingGroup.size);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.fillStyle = f;
|
ctx.fillStyle = f;
|
||||||
ctx.strokeStyle = s;
|
ctx.strokeStyle = s;
|
||||||
}
|
}
|
||||||
} else if (!this.selected_group) {
|
} else if (!this.selected_group) {
|
||||||
selectedAndMovingGroup = null;
|
selectedAndMovingGroup = null;
|
||||||
}
|
}
|
||||||
return drawGroups.apply(this, arguments);
|
return drawGroups.apply(this, arguments);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Handles adding a group in a snapping-enabled state. */
|
||||||
/** Handles adding a group in a snapping-enabled state. */
|
// @ts-ignore
|
||||||
// @ts-ignore
|
const onGroupAdd = LGraphCanvas.onGroupAdd;
|
||||||
const onGroupAdd = LGraphCanvas.onGroupAdd;
|
// @ts-ignore
|
||||||
// @ts-ignore
|
LGraphCanvas.onGroupAdd = function () {
|
||||||
LGraphCanvas.onGroupAdd = function() {
|
const v = onGroupAdd.apply(app.canvas, arguments);
|
||||||
const v = onGroupAdd.apply(app.canvas, arguments);
|
if (app.shiftDown) {
|
||||||
if (app.shiftDown) {
|
// @ts-ignore
|
||||||
// @ts-ignore
|
const lastGroup = app.graph._groups[app.graph._groups.length - 1];
|
||||||
const lastGroup = app.graph._groups[app.graph._groups.length - 1];
|
if (lastGroup) {
|
||||||
if (lastGroup) {
|
// @ts-ignore
|
||||||
// @ts-ignore
|
roundVectorToGrid(lastGroup.pos);
|
||||||
roundVectorToGrid(lastGroup.pos);
|
// @ts-ignore
|
||||||
// @ts-ignore
|
roundVectorToGrid(lastGroup.size);
|
||||||
roundVectorToGrid(lastGroup.size);
|
}
|
||||||
}
|
}
|
||||||
}
|
return v;
|
||||||
return v;
|
};
|
||||||
};
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,10 +11,17 @@ function splitFilePath(path: string): [string, string] {
|
|||||||
if (folder_separator === -1) {
|
if (folder_separator === -1) {
|
||||||
return ["", path];
|
return ["", path];
|
||||||
}
|
}
|
||||||
return [path.substring(0, folder_separator), path.substring(folder_separator + 1)];
|
return [
|
||||||
|
path.substring(0, folder_separator),
|
||||||
|
path.substring(folder_separator + 1),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getResourceURL(subfolder: string, filename: string, type: FolderType = "input"): string {
|
function getResourceURL(
|
||||||
|
subfolder: string,
|
||||||
|
filename: string,
|
||||||
|
type: FolderType = "input"
|
||||||
|
): string {
|
||||||
const params = [
|
const params = [
|
||||||
"filename=" + encodeURIComponent(filename),
|
"filename=" + encodeURIComponent(filename),
|
||||||
"type=" + type,
|
"type=" + type,
|
||||||
@@ -54,7 +61,9 @@ async function uploadFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (updateNode) {
|
if (updateNode) {
|
||||||
audioUIWidget.element.src = api.apiURL(getResourceURL(...splitFilePath(path)));
|
audioUIWidget.element.src = api.apiURL(
|
||||||
|
getResourceURL(...splitFilePath(path))
|
||||||
|
);
|
||||||
audioWidget.value = path;
|
audioWidget.value = path;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -70,7 +79,9 @@ async function uploadFile(
|
|||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "Comfy.AudioWidget",
|
name: "Comfy.AudioWidget",
|
||||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||||
if (["LoadAudio", "SaveAudio", "PreviewAudio"].includes(nodeType.comfyClass)) {
|
if (
|
||||||
|
["LoadAudio", "SaveAudio", "PreviewAudio"].includes(nodeType.comfyClass)
|
||||||
|
) {
|
||||||
nodeData.input.required.audioUI = ["AUDIO_UI"];
|
nodeData.input.required.audioUI = ["AUDIO_UI"];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -82,7 +93,11 @@ app.registerExtension({
|
|||||||
audio.classList.add("comfy-audio");
|
audio.classList.add("comfy-audio");
|
||||||
audio.setAttribute("name", "media");
|
audio.setAttribute("name", "media");
|
||||||
|
|
||||||
const audioUIWidget: DOMWidget<HTMLAudioElement> = node.addDOMWidget(inputName, /* name=*/ "audioUI", audio);
|
const audioUIWidget: DOMWidget<HTMLAudioElement> = node.addDOMWidget(
|
||||||
|
inputName,
|
||||||
|
/* name=*/ "audioUI",
|
||||||
|
audio
|
||||||
|
);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// TODO: Sort out the DOMWidget type.
|
// TODO: Sort out the DOMWidget type.
|
||||||
audioUIWidget.serialize = false;
|
audioUIWidget.serialize = false;
|
||||||
@@ -98,21 +113,27 @@ app.registerExtension({
|
|||||||
const audios = message.audio;
|
const audios = message.audio;
|
||||||
if (!audios) return;
|
if (!audios) return;
|
||||||
const audio = audios[0];
|
const audio = audios[0];
|
||||||
audioUIWidget.element.src = api.apiURL(getResourceURL(audio.subfolder, audio.filename, audio.type));
|
audioUIWidget.element.src = api.apiURL(
|
||||||
|
getResourceURL(audio.subfolder, audio.filename, audio.type)
|
||||||
|
);
|
||||||
audioUIWidget.element.classList.remove("empty-audio-widget");
|
audioUIWidget.element.classList.remove("empty-audio-widget");
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
return { widget: audioUIWidget };
|
return { widget: audioUIWidget };
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
onNodeOutputsUpdated(nodeOutputs: Record<number, any>) {
|
onNodeOutputsUpdated(nodeOutputs: Record<number, any>) {
|
||||||
for (const [nodeId, output] of Object.entries(nodeOutputs)) {
|
for (const [nodeId, output] of Object.entries(nodeOutputs)) {
|
||||||
const node = app.graph.getNodeById(Number.parseInt(nodeId));
|
const node = app.graph.getNodeById(Number.parseInt(nodeId));
|
||||||
if ("audio" in output) {
|
if ("audio" in output) {
|
||||||
const audioUIWidget = node.widgets.find((w) => w.name === "audioUI") as unknown as DOMWidget<HTMLAudioElement>;
|
const audioUIWidget = node.widgets.find(
|
||||||
|
(w) => w.name === "audioUI"
|
||||||
|
) as unknown as DOMWidget<HTMLAudioElement>;
|
||||||
const audio = output.audio[0];
|
const audio = output.audio[0];
|
||||||
audioUIWidget.element.src = api.apiURL(getResourceURL(audio.subfolder, audio.filename, audio.type));
|
audioUIWidget.element.src = api.apiURL(
|
||||||
|
getResourceURL(audio.subfolder, audio.filename, audio.type)
|
||||||
|
);
|
||||||
audioUIWidget.element.classList.remove("empty-audio-widget");
|
audioUIWidget.element.classList.remove("empty-audio-widget");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,11 +151,17 @@ app.registerExtension({
|
|||||||
return {
|
return {
|
||||||
AUDIOUPLOAD(node, inputName: string) {
|
AUDIOUPLOAD(node, inputName: string) {
|
||||||
// The widget that allows user to select file.
|
// The widget that allows user to select file.
|
||||||
const audioWidget: IWidget = node.widgets.find((w: IWidget) => w.name === "audio");
|
const audioWidget: IWidget = node.widgets.find(
|
||||||
const audioUIWidget: DOMWidget<HTMLAudioElement> = node.widgets.find((w: IWidget) => w.name === "audioUI");
|
(w: IWidget) => w.name === "audio"
|
||||||
|
);
|
||||||
|
const audioUIWidget: DOMWidget<HTMLAudioElement> = node.widgets.find(
|
||||||
|
(w: IWidget) => w.name === "audioUI"
|
||||||
|
);
|
||||||
|
|
||||||
const onAudioWidgetUpdate = () => {
|
const onAudioWidgetUpdate = () => {
|
||||||
audioUIWidget.element.src = api.apiURL(getResourceURL(...splitFilePath(audioWidget.value)));
|
audioUIWidget.element.src = api.apiURL(
|
||||||
|
getResourceURL(...splitFilePath(audioWidget.value))
|
||||||
|
);
|
||||||
};
|
};
|
||||||
// Initially load default audio file to audioUIWidget.
|
// Initially load default audio file to audioUIWidget.
|
||||||
if (audioWidget.value) {
|
if (audioWidget.value) {
|
||||||
@@ -152,14 +179,19 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
// The widget to pop up the upload dialog.
|
// The widget to pop up the upload dialog.
|
||||||
const uploadWidget = node.addWidget("button", inputName, /* value=*/"", () => {
|
const uploadWidget = node.addWidget(
|
||||||
fileInput.click();
|
"button",
|
||||||
});
|
inputName,
|
||||||
|
/* value=*/ "",
|
||||||
|
() => {
|
||||||
|
fileInput.click();
|
||||||
|
}
|
||||||
|
);
|
||||||
uploadWidget.label = "choose file to upload";
|
uploadWidget.label = "choose file to upload";
|
||||||
uploadWidget.serialize = false;
|
uploadWidget.serialize = false;
|
||||||
|
|
||||||
return { widget: uploadWidget };
|
return { widget: uploadWidget };
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { ComfyNodeDef } from "/types/apiTypes";
|
|||||||
// Adds an upload button to the nodes
|
// Adds an upload button to the nodes
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "Comfy.UploadImage",
|
name: "Comfy.UploadImage",
|
||||||
async beforeRegisterNodeDef(nodeType, nodeData: ComfyNodeDef, app) {
|
async beforeRegisterNodeDef(nodeType, nodeData: ComfyNodeDef, app) {
|
||||||
if (nodeData?.input?.required?.image?.[1]?.image_upload === true) {
|
if (nodeData?.input?.required?.image?.[1]?.image_upload === true) {
|
||||||
nodeData.input.required.upload = ["IMAGEUPLOAD"];
|
nodeData.input.required.upload = ["IMAGEUPLOAD"];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,123 +4,137 @@ import { api } from "../../scripts/api";
|
|||||||
const WEBCAM_READY = Symbol();
|
const WEBCAM_READY = Symbol();
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "Comfy.WebcamCapture",
|
name: "Comfy.WebcamCapture",
|
||||||
getCustomWidgets(app) {
|
getCustomWidgets(app) {
|
||||||
return {
|
return {
|
||||||
WEBCAM(node, inputName) {
|
WEBCAM(node, inputName) {
|
||||||
let res;
|
let res;
|
||||||
node[WEBCAM_READY] = new Promise((resolve) => (res = resolve));
|
node[WEBCAM_READY] = new Promise((resolve) => (res = resolve));
|
||||||
|
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
container.style.background = "rgba(0,0,0,0.25)";
|
container.style.background = "rgba(0,0,0,0.25)";
|
||||||
container.style.textAlign = "center";
|
container.style.textAlign = "center";
|
||||||
|
|
||||||
const video = document.createElement("video");
|
const video = document.createElement("video");
|
||||||
video.style.height = video.style.width = "100%";
|
video.style.height = video.style.width = "100%";
|
||||||
|
|
||||||
const loadVideo = async () => {
|
const loadVideo = async () => {
|
||||||
try {
|
try {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
container.replaceChildren(video);
|
video: true,
|
||||||
|
audio: false,
|
||||||
|
});
|
||||||
|
container.replaceChildren(video);
|
||||||
|
|
||||||
setTimeout(() => res(video), 500); // Fallback as loadedmetadata doesnt fire sometimes?
|
setTimeout(() => res(video), 500); // Fallback as loadedmetadata doesnt fire sometimes?
|
||||||
video.addEventListener("loadedmetadata", () => res(video), false);
|
video.addEventListener("loadedmetadata", () => res(video), false);
|
||||||
video.srcObject = stream;
|
video.srcObject = stream;
|
||||||
video.play();
|
video.play();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const label = document.createElement("div");
|
const label = document.createElement("div");
|
||||||
label.style.color = "red";
|
label.style.color = "red";
|
||||||
label.style.overflow = "auto";
|
label.style.overflow = "auto";
|
||||||
label.style.maxHeight = "100%";
|
label.style.maxHeight = "100%";
|
||||||
label.style.whiteSpace = "pre-wrap";
|
label.style.whiteSpace = "pre-wrap";
|
||||||
|
|
||||||
if (window.isSecureContext) {
|
if (window.isSecureContext) {
|
||||||
label.textContent = "Unable to load webcam, please ensure access is granted:\n" + error.message;
|
label.textContent =
|
||||||
} else {
|
"Unable to load webcam, please ensure access is granted:\n" +
|
||||||
label.textContent = "Unable to load webcam. A secure context is required, if you are not accessing ComfyUI on localhost (127.0.0.1) you will have to enable TLS (https)\n\n" + error.message;
|
error.message;
|
||||||
}
|
} else {
|
||||||
|
label.textContent =
|
||||||
|
"Unable to load webcam. A secure context is required, if you are not accessing ComfyUI on localhost (127.0.0.1) you will have to enable TLS (https)\n\n" +
|
||||||
|
error.message;
|
||||||
|
}
|
||||||
|
|
||||||
container.replaceChildren(label);
|
container.replaceChildren(label);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadVideo();
|
loadVideo();
|
||||||
|
|
||||||
return { widget: node.addDOMWidget(inputName, "WEBCAM", container) };
|
return { widget: node.addDOMWidget(inputName, "WEBCAM", container) };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
nodeCreated(node) {
|
nodeCreated(node) {
|
||||||
if ((node.type, node.constructor.comfyClass !== "WebcamCapture")) return;
|
if ((node.type, node.constructor.comfyClass !== "WebcamCapture")) return;
|
||||||
|
|
||||||
let video;
|
let video;
|
||||||
const camera = node.widgets.find((w) => w.name === "image");
|
const camera = node.widgets.find((w) => w.name === "image");
|
||||||
const w = node.widgets.find((w) => w.name === "width");
|
const w = node.widgets.find((w) => w.name === "width");
|
||||||
const h = node.widgets.find((w) => w.name === "height");
|
const h = node.widgets.find((w) => w.name === "height");
|
||||||
const captureOnQueue = node.widgets.find((w) => w.name === "capture_on_queue");
|
const captureOnQueue = node.widgets.find(
|
||||||
|
(w) => w.name === "capture_on_queue"
|
||||||
|
);
|
||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
|
|
||||||
const capture = () => {
|
const capture = () => {
|
||||||
canvas.width = w.value;
|
canvas.width = w.value;
|
||||||
canvas.height = h.value;
|
canvas.height = h.value;
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
ctx.drawImage(video, 0, 0, w.value, h.value);
|
ctx.drawImage(video, 0, 0, w.value, h.value);
|
||||||
const data = canvas.toDataURL("image/png");
|
const data = canvas.toDataURL("image/png");
|
||||||
|
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
node.imgs = [img];
|
node.imgs = [img];
|
||||||
app.graph.setDirtyCanvas(true);
|
app.graph.setDirtyCanvas(true);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
node.setSizeForImage?.();
|
node.setSizeForImage?.();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
img.src = data;
|
img.src = data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const btn = node.addWidget("button", "waiting for camera...", "capture", capture);
|
const btn = node.addWidget(
|
||||||
btn.disabled = true;
|
"button",
|
||||||
btn.serializeValue = () => undefined;
|
"waiting for camera...",
|
||||||
|
"capture",
|
||||||
|
capture
|
||||||
|
);
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.serializeValue = () => undefined;
|
||||||
|
|
||||||
camera.serializeValue = async () => {
|
camera.serializeValue = async () => {
|
||||||
if (captureOnQueue.value) {
|
if (captureOnQueue.value) {
|
||||||
capture();
|
capture();
|
||||||
} else if (!node.imgs?.length) {
|
} else if (!node.imgs?.length) {
|
||||||
const err = `No webcam image captured`;
|
const err = `No webcam image captured`;
|
||||||
alert(err);
|
alert(err);
|
||||||
throw new Error(err);
|
throw new Error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload image to temp storage
|
// Upload image to temp storage
|
||||||
const blob = await new Promise<Blob>((r) => canvas.toBlob(r));
|
const blob = await new Promise<Blob>((r) => canvas.toBlob(r));
|
||||||
const name = `${+new Date()}.png`;
|
const name = `${+new Date()}.png`;
|
||||||
const file = new File([blob], name);
|
const file = new File([blob], name);
|
||||||
const body = new FormData();
|
const body = new FormData();
|
||||||
body.append("image", file);
|
body.append("image", file);
|
||||||
body.append("subfolder", "webcam");
|
body.append("subfolder", "webcam");
|
||||||
body.append("type", "temp");
|
body.append("type", "temp");
|
||||||
const resp = await api.fetchApi("/upload/image", {
|
const resp = await api.fetchApi("/upload/image", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body,
|
body,
|
||||||
});
|
});
|
||||||
if (resp.status !== 200) {
|
if (resp.status !== 200) {
|
||||||
const err = `Error uploading camera image: ${resp.status} - ${resp.statusText}`;
|
const err = `Error uploading camera image: ${resp.status} - ${resp.statusText}`;
|
||||||
alert(err);
|
alert(err);
|
||||||
throw new Error(err);
|
throw new Error(err);
|
||||||
}
|
}
|
||||||
return `webcam/${name} [temp]`;
|
return `webcam/${name} [temp]`;
|
||||||
};
|
};
|
||||||
|
|
||||||
node[WEBCAM_READY].then((v) => {
|
node[WEBCAM_READY].then((v) => {
|
||||||
video = v;
|
video = v;
|
||||||
// If width isnt specified then use video output resolution
|
// If width isnt specified then use video output resolution
|
||||||
if (!w.value) {
|
if (!w.value) {
|
||||||
w.value = video.videoWidth || 640;
|
w.value = video.videoWidth || 640;
|
||||||
h.value = video.videoHeight || 480;
|
h.value = video.videoHeight || 480;
|
||||||
}
|
}
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.label = "capture";
|
btn.label = "capture";
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -6,16 +6,19 @@
|
|||||||
* @param {clearBackgroundColor} String
|
* @param {clearBackgroundColor} String
|
||||||
* @
|
* @
|
||||||
*/
|
*/
|
||||||
LGraphCanvas.prototype.updateBackground = function (image, clearBackgroundColor) {
|
LGraphCanvas.prototype.updateBackground = function (
|
||||||
this._bg_img = new Image();
|
image,
|
||||||
this._bg_img.name = image;
|
clearBackgroundColor
|
||||||
this._bg_img.src = image;
|
) {
|
||||||
this._bg_img.onload = () => {
|
this._bg_img = new Image();
|
||||||
this.draw(true, true);
|
this._bg_img.name = image;
|
||||||
};
|
this._bg_img.src = image;
|
||||||
this.background_image = image;
|
this._bg_img.onload = () => {
|
||||||
|
this.draw(true, true);
|
||||||
|
};
|
||||||
|
this.background_image = image;
|
||||||
|
|
||||||
this.clear_background = true;
|
this.clear_background = true;
|
||||||
this.clear_background_color = clearBackgroundColor;
|
this.clear_background_color = clearBackgroundColor;
|
||||||
this._pattern = null
|
this._pattern = null;
|
||||||
}
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
5471
src/scripts/app.ts
5471
src/scripts/app.ts
File diff suppressed because it is too large
Load Diff
@@ -3,255 +3,271 @@
|
|||||||
import { api } from "./api";
|
import { api } from "./api";
|
||||||
import { clone } from "./utils";
|
import { clone } from "./utils";
|
||||||
|
|
||||||
|
|
||||||
export class ChangeTracker {
|
export class ChangeTracker {
|
||||||
static MAX_HISTORY = 50;
|
static MAX_HISTORY = 50;
|
||||||
#app;
|
#app;
|
||||||
undo = [];
|
undo = [];
|
||||||
redo = [];
|
redo = [];
|
||||||
activeState = null;
|
activeState = null;
|
||||||
isOurLoad = false;
|
isOurLoad = false;
|
||||||
/** @type { import("./workflows").ComfyWorkflow | null } */
|
/** @type { import("./workflows").ComfyWorkflow | null } */
|
||||||
workflow;
|
workflow;
|
||||||
|
|
||||||
ds;
|
ds;
|
||||||
nodeOutputs;
|
nodeOutputs;
|
||||||
|
|
||||||
get app() {
|
get app() {
|
||||||
return this.#app ?? this.workflow.manager.app;
|
return this.#app ?? this.workflow.manager.app;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(workflow) {
|
constructor(workflow) {
|
||||||
this.workflow = workflow;
|
this.workflow = workflow;
|
||||||
}
|
}
|
||||||
|
|
||||||
#setApp(app) {
|
#setApp(app) {
|
||||||
this.#app = app;
|
this.#app = app;
|
||||||
}
|
}
|
||||||
|
|
||||||
store() {
|
store() {
|
||||||
this.ds = { scale: this.app.canvas.ds.scale, offset: [...this.app.canvas.ds.offset] };
|
this.ds = {
|
||||||
}
|
scale: this.app.canvas.ds.scale,
|
||||||
|
offset: [...this.app.canvas.ds.offset],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
restore() {
|
restore() {
|
||||||
if (this.ds) {
|
if (this.ds) {
|
||||||
this.app.canvas.ds.scale = this.ds.scale;
|
this.app.canvas.ds.scale = this.ds.scale;
|
||||||
this.app.canvas.ds.offset = this.ds.offset;
|
this.app.canvas.ds.offset = this.ds.offset;
|
||||||
}
|
}
|
||||||
if (this.nodeOutputs) {
|
if (this.nodeOutputs) {
|
||||||
this.app.nodeOutputs = this.nodeOutputs;
|
this.app.nodeOutputs = this.nodeOutputs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkState() {
|
checkState() {
|
||||||
if (!this.app.graph) return;
|
if (!this.app.graph) return;
|
||||||
|
|
||||||
const currentState = this.app.graph.serialize();
|
const currentState = this.app.graph.serialize();
|
||||||
if (!this.activeState) {
|
if (!this.activeState) {
|
||||||
this.activeState = clone(currentState);
|
this.activeState = clone(currentState);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!ChangeTracker.graphEqual(this.activeState, currentState)) {
|
if (!ChangeTracker.graphEqual(this.activeState, currentState)) {
|
||||||
this.undo.push(this.activeState);
|
this.undo.push(this.activeState);
|
||||||
if (this.undo.length > ChangeTracker.MAX_HISTORY) {
|
if (this.undo.length > ChangeTracker.MAX_HISTORY) {
|
||||||
this.undo.shift();
|
this.undo.shift();
|
||||||
}
|
}
|
||||||
this.activeState = clone(currentState);
|
this.activeState = clone(currentState);
|
||||||
this.redo.length = 0;
|
this.redo.length = 0;
|
||||||
this.workflow.unsaved = true;
|
this.workflow.unsaved = true;
|
||||||
api.dispatchEvent(new CustomEvent("graphChanged", { detail: this.activeState }));
|
api.dispatchEvent(
|
||||||
}
|
new CustomEvent("graphChanged", { detail: this.activeState })
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async updateState(source, target) {
|
async updateState(source, target) {
|
||||||
const prevState = source.pop();
|
const prevState = source.pop();
|
||||||
if (prevState) {
|
if (prevState) {
|
||||||
target.push(this.activeState);
|
target.push(this.activeState);
|
||||||
this.isOurLoad = true;
|
this.isOurLoad = true;
|
||||||
await this.app.loadGraphData(prevState, false, false, this.workflow);
|
await this.app.loadGraphData(prevState, false, false, this.workflow);
|
||||||
this.activeState = prevState;
|
this.activeState = prevState;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async undoRedo(e) {
|
async undoRedo(e) {
|
||||||
if (e.ctrlKey || e.metaKey) {
|
if (e.ctrlKey || e.metaKey) {
|
||||||
if (e.key === "y") {
|
if (e.key === "y") {
|
||||||
this.updateState(this.redo, this.undo);
|
this.updateState(this.redo, this.undo);
|
||||||
return true;
|
return true;
|
||||||
} else if (e.key === "z") {
|
} else if (e.key === "z") {
|
||||||
this.updateState(this.undo, this.redo);
|
this.updateState(this.undo, this.redo);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param { import("./app").ComfyApp } app */
|
/** @param { import("./app").ComfyApp } app */
|
||||||
static init(app) {
|
static init(app) {
|
||||||
const changeTracker = () => app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker;
|
const changeTracker = () =>
|
||||||
globalTracker.#setApp(app);
|
app.workflowManager.activeWorkflow?.changeTracker ?? globalTracker;
|
||||||
|
globalTracker.#setApp(app);
|
||||||
|
|
||||||
const loadGraphData = app.loadGraphData;
|
const loadGraphData = app.loadGraphData;
|
||||||
app.loadGraphData = async function () {
|
app.loadGraphData = async function () {
|
||||||
const v = await loadGraphData.apply(this, arguments);
|
const v = await loadGraphData.apply(this, arguments);
|
||||||
const ct = changeTracker();
|
const ct = changeTracker();
|
||||||
if (ct.isOurLoad) {
|
if (ct.isOurLoad) {
|
||||||
ct.isOurLoad = false;
|
ct.isOurLoad = false;
|
||||||
} else {
|
} else {
|
||||||
ct.checkState();
|
ct.checkState();
|
||||||
}
|
}
|
||||||
return v;
|
return v;
|
||||||
};
|
};
|
||||||
|
|
||||||
let keyIgnored = false;
|
let keyIgnored = false;
|
||||||
window.addEventListener(
|
window.addEventListener(
|
||||||
"keydown",
|
"keydown",
|
||||||
(e) => {
|
(e) => {
|
||||||
requestAnimationFrame(async () => {
|
requestAnimationFrame(async () => {
|
||||||
let activeEl;
|
let activeEl;
|
||||||
// If we are auto queue in change mode then we do want to trigger on inputs
|
// If we are auto queue in change mode then we do want to trigger on inputs
|
||||||
if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === "instant") {
|
if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === "instant") {
|
||||||
activeEl = document.activeElement;
|
activeEl = document.activeElement;
|
||||||
if (activeEl?.tagName === "INPUT" || activeEl?.["type"] === "textarea") {
|
if (
|
||||||
// Ignore events on inputs, they have their native history
|
activeEl?.tagName === "INPUT" ||
|
||||||
return;
|
activeEl?.["type"] === "textarea"
|
||||||
}
|
) {
|
||||||
}
|
// Ignore events on inputs, they have their native history
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
keyIgnored = e.key === "Control" || e.key === "Shift" || e.key === "Alt" || e.key === "Meta";
|
keyIgnored =
|
||||||
if (keyIgnored) return;
|
e.key === "Control" ||
|
||||||
|
e.key === "Shift" ||
|
||||||
|
e.key === "Alt" ||
|
||||||
|
e.key === "Meta";
|
||||||
|
if (keyIgnored) return;
|
||||||
|
|
||||||
// Check if this is a ctrl+z ctrl+y
|
// Check if this is a ctrl+z ctrl+y
|
||||||
if (await changeTracker().undoRedo(e)) return;
|
if (await changeTracker().undoRedo(e)) return;
|
||||||
|
|
||||||
// If our active element is some type of input then handle changes after they're done
|
// If our active element is some type of input then handle changes after they're done
|
||||||
if (ChangeTracker.bindInput(activeEl)) return;
|
if (ChangeTracker.bindInput(activeEl)) return;
|
||||||
changeTracker().checkState();
|
changeTracker().checkState();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
window.addEventListener("keyup", (e) => {
|
window.addEventListener("keyup", (e) => {
|
||||||
if (keyIgnored) {
|
if (keyIgnored) {
|
||||||
keyIgnored = false;
|
keyIgnored = false;
|
||||||
changeTracker().checkState();
|
changeTracker().checkState();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle clicking DOM elements (e.g. widgets)
|
// Handle clicking DOM elements (e.g. widgets)
|
||||||
window.addEventListener("mouseup", () => {
|
window.addEventListener("mouseup", () => {
|
||||||
changeTracker().checkState();
|
changeTracker().checkState();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle prompt queue event for dynamic widget changes
|
// Handle prompt queue event for dynamic widget changes
|
||||||
api.addEventListener("promptQueued", () => {
|
api.addEventListener("promptQueued", () => {
|
||||||
changeTracker().checkState();
|
changeTracker().checkState();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle litegraph clicks
|
// Handle litegraph clicks
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const processMouseUp = LGraphCanvas.prototype.processMouseUp;
|
const processMouseUp = LGraphCanvas.prototype.processMouseUp;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
LGraphCanvas.prototype.processMouseUp = function (e) {
|
LGraphCanvas.prototype.processMouseUp = function (e) {
|
||||||
const v = processMouseUp.apply(this, arguments);
|
const v = processMouseUp.apply(this, arguments);
|
||||||
changeTracker().checkState();
|
changeTracker().checkState();
|
||||||
return v;
|
return v;
|
||||||
};
|
};
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const processMouseDown = LGraphCanvas.prototype.processMouseDown;
|
const processMouseDown = LGraphCanvas.prototype.processMouseDown;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
LGraphCanvas.prototype.processMouseDown = function (e) {
|
LGraphCanvas.prototype.processMouseDown = function (e) {
|
||||||
const v = processMouseDown.apply(this, arguments);
|
const v = processMouseDown.apply(this, arguments);
|
||||||
changeTracker().checkState();
|
changeTracker().checkState();
|
||||||
return v;
|
return v;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle litegraph context menu for COMBO widgets
|
// Handle litegraph context menu for COMBO widgets
|
||||||
const close = LiteGraph.ContextMenu.prototype.close;
|
const close = LiteGraph.ContextMenu.prototype.close;
|
||||||
LiteGraph.ContextMenu.prototype.close = function (e) {
|
LiteGraph.ContextMenu.prototype.close = function (e) {
|
||||||
const v = close.apply(this, arguments);
|
const v = close.apply(this, arguments);
|
||||||
changeTracker().checkState();
|
changeTracker().checkState();
|
||||||
return v;
|
return v;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Detects nodes being added via the node search dialog
|
// Detects nodes being added via the node search dialog
|
||||||
const onNodeAdded = LiteGraph.LGraph.prototype.onNodeAdded;
|
const onNodeAdded = LiteGraph.LGraph.prototype.onNodeAdded;
|
||||||
LiteGraph.LGraph.prototype.onNodeAdded = function () {
|
LiteGraph.LGraph.prototype.onNodeAdded = function () {
|
||||||
const v = onNodeAdded?.apply(this, arguments);
|
const v = onNodeAdded?.apply(this, arguments);
|
||||||
const ct = changeTracker();
|
const ct = changeTracker();
|
||||||
if (!ct.isOurLoad) {
|
if (!ct.isOurLoad) {
|
||||||
ct.checkState();
|
ct.checkState();
|
||||||
}
|
}
|
||||||
return v;
|
return v;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store node outputs
|
// Store node outputs
|
||||||
api.addEventListener("executed", ({ detail }) => {
|
api.addEventListener("executed", ({ detail }) => {
|
||||||
const prompt = app.workflowManager.queuedPrompts[detail.prompt_id];
|
const prompt = app.workflowManager.queuedPrompts[detail.prompt_id];
|
||||||
if (!prompt?.workflow) return;
|
if (!prompt?.workflow) return;
|
||||||
const nodeOutputs = (prompt.workflow.changeTracker.nodeOutputs ??= {});
|
const nodeOutputs = (prompt.workflow.changeTracker.nodeOutputs ??= {});
|
||||||
const output = nodeOutputs[detail.node];
|
const output = nodeOutputs[detail.node];
|
||||||
if (detail.merge && output) {
|
if (detail.merge && output) {
|
||||||
for (const k in detail.output ?? {}) {
|
for (const k in detail.output ?? {}) {
|
||||||
const v = output[k];
|
const v = output[k];
|
||||||
if (v instanceof Array) {
|
if (v instanceof Array) {
|
||||||
output[k] = v.concat(detail.output[k]);
|
output[k] = v.concat(detail.output[k]);
|
||||||
} else {
|
} else {
|
||||||
output[k] = detail.output[k];
|
output[k] = detail.output[k];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
nodeOutputs[detail.node] = detail.output;
|
nodeOutputs[detail.node] = detail.output;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static bindInput(app, activeEl) {
|
static bindInput(app, activeEl) {
|
||||||
if (activeEl && activeEl.tagName !== "CANVAS" && activeEl.tagName !== "BODY") {
|
if (
|
||||||
for (const evt of ["change", "input", "blur"]) {
|
activeEl &&
|
||||||
if (`on${evt}` in activeEl) {
|
activeEl.tagName !== "CANVAS" &&
|
||||||
const listener = () => {
|
activeEl.tagName !== "BODY"
|
||||||
app.workflowManager.activeWorkflow.changeTracker.checkState();
|
) {
|
||||||
activeEl.removeEventListener(evt, listener);
|
for (const evt of ["change", "input", "blur"]) {
|
||||||
};
|
if (`on${evt}` in activeEl) {
|
||||||
activeEl.addEventListener(evt, listener);
|
const listener = () => {
|
||||||
return true;
|
app.workflowManager.activeWorkflow.changeTracker.checkState();
|
||||||
}
|
activeEl.removeEventListener(evt, listener);
|
||||||
}
|
};
|
||||||
}
|
activeEl.addEventListener(evt, listener);
|
||||||
}
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static graphEqual(a, b, path = "") {
|
static graphEqual(a, b, path = "") {
|
||||||
if (a === b) return true;
|
if (a === b) return true;
|
||||||
|
|
||||||
if (typeof a == "object" && a && typeof b == "object" && b) {
|
if (typeof a == "object" && a && typeof b == "object" && b) {
|
||||||
const keys = Object.getOwnPropertyNames(a);
|
const keys = Object.getOwnPropertyNames(a);
|
||||||
|
|
||||||
if (keys.length != Object.getOwnPropertyNames(b).length) {
|
if (keys.length != Object.getOwnPropertyNames(b).length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
let av = a[key];
|
let av = a[key];
|
||||||
let bv = b[key];
|
let bv = b[key];
|
||||||
if (!path && key === "nodes") {
|
if (!path && key === "nodes") {
|
||||||
// Nodes need to be sorted as the order changes when selecting nodes
|
// Nodes need to be sorted as the order changes when selecting nodes
|
||||||
av = [...av].sort((a, b) => a.id - b.id);
|
av = [...av].sort((a, b) => a.id - b.id);
|
||||||
bv = [...bv].sort((a, b) => a.id - b.id);
|
bv = [...bv].sort((a, b) => a.id - b.id);
|
||||||
} else if (path === "extra.ds") {
|
} else if (path === "extra.ds") {
|
||||||
// Ignore view changes
|
// Ignore view changes
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!ChangeTracker.graphEqual(av, bv, path + (path ? "." : "") + key)) {
|
if (!ChangeTracker.graphEqual(av, bv, path + (path ? "." : "") + key)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalTracker = new ChangeTracker({});
|
const globalTracker = new ChangeTracker({});
|
||||||
|
|||||||
@@ -1,121 +1,137 @@
|
|||||||
import type { ComfyWorkflow } from "/types/comfyWorkflow";
|
import type { ComfyWorkflow } from "/types/comfyWorkflow";
|
||||||
|
|
||||||
export const defaultGraph: ComfyWorkflow = {
|
export const defaultGraph: ComfyWorkflow = {
|
||||||
last_node_id: 9,
|
last_node_id: 9,
|
||||||
last_link_id: 9,
|
last_link_id: 9,
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
{
|
||||||
id: 7,
|
id: 7,
|
||||||
type: "CLIPTextEncode",
|
type: "CLIPTextEncode",
|
||||||
pos: [413, 389],
|
pos: [413, 389],
|
||||||
size: { 0: 425.27801513671875, 1: 180.6060791015625 },
|
size: { 0: 425.27801513671875, 1: 180.6060791015625 },
|
||||||
flags: {},
|
flags: {},
|
||||||
order: 3,
|
order: 3,
|
||||||
mode: 0,
|
mode: 0,
|
||||||
inputs: [{ name: "clip", type: "CLIP", link: 5 }],
|
inputs: [{ name: "clip", type: "CLIP", link: 5 }],
|
||||||
outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [6], slot_index: 0 }],
|
outputs: [
|
||||||
properties: {},
|
{
|
||||||
widgets_values: ["text, watermark"],
|
name: "CONDITIONING",
|
||||||
},
|
type: "CONDITIONING",
|
||||||
{
|
links: [6],
|
||||||
id: 6,
|
slot_index: 0,
|
||||||
type: "CLIPTextEncode",
|
},
|
||||||
pos: [415, 186],
|
],
|
||||||
size: { 0: 422.84503173828125, 1: 164.31304931640625 },
|
properties: {},
|
||||||
flags: {},
|
widgets_values: ["text, watermark"],
|
||||||
order: 2,
|
},
|
||||||
mode: 0,
|
{
|
||||||
inputs: [{ name: "clip", type: "CLIP", link: 3 }],
|
id: 6,
|
||||||
outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [4], slot_index: 0 }],
|
type: "CLIPTextEncode",
|
||||||
properties: {},
|
pos: [415, 186],
|
||||||
widgets_values: ["beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"],
|
size: { 0: 422.84503173828125, 1: 164.31304931640625 },
|
||||||
},
|
flags: {},
|
||||||
{
|
order: 2,
|
||||||
id: 5,
|
mode: 0,
|
||||||
type: "EmptyLatentImage",
|
inputs: [{ name: "clip", type: "CLIP", link: 3 }],
|
||||||
pos: [473, 609],
|
outputs: [
|
||||||
size: { 0: 315, 1: 106 },
|
{
|
||||||
flags: {},
|
name: "CONDITIONING",
|
||||||
order: 1,
|
type: "CONDITIONING",
|
||||||
mode: 0,
|
links: [4],
|
||||||
outputs: [{ name: "LATENT", type: "LATENT", links: [2], slot_index: 0 }],
|
slot_index: 0,
|
||||||
properties: {},
|
},
|
||||||
widgets_values: [512, 512, 1],
|
],
|
||||||
},
|
properties: {},
|
||||||
{
|
widgets_values: [
|
||||||
id: 3,
|
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,",
|
||||||
type: "KSampler",
|
],
|
||||||
pos: [863, 186],
|
},
|
||||||
size: { 0: 315, 1: 262 },
|
{
|
||||||
flags: {},
|
id: 5,
|
||||||
order: 4,
|
type: "EmptyLatentImage",
|
||||||
mode: 0,
|
pos: [473, 609],
|
||||||
inputs: [
|
size: { 0: 315, 1: 106 },
|
||||||
{ name: "model", type: "MODEL", link: 1 },
|
flags: {},
|
||||||
{ name: "positive", type: "CONDITIONING", link: 4 },
|
order: 1,
|
||||||
{ name: "negative", type: "CONDITIONING", link: 6 },
|
mode: 0,
|
||||||
{ name: "latent_image", type: "LATENT", link: 2 },
|
outputs: [{ name: "LATENT", type: "LATENT", links: [2], slot_index: 0 }],
|
||||||
],
|
properties: {},
|
||||||
outputs: [{ name: "LATENT", type: "LATENT", links: [7], slot_index: 0 }],
|
widgets_values: [512, 512, 1],
|
||||||
properties: {},
|
},
|
||||||
widgets_values: [156680208700286, true, 20, 8, "euler", "normal", 1],
|
{
|
||||||
},
|
id: 3,
|
||||||
{
|
type: "KSampler",
|
||||||
id: 8,
|
pos: [863, 186],
|
||||||
type: "VAEDecode",
|
size: { 0: 315, 1: 262 },
|
||||||
pos: [1209, 188],
|
flags: {},
|
||||||
size: { 0: 210, 1: 46 },
|
order: 4,
|
||||||
flags: {},
|
mode: 0,
|
||||||
order: 5,
|
inputs: [
|
||||||
mode: 0,
|
{ name: "model", type: "MODEL", link: 1 },
|
||||||
inputs: [
|
{ name: "positive", type: "CONDITIONING", link: 4 },
|
||||||
{ name: "samples", type: "LATENT", link: 7 },
|
{ name: "negative", type: "CONDITIONING", link: 6 },
|
||||||
{ name: "vae", type: "VAE", link: 8 },
|
{ name: "latent_image", type: "LATENT", link: 2 },
|
||||||
],
|
],
|
||||||
outputs: [{ name: "IMAGE", type: "IMAGE", links: [9], slot_index: 0 }],
|
outputs: [{ name: "LATENT", type: "LATENT", links: [7], slot_index: 0 }],
|
||||||
properties: {},
|
properties: {},
|
||||||
},
|
widgets_values: [156680208700286, true, 20, 8, "euler", "normal", 1],
|
||||||
{
|
},
|
||||||
id: 9,
|
{
|
||||||
type: "SaveImage",
|
id: 8,
|
||||||
pos: [1451, 189],
|
type: "VAEDecode",
|
||||||
size: { 0: 210, 1: 26 },
|
pos: [1209, 188],
|
||||||
flags: {},
|
size: { 0: 210, 1: 46 },
|
||||||
order: 6,
|
flags: {},
|
||||||
mode: 0,
|
order: 5,
|
||||||
inputs: [{ name: "images", type: "IMAGE", link: 9 }],
|
mode: 0,
|
||||||
properties: {},
|
inputs: [
|
||||||
},
|
{ name: "samples", type: "LATENT", link: 7 },
|
||||||
{
|
{ name: "vae", type: "VAE", link: 8 },
|
||||||
id: 4,
|
],
|
||||||
type: "CheckpointLoaderSimple",
|
outputs: [{ name: "IMAGE", type: "IMAGE", links: [9], slot_index: 0 }],
|
||||||
pos: [26, 474],
|
properties: {},
|
||||||
size: { 0: 315, 1: 98 },
|
},
|
||||||
flags: {},
|
{
|
||||||
order: 0,
|
id: 9,
|
||||||
mode: 0,
|
type: "SaveImage",
|
||||||
outputs: [
|
pos: [1451, 189],
|
||||||
{ name: "MODEL", type: "MODEL", links: [1], slot_index: 0 },
|
size: { 0: 210, 1: 26 },
|
||||||
{ name: "CLIP", type: "CLIP", links: [3, 5], slot_index: 1 },
|
flags: {},
|
||||||
{ name: "VAE", type: "VAE", links: [8], slot_index: 2 },
|
order: 6,
|
||||||
],
|
mode: 0,
|
||||||
properties: {},
|
inputs: [{ name: "images", type: "IMAGE", link: 9 }],
|
||||||
widgets_values: ["v1-5-pruned-emaonly.ckpt"],
|
properties: {},
|
||||||
},
|
},
|
||||||
],
|
{
|
||||||
links: [
|
id: 4,
|
||||||
[1, 4, 0, 3, 0, "MODEL"],
|
type: "CheckpointLoaderSimple",
|
||||||
[2, 5, 0, 3, 3, "LATENT"],
|
pos: [26, 474],
|
||||||
[3, 4, 1, 6, 0, "CLIP"],
|
size: { 0: 315, 1: 98 },
|
||||||
[4, 6, 0, 3, 1, "CONDITIONING"],
|
flags: {},
|
||||||
[5, 4, 1, 7, 0, "CLIP"],
|
order: 0,
|
||||||
[6, 7, 0, 3, 2, "CONDITIONING"],
|
mode: 0,
|
||||||
[7, 3, 0, 8, 0, "LATENT"],
|
outputs: [
|
||||||
[8, 4, 2, 8, 1, "VAE"],
|
{ name: "MODEL", type: "MODEL", links: [1], slot_index: 0 },
|
||||||
[9, 8, 0, 9, 0, "IMAGE"],
|
{ name: "CLIP", type: "CLIP", links: [3, 5], slot_index: 1 },
|
||||||
],
|
{ name: "VAE", type: "VAE", links: [8], slot_index: 2 },
|
||||||
groups: [],
|
],
|
||||||
config: {},
|
properties: {},
|
||||||
extra: {},
|
widgets_values: ["v1-5-pruned-emaonly.ckpt"],
|
||||||
version: 0.4,
|
},
|
||||||
|
],
|
||||||
|
links: [
|
||||||
|
[1, 4, 0, 3, 0, "MODEL"],
|
||||||
|
[2, 5, 0, 3, 3, "LATENT"],
|
||||||
|
[3, 4, 1, 6, 0, "CLIP"],
|
||||||
|
[4, 6, 0, 3, 1, "CONDITIONING"],
|
||||||
|
[5, 4, 1, 7, 0, "CLIP"],
|
||||||
|
[6, 7, 0, 3, 2, "CONDITIONING"],
|
||||||
|
[7, 3, 0, 8, 0, "LATENT"],
|
||||||
|
[8, 4, 2, 8, 1, "VAE"],
|
||||||
|
[9, 8, 0, 9, 0, "IMAGE"],
|
||||||
|
],
|
||||||
|
groups: [],
|
||||||
|
config: {},
|
||||||
|
extra: {},
|
||||||
|
version: 0.4,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,194 +1,216 @@
|
|||||||
import { app, ANIM_PREVIEW_WIDGET } from "./app";
|
import { app, ANIM_PREVIEW_WIDGET } from "./app";
|
||||||
import type { LGraphNode, Vector4 } from "/types/litegraph";
|
import type { LGraphNode, Vector4 } from "/types/litegraph";
|
||||||
|
|
||||||
|
|
||||||
const SIZE = Symbol();
|
const SIZE = Symbol();
|
||||||
|
|
||||||
|
|
||||||
interface Rect {
|
interface Rect {
|
||||||
height: number;
|
height: number;
|
||||||
width: number;
|
width: number;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DOMWidget<T = HTMLElement> {
|
export interface DOMWidget<T = HTMLElement> {
|
||||||
type: string;
|
type: string;
|
||||||
name: string;
|
name: string;
|
||||||
computedHeight?: number;
|
computedHeight?: number;
|
||||||
element?: T;
|
element?: T;
|
||||||
options: any;
|
options: any;
|
||||||
value?: any;
|
value?: any;
|
||||||
y?: number;
|
y?: number;
|
||||||
callback?: (value: any) => void;
|
callback?: (value: any) => void;
|
||||||
draw?: (ctx: CanvasRenderingContext2D, node: LGraphNode, widgetWidth: number, y: number, widgetHeight: number) => void;
|
draw?: (
|
||||||
onRemove?: () => void;
|
ctx: CanvasRenderingContext2D,
|
||||||
|
node: LGraphNode,
|
||||||
|
widgetWidth: number,
|
||||||
|
y: number,
|
||||||
|
widgetHeight: number
|
||||||
|
) => void;
|
||||||
|
onRemove?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function intersect(a: Rect, b: Rect): Vector4 | null {
|
function intersect(a: Rect, b: Rect): Vector4 | null {
|
||||||
const x = Math.max(a.x, b.x);
|
const x = Math.max(a.x, b.x);
|
||||||
const num1 = Math.min(a.x + a.width, b.x + b.width);
|
const num1 = Math.min(a.x + a.width, b.x + b.width);
|
||||||
const y = Math.max(a.y, b.y);
|
const y = Math.max(a.y, b.y);
|
||||||
const num2 = Math.min(a.y + a.height, b.y + b.height);
|
const num2 = Math.min(a.y + a.height, b.y + b.height);
|
||||||
if (num1 >= x && num2 >= y) return [x, y, num1 - x, num2 - y];
|
if (num1 >= x && num2 >= y) return [x, y, num1 - x, num2 - y];
|
||||||
else return null;
|
else return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getClipPath(node: LGraphNode, element: HTMLElement): string {
|
function getClipPath(node: LGraphNode, element: HTMLElement): string {
|
||||||
const selectedNode: LGraphNode = Object.values(app.canvas.selected_nodes)[0] as LGraphNode;
|
const selectedNode: LGraphNode = Object.values(
|
||||||
if (selectedNode && selectedNode !== node) {
|
app.canvas.selected_nodes
|
||||||
const elRect = element.getBoundingClientRect();
|
)[0] as LGraphNode;
|
||||||
const MARGIN = 7;
|
if (selectedNode && selectedNode !== node) {
|
||||||
const scale = app.canvas.ds.scale;
|
const elRect = element.getBoundingClientRect();
|
||||||
|
const MARGIN = 7;
|
||||||
|
const scale = app.canvas.ds.scale;
|
||||||
|
|
||||||
const bounding = selectedNode.getBounding();
|
const bounding = selectedNode.getBounding();
|
||||||
const intersection = intersect(
|
const intersection = intersect(
|
||||||
{ x: elRect.x / scale, y: elRect.y / scale, width: elRect.width / scale, height: elRect.height / scale },
|
{
|
||||||
{
|
x: elRect.x / scale,
|
||||||
x: selectedNode.pos[0] + app.canvas.ds.offset[0] - MARGIN,
|
y: elRect.y / scale,
|
||||||
y: selectedNode.pos[1] + app.canvas.ds.offset[1] - LiteGraph.NODE_TITLE_HEIGHT - MARGIN,
|
width: elRect.width / scale,
|
||||||
width: bounding[2] + MARGIN + MARGIN,
|
height: elRect.height / scale,
|
||||||
height: bounding[3] + MARGIN + MARGIN,
|
},
|
||||||
}
|
{
|
||||||
);
|
x: selectedNode.pos[0] + app.canvas.ds.offset[0] - MARGIN,
|
||||||
|
y:
|
||||||
|
selectedNode.pos[1] +
|
||||||
|
app.canvas.ds.offset[1] -
|
||||||
|
LiteGraph.NODE_TITLE_HEIGHT -
|
||||||
|
MARGIN,
|
||||||
|
width: bounding[2] + MARGIN + MARGIN,
|
||||||
|
height: bounding[3] + MARGIN + MARGIN,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!intersection) {
|
if (!intersection) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const widgetRect = element.getBoundingClientRect();
|
const widgetRect = element.getBoundingClientRect();
|
||||||
const clipX = elRect.left + intersection[0] - widgetRect.x / scale + "px";
|
const clipX = elRect.left + intersection[0] - widgetRect.x / scale + "px";
|
||||||
const clipY = elRect.top + intersection[1] - widgetRect.y / scale + "px";
|
const clipY = elRect.top + intersection[1] - widgetRect.y / scale + "px";
|
||||||
const clipWidth = intersection[2] + "px";
|
const clipWidth = intersection[2] + "px";
|
||||||
const clipHeight = intersection[3] + "px";
|
const clipHeight = intersection[3] + "px";
|
||||||
const path = `polygon(0% 0%, 0% 100%, ${clipX} 100%, ${clipX} ${clipY}, calc(${clipX} + ${clipWidth}) ${clipY}, calc(${clipX} + ${clipWidth}) calc(${clipY} + ${clipHeight}), ${clipX} calc(${clipY} + ${clipHeight}), ${clipX} 100%, 100% 100%, 100% 0%)`;
|
const path = `polygon(0% 0%, 0% 100%, ${clipX} 100%, ${clipX} ${clipY}, calc(${clipX} + ${clipWidth}) ${clipY}, calc(${clipX} + ${clipWidth}) calc(${clipY} + ${clipHeight}), ${clipX} calc(${clipY} + ${clipHeight}), ${clipX} 100%, 100% 100%, 100% 0%)`;
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeSize(size: [number, number]): void {
|
function computeSize(size: [number, number]): void {
|
||||||
if (this.widgets?.[0]?.last_y == null) return;
|
if (this.widgets?.[0]?.last_y == null) return;
|
||||||
|
|
||||||
let y = this.widgets[0].last_y;
|
let y = this.widgets[0].last_y;
|
||||||
let freeSpace = size[1] - y;
|
let freeSpace = size[1] - y;
|
||||||
|
|
||||||
let widgetHeight = 0;
|
let widgetHeight = 0;
|
||||||
let dom = [];
|
let dom = [];
|
||||||
for (const w of this.widgets) {
|
for (const w of this.widgets) {
|
||||||
if (w.type === "converted-widget") {
|
if (w.type === "converted-widget") {
|
||||||
// Ignore
|
// Ignore
|
||||||
delete w.computedHeight;
|
delete w.computedHeight;
|
||||||
} else if (w.computeSize) {
|
} else if (w.computeSize) {
|
||||||
widgetHeight += w.computeSize()[1] + 4;
|
widgetHeight += w.computeSize()[1] + 4;
|
||||||
} else if (w.element) {
|
} else if (w.element) {
|
||||||
// Extract DOM widget size info
|
// Extract DOM widget size info
|
||||||
const styles = getComputedStyle(w.element);
|
const styles = getComputedStyle(w.element);
|
||||||
let minHeight = w.options.getMinHeight?.() ?? parseInt(styles.getPropertyValue("--comfy-widget-min-height"));
|
let minHeight =
|
||||||
let maxHeight = w.options.getMaxHeight?.() ?? parseInt(styles.getPropertyValue("--comfy-widget-max-height"));
|
w.options.getMinHeight?.() ??
|
||||||
|
parseInt(styles.getPropertyValue("--comfy-widget-min-height"));
|
||||||
|
let maxHeight =
|
||||||
|
w.options.getMaxHeight?.() ??
|
||||||
|
parseInt(styles.getPropertyValue("--comfy-widget-max-height"));
|
||||||
|
|
||||||
let prefHeight = w.options.getHeight?.() ?? styles.getPropertyValue("--comfy-widget-height");
|
let prefHeight =
|
||||||
if (prefHeight.endsWith?.("%")) {
|
w.options.getHeight?.() ??
|
||||||
prefHeight = size[1] * (parseFloat(prefHeight.substring(0, prefHeight.length - 1)) / 100);
|
styles.getPropertyValue("--comfy-widget-height");
|
||||||
} else {
|
if (prefHeight.endsWith?.("%")) {
|
||||||
prefHeight = parseInt(prefHeight);
|
prefHeight =
|
||||||
if (isNaN(minHeight)) {
|
size[1] *
|
||||||
minHeight = prefHeight;
|
(parseFloat(prefHeight.substring(0, prefHeight.length - 1)) / 100);
|
||||||
}
|
} else {
|
||||||
}
|
prefHeight = parseInt(prefHeight);
|
||||||
if (isNaN(minHeight)) {
|
if (isNaN(minHeight)) {
|
||||||
minHeight = 50;
|
minHeight = prefHeight;
|
||||||
}
|
}
|
||||||
if (!isNaN(maxHeight)) {
|
}
|
||||||
if (!isNaN(prefHeight)) {
|
if (isNaN(minHeight)) {
|
||||||
prefHeight = Math.min(prefHeight, maxHeight);
|
minHeight = 50;
|
||||||
} else {
|
}
|
||||||
prefHeight = maxHeight;
|
if (!isNaN(maxHeight)) {
|
||||||
}
|
if (!isNaN(prefHeight)) {
|
||||||
}
|
prefHeight = Math.min(prefHeight, maxHeight);
|
||||||
dom.push({
|
} else {
|
||||||
minHeight,
|
prefHeight = maxHeight;
|
||||||
prefHeight,
|
}
|
||||||
w,
|
}
|
||||||
});
|
dom.push({
|
||||||
} else {
|
minHeight,
|
||||||
widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4;
|
prefHeight,
|
||||||
}
|
w,
|
||||||
}
|
});
|
||||||
|
} else {
|
||||||
|
widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
freeSpace -= widgetHeight;
|
freeSpace -= widgetHeight;
|
||||||
|
|
||||||
// Calculate sizes with all widgets at their min height
|
// Calculate sizes with all widgets at their min height
|
||||||
const prefGrow = []; // Nodes that want to grow to their prefd size
|
const prefGrow = []; // Nodes that want to grow to their prefd size
|
||||||
const canGrow = []; // Nodes that can grow to auto size
|
const canGrow = []; // Nodes that can grow to auto size
|
||||||
let growBy = 0;
|
let growBy = 0;
|
||||||
for (const d of dom) {
|
for (const d of dom) {
|
||||||
freeSpace -= d.minHeight;
|
freeSpace -= d.minHeight;
|
||||||
if (isNaN(d.prefHeight)) {
|
if (isNaN(d.prefHeight)) {
|
||||||
canGrow.push(d);
|
canGrow.push(d);
|
||||||
d.w.computedHeight = d.minHeight;
|
d.w.computedHeight = d.minHeight;
|
||||||
} else {
|
} else {
|
||||||
const diff = d.prefHeight - d.minHeight;
|
const diff = d.prefHeight - d.minHeight;
|
||||||
if (diff > 0) {
|
if (diff > 0) {
|
||||||
prefGrow.push(d);
|
prefGrow.push(d);
|
||||||
growBy += diff;
|
growBy += diff;
|
||||||
d.diff = diff;
|
d.diff = diff;
|
||||||
} else {
|
} else {
|
||||||
d.w.computedHeight = d.minHeight;
|
d.w.computedHeight = d.minHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.imgs && !this.widgets.find((w) => w.name === ANIM_PREVIEW_WIDGET)) {
|
if (this.imgs && !this.widgets.find((w) => w.name === ANIM_PREVIEW_WIDGET)) {
|
||||||
// Allocate space for image
|
// Allocate space for image
|
||||||
freeSpace -= 220;
|
freeSpace -= 220;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.freeWidgetSpace = freeSpace;
|
this.freeWidgetSpace = freeSpace;
|
||||||
|
|
||||||
if (freeSpace < 0) {
|
if (freeSpace < 0) {
|
||||||
// Not enough space for all widgets so we need to grow
|
// Not enough space for all widgets so we need to grow
|
||||||
size[1] -= freeSpace;
|
size[1] -= freeSpace;
|
||||||
this.graph.setDirtyCanvas(true);
|
this.graph.setDirtyCanvas(true);
|
||||||
} else {
|
} else {
|
||||||
// Share the space between each
|
// Share the space between each
|
||||||
const growDiff = freeSpace - growBy;
|
const growDiff = freeSpace - growBy;
|
||||||
if (growDiff > 0) {
|
if (growDiff > 0) {
|
||||||
// All pref sizes can be fulfilled
|
// All pref sizes can be fulfilled
|
||||||
freeSpace = growDiff;
|
freeSpace = growDiff;
|
||||||
for (const d of prefGrow) {
|
for (const d of prefGrow) {
|
||||||
d.w.computedHeight = d.prefHeight;
|
d.w.computedHeight = d.prefHeight;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// We need to grow evenly
|
// We need to grow evenly
|
||||||
const shared = -growDiff / prefGrow.length;
|
const shared = -growDiff / prefGrow.length;
|
||||||
for (const d of prefGrow) {
|
for (const d of prefGrow) {
|
||||||
d.w.computedHeight = d.prefHeight - shared;
|
d.w.computedHeight = d.prefHeight - shared;
|
||||||
}
|
}
|
||||||
freeSpace = 0;
|
freeSpace = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (freeSpace > 0 && canGrow.length) {
|
if (freeSpace > 0 && canGrow.length) {
|
||||||
// Grow any that are auto height
|
// Grow any that are auto height
|
||||||
const shared = freeSpace / canGrow.length;
|
const shared = freeSpace / canGrow.length;
|
||||||
for (const d of canGrow) {
|
for (const d of canGrow) {
|
||||||
d.w.computedHeight += shared;
|
d.w.computedHeight += shared;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Position each of the widgets
|
// Position each of the widgets
|
||||||
for (const w of this.widgets) {
|
for (const w of this.widgets) {
|
||||||
w.y = y;
|
w.y = y;
|
||||||
if (w.computedHeight) {
|
if (w.computedHeight) {
|
||||||
y += w.computedHeight;
|
y += w.computedHeight;
|
||||||
} else if (w.computeSize) {
|
} else if (w.computeSize) {
|
||||||
y += w.computeSize()[1] + 4;
|
y += w.computeSize()[1] + 4;
|
||||||
} else {
|
} else {
|
||||||
y += LiteGraph.NODE_WIDGET_HEIGHT + 4;
|
y += LiteGraph.NODE_WIDGET_HEIGHT + 4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override the compute visible nodes function to allow us to hide/show DOM elements when the node goes offscreen
|
// Override the compute visible nodes function to allow us to hide/show DOM elements when the node goes offscreen
|
||||||
@@ -197,170 +219,179 @@ const elementWidgets = new Set();
|
|||||||
const computeVisibleNodes = LGraphCanvas.prototype.computeVisibleNodes;
|
const computeVisibleNodes = LGraphCanvas.prototype.computeVisibleNodes;
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
LGraphCanvas.prototype.computeVisibleNodes = function (): LGraphNode[] {
|
LGraphCanvas.prototype.computeVisibleNodes = function (): LGraphNode[] {
|
||||||
const visibleNodes = computeVisibleNodes.apply(this, arguments);
|
const visibleNodes = computeVisibleNodes.apply(this, arguments);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
for (const node of app.graph._nodes) {
|
for (const node of app.graph._nodes) {
|
||||||
if (elementWidgets.has(node)) {
|
if (elementWidgets.has(node)) {
|
||||||
const hidden = visibleNodes.indexOf(node) === -1;
|
const hidden = visibleNodes.indexOf(node) === -1;
|
||||||
for (const w of node.widgets) {
|
for (const w of node.widgets) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (w.element) {
|
if (w.element) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
w.element.hidden = hidden;
|
w.element.hidden = hidden;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
w.element.style.display = hidden ? "none" : undefined;
|
w.element.style.display = hidden ? "none" : undefined;
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
w.options.onHide?.(w);
|
w.options.onHide?.(w);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return visibleNodes;
|
return visibleNodes;
|
||||||
};
|
};
|
||||||
|
|
||||||
let enableDomClipping = true;
|
let enableDomClipping = true;
|
||||||
|
|
||||||
export function addDomClippingSetting(): void {
|
export function addDomClippingSetting(): void {
|
||||||
app.ui.settings.addSetting({
|
app.ui.settings.addSetting({
|
||||||
id: "Comfy.DOMClippingEnabled",
|
id: "Comfy.DOMClippingEnabled",
|
||||||
name: "Enable DOM element clipping (enabling may reduce performance)",
|
name: "Enable DOM element clipping (enabling may reduce performance)",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
defaultValue: enableDomClipping,
|
defaultValue: enableDomClipping,
|
||||||
onChange(value) {
|
onChange(value) {
|
||||||
enableDomClipping = !!value;
|
enableDomClipping = !!value;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
LGraphNode.prototype.addDOMWidget = function (
|
LGraphNode.prototype.addDOMWidget = function (
|
||||||
name: string,
|
name: string,
|
||||||
type: string,
|
type: string,
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
options: Record<string, any>
|
options: Record<string, any>
|
||||||
): DOMWidget {
|
): DOMWidget {
|
||||||
options = { hideOnZoom: true, selectOn: ["focus", "click"], ...options };
|
options = { hideOnZoom: true, selectOn: ["focus", "click"], ...options };
|
||||||
|
|
||||||
if (!element.parentElement) {
|
if (!element.parentElement) {
|
||||||
document.body.append(element);
|
document.body.append(element);
|
||||||
}
|
}
|
||||||
element.hidden = true;
|
element.hidden = true;
|
||||||
element.style.display = "none";
|
element.style.display = "none";
|
||||||
|
|
||||||
let mouseDownHandler;
|
let mouseDownHandler;
|
||||||
if (element.blur) {
|
if (element.blur) {
|
||||||
mouseDownHandler = (event) => {
|
mouseDownHandler = (event) => {
|
||||||
if (!element.contains(event.target)) {
|
if (!element.contains(event.target)) {
|
||||||
element.blur();
|
element.blur();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener("mousedown", mouseDownHandler);
|
document.addEventListener("mousedown", mouseDownHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
const widget: DOMWidget = {
|
const widget: DOMWidget = {
|
||||||
type,
|
type,
|
||||||
name,
|
name,
|
||||||
get value() {
|
get value() {
|
||||||
return options.getValue?.() ?? undefined;
|
return options.getValue?.() ?? undefined;
|
||||||
},
|
},
|
||||||
set value(v) {
|
set value(v) {
|
||||||
options.setValue?.(v);
|
options.setValue?.(v);
|
||||||
widget.callback?.(widget.value);
|
widget.callback?.(widget.value);
|
||||||
},
|
},
|
||||||
draw: function (ctx: CanvasRenderingContext2D, node: LGraphNode, widgetWidth: number, y: number, widgetHeight: number) {
|
draw: function (
|
||||||
if (widget.computedHeight == null) {
|
ctx: CanvasRenderingContext2D,
|
||||||
computeSize.call(node, node.size);
|
node: LGraphNode,
|
||||||
}
|
widgetWidth: number,
|
||||||
|
y: number,
|
||||||
|
widgetHeight: number
|
||||||
|
) {
|
||||||
|
if (widget.computedHeight == null) {
|
||||||
|
computeSize.call(node, node.size);
|
||||||
|
}
|
||||||
|
|
||||||
const hidden =
|
const hidden =
|
||||||
node.flags?.collapsed ||
|
node.flags?.collapsed ||
|
||||||
(!!options.hideOnZoom && app.canvas.ds.scale < 0.5) ||
|
(!!options.hideOnZoom && app.canvas.ds.scale < 0.5) ||
|
||||||
widget.computedHeight <= 0 ||
|
widget.computedHeight <= 0 ||
|
||||||
widget.type === "converted-widget" ||
|
widget.type === "converted-widget" ||
|
||||||
widget.type === "hidden";
|
widget.type === "hidden";
|
||||||
element.hidden = hidden;
|
element.hidden = hidden;
|
||||||
element.style.display = hidden ? "none" : null;
|
element.style.display = hidden ? "none" : null;
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
widget.options.onHide?.(widget);
|
widget.options.onHide?.(widget);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const margin = 10;
|
const margin = 10;
|
||||||
const elRect = ctx.canvas.getBoundingClientRect();
|
const elRect = ctx.canvas.getBoundingClientRect();
|
||||||
const transform = new DOMMatrix()
|
const transform = new DOMMatrix()
|
||||||
.scaleSelf(elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height)
|
.scaleSelf(
|
||||||
.multiplySelf(ctx.getTransform())
|
elRect.width / ctx.canvas.width,
|
||||||
.translateSelf(margin, margin + y);
|
elRect.height / ctx.canvas.height
|
||||||
|
)
|
||||||
|
.multiplySelf(ctx.getTransform())
|
||||||
|
.translateSelf(margin, margin + y);
|
||||||
|
|
||||||
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d);
|
const scale = new DOMMatrix().scaleSelf(transform.a, transform.d);
|
||||||
|
|
||||||
Object.assign(element.style, {
|
Object.assign(element.style, {
|
||||||
transformOrigin: "0 0",
|
transformOrigin: "0 0",
|
||||||
transform: scale,
|
transform: scale,
|
||||||
left: `${transform.a + transform.e + elRect.left}px`,
|
left: `${transform.a + transform.e + elRect.left}px`,
|
||||||
top: `${transform.d + transform.f + elRect.top}px`,
|
top: `${transform.d + transform.f + elRect.top}px`,
|
||||||
width: `${widgetWidth - margin * 2}px`,
|
width: `${widgetWidth - margin * 2}px`,
|
||||||
height: `${(widget.computedHeight ?? 50) - margin * 2}px`,
|
height: `${(widget.computedHeight ?? 50) - margin * 2}px`,
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
zIndex: app.graph._nodes.indexOf(node),
|
zIndex: app.graph._nodes.indexOf(node),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (enableDomClipping) {
|
if (enableDomClipping) {
|
||||||
element.style.clipPath = getClipPath(node, element);
|
element.style.clipPath = getClipPath(node, element);
|
||||||
element.style.willChange = "clip-path";
|
element.style.willChange = "clip-path";
|
||||||
}
|
}
|
||||||
|
|
||||||
this.options.onDraw?.(widget);
|
this.options.onDraw?.(widget);
|
||||||
},
|
},
|
||||||
element,
|
element,
|
||||||
options,
|
options,
|
||||||
onRemove() {
|
onRemove() {
|
||||||
if (mouseDownHandler) {
|
if (mouseDownHandler) {
|
||||||
document.removeEventListener("mousedown", mouseDownHandler);
|
document.removeEventListener("mousedown", mouseDownHandler);
|
||||||
}
|
}
|
||||||
element.remove();
|
element.remove();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const evt of options.selectOn) {
|
for (const evt of options.selectOn) {
|
||||||
element.addEventListener(evt, () => {
|
element.addEventListener(evt, () => {
|
||||||
app.canvas.selectNode(this);
|
app.canvas.selectNode(this);
|
||||||
app.canvas.bringToFront(this);
|
app.canvas.bringToFront(this);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.addCustomWidget(widget);
|
this.addCustomWidget(widget);
|
||||||
elementWidgets.add(this);
|
elementWidgets.add(this);
|
||||||
|
|
||||||
const collapse = this.collapse;
|
const collapse = this.collapse;
|
||||||
this.collapse = function () {
|
this.collapse = function () {
|
||||||
collapse.apply(this, arguments);
|
collapse.apply(this, arguments);
|
||||||
if (this.flags?.collapsed) {
|
if (this.flags?.collapsed) {
|
||||||
element.hidden = true;
|
element.hidden = true;
|
||||||
element.style.display = "none";
|
element.style.display = "none";
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const onRemoved = this.onRemoved;
|
const onRemoved = this.onRemoved;
|
||||||
this.onRemoved = function () {
|
this.onRemoved = function () {
|
||||||
element.remove();
|
element.remove();
|
||||||
elementWidgets.delete(this);
|
elementWidgets.delete(this);
|
||||||
onRemoved?.apply(this, arguments);
|
onRemoved?.apply(this, arguments);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!this[SIZE]) {
|
if (!this[SIZE]) {
|
||||||
this[SIZE] = true;
|
this[SIZE] = true;
|
||||||
const onResize = this.onResize;
|
const onResize = this.onResize;
|
||||||
this.onResize = function (size) {
|
this.onResize = function (size) {
|
||||||
options.beforeResize?.call(widget, this);
|
options.beforeResize?.call(widget, this);
|
||||||
computeSize.call(this, size);
|
computeSize.call(this, size);
|
||||||
onResize?.apply(this, arguments);
|
onResize?.apply(this, arguments);
|
||||||
options.afterResize?.call(widget, this);
|
options.afterResize?.call(widget, this);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return widget;
|
return widget;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,13 +41,13 @@ function stringify(val, depth, replacer, space, onGetObjID?) {
|
|||||||
r
|
r
|
||||||
? (o = (onGetObjID && onGetObjID(val)) || null)
|
? (o = (onGetObjID && onGetObjID(val)) || null)
|
||||||
: JSON.stringify(val, function (k, v) {
|
: JSON.stringify(val, function (k, v) {
|
||||||
if (a || depth > 0) {
|
if (a || depth > 0) {
|
||||||
if (replacer) v = replacer(k, v);
|
if (replacer) v = replacer(k, v);
|
||||||
if (!k) return (a = Array.isArray(v)), (val = v);
|
if (!k) return (a = Array.isArray(v)), (val = v);
|
||||||
!o && (o = a ? [] : {});
|
!o && (o = a ? [] : {});
|
||||||
o[k] = _build(v, a ? depth : depth - 1);
|
o[k] = _build(v, a ? depth : depth - 1);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
o === void 0 ? (a ? [] : {}) : o);
|
o === void 0 ? (a ? [] : {}) : o);
|
||||||
}
|
}
|
||||||
return JSON.stringify(_build(val, depth), null, space);
|
return JSON.stringify(_build(val, depth), null, space);
|
||||||
@@ -97,9 +97,12 @@ class ComfyLoggingDialog extends ComfyDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export() {
|
export() {
|
||||||
const blob = new Blob([stringify([...this.logging.entries], 20, jsonReplacer, "\t")], {
|
const blob = new Blob(
|
||||||
type: "application/json",
|
[stringify([...this.logging.entries], 20, jsonReplacer, "\t")],
|
||||||
});
|
{
|
||||||
|
type: "application/json",
|
||||||
|
}
|
||||||
|
);
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = $el("a", {
|
const a = $el("a", {
|
||||||
href: url,
|
href: url,
|
||||||
@@ -147,230 +150,234 @@ class ComfyLoggingDialog extends ComfyDialog {
|
|||||||
textContent: "Export logs...",
|
textContent: "Export logs...",
|
||||||
onclick: () => this.export(),
|
onclick: () => this.export(),
|
||||||
}),
|
}),
|
||||||
$el("button", {
|
$el("button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: "View exported logs...",
|
textContent: "View exported logs...",
|
||||||
onclick: () => this.import(),
|
onclick: () => this.import(),
|
||||||
}),
|
}),
|
||||||
...super.createButtons(),
|
...super.createButtons(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
getTypeColor(type) {
|
getTypeColor(type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "error":
|
case "error":
|
||||||
return "red";
|
return "red";
|
||||||
case "warn":
|
case "warn":
|
||||||
return "orange";
|
return "orange";
|
||||||
case "debug":
|
case "debug":
|
||||||
return "dodgerblue";
|
return "dodgerblue";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
show(entries?: any[]) {
|
show(entries?: any[]) {
|
||||||
if (!entries) entries = this.logging.entries;
|
if (!entries) entries = this.logging.entries;
|
||||||
this.element.style.width = "100%";
|
this.element.style.width = "100%";
|
||||||
const cols = {
|
const cols = {
|
||||||
source: "Source",
|
source: "Source",
|
||||||
type: "Type",
|
type: "Type",
|
||||||
timestamp: "Timestamp",
|
timestamp: "Timestamp",
|
||||||
message: "Message",
|
message: "Message",
|
||||||
};
|
};
|
||||||
const keys = Object.keys(cols);
|
const keys = Object.keys(cols);
|
||||||
const headers = Object.values(cols).map((title) =>
|
const headers = Object.values(cols).map((title) =>
|
||||||
$el("div.comfy-logging-title", {
|
$el("div.comfy-logging-title", {
|
||||||
textContent: title,
|
textContent: title,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const rows = entries.map((entry, i) => {
|
const rows = entries.map((entry, i) => {
|
||||||
return $el(
|
return $el(
|
||||||
"div.comfy-logging-log",
|
"div.comfy-logging-log",
|
||||||
{
|
{
|
||||||
$: (el) => el.style.setProperty("--row-bg", `var(--tr-${i % 2 ? "even" : "odd"}-bg-color)`),
|
$: (el) =>
|
||||||
},
|
el.style.setProperty(
|
||||||
keys.map((key) => {
|
"--row-bg",
|
||||||
let v = entry[key];
|
`var(--tr-${i % 2 ? "even" : "odd"}-bg-color)`
|
||||||
let color;
|
),
|
||||||
if (key === "type") {
|
},
|
||||||
color = this.getTypeColor(v);
|
keys.map((key) => {
|
||||||
} else {
|
let v = entry[key];
|
||||||
v = jsonReplacer(key, v, true);
|
let color;
|
||||||
|
if (key === "type") {
|
||||||
|
color = this.getTypeColor(v);
|
||||||
|
} else {
|
||||||
|
v = jsonReplacer(key, v, true);
|
||||||
|
|
||||||
if (typeof v === "object") {
|
if (typeof v === "object") {
|
||||||
v = stringify(v, 5, jsonReplacer, " ");
|
v = stringify(v, 5, jsonReplacer, " ");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $el("div", {
|
return $el("div", {
|
||||||
style: {
|
style: {
|
||||||
color,
|
color,
|
||||||
},
|
},
|
||||||
textContent: v,
|
textContent: v,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const grid = $el(
|
const grid = $el(
|
||||||
"div.comfy-logging-logs",
|
"div.comfy-logging-logs",
|
||||||
{
|
{
|
||||||
style: {
|
style: {
|
||||||
gridTemplateColumns: `repeat(${headers.length}, 1fr)`,
|
gridTemplateColumns: `repeat(${headers.length}, 1fr)`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[...headers, ...rows]
|
[...headers, ...rows]
|
||||||
);
|
);
|
||||||
const els = [grid];
|
const els = [grid];
|
||||||
if (!this.logging.enabled) {
|
if (!this.logging.enabled) {
|
||||||
els.unshift(
|
els.unshift(
|
||||||
$el("h3", {
|
$el("h3", {
|
||||||
style: { textAlign: "center" },
|
style: { textAlign: "center" },
|
||||||
textContent: "Logging is disabled",
|
textContent: "Logging is disabled",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
super.show($el("div", els));
|
super.show($el("div", els));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ComfyLogging {
|
export class ComfyLogging {
|
||||||
/**
|
/**
|
||||||
* @type Array<{ source: string, type: string, timestamp: Date, message: any }>
|
* @type Array<{ source: string, type: string, timestamp: Date, message: any }>
|
||||||
*/
|
*/
|
||||||
entries = [];
|
entries = [];
|
||||||
|
|
||||||
#enabled;
|
#enabled;
|
||||||
#console = {};
|
#console = {};
|
||||||
|
|
||||||
app: ComfyApp;
|
app: ComfyApp;
|
||||||
dialog: ComfyLoggingDialog;
|
dialog: ComfyLoggingDialog;
|
||||||
|
|
||||||
get enabled() {
|
get enabled() {
|
||||||
return this.#enabled;
|
return this.#enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
set enabled(value) {
|
set enabled(value) {
|
||||||
if (value === this.#enabled) return;
|
if (value === this.#enabled) return;
|
||||||
if (value) {
|
if (value) {
|
||||||
this.patchConsole();
|
this.patchConsole();
|
||||||
} else {
|
} else {
|
||||||
this.unpatchConsole();
|
this.unpatchConsole();
|
||||||
}
|
}
|
||||||
this.#enabled = value;
|
this.#enabled = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(app) {
|
constructor(app) {
|
||||||
this.app = app;
|
this.app = app;
|
||||||
|
|
||||||
this.dialog = new ComfyLoggingDialog(this);
|
this.dialog = new ComfyLoggingDialog(this);
|
||||||
this.addSetting();
|
this.addSetting();
|
||||||
this.catchUnhandled();
|
this.catchUnhandled();
|
||||||
this.addInitData();
|
this.addInitData();
|
||||||
}
|
}
|
||||||
|
|
||||||
addSetting() {
|
addSetting() {
|
||||||
const settingId: string = "Comfy.Logging.Enabled";
|
const settingId: string = "Comfy.Logging.Enabled";
|
||||||
const htmlSettingId = settingId.replaceAll(".", "-");
|
const htmlSettingId = settingId.replaceAll(".", "-");
|
||||||
const setting = this.app.ui.settings.addSetting({
|
const setting = this.app.ui.settings.addSetting({
|
||||||
id: settingId,
|
id: settingId,
|
||||||
name: settingId,
|
name: settingId,
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
onChange: (value) => {
|
onChange: (value) => {
|
||||||
this.enabled = value;
|
this.enabled = value;
|
||||||
},
|
},
|
||||||
type: (name, setter, value) => {
|
type: (name, setter, value) => {
|
||||||
return $el("tr", [
|
return $el("tr", [
|
||||||
$el("td", [
|
$el("td", [
|
||||||
$el("label", {
|
$el("label", {
|
||||||
textContent: "Logging",
|
textContent: "Logging",
|
||||||
for: htmlSettingId,
|
for: htmlSettingId,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
$el("td", [
|
$el("td", [
|
||||||
$el("input", {
|
$el("input", {
|
||||||
id: htmlSettingId,
|
id: htmlSettingId,
|
||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
checked: value,
|
checked: value,
|
||||||
onchange: (event) => {
|
onchange: (event) => {
|
||||||
setter(event.target.checked);
|
setter(event.target.checked);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
$el("button", {
|
$el("button", {
|
||||||
textContent: "View Logs",
|
textContent: "View Logs",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
this.app.ui.settings.element.close();
|
this.app.ui.settings.element.close();
|
||||||
this.dialog.show();
|
this.dialog.show();
|
||||||
},
|
},
|
||||||
style: {
|
style: {
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
display: "block",
|
display: "block",
|
||||||
marginTop: "5px",
|
marginTop: "5px",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.enabled = setting.value;
|
this.enabled = setting.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
patchConsole() {
|
patchConsole() {
|
||||||
// Capture common console outputs
|
// Capture common console outputs
|
||||||
const self = this;
|
const self = this;
|
||||||
for (const type of ["log", "warn", "error", "debug"]) {
|
for (const type of ["log", "warn", "error", "debug"]) {
|
||||||
const orig = console[type];
|
const orig = console[type];
|
||||||
this.#console[type] = orig;
|
this.#console[type] = orig;
|
||||||
console[type] = function () {
|
console[type] = function () {
|
||||||
orig.apply(console, arguments);
|
orig.apply(console, arguments);
|
||||||
self.addEntry("console", type, ...arguments);
|
self.addEntry("console", type, ...arguments);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unpatchConsole() {
|
unpatchConsole() {
|
||||||
// Restore original console functions
|
// Restore original console functions
|
||||||
for (const type of Object.keys(this.#console)) {
|
for (const type of Object.keys(this.#console)) {
|
||||||
console[type] = this.#console[type];
|
console[type] = this.#console[type];
|
||||||
}
|
}
|
||||||
this.#console = {};
|
this.#console = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
catchUnhandled() {
|
catchUnhandled() {
|
||||||
// Capture uncaught errors
|
// Capture uncaught errors
|
||||||
window.addEventListener("error", (e) => {
|
window.addEventListener("error", (e) => {
|
||||||
this.addEntry("window", "error", e.error ?? "Unknown error");
|
this.addEntry("window", "error", e.error ?? "Unknown error");
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("unhandledrejection", (e) => {
|
window.addEventListener("unhandledrejection", (e) => {
|
||||||
this.addEntry("unhandledrejection", "error", e.reason ?? "Unknown error");
|
this.addEntry("unhandledrejection", "error", e.reason ?? "Unknown error");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.entries = [];
|
this.entries = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
addEntry(source, type, ...args) {
|
addEntry(source, type, ...args) {
|
||||||
if (this.enabled) {
|
if (this.enabled) {
|
||||||
this.entries.push({
|
this.entries.push({
|
||||||
source,
|
source,
|
||||||
type,
|
type,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
message: args,
|
message: args,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log(source, ...args) {
|
log(source, ...args) {
|
||||||
this.addEntry(source, "log", ...args);
|
this.addEntry(source, "log", ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addInitData() {
|
async addInitData() {
|
||||||
if (!this.enabled) return;
|
if (!this.enabled) return;
|
||||||
const source = "ComfyUI.Logging";
|
const source = "ComfyUI.Logging";
|
||||||
this.addEntry(source, "debug", { UserAgent: navigator.userAgent });
|
this.addEntry(source, "debug", { UserAgent: navigator.userAgent });
|
||||||
const systemStats = await api.getSystemStats();
|
const systemStats = await api.getSystemStats();
|
||||||
this.addEntry(source, "debug", systemStats);
|
this.addEntry(source, "debug", systemStats);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,17 +23,26 @@ export function getPngMetadata(file) {
|
|||||||
// Get the length of the chunk
|
// Get the length of the chunk
|
||||||
const length = dataView.getUint32(offset);
|
const length = dataView.getUint32(offset);
|
||||||
// Get the chunk type
|
// Get the chunk type
|
||||||
const type = String.fromCharCode(...pngData.slice(offset + 4, offset + 8));
|
const type = String.fromCharCode(
|
||||||
|
...pngData.slice(offset + 4, offset + 8)
|
||||||
|
);
|
||||||
if (type === "tEXt" || type == "comf" || type === "iTXt") {
|
if (type === "tEXt" || type == "comf" || type === "iTXt") {
|
||||||
// Get the keyword
|
// Get the keyword
|
||||||
let keyword_end = offset + 8;
|
let keyword_end = offset + 8;
|
||||||
while (pngData[keyword_end] !== 0) {
|
while (pngData[keyword_end] !== 0) {
|
||||||
keyword_end++;
|
keyword_end++;
|
||||||
}
|
}
|
||||||
const keyword = String.fromCharCode(...pngData.slice(offset + 8, keyword_end));
|
const keyword = String.fromCharCode(
|
||||||
|
...pngData.slice(offset + 8, keyword_end)
|
||||||
|
);
|
||||||
// Get the text
|
// Get the text
|
||||||
const contentArraySegment = pngData.slice(keyword_end + 1, offset + 8 + length);
|
const contentArraySegment = pngData.slice(
|
||||||
const contentJson = new TextDecoder("utf-8").decode(contentArraySegment);
|
keyword_end + 1,
|
||||||
|
offset + 8 + length
|
||||||
|
);
|
||||||
|
const contentJson = new TextDecoder("utf-8").decode(
|
||||||
|
contentArraySegment
|
||||||
|
);
|
||||||
txt_chunks[keyword] = contentJson;
|
txt_chunks[keyword] = contentJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,11 +62,17 @@ function parseExifData(exifData) {
|
|||||||
|
|
||||||
// Function to read 16-bit and 32-bit integers from binary data
|
// Function to read 16-bit and 32-bit integers from binary data
|
||||||
function readInt(offset, isLittleEndian, length) {
|
function readInt(offset, isLittleEndian, length) {
|
||||||
let arr = exifData.slice(offset, offset + length)
|
let arr = exifData.slice(offset, offset + length);
|
||||||
if (length === 2) {
|
if (length === 2) {
|
||||||
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint16(0, isLittleEndian);
|
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint16(
|
||||||
|
0,
|
||||||
|
isLittleEndian
|
||||||
|
);
|
||||||
} else if (length === 4) {
|
} else if (length === 4) {
|
||||||
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint32(0, isLittleEndian);
|
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint32(
|
||||||
|
0,
|
||||||
|
isLittleEndian
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +94,9 @@ function parseExifData(exifData) {
|
|||||||
let value;
|
let value;
|
||||||
if (type === 2) {
|
if (type === 2) {
|
||||||
// ASCII string
|
// ASCII string
|
||||||
value = String.fromCharCode(...exifData.slice(valueOffset, valueOffset + numValues - 1));
|
value = String.fromCharCode(
|
||||||
|
...exifData.slice(valueOffset, valueOffset + numValues - 1)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
result[tag] = value;
|
result[tag] = value;
|
||||||
@@ -94,13 +111,13 @@ function parseExifData(exifData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function splitValues(input) {
|
function splitValues(input) {
|
||||||
var output = {};
|
var output = {};
|
||||||
for (var key in input) {
|
for (var key in input) {
|
||||||
var value = input[key];
|
var value = input[key];
|
||||||
var splitValues = value.split(':', 2);
|
var splitValues = value.split(":", 2);
|
||||||
output[splitValues[0]] = splitValues[1];
|
output[splitValues[0]] = splitValues[1];
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWebpMetadata(file) {
|
export function getWebpMetadata(file) {
|
||||||
@@ -111,7 +128,10 @@ export function getWebpMetadata(file) {
|
|||||||
const dataView = new DataView(webp.buffer);
|
const dataView = new DataView(webp.buffer);
|
||||||
|
|
||||||
// Check that the WEBP signature is present
|
// Check that the WEBP signature is present
|
||||||
if (dataView.getUint32(0) !== 0x52494646 || dataView.getUint32(8) !== 0x57454250) {
|
if (
|
||||||
|
dataView.getUint32(0) !== 0x52494646 ||
|
||||||
|
dataView.getUint32(8) !== 0x57454250
|
||||||
|
) {
|
||||||
console.error("Not a valid WEBP file");
|
console.error("Not a valid WEBP file");
|
||||||
r({});
|
r({});
|
||||||
return;
|
return;
|
||||||
@@ -123,15 +143,22 @@ export function getWebpMetadata(file) {
|
|||||||
// Loop through the chunks in the WEBP file
|
// Loop through the chunks in the WEBP file
|
||||||
while (offset < webp.length) {
|
while (offset < webp.length) {
|
||||||
const chunk_length = dataView.getUint32(offset + 4, true);
|
const chunk_length = dataView.getUint32(offset + 4, true);
|
||||||
const chunk_type = String.fromCharCode(...webp.slice(offset, offset + 4));
|
const chunk_type = String.fromCharCode(
|
||||||
|
...webp.slice(offset, offset + 4)
|
||||||
|
);
|
||||||
if (chunk_type === "EXIF") {
|
if (chunk_type === "EXIF") {
|
||||||
if (String.fromCharCode(...webp.slice(offset + 8, offset + 8 + 6)) == "Exif\0\0") {
|
if (
|
||||||
|
String.fromCharCode(...webp.slice(offset + 8, offset + 8 + 6)) ==
|
||||||
|
"Exif\0\0"
|
||||||
|
) {
|
||||||
offset += 6;
|
offset += 6;
|
||||||
}
|
}
|
||||||
let data = parseExifData(webp.slice(offset + 8, offset + 8 + chunk_length));
|
let data = parseExifData(
|
||||||
|
webp.slice(offset + 8, offset + 8 + chunk_length)
|
||||||
|
);
|
||||||
for (var key in data) {
|
for (var key in data) {
|
||||||
var value = data[key] as string;
|
var value = data[key] as string;
|
||||||
let index = value.indexOf(':');
|
let index = value.indexOf(":");
|
||||||
txt_chunks[value.slice(0, index)] = value.slice(index + 1);
|
txt_chunks[value.slice(0, index)] = value.slice(index + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,11 +177,17 @@ export function getLatentMetadata(file) {
|
|||||||
return new Promise((r) => {
|
return new Promise((r) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (event) => {
|
reader.onload = (event) => {
|
||||||
const safetensorsData = new Uint8Array(event.target.result as ArrayBuffer);
|
const safetensorsData = new Uint8Array(
|
||||||
|
event.target.result as ArrayBuffer
|
||||||
|
);
|
||||||
const dataView = new DataView(safetensorsData.buffer);
|
const dataView = new DataView(safetensorsData.buffer);
|
||||||
let header_size = dataView.getUint32(0, true);
|
let header_size = dataView.getUint32(0, true);
|
||||||
let offset = 8;
|
let offset = 8;
|
||||||
let header = JSON.parse(new TextDecoder().decode(safetensorsData.slice(offset, offset + header_size)));
|
let header = JSON.parse(
|
||||||
|
new TextDecoder().decode(
|
||||||
|
safetensorsData.slice(offset, offset + header_size)
|
||||||
|
)
|
||||||
|
);
|
||||||
r(header.__metadata__);
|
r(header.__metadata__);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -164,7 +197,7 @@ export function getLatentMetadata(file) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getString(dataView: DataView, offset: number, length: number): string {
|
function getString(dataView: DataView, offset: number, length: number): string {
|
||||||
let string = '';
|
let string = "";
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
string += String.fromCharCode(dataView.getUint8(offset + i));
|
string += String.fromCharCode(dataView.getUint8(offset + i));
|
||||||
}
|
}
|
||||||
@@ -188,7 +221,7 @@ function parseVorbisComment(dataView: DataView): Record<string, string> {
|
|||||||
const comment = getString(dataView, offset, commentLength);
|
const comment = getString(dataView, offset, commentLength);
|
||||||
offset += commentLength;
|
offset += commentLength;
|
||||||
|
|
||||||
const [key, value] = comment.split('=');
|
const [key, value] = comment.split("=");
|
||||||
|
|
||||||
comments[key] = value;
|
comments[key] = value;
|
||||||
}
|
}
|
||||||
@@ -200,14 +233,16 @@ function parseVorbisComment(dataView: DataView): Record<string, string> {
|
|||||||
export function getFlacMetadata(file: Blob): Promise<Record<string, string>> {
|
export function getFlacMetadata(file: Blob): Promise<Record<string, string>> {
|
||||||
return new Promise((r) => {
|
return new Promise((r) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function(event) {
|
reader.onload = function (event) {
|
||||||
const arrayBuffer = event.target.result as ArrayBuffer;
|
const arrayBuffer = event.target.result as ArrayBuffer;
|
||||||
const dataView = new DataView(arrayBuffer);
|
const dataView = new DataView(arrayBuffer);
|
||||||
|
|
||||||
// Verify the FLAC signature
|
// Verify the FLAC signature
|
||||||
const signature = String.fromCharCode(...new Uint8Array(arrayBuffer, 0, 4));
|
const signature = String.fromCharCode(
|
||||||
if (signature !== 'fLaC') {
|
...new Uint8Array(arrayBuffer, 0, 4)
|
||||||
console.error('Not a valid FLAC file');
|
);
|
||||||
|
if (signature !== "fLaC") {
|
||||||
|
console.error("Not a valid FLAC file");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,12 +251,15 @@ export function getFlacMetadata(file: Blob): Promise<Record<string, string>> {
|
|||||||
let vorbisComment = null;
|
let vorbisComment = null;
|
||||||
while (offset < dataView.byteLength) {
|
while (offset < dataView.byteLength) {
|
||||||
const isLastBlock = dataView.getUint8(offset) & 0x80;
|
const isLastBlock = dataView.getUint8(offset) & 0x80;
|
||||||
const blockType = dataView.getUint8(offset) & 0x7F;
|
const blockType = dataView.getUint8(offset) & 0x7f;
|
||||||
const blockSize = dataView.getUint32(offset, false) & 0xFFFFFF;
|
const blockSize = dataView.getUint32(offset, false) & 0xffffff;
|
||||||
offset += 4;
|
offset += 4;
|
||||||
|
|
||||||
if (blockType === 4) { // Vorbis Comment block type
|
if (blockType === 4) {
|
||||||
vorbisComment = parseVorbisComment(new DataView(arrayBuffer, offset, blockSize));
|
// Vorbis Comment block type
|
||||||
|
vorbisComment = parseVorbisComment(
|
||||||
|
new DataView(arrayBuffer, offset, blockSize)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
offset += blockSize;
|
offset += blockSize;
|
||||||
@@ -241,11 +279,13 @@ export async function importA1111(graph, parameters) {
|
|||||||
const opts = parameters
|
const opts = parameters
|
||||||
.substr(p)
|
.substr(p)
|
||||||
.split("\n")[1]
|
.split("\n")[1]
|
||||||
.match(new RegExp("\\s*([^:]+:\\s*([^\"\\{].*?|\".*?\"|\\{.*?\\}))\\s*(,|$)", "g"))
|
.match(
|
||||||
|
new RegExp('\\s*([^:]+:\\s*([^"\\{].*?|".*?"|\\{.*?\\}))\\s*(,|$)', "g")
|
||||||
|
)
|
||||||
.reduce((p, n) => {
|
.reduce((p, n) => {
|
||||||
const s = n.split(":");
|
const s = n.split(":");
|
||||||
if (s[1].endsWith(',')) {
|
if (s[1].endsWith(",")) {
|
||||||
s[1] = s[1].substr(0, s[1].length -1);
|
s[1] = s[1].substr(0, s[1].length - 1);
|
||||||
}
|
}
|
||||||
p[s[0].trim().toLowerCase()] = s[1].trim();
|
p[s[0].trim().toLowerCase()] = s[1].trim();
|
||||||
return p;
|
return p;
|
||||||
@@ -271,7 +311,7 @@ export async function importA1111(graph, parameters) {
|
|||||||
|
|
||||||
const getWidget = (node, name) => {
|
const getWidget = (node, name) => {
|
||||||
return node.widgets.find((w) => w.name === name);
|
return node.widgets.find((w) => w.name === name);
|
||||||
}
|
};
|
||||||
|
|
||||||
const setWidgetValue = (node, name, value, isOptionPrefix?) => {
|
const setWidgetValue = (node, name, value, isOptionPrefix?) => {
|
||||||
const w = getWidget(node, name);
|
const w = getWidget(node, name);
|
||||||
@@ -286,7 +326,7 @@ export async function importA1111(graph, parameters) {
|
|||||||
} else {
|
} else {
|
||||||
w.value = value;
|
w.value = value;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const createLoraNodes = (clipNode, text, prevClip, prevModel) => {
|
const createLoraNodes = (clipNode, text, prevClip, prevModel) => {
|
||||||
const loras = [];
|
const loras = [];
|
||||||
@@ -320,24 +360,28 @@ export async function importA1111(graph, parameters) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { text, prevModel, prevClip };
|
return { text, prevModel, prevClip };
|
||||||
}
|
};
|
||||||
|
|
||||||
const replaceEmbeddings = (text) => {
|
const replaceEmbeddings = (text) => {
|
||||||
if(!embeddings.length) return text;
|
if (!embeddings.length) return text;
|
||||||
return text.replaceAll(
|
return text.replaceAll(
|
||||||
new RegExp(
|
new RegExp(
|
||||||
"\\b(" + embeddings.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\b|\\b") + ")\\b",
|
"\\b(" +
|
||||||
|
embeddings
|
||||||
|
.map((e) => e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
||||||
|
.join("\\b|\\b") +
|
||||||
|
")\\b",
|
||||||
"ig"
|
"ig"
|
||||||
),
|
),
|
||||||
"embedding:$1"
|
"embedding:$1"
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const popOpt = (name) => {
|
const popOpt = (name) => {
|
||||||
const v = opts[name];
|
const v = opts[name];
|
||||||
delete opts[name];
|
delete opts[name];
|
||||||
return v;
|
return v;
|
||||||
}
|
};
|
||||||
|
|
||||||
graph.clear();
|
graph.clear();
|
||||||
graph.add(ckptNode);
|
graph.add(ckptNode);
|
||||||
@@ -365,7 +409,7 @@ export async function importA1111(graph, parameters) {
|
|||||||
model(v) {
|
model(v) {
|
||||||
setWidgetValue(ckptNode, "ckpt_name", v, true);
|
setWidgetValue(ckptNode, "ckpt_name", v, true);
|
||||||
},
|
},
|
||||||
"vae"(v) {
|
vae(v) {
|
||||||
setWidgetValue(vaeLoaderNode, "vae_name", v, true);
|
setWidgetValue(vaeLoaderNode, "vae_name", v, true);
|
||||||
},
|
},
|
||||||
"cfg scale"(v) {
|
"cfg scale"(v) {
|
||||||
@@ -383,7 +427,9 @@ export async function importA1111(graph, parameters) {
|
|||||||
setWidgetValue(samplerNode, "scheduler", "normal");
|
setWidgetValue(samplerNode, "scheduler", "normal");
|
||||||
}
|
}
|
||||||
const w = getWidget(samplerNode, "sampler_name");
|
const w = getWidget(samplerNode, "sampler_name");
|
||||||
const o = w.options.values.find((w) => w === name || w === "sample_" + name);
|
const o = w.options.values.find(
|
||||||
|
(w) => w === name || w === "sample_" + name
|
||||||
|
);
|
||||||
if (o) {
|
if (o) {
|
||||||
setWidgetValue(samplerNode, "sampler_name", o);
|
setWidgetValue(samplerNode, "sampler_name", o);
|
||||||
}
|
}
|
||||||
@@ -431,11 +477,14 @@ export async function importA1111(graph, parameters) {
|
|||||||
samplerNode.connect(0, decode, 0);
|
samplerNode.connect(0, decode, 0);
|
||||||
vaeLoaderNode.connect(0, decode, 1);
|
vaeLoaderNode.connect(0, decode, 1);
|
||||||
|
|
||||||
const upscaleLoaderNode = LiteGraph.createNode("UpscaleModelLoader");
|
const upscaleLoaderNode =
|
||||||
|
LiteGraph.createNode("UpscaleModelLoader");
|
||||||
graph.add(upscaleLoaderNode);
|
graph.add(upscaleLoaderNode);
|
||||||
setWidgetValue(upscaleLoaderNode, "model_name", hrMethod, true);
|
setWidgetValue(upscaleLoaderNode, "model_name", hrMethod, true);
|
||||||
|
|
||||||
const modelUpscaleNode = LiteGraph.createNode("ImageUpscaleWithModel");
|
const modelUpscaleNode = LiteGraph.createNode(
|
||||||
|
"ImageUpscaleWithModel"
|
||||||
|
);
|
||||||
graph.add(modelUpscaleNode);
|
graph.add(modelUpscaleNode);
|
||||||
decode.connect(0, modelUpscaleNode, 1);
|
decode.connect(0, modelUpscaleNode, 1);
|
||||||
upscaleLoaderNode.connect(0, modelUpscaleNode, 0);
|
upscaleLoaderNode.connect(0, modelUpscaleNode, 0);
|
||||||
@@ -444,7 +493,8 @@ export async function importA1111(graph, parameters) {
|
|||||||
graph.add(upscaleNode);
|
graph.add(upscaleNode);
|
||||||
modelUpscaleNode.connect(0, upscaleNode, 0);
|
modelUpscaleNode.connect(0, upscaleNode, 0);
|
||||||
|
|
||||||
const vaeEncodeNode = (latentNode = LiteGraph.createNode("VAEEncodeTiled"));
|
const vaeEncodeNode = (latentNode =
|
||||||
|
LiteGraph.createNode("VAEEncodeTiled"));
|
||||||
graph.add(vaeEncodeNode);
|
graph.add(vaeEncodeNode);
|
||||||
upscaleNode.connect(0, vaeEncodeNode, 0);
|
upscaleNode.connect(0, vaeEncodeNode, 0);
|
||||||
vaeLoaderNode.connect(0, vaeEncodeNode, 1);
|
vaeLoaderNode.connect(0, vaeEncodeNode, 1);
|
||||||
@@ -477,14 +527,39 @@ export async function importA1111(graph, parameters) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hrSamplerNode) {
|
if (hrSamplerNode) {
|
||||||
setWidgetValue(hrSamplerNode, "steps", hrSteps? +hrSteps : getWidget(samplerNode, "steps").value);
|
setWidgetValue(
|
||||||
setWidgetValue(hrSamplerNode, "cfg", getWidget(samplerNode, "cfg").value);
|
hrSamplerNode,
|
||||||
setWidgetValue(hrSamplerNode, "scheduler", getWidget(samplerNode, "scheduler").value);
|
"steps",
|
||||||
setWidgetValue(hrSamplerNode, "sampler_name", getWidget(samplerNode, "sampler_name").value);
|
hrSteps ? +hrSteps : getWidget(samplerNode, "steps").value
|
||||||
setWidgetValue(hrSamplerNode, "denoise", +(popOpt("denoising strength") || "1"));
|
);
|
||||||
|
setWidgetValue(
|
||||||
|
hrSamplerNode,
|
||||||
|
"cfg",
|
||||||
|
getWidget(samplerNode, "cfg").value
|
||||||
|
);
|
||||||
|
setWidgetValue(
|
||||||
|
hrSamplerNode,
|
||||||
|
"scheduler",
|
||||||
|
getWidget(samplerNode, "scheduler").value
|
||||||
|
);
|
||||||
|
setWidgetValue(
|
||||||
|
hrSamplerNode,
|
||||||
|
"sampler_name",
|
||||||
|
getWidget(samplerNode, "sampler_name").value
|
||||||
|
);
|
||||||
|
setWidgetValue(
|
||||||
|
hrSamplerNode,
|
||||||
|
"denoise",
|
||||||
|
+(popOpt("denoising strength") || "1")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let n = createLoraNodes(positiveNode, positive, { node: clipSkipNode, index: 0 }, { node: ckptNode, index: 0 });
|
let n = createLoraNodes(
|
||||||
|
positiveNode,
|
||||||
|
positive,
|
||||||
|
{ node: clipSkipNode, index: 0 },
|
||||||
|
{ node: ckptNode, index: 0 }
|
||||||
|
);
|
||||||
positive = n.text;
|
positive = n.text;
|
||||||
n = createLoraNodes(negativeNode, negative, n.prevClip, n.prevModel);
|
n = createLoraNodes(negativeNode, negative, n.prevClip, n.prevModel);
|
||||||
negative = n.text;
|
negative = n.text;
|
||||||
@@ -494,7 +569,15 @@ export async function importA1111(graph, parameters) {
|
|||||||
|
|
||||||
graph.arrange();
|
graph.arrange();
|
||||||
|
|
||||||
for (const opt of ["model hash", "ensd", "version", "vae hash", "ti hashes", "lora hashes", "hashes"]) {
|
for (const opt of [
|
||||||
|
"model hash",
|
||||||
|
"ensd",
|
||||||
|
"version",
|
||||||
|
"vae hash",
|
||||||
|
"ti hashes",
|
||||||
|
"lora hashes",
|
||||||
|
"hashes",
|
||||||
|
]) {
|
||||||
delete opts[opt];
|
delete opts[opt];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,67 +8,77 @@ import { TaskItem } from "/types/apiTypes";
|
|||||||
export const ComfyDialog = _ComfyDialog;
|
export const ComfyDialog = _ComfyDialog;
|
||||||
|
|
||||||
type Position2D = {
|
type Position2D = {
|
||||||
x: number,
|
x: number;
|
||||||
y: number
|
y: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
parent?: HTMLElement,
|
parent?: HTMLElement;
|
||||||
$?: (el: HTMLElement) => void,
|
$?: (el: HTMLElement) => void;
|
||||||
dataset?: DOMStringMap,
|
dataset?: DOMStringMap;
|
||||||
style?: Partial<CSSStyleDeclaration>,
|
style?: Partial<CSSStyleDeclaration>;
|
||||||
for?: string,
|
for?: string;
|
||||||
textContent?: string,
|
textContent?: string;
|
||||||
[key: string]: any
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Children = Element[] | Element | string | string[];
|
type Children = Element[] | Element | string | string[];
|
||||||
|
|
||||||
export function $el(tag: string, propsOrChildren?: Children | Props, children?: Children): HTMLElement {
|
export function $el(
|
||||||
const split = tag.split(".");
|
tag: string,
|
||||||
const element = document.createElement(split.shift() as string);
|
propsOrChildren?: Children | Props,
|
||||||
if (split.length > 0) {
|
children?: Children
|
||||||
element.classList.add(...split);
|
): HTMLElement {
|
||||||
|
const split = tag.split(".");
|
||||||
|
const element = document.createElement(split.shift() as string);
|
||||||
|
if (split.length > 0) {
|
||||||
|
element.classList.add(...split);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (propsOrChildren) {
|
||||||
|
if (typeof propsOrChildren === "string") {
|
||||||
|
propsOrChildren = { textContent: propsOrChildren };
|
||||||
|
} else if (propsOrChildren instanceof Element) {
|
||||||
|
propsOrChildren = [propsOrChildren];
|
||||||
}
|
}
|
||||||
|
if (Array.isArray(propsOrChildren)) {
|
||||||
|
element.append(...propsOrChildren);
|
||||||
|
} else {
|
||||||
|
const {
|
||||||
|
parent,
|
||||||
|
$: cb,
|
||||||
|
dataset,
|
||||||
|
style,
|
||||||
|
...rest
|
||||||
|
} = propsOrChildren as Props;
|
||||||
|
|
||||||
if (propsOrChildren) {
|
if (rest.for) {
|
||||||
if (typeof propsOrChildren === "string") {
|
element.setAttribute("for", rest.for);
|
||||||
propsOrChildren = { textContent: propsOrChildren };
|
}
|
||||||
} else if (propsOrChildren instanceof Element) {
|
|
||||||
propsOrChildren = [propsOrChildren];
|
|
||||||
}
|
|
||||||
if (Array.isArray(propsOrChildren)) {
|
|
||||||
element.append(...propsOrChildren);
|
|
||||||
} else {
|
|
||||||
const { parent, $: cb, dataset, style, ...rest } = propsOrChildren as Props;
|
|
||||||
|
|
||||||
if (rest.for) {
|
if (style) {
|
||||||
element.setAttribute("for", rest.for)
|
Object.assign(element.style, style);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (style) {
|
if (dataset) {
|
||||||
Object.assign(element.style, style);
|
Object.assign(element.dataset, dataset);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dataset) {
|
Object.assign(element, rest);
|
||||||
Object.assign(element.dataset, dataset);
|
if (children) {
|
||||||
}
|
element.append(...(Array.isArray(children) ? children : [children]));
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(element, rest);
|
if (parent) {
|
||||||
if (children) {
|
parent.append(element);
|
||||||
element.append(...(Array.isArray(children) ? children : [children]));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (parent) {
|
if (cb) {
|
||||||
parent.append(element);
|
cb(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cb) {
|
|
||||||
cb(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return element;
|
}
|
||||||
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dragElement(dragEl, settings) {
|
function dragElement(dragEl, settings) {
|
||||||
@@ -93,12 +103,17 @@ function dragElement(dragEl, settings) {
|
|||||||
|
|
||||||
function ensureInBounds() {
|
function ensureInBounds() {
|
||||||
try {
|
try {
|
||||||
newPosX = Math.min(document.body.clientWidth - dragEl.clientWidth, Math.max(0, dragEl.offsetLeft));
|
newPosX = Math.min(
|
||||||
newPosY = Math.min(document.body.clientHeight - dragEl.clientHeight, Math.max(0, dragEl.offsetTop));
|
document.body.clientWidth - dragEl.clientWidth,
|
||||||
|
Math.max(0, dragEl.offsetLeft)
|
||||||
|
);
|
||||||
|
newPosY = Math.min(
|
||||||
|
document.body.clientHeight - dragEl.clientHeight,
|
||||||
|
Math.max(0, dragEl.offsetTop)
|
||||||
|
);
|
||||||
|
|
||||||
positionElement();
|
positionElement();
|
||||||
}
|
} catch (exception) {
|
||||||
catch (exception) {
|
|
||||||
// robust
|
// robust
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,7 +127,8 @@ function dragElement(dragEl, settings) {
|
|||||||
// set the element's new position:
|
// set the element's new position:
|
||||||
if (anchorRight) {
|
if (anchorRight) {
|
||||||
dragEl.style.left = "unset";
|
dragEl.style.left = "unset";
|
||||||
dragEl.style.right = document.body.clientWidth - newPosX - dragEl.clientWidth + "px";
|
dragEl.style.right =
|
||||||
|
document.body.clientWidth - newPosX - dragEl.clientWidth + "px";
|
||||||
} else {
|
} else {
|
||||||
dragEl.style.left = newPosX + "px";
|
dragEl.style.left = newPosX + "px";
|
||||||
dragEl.style.right = "unset";
|
dragEl.style.right = "unset";
|
||||||
@@ -180,8 +196,14 @@ function dragElement(dragEl, settings) {
|
|||||||
posStartX = e.clientX;
|
posStartX = e.clientX;
|
||||||
posStartY = e.clientY;
|
posStartY = e.clientY;
|
||||||
|
|
||||||
newPosX = Math.min(document.body.clientWidth - dragEl.clientWidth, Math.max(0, dragEl.offsetLeft + posDiffX));
|
newPosX = Math.min(
|
||||||
newPosY = Math.min(document.body.clientHeight - dragEl.clientHeight, Math.max(0, dragEl.offsetTop + posDiffY));
|
document.body.clientWidth - dragEl.clientWidth,
|
||||||
|
Math.max(0, dragEl.offsetLeft + posDiffX)
|
||||||
|
);
|
||||||
|
newPosY = Math.min(
|
||||||
|
document.body.clientHeight - dragEl.clientHeight,
|
||||||
|
Math.max(0, dragEl.offsetTop + posDiffY)
|
||||||
|
);
|
||||||
|
|
||||||
positionElement();
|
positionElement();
|
||||||
}
|
}
|
||||||
@@ -226,31 +248,40 @@ class ComfyList {
|
|||||||
textContent: section,
|
textContent: section,
|
||||||
}),
|
}),
|
||||||
$el("div.comfy-list-items", [
|
$el("div.comfy-list-items", [
|
||||||
...(this.#reverse ? items[section].reverse() : items[section]).map((item: TaskItem) => {
|
...(this.#reverse ? items[section].reverse() : items[section]).map(
|
||||||
// Allow items to specify a custom remove action (e.g. for interrupt current prompt)
|
(item: TaskItem) => {
|
||||||
const removeAction = "remove" in item ? item.remove : {
|
// Allow items to specify a custom remove action (e.g. for interrupt current prompt)
|
||||||
name: "Delete",
|
const removeAction =
|
||||||
cb: () => api.deleteItem(this.#type, item.prompt[1]),
|
"remove" in item
|
||||||
};
|
? item.remove
|
||||||
return $el("div", { textContent: item.prompt[0] + ": " }, [
|
: {
|
||||||
$el("button", {
|
name: "Delete",
|
||||||
textContent: "Load",
|
cb: () => api.deleteItem(this.#type, item.prompt[1]),
|
||||||
onclick: async () => {
|
};
|
||||||
await app.loadGraphData(item.prompt[3].extra_pnginfo.workflow, true, false);
|
return $el("div", { textContent: item.prompt[0] + ": " }, [
|
||||||
if ("outputs" in item) {
|
$el("button", {
|
||||||
app.nodeOutputs = item.outputs;
|
textContent: "Load",
|
||||||
}
|
onclick: async () => {
|
||||||
},
|
await app.loadGraphData(
|
||||||
}),
|
item.prompt[3].extra_pnginfo.workflow,
|
||||||
$el("button", {
|
true,
|
||||||
textContent: removeAction.name,
|
false
|
||||||
onclick: async () => {
|
);
|
||||||
await removeAction.cb();
|
if ("outputs" in item) {
|
||||||
await this.update();
|
app.nodeOutputs = item.outputs;
|
||||||
},
|
}
|
||||||
}),
|
},
|
||||||
]);
|
}),
|
||||||
}),
|
$el("button", {
|
||||||
|
textContent: removeAction.name,
|
||||||
|
onclick: async () => {
|
||||||
|
await removeAction.cb();
|
||||||
|
await this.update();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
$el("div.comfy-list-actions", [
|
$el("div.comfy-list-actions", [
|
||||||
@@ -400,8 +431,15 @@ export class ComfyUI {
|
|||||||
const autoQueueModeEl = toggleSwitch(
|
const autoQueueModeEl = toggleSwitch(
|
||||||
"autoQueueMode",
|
"autoQueueMode",
|
||||||
[
|
[
|
||||||
{ text: "instant", tooltip: "A new prompt will be queued as soon as the queue reaches 0" },
|
{
|
||||||
{ text: "change", tooltip: "A new prompt will be queued when the queue is at 0 and the graph is/has changed" },
|
text: "instant",
|
||||||
|
tooltip: "A new prompt will be queued as soon as the queue reaches 0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "change",
|
||||||
|
tooltip:
|
||||||
|
"A new prompt will be queued when the queue is at 0 and the graph is/has changed",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
onChange: (value) => {
|
onChange: (value) => {
|
||||||
@@ -435,30 +473,34 @@ export class ComfyUI {
|
|||||||
) as HTMLDivElement;
|
) as HTMLDivElement;
|
||||||
|
|
||||||
this.menuContainer = $el("div.comfy-menu", { parent: document.body }, [
|
this.menuContainer = $el("div.comfy-menu", { parent: document.body }, [
|
||||||
$el("div.drag-handle.comfy-menu-header", {
|
$el(
|
||||||
style: {
|
"div.drag-handle.comfy-menu-header",
|
||||||
overflow: "hidden",
|
{
|
||||||
position: "relative",
|
style: {
|
||||||
width: "100%",
|
overflow: "hidden",
|
||||||
cursor: "default"
|
position: "relative",
|
||||||
}
|
width: "100%",
|
||||||
}, [
|
cursor: "default",
|
||||||
$el("span.drag-handle"),
|
},
|
||||||
$el("span.comfy-menu-queue-size", { $: (q) => (this.queueSize = q) }),
|
},
|
||||||
$el("div.comfy-menu-actions", [
|
[
|
||||||
$el("button.comfy-settings-btn", {
|
$el("span.drag-handle"),
|
||||||
textContent: "⚙️",
|
$el("span.comfy-menu-queue-size", { $: (q) => (this.queueSize = q) }),
|
||||||
onclick: () => this.settings.show(),
|
$el("div.comfy-menu-actions", [
|
||||||
}),
|
$el("button.comfy-settings-btn", {
|
||||||
$el("button.comfy-close-menu-btn", {
|
textContent: "⚙️",
|
||||||
textContent: "\u00d7",
|
onclick: () => this.settings.show(),
|
||||||
onclick: () => {
|
}),
|
||||||
this.menuContainer.style.display = "none";
|
$el("button.comfy-close-menu-btn", {
|
||||||
this.menuHamburger.style.display = "flex";
|
textContent: "\u00d7",
|
||||||
},
|
onclick: () => {
|
||||||
}),
|
this.menuContainer.style.display = "none";
|
||||||
]),
|
this.menuHamburger.style.display = "flex";
|
||||||
]),
|
},
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
),
|
||||||
$el("button.comfy-queue-btn", {
|
$el("button.comfy-queue-btn", {
|
||||||
id: "queue-button",
|
id: "queue-button",
|
||||||
textContent: "Queue Prompt",
|
textContent: "Queue Prompt",
|
||||||
@@ -469,70 +511,95 @@ export class ComfyUI {
|
|||||||
$el("input", {
|
$el("input", {
|
||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
onchange: (i) => {
|
onchange: (i) => {
|
||||||
document.getElementById("extraOptions").style.display = i.srcElement.checked ? "block" : "none";
|
document.getElementById("extraOptions").style.display = i
|
||||||
this.batchCount = i.srcElement.checked ?
|
.srcElement.checked
|
||||||
Number.parseInt((document.getElementById("batchCountInputRange") as HTMLInputElement).value) : 1;
|
? "block"
|
||||||
(document.getElementById("autoQueueCheckbox") as HTMLInputElement).checked = false;
|
: "none";
|
||||||
|
this.batchCount = i.srcElement.checked
|
||||||
|
? Number.parseInt(
|
||||||
|
(
|
||||||
|
document.getElementById(
|
||||||
|
"batchCountInputRange"
|
||||||
|
) as HTMLInputElement
|
||||||
|
).value
|
||||||
|
)
|
||||||
|
: 1;
|
||||||
|
(
|
||||||
|
document.getElementById("autoQueueCheckbox") as HTMLInputElement
|
||||||
|
).checked = false;
|
||||||
this.autoQueueEnabled = false;
|
this.autoQueueEnabled = false;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
$el("div", { id: "extraOptions", style: { width: "100%", display: "none" } }, [
|
$el(
|
||||||
$el("div", [
|
"div",
|
||||||
|
{ id: "extraOptions", style: { width: "100%", display: "none" } },
|
||||||
$el("label", { innerHTML: "Batch count" }),
|
[
|
||||||
$el("input", {
|
$el("div", [
|
||||||
id: "batchCountInputNumber",
|
$el("label", { innerHTML: "Batch count" }),
|
||||||
type: "number",
|
$el("input", {
|
||||||
value: this.batchCount,
|
id: "batchCountInputNumber",
|
||||||
min: "1",
|
type: "number",
|
||||||
style: { width: "35%", "marginLeft": "0.4em" },
|
value: this.batchCount,
|
||||||
oninput: (i) => {
|
min: "1",
|
||||||
this.batchCount = i.target.value;
|
style: { width: "35%", marginLeft: "0.4em" },
|
||||||
/* Even though an <input> element with a type of range logically represents a number (since
|
oninput: (i) => {
|
||||||
|
this.batchCount = i.target.value;
|
||||||
|
/* Even though an <input> element with a type of range logically represents a number (since
|
||||||
it's used for numeric input), the value it holds is still treated as a string in HTML and
|
it's used for numeric input), the value it holds is still treated as a string in HTML and
|
||||||
JavaScript. This behavior is consistent across all <input> elements regardless of their type
|
JavaScript. This behavior is consistent across all <input> elements regardless of their type
|
||||||
(like text, number, or range), where the .value property is always a string. */
|
(like text, number, or range), where the .value property is always a string. */
|
||||||
(document.getElementById("batchCountInputRange") as HTMLInputElement).value = this.batchCount.toString();
|
(
|
||||||
},
|
document.getElementById(
|
||||||
}),
|
"batchCountInputRange"
|
||||||
$el("input", {
|
) as HTMLInputElement
|
||||||
id: "batchCountInputRange",
|
).value = this.batchCount.toString();
|
||||||
type: "range",
|
},
|
||||||
min: "1",
|
}),
|
||||||
max: "100",
|
$el("input", {
|
||||||
value: this.batchCount,
|
id: "batchCountInputRange",
|
||||||
oninput: (i) => {
|
type: "range",
|
||||||
this.batchCount = i.srcElement.value;
|
min: "1",
|
||||||
// Note
|
max: "100",
|
||||||
(document.getElementById("batchCountInputNumber") as HTMLInputElement).value = i.srcElement.value;
|
value: this.batchCount,
|
||||||
},
|
oninput: (i) => {
|
||||||
}),
|
this.batchCount = i.srcElement.value;
|
||||||
]),
|
// Note
|
||||||
$el("div", [
|
(
|
||||||
$el("label", {
|
document.getElementById(
|
||||||
for: "autoQueueCheckbox",
|
"batchCountInputNumber"
|
||||||
innerHTML: "Auto Queue"
|
) as HTMLInputElement
|
||||||
}),
|
).value = i.srcElement.value;
|
||||||
$el("input", {
|
},
|
||||||
id: "autoQueueCheckbox",
|
}),
|
||||||
type: "checkbox",
|
]),
|
||||||
checked: false,
|
$el("div", [
|
||||||
title: "Automatically queue prompt when the queue size hits 0",
|
$el("label", {
|
||||||
onchange: (e) => {
|
for: "autoQueueCheckbox",
|
||||||
this.autoQueueEnabled = e.target.checked;
|
innerHTML: "Auto Queue",
|
||||||
autoQueueModeEl.style.display = this.autoQueueEnabled ? "" : "none";
|
}),
|
||||||
}
|
$el("input", {
|
||||||
}),
|
id: "autoQueueCheckbox",
|
||||||
autoQueueModeEl
|
type: "checkbox",
|
||||||
])
|
checked: false,
|
||||||
]),
|
title: "Automatically queue prompt when the queue size hits 0",
|
||||||
|
onchange: (e) => {
|
||||||
|
this.autoQueueEnabled = e.target.checked;
|
||||||
|
autoQueueModeEl.style.display = this.autoQueueEnabled
|
||||||
|
? ""
|
||||||
|
: "none";
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
autoQueueModeEl,
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
),
|
||||||
$el("div.comfy-menu-btns", [
|
$el("div.comfy-menu-btns", [
|
||||||
$el("button", {
|
$el("button", {
|
||||||
id: "queue-front-button",
|
id: "queue-front-button",
|
||||||
textContent: "Queue Front",
|
textContent: "Queue Front",
|
||||||
onclick: () => app.queuePrompt(-1, this.batchCount)
|
onclick: () => app.queuePrompt(-1, this.batchCount),
|
||||||
}),
|
}),
|
||||||
$el("button", {
|
$el("button", {
|
||||||
$: (b) => (this.queue.button = b as HTMLButtonElement),
|
$: (b) => (this.queue.button = b as HTMLButtonElement),
|
||||||
@@ -567,7 +634,7 @@ export class ComfyUI {
|
|||||||
filename += ".json";
|
filename += ".json";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app.graphToPrompt().then(p => {
|
app.graphToPrompt().then((p) => {
|
||||||
const json = JSON.stringify(p.workflow, null, 2); // convert the data to a JSON string
|
const json = JSON.stringify(p.workflow, null, 2); // convert the data to a JSON string
|
||||||
const blob = new Blob([json], { type: "application/json" });
|
const blob = new Blob([json], { type: "application/json" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -598,7 +665,7 @@ export class ComfyUI {
|
|||||||
filename += ".json";
|
filename += ".json";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app.graphToPrompt().then(p => {
|
app.graphToPrompt().then((p) => {
|
||||||
const json = JSON.stringify(p.output, null, 2); // convert the data to a JSON string
|
const json = JSON.stringify(p.output, null, 2); // convert the data to a JSON string
|
||||||
const blob = new Blob([json], { type: "application/json" });
|
const blob = new Blob([json], { type: "application/json" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -616,34 +683,48 @@ export class ComfyUI {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
$el("button", { id: "comfy-load-button", textContent: "Load", onclick: () => fileInput.click() }),
|
$el("button", {
|
||||||
|
id: "comfy-load-button",
|
||||||
|
textContent: "Load",
|
||||||
|
onclick: () => fileInput.click(),
|
||||||
|
}),
|
||||||
$el("button", {
|
$el("button", {
|
||||||
id: "comfy-refresh-button",
|
id: "comfy-refresh-button",
|
||||||
textContent: "Refresh",
|
textContent: "Refresh",
|
||||||
onclick: () => app.refreshComboInNodes()
|
onclick: () => app.refreshComboInNodes(),
|
||||||
}),
|
}),
|
||||||
$el("button", { id: "comfy-clipspace-button", textContent: "Clipspace", onclick: () => app.openClipspace() }),
|
|
||||||
$el("button", {
|
$el("button", {
|
||||||
id: "comfy-clear-button", textContent: "Clear", onclick: () => {
|
id: "comfy-clipspace-button",
|
||||||
|
textContent: "Clipspace",
|
||||||
|
onclick: () => app.openClipspace(),
|
||||||
|
}),
|
||||||
|
$el("button", {
|
||||||
|
id: "comfy-clear-button",
|
||||||
|
textContent: "Clear",
|
||||||
|
onclick: () => {
|
||||||
if (!confirmClear.value || confirm("Clear workflow?")) {
|
if (!confirmClear.value || confirm("Clear workflow?")) {
|
||||||
app.clean();
|
app.clean();
|
||||||
app.graph.clear();
|
app.graph.clear();
|
||||||
app.resetView();
|
app.resetView();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}),
|
}),
|
||||||
$el("button", {
|
$el("button", {
|
||||||
id: "comfy-load-default-button", textContent: "Load Default", onclick: async () => {
|
id: "comfy-load-default-button",
|
||||||
|
textContent: "Load Default",
|
||||||
|
onclick: async () => {
|
||||||
if (!confirmClear.value || confirm("Load default workflow?")) {
|
if (!confirmClear.value || confirm("Load default workflow?")) {
|
||||||
app.resetView();
|
app.resetView();
|
||||||
await app.loadGraphData()
|
await app.loadGraphData();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}),
|
}),
|
||||||
$el("button", {
|
$el("button", {
|
||||||
id: "comfy-reset-view-button", textContent: "Reset View", onclick: async () => {
|
id: "comfy-reset-view-button",
|
||||||
|
textContent: "Reset View",
|
||||||
|
onclick: async () => {
|
||||||
app.resetView();
|
app.resetView();
|
||||||
}
|
},
|
||||||
}),
|
}),
|
||||||
]) as HTMLDivElement;
|
]) as HTMLDivElement;
|
||||||
|
|
||||||
@@ -652,7 +733,10 @@ export class ComfyUI {
|
|||||||
name: "Enable Dev mode Options",
|
name: "Enable Dev mode Options",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
onChange: function (value) { document.getElementById("comfy-dev-save-api-button").style.display = value ? "flex" : "none" },
|
onChange: function (value) {
|
||||||
|
document.getElementById("comfy-dev-save-api-button").style.display =
|
||||||
|
value ? "flex" : "none";
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -662,7 +746,8 @@ export class ComfyUI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setStatus(status) {
|
setStatus(status) {
|
||||||
this.queueSize.textContent = "Queue size: " + (status ? status.exec_info.queue_remaining : "ERR");
|
this.queueSize.textContent =
|
||||||
|
"Queue size: " + (status ? status.exec_info.queue_remaining : "ERR");
|
||||||
if (status) {
|
if (status) {
|
||||||
if (
|
if (
|
||||||
this.lastQueueSize != 0 &&
|
this.lastQueueSize != 0 &&
|
||||||
|
|||||||
@@ -2,63 +2,63 @@ import { ComfyDialog } from "../dialog";
|
|||||||
import { $el } from "../../ui";
|
import { $el } from "../../ui";
|
||||||
|
|
||||||
export class ComfyAsyncDialog extends ComfyDialog {
|
export class ComfyAsyncDialog extends ComfyDialog {
|
||||||
#resolve;
|
#resolve;
|
||||||
|
|
||||||
constructor(actions) {
|
constructor(actions) {
|
||||||
super(
|
super(
|
||||||
"dialog.comfy-dialog.comfyui-dialog",
|
"dialog.comfy-dialog.comfyui-dialog",
|
||||||
actions?.map((opt) => {
|
actions?.map((opt) => {
|
||||||
if (typeof opt === "string") {
|
if (typeof opt === "string") {
|
||||||
opt = { text: opt };
|
opt = { text: opt };
|
||||||
}
|
}
|
||||||
return $el("button.comfyui-button", {
|
return $el("button.comfyui-button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
textContent: opt.text,
|
textContent: opt.text,
|
||||||
onclick: () => this.close(opt.value ?? opt.text),
|
onclick: () => this.close(opt.value ?? opt.text),
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
show(html) {
|
show(html) {
|
||||||
this.element.addEventListener("close", () => {
|
this.element.addEventListener("close", () => {
|
||||||
this.close();
|
this.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
super.show(html);
|
super.show(html);
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.#resolve = resolve;
|
this.#resolve = resolve;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
showModal(html) {
|
showModal(html) {
|
||||||
this.element.addEventListener("close", () => {
|
this.element.addEventListener("close", () => {
|
||||||
this.close();
|
this.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
super.show(html);
|
super.show(html);
|
||||||
this.element.showModal();
|
this.element.showModal();
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.#resolve = resolve;
|
this.#resolve = resolve;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
close(result = null) {
|
close(result = null) {
|
||||||
this.#resolve(result);
|
this.#resolve(result);
|
||||||
this.element.close();
|
this.element.close();
|
||||||
super.close();
|
super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
static async prompt({ title = null, message, actions }) {
|
static async prompt({ title = null, message, actions }) {
|
||||||
const dialog = new ComfyAsyncDialog(actions);
|
const dialog = new ComfyAsyncDialog(actions);
|
||||||
const content = [$el("span", message)];
|
const content = [$el("span", message)];
|
||||||
if (title) {
|
if (title) {
|
||||||
content.unshift($el("h3", title));
|
content.unshift($el("h3", title));
|
||||||
}
|
}
|
||||||
const res = await dialog.showModal(content);
|
const res = await dialog.showModal(content);
|
||||||
dialog.element.remove();
|
dialog.element.remove();
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,145 +19,159 @@ import { prop } from "../../utils";
|
|||||||
* }} ComfyButtonProps
|
* }} ComfyButtonProps
|
||||||
*/
|
*/
|
||||||
export class ComfyButton {
|
export class ComfyButton {
|
||||||
#over = 0;
|
#over = 0;
|
||||||
#popupOpen = false;
|
#popupOpen = false;
|
||||||
isOver = false;
|
isOver = false;
|
||||||
iconElement = $el("i.mdi");
|
iconElement = $el("i.mdi");
|
||||||
contentElement = $el("span");
|
contentElement = $el("span");
|
||||||
/**
|
/**
|
||||||
* @type {import("./popup").ComfyPopup}
|
* @type {import("./popup").ComfyPopup}
|
||||||
*/
|
*/
|
||||||
popup;
|
popup;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ComfyButtonProps} opts
|
* @param {ComfyButtonProps} opts
|
||||||
*/
|
*/
|
||||||
constructor({
|
constructor({
|
||||||
icon,
|
icon,
|
||||||
overIcon,
|
overIcon,
|
||||||
iconSize,
|
iconSize,
|
||||||
content,
|
content,
|
||||||
tooltip,
|
tooltip,
|
||||||
action,
|
action,
|
||||||
classList = "comfyui-button",
|
classList = "comfyui-button",
|
||||||
visibilitySetting,
|
visibilitySetting,
|
||||||
app,
|
app,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
}) {
|
}) {
|
||||||
this.element = $el("button", {
|
this.element = $el(
|
||||||
onmouseenter: () => {
|
"button",
|
||||||
this.isOver = true;
|
{
|
||||||
if(this.overIcon) {
|
onmouseenter: () => {
|
||||||
this.updateIcon();
|
this.isOver = true;
|
||||||
}
|
if (this.overIcon) {
|
||||||
},
|
this.updateIcon();
|
||||||
onmouseleave: () => {
|
}
|
||||||
this.isOver = false;
|
},
|
||||||
if(this.overIcon) {
|
onmouseleave: () => {
|
||||||
this.updateIcon();
|
this.isOver = false;
|
||||||
}
|
if (this.overIcon) {
|
||||||
}
|
this.updateIcon();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[this.iconElement, this.contentElement]
|
||||||
|
);
|
||||||
|
|
||||||
}, [this.iconElement, this.contentElement]);
|
this.icon = prop(
|
||||||
|
this,
|
||||||
|
"icon",
|
||||||
|
icon,
|
||||||
|
toggleElement(this.iconElement, { onShow: this.updateIcon })
|
||||||
|
);
|
||||||
|
this.overIcon = prop(this, "overIcon", overIcon, () => {
|
||||||
|
if (this.isOver) {
|
||||||
|
this.updateIcon();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.iconSize = prop(this, "iconSize", iconSize, this.updateIcon);
|
||||||
|
this.content = prop(
|
||||||
|
this,
|
||||||
|
"content",
|
||||||
|
content,
|
||||||
|
toggleElement(this.contentElement, {
|
||||||
|
onShow: (el, v) => {
|
||||||
|
if (typeof v === "string") {
|
||||||
|
el.textContent = v;
|
||||||
|
} else {
|
||||||
|
el.replaceChildren(v);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
this.icon = prop(this, "icon", icon, toggleElement(this.iconElement, { onShow: this.updateIcon }));
|
this.tooltip = prop(this, "tooltip", tooltip, (v) => {
|
||||||
this.overIcon = prop(this, "overIcon", overIcon, () => {
|
if (v) {
|
||||||
if(this.isOver) {
|
this.element.title = v;
|
||||||
this.updateIcon();
|
} else {
|
||||||
}
|
this.element.removeAttribute("title");
|
||||||
});
|
}
|
||||||
this.iconSize = prop(this, "iconSize", iconSize, this.updateIcon);
|
});
|
||||||
this.content = prop(
|
this.classList = prop(this, "classList", classList, this.updateClasses);
|
||||||
this,
|
this.hidden = prop(this, "hidden", false, this.updateClasses);
|
||||||
"content",
|
this.enabled = prop(this, "enabled", enabled, () => {
|
||||||
content,
|
this.updateClasses();
|
||||||
toggleElement(this.contentElement, {
|
this.element.disabled = !this.enabled;
|
||||||
onShow: (el, v) => {
|
});
|
||||||
if (typeof v === "string") {
|
this.action = prop(this, "action", action);
|
||||||
el.textContent = v;
|
this.element.addEventListener("click", (e) => {
|
||||||
} else {
|
if (this.popup) {
|
||||||
el.replaceChildren(v);
|
// we are either a touch device or triggered by click not hover
|
||||||
}
|
if (!this.#over) {
|
||||||
},
|
this.popup.toggle();
|
||||||
})
|
}
|
||||||
);
|
}
|
||||||
|
this.action?.(e, this);
|
||||||
|
});
|
||||||
|
|
||||||
this.tooltip = prop(this, "tooltip", tooltip, (v) => {
|
if (visibilitySetting?.id) {
|
||||||
if (v) {
|
const settingUpdated = () => {
|
||||||
this.element.title = v;
|
this.hidden =
|
||||||
} else {
|
app.ui.settings.getSettingValue(visibilitySetting.id) !==
|
||||||
this.element.removeAttribute("title");
|
visibilitySetting.showValue;
|
||||||
}
|
};
|
||||||
});
|
app.ui.settings.addEventListener(
|
||||||
this.classList = prop(this, "classList", classList, this.updateClasses);
|
visibilitySetting.id + ".change",
|
||||||
this.hidden = prop(this, "hidden", false, this.updateClasses);
|
settingUpdated
|
||||||
this.enabled = prop(this, "enabled", enabled, () => {
|
);
|
||||||
this.updateClasses();
|
settingUpdated();
|
||||||
this.element.disabled = !this.enabled;
|
}
|
||||||
});
|
}
|
||||||
this.action = prop(this, "action", action);
|
|
||||||
this.element.addEventListener("click", (e) => {
|
|
||||||
if (this.popup) {
|
|
||||||
// we are either a touch device or triggered by click not hover
|
|
||||||
if (!this.#over) {
|
|
||||||
this.popup.toggle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.action?.(e, this);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (visibilitySetting?.id) {
|
updateIcon = () =>
|
||||||
const settingUpdated = () => {
|
(this.iconElement.className = `mdi mdi-${(this.isOver && this.overIcon) || this.icon}${this.iconSize ? " mdi-" + this.iconSize + "px" : ""}`);
|
||||||
this.hidden = app.ui.settings.getSettingValue(visibilitySetting.id) !== visibilitySetting.showValue;
|
updateClasses = () => {
|
||||||
};
|
const internalClasses = [];
|
||||||
app.ui.settings.addEventListener(visibilitySetting.id + ".change", settingUpdated);
|
if (this.hidden) {
|
||||||
settingUpdated();
|
internalClasses.push("hidden");
|
||||||
}
|
}
|
||||||
}
|
if (!this.enabled) {
|
||||||
|
internalClasses.push("disabled");
|
||||||
|
}
|
||||||
|
if (this.popup) {
|
||||||
|
if (this.#popupOpen) {
|
||||||
|
internalClasses.push("popup-open");
|
||||||
|
} else {
|
||||||
|
internalClasses.push("popup-closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
applyClasses(this.element, this.classList, ...internalClasses);
|
||||||
|
};
|
||||||
|
|
||||||
updateIcon = () => (this.iconElement.className = `mdi mdi-${(this.isOver && this.overIcon) || this.icon}${this.iconSize ? " mdi-" + this.iconSize + "px" : ""}`);
|
/**
|
||||||
updateClasses = () => {
|
*
|
||||||
const internalClasses = [];
|
* @param { import("./popup").ComfyPopup } popup
|
||||||
if (this.hidden) {
|
* @param { "click" | "hover" } mode
|
||||||
internalClasses.push("hidden");
|
*/
|
||||||
}
|
withPopup(popup, mode = "click") {
|
||||||
if (!this.enabled) {
|
this.popup = popup;
|
||||||
internalClasses.push("disabled");
|
|
||||||
}
|
|
||||||
if (this.popup) {
|
|
||||||
if (this.#popupOpen) {
|
|
||||||
internalClasses.push("popup-open");
|
|
||||||
} else {
|
|
||||||
internalClasses.push("popup-closed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
applyClasses(this.element, this.classList, ...internalClasses);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
if (mode === "hover") {
|
||||||
*
|
for (const el of [this.element, this.popup.element]) {
|
||||||
* @param { import("./popup").ComfyPopup } popup
|
el.addEventListener("mouseenter", () => {
|
||||||
* @param { "click" | "hover" } mode
|
this.popup.open = !!++this.#over;
|
||||||
*/
|
});
|
||||||
withPopup(popup, mode = "click") {
|
el.addEventListener("mouseleave", () => {
|
||||||
this.popup = popup;
|
this.popup.open = !!--this.#over;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (mode === "hover") {
|
popup.addEventListener("change", () => {
|
||||||
for (const el of [this.element, this.popup.element]) {
|
this.#popupOpen = popup.open;
|
||||||
el.addEventListener("mouseenter", () => {
|
this.updateClasses();
|
||||||
this.popup.open = !!++this.#over;
|
});
|
||||||
});
|
|
||||||
el.addEventListener("mouseleave", () => {
|
|
||||||
this.popup.open = !!--this.#over;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
popup.addEventListener("change", () => {
|
return this;
|
||||||
this.#popupOpen = popup.open;
|
}
|
||||||
this.updateClasses();
|
|
||||||
});
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,41 +5,41 @@ import { ComfyButton } from "./button";
|
|||||||
import { prop } from "../../utils";
|
import { prop } from "../../utils";
|
||||||
|
|
||||||
export class ComfyButtonGroup {
|
export class ComfyButtonGroup {
|
||||||
element = $el("div.comfyui-button-group");
|
element = $el("div.comfyui-button-group");
|
||||||
|
|
||||||
/** @param {Array<ComfyButton | HTMLElement>} buttons */
|
/** @param {Array<ComfyButton | HTMLElement>} buttons */
|
||||||
constructor(...buttons) {
|
constructor(...buttons) {
|
||||||
this.buttons = prop(this, "buttons", buttons, () => this.update());
|
this.buttons = prop(this, "buttons", buttons, () => this.update());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ComfyButton} button
|
* @param {ComfyButton} button
|
||||||
* @param {number} index
|
* @param {number} index
|
||||||
*/
|
*/
|
||||||
insert(button, index) {
|
insert(button, index) {
|
||||||
this.buttons.splice(index, 0, button);
|
this.buttons.splice(index, 0, button);
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {ComfyButton} button */
|
/** @param {ComfyButton} button */
|
||||||
append(button) {
|
append(button) {
|
||||||
this.buttons.push(button);
|
this.buttons.push(button);
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {ComfyButton|number} indexOrButton */
|
/** @param {ComfyButton|number} indexOrButton */
|
||||||
remove(indexOrButton) {
|
remove(indexOrButton) {
|
||||||
if (typeof indexOrButton !== "number") {
|
if (typeof indexOrButton !== "number") {
|
||||||
indexOrButton = this.buttons.indexOf(indexOrButton);
|
indexOrButton = this.buttons.indexOf(indexOrButton);
|
||||||
}
|
}
|
||||||
if (indexOrButton > -1) {
|
if (indexOrButton > -1) {
|
||||||
const r = this.buttons.splice(indexOrButton, 1);
|
const r = this.buttons.splice(indexOrButton, 1);
|
||||||
this.update();
|
this.update();
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
this.element.replaceChildren(...this.buttons.map((b) => b["element"] ?? b));
|
this.element.replaceChildren(...this.buttons.map((b) => b["element"] ?? b));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,124 +5,133 @@ import { $el } from "../../ui";
|
|||||||
import { applyClasses } from "../utils";
|
import { applyClasses } from "../utils";
|
||||||
|
|
||||||
export class ComfyPopup extends EventTarget {
|
export class ComfyPopup extends EventTarget {
|
||||||
element = $el("div.comfyui-popup");
|
element = $el("div.comfyui-popup");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{
|
* @param {{
|
||||||
* target: HTMLElement,
|
* target: HTMLElement,
|
||||||
* container?: HTMLElement,
|
* container?: HTMLElement,
|
||||||
* classList?: import("../utils").ClassList,
|
* classList?: import("../utils").ClassList,
|
||||||
* ignoreTarget?: boolean,
|
* ignoreTarget?: boolean,
|
||||||
* closeOnEscape?: boolean,
|
* closeOnEscape?: boolean,
|
||||||
* position?: "absolute" | "relative",
|
* position?: "absolute" | "relative",
|
||||||
* horizontal?: "left" | "right"
|
* horizontal?: "left" | "right"
|
||||||
* }} param0
|
* }} param0
|
||||||
* @param {...HTMLElement} children
|
* @param {...HTMLElement} children
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
{
|
{
|
||||||
target,
|
target,
|
||||||
container = document.body,
|
container = document.body,
|
||||||
classList = "",
|
classList = "",
|
||||||
ignoreTarget = true,
|
ignoreTarget = true,
|
||||||
closeOnEscape = true,
|
closeOnEscape = true,
|
||||||
position = "absolute",
|
position = "absolute",
|
||||||
horizontal = "left",
|
horizontal = "left",
|
||||||
},
|
},
|
||||||
...children
|
...children
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.target = target;
|
this.target = target;
|
||||||
this.ignoreTarget = ignoreTarget;
|
this.ignoreTarget = ignoreTarget;
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.position = position;
|
this.position = position;
|
||||||
this.closeOnEscape = closeOnEscape;
|
this.closeOnEscape = closeOnEscape;
|
||||||
this.horizontal = horizontal;
|
this.horizontal = horizontal;
|
||||||
|
|
||||||
container.append(this.element);
|
container.append(this.element);
|
||||||
|
|
||||||
this.children = prop(this, "children", children, () => {
|
this.children = prop(this, "children", children, () => {
|
||||||
this.element.replaceChildren(...this.children);
|
this.element.replaceChildren(...this.children);
|
||||||
this.update();
|
this.update();
|
||||||
});
|
});
|
||||||
this.classList = prop(this, "classList", classList, () => applyClasses(this.element, this.classList, "comfyui-popup", horizontal));
|
this.classList = prop(this, "classList", classList, () =>
|
||||||
this.open = prop(this, "open", false, (v, o) => {
|
applyClasses(this.element, this.classList, "comfyui-popup", horizontal)
|
||||||
if (v === o) return;
|
);
|
||||||
if (v) {
|
this.open = prop(this, "open", false, (v, o) => {
|
||||||
this.#show();
|
if (v === o) return;
|
||||||
} else {
|
if (v) {
|
||||||
this.#hide();
|
this.#show();
|
||||||
}
|
} else {
|
||||||
});
|
this.#hide();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
toggle() {
|
toggle() {
|
||||||
this.open = !this.open;
|
this.open = !this.open;
|
||||||
}
|
}
|
||||||
|
|
||||||
#hide() {
|
#hide() {
|
||||||
this.element.classList.remove("open");
|
this.element.classList.remove("open");
|
||||||
window.removeEventListener("resize", this.update);
|
window.removeEventListener("resize", this.update);
|
||||||
window.removeEventListener("click", this.#clickHandler, { capture: true });
|
window.removeEventListener("click", this.#clickHandler, { capture: true });
|
||||||
window.removeEventListener("keydown", this.#escHandler, { capture: true });
|
window.removeEventListener("keydown", this.#escHandler, { capture: true });
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent("close"));
|
this.dispatchEvent(new CustomEvent("close"));
|
||||||
this.dispatchEvent(new CustomEvent("change"));
|
this.dispatchEvent(new CustomEvent("change"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#show() {
|
#show() {
|
||||||
this.element.classList.add("open");
|
this.element.classList.add("open");
|
||||||
this.update();
|
this.update();
|
||||||
|
|
||||||
window.addEventListener("resize", this.update);
|
window.addEventListener("resize", this.update);
|
||||||
window.addEventListener("click", this.#clickHandler, { capture: true });
|
window.addEventListener("click", this.#clickHandler, { capture: true });
|
||||||
if (this.closeOnEscape) {
|
if (this.closeOnEscape) {
|
||||||
window.addEventListener("keydown", this.#escHandler, { capture: true });
|
window.addEventListener("keydown", this.#escHandler, { capture: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent("open"));
|
this.dispatchEvent(new CustomEvent("open"));
|
||||||
this.dispatchEvent(new CustomEvent("change"));
|
this.dispatchEvent(new CustomEvent("change"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#escHandler = (e) => {
|
#escHandler = (e) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
this.open = false;
|
this.open = false;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#clickHandler = (e) => {
|
#clickHandler = (e) => {
|
||||||
/** @type {any} */
|
/** @type {any} */
|
||||||
const target = e.target;
|
const target = e.target;
|
||||||
if (!this.element.contains(target) && this.ignoreTarget && !this.target.contains(target)) {
|
if (
|
||||||
this.open = false;
|
!this.element.contains(target) &&
|
||||||
}
|
this.ignoreTarget &&
|
||||||
};
|
!this.target.contains(target)
|
||||||
|
) {
|
||||||
|
this.open = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
update = () => {
|
update = () => {
|
||||||
const rect = this.target.getBoundingClientRect();
|
const rect = this.target.getBoundingClientRect();
|
||||||
this.element.style.setProperty("--bottom", "unset");
|
this.element.style.setProperty("--bottom", "unset");
|
||||||
if (this.position === "absolute") {
|
if (this.position === "absolute") {
|
||||||
if (this.horizontal === "left") {
|
if (this.horizontal === "left") {
|
||||||
this.element.style.setProperty("--left", rect.left + "px");
|
this.element.style.setProperty("--left", rect.left + "px");
|
||||||
} else {
|
} else {
|
||||||
this.element.style.setProperty("--left", rect.right - this.element.clientWidth + "px");
|
this.element.style.setProperty(
|
||||||
}
|
"--left",
|
||||||
this.element.style.setProperty("--top", rect.bottom + "px");
|
rect.right - this.element.clientWidth + "px"
|
||||||
this.element.style.setProperty("--limit", rect.bottom + "px");
|
);
|
||||||
} else {
|
}
|
||||||
this.element.style.setProperty("--left", 0 + "px");
|
this.element.style.setProperty("--top", rect.bottom + "px");
|
||||||
this.element.style.setProperty("--top", rect.height + "px");
|
this.element.style.setProperty("--limit", rect.bottom + "px");
|
||||||
this.element.style.setProperty("--limit", rect.height + "px");
|
} else {
|
||||||
}
|
this.element.style.setProperty("--left", 0 + "px");
|
||||||
|
this.element.style.setProperty("--top", rect.height + "px");
|
||||||
|
this.element.style.setProperty("--limit", rect.height + "px");
|
||||||
|
}
|
||||||
|
|
||||||
const thisRect = this.element.getBoundingClientRect();
|
const thisRect = this.element.getBoundingClientRect();
|
||||||
if (thisRect.height < 30) {
|
if (thisRect.height < 30) {
|
||||||
// Move up instead
|
// Move up instead
|
||||||
this.element.style.setProperty("--top", "unset");
|
this.element.style.setProperty("--top", "unset");
|
||||||
this.element.style.setProperty("--bottom", rect.height + 5 + "px");
|
this.element.style.setProperty("--bottom", rect.height + 5 + "px");
|
||||||
this.element.style.setProperty("--limit", rect.height + 5 + "px");
|
this.element.style.setProperty("--limit", rect.height + 5 + "px");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,38 +6,47 @@ import { prop } from "../../utils";
|
|||||||
import { ComfyPopup } from "./popup";
|
import { ComfyPopup } from "./popup";
|
||||||
|
|
||||||
export class ComfySplitButton {
|
export class ComfySplitButton {
|
||||||
/**
|
/**
|
||||||
* @param {{
|
* @param {{
|
||||||
* primary: ComfyButton,
|
* primary: ComfyButton,
|
||||||
* mode?: "hover" | "click",
|
* mode?: "hover" | "click",
|
||||||
* horizontal?: "left" | "right",
|
* horizontal?: "left" | "right",
|
||||||
* position?: "relative" | "absolute"
|
* position?: "relative" | "absolute"
|
||||||
* }} param0
|
* }} param0
|
||||||
* @param {Array<ComfyButton> | Array<HTMLElement>} items
|
* @param {Array<ComfyButton> | Array<HTMLElement>} items
|
||||||
*/
|
*/
|
||||||
constructor({ primary, mode, horizontal = "left", position = "relative" }, ...items) {
|
constructor(
|
||||||
this.arrow = new ComfyButton({
|
{ primary, mode, horizontal = "left", position = "relative" },
|
||||||
icon: "chevron-down",
|
...items
|
||||||
});
|
) {
|
||||||
this.element = $el("div.comfyui-split-button" + (mode === "hover" ? ".hover" : ""), [
|
this.arrow = new ComfyButton({
|
||||||
$el("div.comfyui-split-primary", primary.element),
|
icon: "chevron-down",
|
||||||
$el("div.comfyui-split-arrow", this.arrow.element),
|
});
|
||||||
]);
|
this.element = $el(
|
||||||
this.popup = new ComfyPopup({
|
"div.comfyui-split-button" + (mode === "hover" ? ".hover" : ""),
|
||||||
target: this.element,
|
[
|
||||||
container: position === "relative" ? this.element : document.body,
|
$el("div.comfyui-split-primary", primary.element),
|
||||||
classList: "comfyui-split-button-popup" + (mode === "hover" ? " hover" : ""),
|
$el("div.comfyui-split-arrow", this.arrow.element),
|
||||||
closeOnEscape: mode === "click",
|
]
|
||||||
position,
|
);
|
||||||
horizontal,
|
this.popup = new ComfyPopup({
|
||||||
});
|
target: this.element,
|
||||||
|
container: position === "relative" ? this.element : document.body,
|
||||||
|
classList:
|
||||||
|
"comfyui-split-button-popup" + (mode === "hover" ? " hover" : ""),
|
||||||
|
closeOnEscape: mode === "click",
|
||||||
|
position,
|
||||||
|
horizontal,
|
||||||
|
});
|
||||||
|
|
||||||
this.arrow.withPopup(this.popup, mode);
|
this.arrow.withPopup(this.popup, mode);
|
||||||
|
|
||||||
this.items = prop(this, "items", items, () => this.update());
|
this.items = prop(this, "items", items, () => this.update());
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
this.popup.element.replaceChildren(...this.items.map((b) => b.element ?? b));
|
this.popup.element.replaceChildren(
|
||||||
}
|
...this.items.map((b) => b.element ?? b)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { $el } from "../ui";
|
import { $el } from "../ui";
|
||||||
|
|
||||||
export class ComfyDialog<T extends HTMLElement = HTMLElement> extends EventTarget {
|
export class ComfyDialog<
|
||||||
|
T extends HTMLElement = HTMLElement,
|
||||||
|
> extends EventTarget {
|
||||||
element: T;
|
element: T;
|
||||||
textElement: HTMLElement;
|
textElement: HTMLElement;
|
||||||
#buttons: HTMLButtonElement[] | null;
|
#buttons: HTMLButtonElement[] | null;
|
||||||
@@ -9,7 +11,10 @@ export class ComfyDialog<T extends HTMLElement = HTMLElement> extends EventTarge
|
|||||||
super();
|
super();
|
||||||
this.#buttons = buttons;
|
this.#buttons = buttons;
|
||||||
this.element = $el(type + ".comfy-modal", { parent: document.body }, [
|
this.element = $el(type + ".comfy-modal", { parent: document.body }, [
|
||||||
$el("div.comfy-modal-content", [$el("p", { $: (p) => (this.textElement = p) }), ...this.createButtons()]),
|
$el("div.comfy-modal-content", [
|
||||||
|
$el("p", { $: (p) => (this.textElement = p) }),
|
||||||
|
...this.createButtons(),
|
||||||
|
]),
|
||||||
]) as T;
|
]) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +38,9 @@ export class ComfyDialog<T extends HTMLElement = HTMLElement> extends EventTarge
|
|||||||
if (typeof html === "string") {
|
if (typeof html === "string") {
|
||||||
this.textElement.innerHTML = html;
|
this.textElement.innerHTML = html;
|
||||||
} else {
|
} else {
|
||||||
this.textElement.replaceChildren(...(html instanceof Array ? html : [html]));
|
this.textElement.replaceChildren(
|
||||||
|
...(html instanceof Array ? html : [html])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.element.style.display = "flex";
|
this.element.style.display = "flex";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,8 @@
|
|||||||
import { $el } from "../ui";
|
import { $el } from "../ui";
|
||||||
|
|
||||||
$el("style", {
|
$el("style", {
|
||||||
parent: document.head,
|
parent: document.head,
|
||||||
textContent: `
|
textContent: `
|
||||||
.draggable-item {
|
.draggable-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
@@ -40,7 +40,7 @@ $el("style", {
|
|||||||
.draggable-item.is-draggable {
|
.draggable-item.is-draggable {
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
`
|
`,
|
||||||
});
|
});
|
||||||
|
|
||||||
export class DraggableList extends EventTarget {
|
export class DraggableList extends EventTarget {
|
||||||
@@ -57,9 +57,9 @@ export class DraggableList extends EventTarget {
|
|||||||
offDrag = [];
|
offDrag = [];
|
||||||
|
|
||||||
constructor(element, itemSelector) {
|
constructor(element, itemSelector) {
|
||||||
super();
|
super();
|
||||||
this.listContainer = element;
|
this.listContainer = element;
|
||||||
this.itemSelector = itemSelector;
|
this.itemSelector = itemSelector;
|
||||||
|
|
||||||
if (!this.listContainer) return;
|
if (!this.listContainer) return;
|
||||||
|
|
||||||
@@ -71,7 +71,9 @@ export class DraggableList extends EventTarget {
|
|||||||
|
|
||||||
getAllItems() {
|
getAllItems() {
|
||||||
if (!this.items?.length) {
|
if (!this.items?.length) {
|
||||||
this.items = Array.from(this.listContainer.querySelectorAll(this.itemSelector));
|
this.items = Array.from(
|
||||||
|
this.listContainer.querySelectorAll(this.itemSelector)
|
||||||
|
);
|
||||||
this.items.forEach((element) => {
|
this.items.forEach((element) => {
|
||||||
element.classList.add("is-idle");
|
element.classList.add("is-idle");
|
||||||
});
|
});
|
||||||
@@ -80,7 +82,9 @@ export class DraggableList extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getIdleItems() {
|
getIdleItems() {
|
||||||
return this.getAllItems().filter((item) => item.classList.contains("is-idle"));
|
return this.getAllItems().filter((item) =>
|
||||||
|
item.classList.contains("is-idle")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isItemAbove(item) {
|
isItemAbove(item) {
|
||||||
@@ -106,18 +110,24 @@ export class DraggableList extends EventTarget {
|
|||||||
|
|
||||||
this.pointerStartX = e.clientX || e.touches[0].clientX;
|
this.pointerStartX = e.clientX || e.touches[0].clientX;
|
||||||
this.pointerStartY = e.clientY || e.touches[0].clientY;
|
this.pointerStartY = e.clientY || e.touches[0].clientY;
|
||||||
this.scrollYMax = this.listContainer.scrollHeight - this.listContainer.clientHeight;
|
this.scrollYMax =
|
||||||
|
this.listContainer.scrollHeight - this.listContainer.clientHeight;
|
||||||
|
|
||||||
this.setItemsGap();
|
this.setItemsGap();
|
||||||
this.initDraggableItem();
|
this.initDraggableItem();
|
||||||
this.initItemsState();
|
this.initItemsState();
|
||||||
|
|
||||||
this.offDrag.push(this.on(document, "mousemove", this.drag));
|
this.offDrag.push(this.on(document, "mousemove", this.drag));
|
||||||
this.offDrag.push(this.on(document, "touchmove", this.drag, { passive: false }));
|
this.offDrag.push(
|
||||||
|
this.on(document, "touchmove", this.drag, { passive: false })
|
||||||
|
);
|
||||||
|
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent("dragstart", {
|
new CustomEvent("dragstart", {
|
||||||
detail: { element: this.draggableItem, position: this.getAllItems().indexOf(this.draggableItem) },
|
detail: {
|
||||||
|
element: this.draggableItem,
|
||||||
|
position: this.getAllItems().indexOf(this.draggableItem),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -250,7 +260,11 @@ export class DraggableList extends EventTarget {
|
|||||||
|
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent("dragend", {
|
new CustomEvent("dragend", {
|
||||||
detail: { element: this.draggableItem, oldPosition, newPosition: reorderedItems.indexOf(this.draggableItem) },
|
detail: {
|
||||||
|
element: this.draggableItem,
|
||||||
|
oldPosition,
|
||||||
|
newPosition: reorderedItems.indexOf(this.draggableItem),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,11 @@ export function createImageHost(node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nw = node.size[0];
|
const nw = node.size[0];
|
||||||
({ cellWidth: w, cellHeight: h } = calculateImageGrid(currentImgs, nw - 20, elH));
|
({ cellWidth: w, cellHeight: h } = calculateImageGrid(
|
||||||
|
currentImgs,
|
||||||
|
nw - 20,
|
||||||
|
elH
|
||||||
|
));
|
||||||
w += "px";
|
w += "px";
|
||||||
h += "px";
|
h += "px";
|
||||||
|
|
||||||
@@ -86,10 +90,13 @@ export function createImageHost(node) {
|
|||||||
onDraw() {
|
onDraw() {
|
||||||
// Element from point uses a hittest find elements so we need to toggle pointer events
|
// Element from point uses a hittest find elements so we need to toggle pointer events
|
||||||
el.style.pointerEvents = "all";
|
el.style.pointerEvents = "all";
|
||||||
const over = document.elementFromPoint(app.canvas.mouse[0], app.canvas.mouse[1]);
|
const over = document.elementFromPoint(
|
||||||
|
app.canvas.mouse[0],
|
||||||
|
app.canvas.mouse[1]
|
||||||
|
);
|
||||||
el.style.pointerEvents = "none";
|
el.style.pointerEvents = "none";
|
||||||
|
|
||||||
if(!over) return;
|
if (!over) return;
|
||||||
// Set the overIndex so Open Image etc work
|
// Set the overIndex so Open Image etc work
|
||||||
const idx = currentImgs.indexOf(over);
|
const idx = currentImgs.indexOf(over);
|
||||||
node.overIndex = idx;
|
node.overIndex = idx;
|
||||||
|
|||||||
@@ -48,7 +48,11 @@ export class ComfyAppMenu {
|
|||||||
content: t,
|
content: t,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logo = $el("h1.comfyui-logo.nlg-hide", { title: "ComfyUI" }, "ComfyUI");
|
this.logo = $el(
|
||||||
|
"h1.comfyui-logo.nlg-hide",
|
||||||
|
{ title: "ComfyUI" },
|
||||||
|
"ComfyUI"
|
||||||
|
);
|
||||||
this.saveButton = new ComfySplitButton(
|
this.saveButton = new ComfySplitButton(
|
||||||
{
|
{
|
||||||
primary: getSaveButton(),
|
primary: getSaveButton(),
|
||||||
@@ -71,7 +75,8 @@ export class ComfyAppMenu {
|
|||||||
new ComfyButton({
|
new ComfyButton({
|
||||||
icon: "api",
|
icon: "api",
|
||||||
content: "Export (API Format)",
|
content: "Export (API Format)",
|
||||||
tooltip: "Export the current workflow as JSON for use with the ComfyUI API",
|
tooltip:
|
||||||
|
"Export the current workflow as JSON for use with the ComfyUI API",
|
||||||
action: () => this.exportWorkflow("workflow_api", "output"),
|
action: () => this.exportWorkflow("workflow_api", "output"),
|
||||||
visibilitySetting: { id: "Comfy.DevMode", showValue: true },
|
visibilitySetting: { id: "Comfy.DevMode", showValue: true },
|
||||||
app,
|
app,
|
||||||
@@ -101,7 +106,10 @@ export class ComfyAppMenu {
|
|||||||
content: "Clear",
|
content: "Clear",
|
||||||
tooltip: "Clears current workflow",
|
tooltip: "Clears current workflow",
|
||||||
action: () => {
|
action: () => {
|
||||||
if (!app.ui.settings.getSettingValue("Comfy.ConfirmClear", true) || confirm("Clear workflow?")) {
|
if (
|
||||||
|
!app.ui.settings.getSettingValue("Comfy.ConfirmClear", true) ||
|
||||||
|
confirm("Clear workflow?")
|
||||||
|
) {
|
||||||
app.clean();
|
app.clean();
|
||||||
app.graph.clear();
|
app.graph.clear();
|
||||||
}
|
}
|
||||||
@@ -126,7 +134,9 @@ export class ComfyAppMenu {
|
|||||||
this.mobileMenuButton = new ComfyButton({
|
this.mobileMenuButton = new ComfyButton({
|
||||||
icon: "menu",
|
icon: "menu",
|
||||||
action: (_, btn) => {
|
action: (_, btn) => {
|
||||||
btn.icon = this.element.classList.toggle("expanded") ? "menu-open" : "menu";
|
btn.icon = this.element.classList.toggle("expanded")
|
||||||
|
? "menu-open"
|
||||||
|
: "menu";
|
||||||
window.dispatchEvent(new Event("resize"));
|
window.dispatchEvent(new Event("resize"));
|
||||||
},
|
},
|
||||||
classList: "comfyui-button comfyui-menu-button",
|
classList: "comfyui-button comfyui-menu-button",
|
||||||
@@ -239,7 +249,10 @@ export class ComfyAppMenu {
|
|||||||
idx--;
|
idx--;
|
||||||
}
|
}
|
||||||
} else if (innerSize > this.element.clientWidth) {
|
} else if (innerSize > this.element.clientWidth) {
|
||||||
this.#lastSizeBreaks[this.#sizeBreak] = Math.max(window.innerWidth, innerSize);
|
this.#lastSizeBreaks[this.#sizeBreak] = Math.max(
|
||||||
|
window.innerWidth,
|
||||||
|
innerSize
|
||||||
|
);
|
||||||
// We need to shrink
|
// We need to shrink
|
||||||
if (idx < this.#sizeBreaks.length - 1) {
|
if (idx < this.#sizeBreaks.length - 1) {
|
||||||
idx++;
|
idx++;
|
||||||
@@ -254,19 +267,26 @@ export class ComfyAppMenu {
|
|||||||
clearTimeout(this.#cacheTimeout);
|
clearTimeout(this.#cacheTimeout);
|
||||||
if (this.#cachedInnerSize) {
|
if (this.#cachedInnerSize) {
|
||||||
// Extend cache time
|
// Extend cache time
|
||||||
this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100);
|
this.#cacheTimeout = setTimeout(
|
||||||
|
() => (this.#cachedInnerSize = null),
|
||||||
|
100
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
let innerSize = 0;
|
let innerSize = 0;
|
||||||
let count = 1;
|
let count = 1;
|
||||||
for (const c of this.element.children) {
|
for (const c of this.element.children) {
|
||||||
if (c.classList.contains("comfyui-menu-push")) continue; // ignore right push
|
if (c.classList.contains("comfyui-menu-push")) continue; // ignore right push
|
||||||
if (idx && c.classList.contains("comfyui-menu-mobile-collapse")) continue; // ignore collapse items
|
if (idx && c.classList.contains("comfyui-menu-mobile-collapse"))
|
||||||
|
continue; // ignore collapse items
|
||||||
innerSize += c.clientWidth;
|
innerSize += c.clientWidth;
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
innerSize += 8 * count;
|
innerSize += 8 * count;
|
||||||
this.#cachedInnerSize = innerSize;
|
this.#cachedInnerSize = innerSize;
|
||||||
this.#cacheTimeout = setTimeout(() => (this.#cachedInnerSize = null), 100);
|
this.#cacheTimeout = setTimeout(
|
||||||
|
() => (this.#cachedInnerSize = null),
|
||||||
|
100
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return this.#cachedInnerSize;
|
return this.#cachedInnerSize;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ export class ComfyQueueButton {
|
|||||||
queuePrompt = async (e) => {
|
queuePrompt = async (e) => {
|
||||||
this.#internalQueueSize += this.queueOptions.batchCount;
|
this.#internalQueueSize += this.queueOptions.batchCount;
|
||||||
// Hold shift to queue front, event is undefined when auto-queue is enabled
|
// Hold shift to queue front, event is undefined when auto-queue is enabled
|
||||||
await this.app.queuePrompt(e?.shiftKey ? -1 : 0, this.queueOptions.batchCount);
|
await this.app.queuePrompt(
|
||||||
|
e?.shiftKey ? -1 : 0,
|
||||||
|
this.queueOptions.batchCount
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(app) {
|
constructor(app) {
|
||||||
@@ -63,7 +66,10 @@ export class ComfyQueueButton {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.queueOptions.addEventListener("autoQueueMode", (e) => (this.autoQueueMode = e["detail"]));
|
this.queueOptions.addEventListener(
|
||||||
|
"autoQueueMode",
|
||||||
|
(e) => (this.autoQueueMode = e["detail"])
|
||||||
|
);
|
||||||
|
|
||||||
api.addEventListener("graphChanged", () => {
|
api.addEventListener("graphChanged", () => {
|
||||||
if (this.autoQueueMode === "change") {
|
if (this.autoQueueMode === "change") {
|
||||||
@@ -79,10 +85,14 @@ export class ComfyQueueButton {
|
|||||||
api.addEventListener("status", ({ detail }) => {
|
api.addEventListener("status", ({ detail }) => {
|
||||||
this.#internalQueueSize = detail?.exec_info?.queue_remaining;
|
this.#internalQueueSize = detail?.exec_info?.queue_remaining;
|
||||||
if (this.#internalQueueSize != null) {
|
if (this.#internalQueueSize != null) {
|
||||||
this.queueSizeElement.textContent = this.#internalQueueSize > 99 ? "99+" : this.#internalQueueSize + "";
|
this.queueSizeElement.textContent =
|
||||||
|
this.#internalQueueSize > 99 ? "99+" : this.#internalQueueSize + "";
|
||||||
this.queueSizeElement.title = `${this.#internalQueueSize} prompts in queue`;
|
this.queueSizeElement.title = `${this.#internalQueueSize} prompts in queue`;
|
||||||
if (!this.#internalQueueSize && !app.lastExecutionError) {
|
if (!this.#internalQueueSize && !app.lastExecutionError) {
|
||||||
if (this.autoQueueMode === "instant" || (this.autoQueueMode === "change" && this.graphHasChanged)) {
|
if (
|
||||||
|
this.autoQueueMode === "instant" ||
|
||||||
|
(this.autoQueueMode === "change" && this.graphHasChanged)
|
||||||
|
) {
|
||||||
this.graphHasChanged = false;
|
this.graphHasChanged = false;
|
||||||
this.queuePrompt();
|
this.queuePrompt();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,11 +69,14 @@ export class ComfyViewList {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.element = $el(`div.comfyui-${this.type}-popup.comfyui-view-list-popup`, [
|
this.element = $el(
|
||||||
$el("h3", mode),
|
`div.comfyui-${this.type}-popup.comfyui-view-list-popup`,
|
||||||
$el("header", [this.clear.element, this.refresh.element]),
|
[
|
||||||
this.items,
|
$el("h3", mode),
|
||||||
]);
|
$el("header", [this.clear.element, this.refresh.element]),
|
||||||
|
this.items,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
api.addEventListener("status", () => {
|
api.addEventListener("status", () => {
|
||||||
if (this.popup.open) {
|
if (this.popup.open) {
|
||||||
@@ -155,7 +158,9 @@ export class ComfyViewList {
|
|||||||
text: "Load",
|
text: "Load",
|
||||||
action: async () => {
|
action: async () => {
|
||||||
try {
|
try {
|
||||||
await this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow);
|
await this.app.loadGraphData(
|
||||||
|
item.prompt[3].extra_pnginfo.workflow
|
||||||
|
);
|
||||||
if (item.outputs) {
|
if (item.outputs) {
|
||||||
this.app.nodeOutputs = item.outputs;
|
this.app.nodeOutputs = item.outputs;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ export class ComfyViewQueueList extends ComfyViewList {
|
|||||||
text: "Load",
|
text: "Load",
|
||||||
action: async () => {
|
action: async () => {
|
||||||
try {
|
try {
|
||||||
await this.app.loadGraphData(item.prompt[3].extra_pnginfo.workflow);
|
await this.app.loadGraphData(
|
||||||
|
item.prompt[3].extra_pnginfo.workflow
|
||||||
|
);
|
||||||
if (item.outputs) {
|
if (item.outputs) {
|
||||||
this.app.nodeOutputs = item.outputs;
|
this.app.nodeOutputs = item.outputs;
|
||||||
}
|
}
|
||||||
@@ -51,5 +53,5 @@ export class ComfyViewQueueList extends ComfyViewList {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,14 +37,21 @@ export class ComfyWorkflowsMenu {
|
|||||||
this.buttonProgress = $el("div.comfyui-workflows-button-progress");
|
this.buttonProgress = $el("div.comfyui-workflows-button-progress");
|
||||||
this.workflowLabel = $el("span.comfyui-workflows-label", "");
|
this.workflowLabel = $el("span.comfyui-workflows-label", "");
|
||||||
this.button = new ComfyButton({
|
this.button = new ComfyButton({
|
||||||
content: $el("div.comfyui-workflows-button-inner", [$el("i.mdi.mdi-graph"), this.workflowLabel, this.buttonProgress]),
|
content: $el("div.comfyui-workflows-button-inner", [
|
||||||
|
$el("i.mdi.mdi-graph"),
|
||||||
|
this.workflowLabel,
|
||||||
|
this.buttonProgress,
|
||||||
|
]),
|
||||||
icon: "chevron-down",
|
icon: "chevron-down",
|
||||||
classList,
|
classList,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.element.append(this.button.element);
|
this.element.append(this.button.element);
|
||||||
|
|
||||||
this.popup = new ComfyPopup({ target: this.element, classList: "comfyui-workflows-popup" });
|
this.popup = new ComfyPopup({
|
||||||
|
target: this.element,
|
||||||
|
classList: "comfyui-workflows-popup",
|
||||||
|
});
|
||||||
this.content = new ComfyWorkflowsContent(app, this.popup);
|
this.content = new ComfyWorkflowsContent(app, this.popup);
|
||||||
this.popup.children = [this.content.element];
|
this.popup.children = [this.content.element];
|
||||||
this.popup.addEventListener("change", () => {
|
this.popup.addEventListener("change", () => {
|
||||||
@@ -85,7 +92,10 @@ export class ComfyWorkflowsMenu {
|
|||||||
};
|
};
|
||||||
|
|
||||||
#bindEvents() {
|
#bindEvents() {
|
||||||
this.app.workflowManager.addEventListener("changeWorkflow", this.#updateActive);
|
this.app.workflowManager.addEventListener(
|
||||||
|
"changeWorkflow",
|
||||||
|
this.#updateActive
|
||||||
|
);
|
||||||
this.app.workflowManager.addEventListener("rename", this.#updateActive);
|
this.app.workflowManager.addEventListener("rename", this.#updateActive);
|
||||||
this.app.workflowManager.addEventListener("delete", this.#updateActive);
|
this.app.workflowManager.addEventListener("delete", this.#updateActive);
|
||||||
|
|
||||||
@@ -157,10 +167,15 @@ export class ComfyWorkflowsMenu {
|
|||||||
name: "Comfy.Workflows",
|
name: "Comfy.Workflows",
|
||||||
async beforeRegisterNodeDef(nodeType) {
|
async beforeRegisterNodeDef(nodeType) {
|
||||||
function getImageWidget(node) {
|
function getImageWidget(node) {
|
||||||
const inputs = { ...node.constructor?.nodeData?.input?.required, ...node.constructor?.nodeData?.input?.optional };
|
const inputs = {
|
||||||
|
...node.constructor?.nodeData?.input?.required,
|
||||||
|
...node.constructor?.nodeData?.input?.optional,
|
||||||
|
};
|
||||||
for (const input in inputs) {
|
for (const input in inputs) {
|
||||||
if (inputs[input][0] === "IMAGEUPLOAD") {
|
if (inputs[input][0] === "IMAGEUPLOAD") {
|
||||||
const imageWidget = node.widgets.find((w) => w.name === (inputs[input]?.[1]?.widget ?? "image"));
|
const imageWidget = node.widgets.find(
|
||||||
|
(w) => w.name === (inputs[input]?.[1]?.widget ?? "image")
|
||||||
|
);
|
||||||
if (imageWidget) return imageWidget;
|
if (imageWidget) return imageWidget;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,8 +228,13 @@ export class ComfyWorkflowsMenu {
|
|||||||
const getExtraMenuOptions = nodeType.prototype["getExtraMenuOptions"];
|
const getExtraMenuOptions = nodeType.prototype["getExtraMenuOptions"];
|
||||||
nodeType.prototype["getExtraMenuOptions"] = function (_, options) {
|
nodeType.prototype["getExtraMenuOptions"] = function (_, options) {
|
||||||
const r = getExtraMenuOptions?.apply?.(this, arguments);
|
const r = getExtraMenuOptions?.apply?.(this, arguments);
|
||||||
if (app.ui.settings.getSettingValue("Comfy.UseNewMenu", false) === true) {
|
if (
|
||||||
const t = /** @type { {imageIndex?: number, overIndex?: number, imgs: string[]} } */ /** @type {any} */ (this);
|
app.ui.settings.getSettingValue("Comfy.UseNewMenu", false) === true
|
||||||
|
) {
|
||||||
|
const t =
|
||||||
|
/** @type { {imageIndex?: number, overIndex?: number, imgs: string[]} } */ /** @type {any} */ (
|
||||||
|
this
|
||||||
|
);
|
||||||
let img;
|
let img;
|
||||||
if (t.imageIndex != null) {
|
if (t.imageIndex != null) {
|
||||||
// An image is selected so select that
|
// An image is selected so select that
|
||||||
@@ -238,10 +258,13 @@ export class ComfyWorkflowsMenu {
|
|||||||
submenu: {
|
submenu: {
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
callback: () => sendToWorkflow(img, app.workflowManager.activeWorkflow),
|
callback: () =>
|
||||||
|
sendToWorkflow(img, app.workflowManager.activeWorkflow),
|
||||||
title: "[Current workflow]",
|
title: "[Current workflow]",
|
||||||
},
|
},
|
||||||
...self.#getFavoriteMenuOptions(sendToWorkflow.bind(null, img)),
|
...self.#getFavoriteMenuOptions(
|
||||||
|
sendToWorkflow.bind(null, img)
|
||||||
|
),
|
||||||
null,
|
null,
|
||||||
...self.#getMenuOptions(sendToWorkflow.bind(null, img)),
|
...self.#getMenuOptions(sendToWorkflow.bind(null, img)),
|
||||||
],
|
],
|
||||||
@@ -315,7 +338,9 @@ export class ComfyWorkflowsContent {
|
|||||||
this.element.replaceChildren(this.actions, this.spinner);
|
this.element.replaceChildren(this.actions, this.spinner);
|
||||||
|
|
||||||
this.popup.addEventListener("open", () => this.load());
|
this.popup.addEventListener("open", () => this.load());
|
||||||
this.popup.addEventListener("close", () => this.element.replaceChildren(this.actions, this.spinner));
|
this.popup.addEventListener("close", () =>
|
||||||
|
this.element.replaceChildren(this.actions, this.spinner)
|
||||||
|
);
|
||||||
|
|
||||||
this.app.workflowManager.addEventListener("favorite", (e) => {
|
this.app.workflowManager.addEventListener("favorite", (e) => {
|
||||||
const workflow = e["detail"];
|
const workflow = e["detail"];
|
||||||
@@ -331,7 +356,9 @@ export class ComfyWorkflowsContent {
|
|||||||
app.workflowManager.addEventListener(e, () => this.updateOpen());
|
app.workflowManager.addEventListener(e, () => this.updateOpen());
|
||||||
}
|
}
|
||||||
this.app.workflowManager.addEventListener("rename", () => this.load());
|
this.app.workflowManager.addEventListener("rename", () => this.load());
|
||||||
this.app.workflowManager.addEventListener("execute", (e) => this.#updateActive());
|
this.app.workflowManager.addEventListener("execute", (e) =>
|
||||||
|
this.#updateActive()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
@@ -339,7 +366,12 @@ export class ComfyWorkflowsContent {
|
|||||||
this.updateTree();
|
this.updateTree();
|
||||||
this.updateFavorites();
|
this.updateFavorites();
|
||||||
this.updateOpen();
|
this.updateOpen();
|
||||||
this.element.replaceChildren(this.actions, this.openElement, this.favoritesElement, this.treeElement);
|
this.element.replaceChildren(
|
||||||
|
this.actions,
|
||||||
|
this.openElement,
|
||||||
|
this.favoritesElement,
|
||||||
|
this.treeElement
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateOpen() {
|
updateOpen() {
|
||||||
@@ -368,7 +400,7 @@ export class ComfyWorkflowsContent {
|
|||||||
if (w.unsaved) {
|
if (w.unsaved) {
|
||||||
wrapper.element.classList.add("unsaved");
|
wrapper.element.classList.add("unsaved");
|
||||||
}
|
}
|
||||||
if(w === this.app.workflowManager.activeWorkflow) {
|
if (w === this.app.workflowManager.activeWorkflow) {
|
||||||
wrapper.element.classList.add("active");
|
wrapper.element.classList.add("active");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,7 +415,9 @@ export class ComfyWorkflowsContent {
|
|||||||
|
|
||||||
updateFavorites() {
|
updateFavorites() {
|
||||||
const current = this.favoritesElement;
|
const current = this.favoritesElement;
|
||||||
const favorites = [...this.app.workflowManager.workflows.filter((w) => w.isFavorite)];
|
const favorites = [
|
||||||
|
...this.app.workflowManager.workflows.filter((w) => w.isFavorite),
|
||||||
|
];
|
||||||
|
|
||||||
this.favoritesElement = $el("div.comfyui-workflows-favorites", [
|
this.favoritesElement = $el("div.comfyui-workflows-favorites", [
|
||||||
$el("h3", "Favorites"),
|
$el("h3", "Favorites"),
|
||||||
@@ -437,7 +471,10 @@ export class ComfyWorkflowsContent {
|
|||||||
|
|
||||||
hideTreeParents(element) {
|
hideTreeParents(element) {
|
||||||
// Hide all parents if no children are visible
|
// Hide all parents if no children are visible
|
||||||
if (element.parentElement?.classList.contains("comfyui-workflows-tree") === false) {
|
if (
|
||||||
|
element.parentElement?.classList.contains("comfyui-workflows-tree") ===
|
||||||
|
false
|
||||||
|
) {
|
||||||
for (let i = 1; i < element.parentElement.children.length; i++) {
|
for (let i = 1; i < element.parentElement.children.length; i++) {
|
||||||
const c = element.parentElement.children[i];
|
const c = element.parentElement.children[i];
|
||||||
if (c.style.display !== "none") {
|
if (c.style.display !== "none") {
|
||||||
@@ -450,7 +487,10 @@ export class ComfyWorkflowsContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showTreeParents(element) {
|
showTreeParents(element) {
|
||||||
if (element.parentElement?.classList.contains("comfyui-workflows-tree") === false) {
|
if (
|
||||||
|
element.parentElement?.classList.contains("comfyui-workflows-tree") ===
|
||||||
|
false
|
||||||
|
) {
|
||||||
element.parentElement.style.removeProperty("display");
|
element.parentElement.style.removeProperty("display");
|
||||||
this.showTreeParents(element.parentElement);
|
this.showTreeParents(element.parentElement);
|
||||||
}
|
}
|
||||||
@@ -490,7 +530,9 @@ export class ComfyWorkflowsContent {
|
|||||||
|
|
||||||
for (let i = 0; i < workflow.pathParts.length; i++) {
|
for (let i = 0; i < workflow.pathParts.length; i++) {
|
||||||
currentPath += (currentPath ? "\\" : "") + workflow.pathParts[i];
|
currentPath += (currentPath ? "\\" : "") + workflow.pathParts[i];
|
||||||
const parentNode = nodes[currentPath] ?? this.#createNode(currentPath, workflow, i, currentRoot);
|
const parentNode =
|
||||||
|
nodes[currentPath] ??
|
||||||
|
this.#createNode(currentPath, workflow, i, currentRoot);
|
||||||
|
|
||||||
nodes[currentPath] = parentNode;
|
nodes[currentPath] = parentNode;
|
||||||
currentRoot = parentNode;
|
currentRoot = parentNode;
|
||||||
@@ -559,7 +601,9 @@ export class ComfyWorkflowsContent {
|
|||||||
|
|
||||||
/** @param {ComfyWorkflow} workflow */
|
/** @param {ComfyWorkflow} workflow */
|
||||||
#getFavoriteTooltip(workflow) {
|
#getFavoriteTooltip(workflow) {
|
||||||
return workflow.isFavorite ? "Remove this workflow from your favorites" : "Add this workflow to your favorites";
|
return workflow.isFavorite
|
||||||
|
? "Remove this workflow from your favorites"
|
||||||
|
: "Add this workflow to your favorites";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {ComfyWorkflow} workflow */
|
/** @param {ComfyWorkflow} workflow */
|
||||||
@@ -568,7 +612,9 @@ export class ComfyWorkflowsContent {
|
|||||||
icon: this.#getFavoriteIcon(workflow),
|
icon: this.#getFavoriteIcon(workflow),
|
||||||
overIcon: this.#getFavoriteOverIcon(workflow),
|
overIcon: this.#getFavoriteOverIcon(workflow),
|
||||||
iconSize: 18,
|
iconSize: 18,
|
||||||
classList: "comfyui-button comfyui-workflows-file-action-favorite" + (primary ? " comfyui-workflows-file-action-primary" : ""),
|
classList:
|
||||||
|
"comfyui-button comfyui-workflows-file-action-favorite" +
|
||||||
|
(primary ? " comfyui-workflows-file-action-primary" : ""),
|
||||||
tooltip: this.#getFavoriteTooltip(workflow),
|
tooltip: this.#getFavoriteTooltip(workflow),
|
||||||
action: (e) => {
|
action: (e) => {
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
@@ -628,7 +674,9 @@ export class ComfyWorkflowsContent {
|
|||||||
#getRenameButton(workflow) {
|
#getRenameButton(workflow) {
|
||||||
return new ComfyButton({
|
return new ComfyButton({
|
||||||
icon: "pencil",
|
icon: "pencil",
|
||||||
tooltip: workflow.path ? "Rename this workflow" : "This workflow can't be renamed as it hasn't been saved.",
|
tooltip: workflow.path
|
||||||
|
? "Rename this workflow"
|
||||||
|
: "This workflow can't be renamed as it hasn't been saved.",
|
||||||
classList: "comfyui-button comfyui-workflows-file-action",
|
classList: "comfyui-button comfyui-workflows-file-action",
|
||||||
iconSize: 18,
|
iconSize: 18,
|
||||||
enabled: !!workflow.path,
|
enabled: !!workflow.path,
|
||||||
@@ -646,7 +694,11 @@ export class ComfyWorkflowsContent {
|
|||||||
#getWorkflowElement(workflow) {
|
#getWorkflowElement(workflow) {
|
||||||
return new WorkflowElement(this, workflow, {
|
return new WorkflowElement(this, workflow, {
|
||||||
primary: this.#getFavoriteButton(workflow, true),
|
primary: this.#getFavoriteButton(workflow, true),
|
||||||
buttons: [this.#getInsertButton(workflow), this.#getRenameButton(workflow), this.#getDeleteButton(workflow)],
|
buttons: [
|
||||||
|
this.#getInsertButton(workflow),
|
||||||
|
this.#getRenameButton(workflow),
|
||||||
|
this.#getDeleteButton(workflow),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -660,14 +712,17 @@ export class ComfyWorkflowsContent {
|
|||||||
#createNode(currentPath, workflow, i, currentRoot) {
|
#createNode(currentPath, workflow, i, currentRoot) {
|
||||||
const part = workflow.pathParts[i];
|
const part = workflow.pathParts[i];
|
||||||
|
|
||||||
const parentNode = $el("ul" + (this.treeState[currentPath] ? "" : ".closed"), {
|
const parentNode = $el(
|
||||||
$: (el) => {
|
"ul" + (this.treeState[currentPath] ? "" : ".closed"),
|
||||||
el.onclick = (e) => {
|
{
|
||||||
this.#expandNode(el, workflow, currentPath, i);
|
$: (el) => {
|
||||||
e.stopImmediatePropagation();
|
el.onclick = (e) => {
|
||||||
};
|
this.#expandNode(el, workflow, currentPath, i);
|
||||||
},
|
e.stopImmediatePropagation();
|
||||||
});
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
currentRoot.append(parentNode);
|
currentRoot.append(parentNode);
|
||||||
|
|
||||||
// Create a node for the current part and an inner UL for its children if it isnt a leaf node
|
// Create a node for the current part and an inner UL for its children if it isnt a leaf node
|
||||||
@@ -676,7 +731,10 @@ export class ComfyWorkflowsContent {
|
|||||||
if (leaf) {
|
if (leaf) {
|
||||||
nodeElement = this.#createLeafNode(workflow).element;
|
nodeElement = this.#createLeafNode(workflow).element;
|
||||||
} else {
|
} else {
|
||||||
nodeElement = $el("li", [$el("i.mdi.mdi-18px.mdi-folder"), $el("span", part)]);
|
nodeElement = $el("li", [
|
||||||
|
$el("i.mdi.mdi-18px.mdi-folder"),
|
||||||
|
$el("span", part),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
parentNode.append(nodeElement);
|
parentNode.append(nodeElement);
|
||||||
return parentNode;
|
return parentNode;
|
||||||
@@ -703,7 +761,11 @@ class WorkflowElement {
|
|||||||
},
|
},
|
||||||
title: this.workflow.path,
|
title: this.workflow.path,
|
||||||
},
|
},
|
||||||
[this.primary?.element, $el("span", workflow.name), ...buttons.map((b) => b.element)]
|
[
|
||||||
|
this.primary?.element,
|
||||||
|
$el("span", workflow.name),
|
||||||
|
...buttons.map((b) => b.element),
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -732,7 +794,11 @@ class WidgetSelectionDialog extends ComfyAsyncDialog {
|
|||||||
"section",
|
"section",
|
||||||
this.#options.map((opt) => {
|
this.#options.map((opt) => {
|
||||||
return $el("div.comfy-widget-selection-item", [
|
return $el("div.comfy-widget-selection-item", [
|
||||||
$el("span", { dataset: { id: opt.node.id } }, `${opt.node.title ?? opt.node.type} ${opt.widget.name}`),
|
$el(
|
||||||
|
"span",
|
||||||
|
{ dataset: { id: opt.node.id } },
|
||||||
|
`${opt.node.title ?? opt.node.type} ${opt.widget.name}`
|
||||||
|
),
|
||||||
$el(
|
$el(
|
||||||
"button.comfyui-button",
|
"button.comfyui-button",
|
||||||
{
|
{
|
||||||
@@ -760,4 +826,4 @@ class WidgetSelectionDialog extends ComfyAsyncDialog {
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,14 @@ interface SettingOption {
|
|||||||
interface SettingParams {
|
interface SettingParams {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: string | ((name: string, setter: (v: any) => void, value: any, attrs: any) => HTMLElement);
|
type:
|
||||||
|
| string
|
||||||
|
| ((
|
||||||
|
name: string,
|
||||||
|
setter: (v: any) => void,
|
||||||
|
value: any,
|
||||||
|
attrs: any
|
||||||
|
) => HTMLElement);
|
||||||
defaultValue: any;
|
defaultValue: any;
|
||||||
onChange?: (newValue: any, oldValue?: any) => void;
|
onChange?: (newValue: any, oldValue?: any) => void;
|
||||||
attrs?: any;
|
attrs?: any;
|
||||||
@@ -81,7 +88,7 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
|
|||||||
new CustomEvent(id + ".change", {
|
new CustomEvent(id + ".change", {
|
||||||
detail: {
|
detail: {
|
||||||
value,
|
value,
|
||||||
oldValue
|
oldValue,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -115,8 +122,7 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
|
|||||||
if (this.app.storageLocation === "browser") {
|
if (this.app.storageLocation === "browser") {
|
||||||
try {
|
try {
|
||||||
value = JSON.parse(value);
|
value = JSON.parse(value);
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return value ?? defaultValue;
|
return value ?? defaultValue;
|
||||||
@@ -145,7 +151,16 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addSetting(params: SettingParams) {
|
addSetting(params: SettingParams) {
|
||||||
const { id, name, type, defaultValue, onChange, attrs = {}, tooltip = "", options = undefined } = params;
|
const {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
defaultValue,
|
||||||
|
onChange,
|
||||||
|
attrs = {},
|
||||||
|
tooltip = "",
|
||||||
|
options = undefined,
|
||||||
|
} = params;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
throw new Error("Settings must have an ID");
|
throw new Error("Settings must have an ID");
|
||||||
}
|
}
|
||||||
@@ -272,7 +287,8 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
|
|||||||
style: { maxWidth: "4rem" },
|
style: { maxWidth: "4rem" },
|
||||||
oninput: (e) => {
|
oninput: (e) => {
|
||||||
setter(e.target.value);
|
setter(e.target.value);
|
||||||
e.target.previousElementSibling.value = e.target.value;
|
e.target.previousElementSibling.value =
|
||||||
|
e.target.value;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
@@ -291,7 +307,10 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
|
|||||||
setter(e.target.value);
|
setter(e.target.value);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
(typeof options === "function" ? options(value) : options || []).map((opt) => {
|
(typeof options === "function"
|
||||||
|
? options(value)
|
||||||
|
: options || []
|
||||||
|
).map((opt) => {
|
||||||
if (typeof opt === "string") {
|
if (typeof opt === "string") {
|
||||||
opt = { text: opt };
|
opt = { text: opt };
|
||||||
}
|
}
|
||||||
@@ -309,7 +328,9 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
|
|||||||
case "text":
|
case "text":
|
||||||
default:
|
default:
|
||||||
if (type !== "text") {
|
if (type !== "text") {
|
||||||
console.warn(`Unsupported setting type '${type}, defaulting to text`);
|
console.warn(
|
||||||
|
`Unsupported setting type '${type}, defaulting to text`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
element = $el("tr", [
|
element = $el("tr", [
|
||||||
@@ -356,7 +377,10 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
|
|||||||
},
|
},
|
||||||
[$el("th"), $el("th", { style: { width: "33%" } })]
|
[$el("th"), $el("th", { style: { width: "33%" } })]
|
||||||
),
|
),
|
||||||
...this.settings.sort((a, b) => a.name.localeCompare(b.name)).map((s) => s.render()).filter(Boolean)
|
...this.settings
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.map((s) => s.render())
|
||||||
|
.filter(Boolean)
|
||||||
);
|
);
|
||||||
this.element.showModal();
|
this.element.showModal();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import "./spinner.css";
|
import "./spinner.css";
|
||||||
|
|
||||||
|
|
||||||
export function createSpinner() {
|
export function createSpinner() {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.innerHTML = `<div class="lds-ring"><div></div><div></div><div></div><div></div></div>`;
|
div.innerHTML = `<div class="lds-ring"><div></div><div></div><div></div><div></div></div>`;
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ export function toggleSwitch(name, items, e?) {
|
|||||||
if (selectedIndex != null) {
|
if (selectedIndex != null) {
|
||||||
elements[selectedIndex].classList.remove("comfy-toggle-selected");
|
elements[selectedIndex].classList.remove("comfy-toggle-selected");
|
||||||
}
|
}
|
||||||
onChange?.({ item: items[index], prev: selectedIndex == null ? undefined : items[selectedIndex] });
|
onChange?.({
|
||||||
|
item: items[index],
|
||||||
|
prev: selectedIndex == null ? undefined : items[selectedIndex],
|
||||||
|
});
|
||||||
selectedIndex = index;
|
selectedIndex = index;
|
||||||
elements[selectedIndex].classList.add("comfy-toggle-selected");
|
elements[selectedIndex].classList.add("comfy-toggle-selected");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,14 @@ import { $el } from "../ui";
|
|||||||
import { createSpinner } from "./spinner";
|
import { createSpinner } from "./spinner";
|
||||||
import "./userSelection.css";
|
import "./userSelection.css";
|
||||||
|
|
||||||
|
|
||||||
interface SelectedUser {
|
interface SelectedUser {
|
||||||
username: string;
|
username: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
created: boolean;
|
created: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class UserSelectionScreen {
|
export class UserSelectionScreen {
|
||||||
async show(users, user): Promise<SelectedUser>{
|
async show(users, user): Promise<SelectedUser> {
|
||||||
const userSelection = document.getElementById("comfy-user-selection");
|
const userSelection = document.getElementById("comfy-user-selection");
|
||||||
userSelection.style.display = "";
|
userSelection.style.display = "";
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -22,7 +20,9 @@ export class UserSelectionScreen {
|
|||||||
const selectSection = select.closest("section");
|
const selectSection = select.closest("section");
|
||||||
const form = userSelection.getElementsByTagName("form")[0];
|
const form = userSelection.getElementsByTagName("form")[0];
|
||||||
const error = userSelection.getElementsByClassName("comfy-user-error")[0];
|
const error = userSelection.getElementsByClassName("comfy-user-error")[0];
|
||||||
const button = userSelection.getElementsByClassName("comfy-user-button-next")[0];
|
const button = userSelection.getElementsByClassName(
|
||||||
|
"comfy-user-button-next"
|
||||||
|
)[0];
|
||||||
|
|
||||||
let inputActive = null;
|
let inputActive = null;
|
||||||
input.addEventListener("focus", () => {
|
input.addEventListener("focus", () => {
|
||||||
@@ -45,7 +45,8 @@ export class UserSelectionScreen {
|
|||||||
form.addEventListener("submit", async (e) => {
|
form.addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (inputActive == null) {
|
if (inputActive == null) {
|
||||||
error.textContent = "Please enter a username or select an existing user.";
|
error.textContent =
|
||||||
|
"Please enter a username or select an existing user.";
|
||||||
} else if (inputActive) {
|
} else if (inputActive) {
|
||||||
const username = input.value.trim();
|
const username = input.value.trim();
|
||||||
if (!username) {
|
if (!username) {
|
||||||
@@ -54,41 +55,59 @@ export class UserSelectionScreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create new user
|
// Create new user
|
||||||
// @ts-ignore
|
|
||||||
// Property 'readonly' does not exist on type 'HTMLSelectElement'.ts(2339)
|
// Property 'readonly' does not exist on type 'HTMLSelectElement'.ts(2339)
|
||||||
// Property 'readonly' does not exist on type 'HTMLInputElement'. Did you mean 'readOnly'?ts(2551)
|
// Property 'readonly' does not exist on type 'HTMLInputElement'. Did you mean 'readOnly'?ts(2551)
|
||||||
input.disabled = select.disabled = input.readonly = select.readonly = true;
|
input.disabled =
|
||||||
|
select.disabled =
|
||||||
|
// @ts-ignore
|
||||||
|
input.readonly =
|
||||||
|
// @ts-ignore
|
||||||
|
select.readonly =
|
||||||
|
true;
|
||||||
const spinner = createSpinner();
|
const spinner = createSpinner();
|
||||||
button.prepend(spinner);
|
button.prepend(spinner);
|
||||||
try {
|
try {
|
||||||
const resp = await api.createUser(username);
|
const resp = await api.createUser(username);
|
||||||
if (resp.status >= 300) {
|
if (resp.status >= 300) {
|
||||||
let message = "Error creating user: " + resp.status + " " + resp.statusText;
|
let message =
|
||||||
|
"Error creating user: " + resp.status + " " + resp.statusText;
|
||||||
try {
|
try {
|
||||||
const res = await resp.json();
|
const res = await resp.json();
|
||||||
if(res.error) {
|
if (res.error) {
|
||||||
message = res.error;
|
message = res.error;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
}
|
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve({ username, userId: await resp.json(), created: true });
|
resolve({ username, userId: await resp.json(), created: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
spinner.remove();
|
spinner.remove();
|
||||||
error.textContent = err.message ?? err.statusText ?? err ?? "An unknown error occurred.";
|
error.textContent =
|
||||||
// @ts-ignore
|
err.message ??
|
||||||
|
err.statusText ??
|
||||||
|
err ??
|
||||||
|
"An unknown error occurred.";
|
||||||
// Property 'readonly' does not exist on type 'HTMLSelectElement'.ts(2339)
|
// Property 'readonly' does not exist on type 'HTMLSelectElement'.ts(2339)
|
||||||
// Property 'readonly' does not exist on type 'HTMLInputElement'. Did you mean 'readOnly'?ts(2551)
|
// Property 'readonly' does not exist on type 'HTMLInputElement'. Did you mean 'readOnly'?ts(2551)
|
||||||
input.disabled = select.disabled = input.readonly = select.readonly = false;
|
input.disabled =
|
||||||
|
select.disabled =
|
||||||
|
// @ts-ignore
|
||||||
|
input.readonly =
|
||||||
|
// @ts-ignore
|
||||||
|
select.readonly =
|
||||||
|
false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (!select.value) {
|
} else if (!select.value) {
|
||||||
error.textContent = "Please select an existing user.";
|
error.textContent = "Please select an existing user.";
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
resolve({ username: users[select.value], userId: select.value, created: false });
|
resolve({
|
||||||
|
username: users[select.value],
|
||||||
|
userId: select.value,
|
||||||
|
created: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ export function applyTextReplacements(app: ComfyApp, value: string): string {
|
|||||||
|
|
||||||
// Find node with matching S&R property name
|
// Find node with matching S&R property name
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
let nodes = app.graph._nodes.filter((n) => n.properties?.["Node name for S&R"] === split[0]);
|
let nodes = app.graph._nodes.filter(
|
||||||
|
(n) => n.properties?.["Node name for S&R"] === split[0]
|
||||||
|
);
|
||||||
// If we cant, see if there is a node with that title
|
// If we cant, see if there is a node with that title
|
||||||
if (!nodes.length) {
|
if (!nodes.length) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -76,7 +78,13 @@ export function applyTextReplacements(app: ComfyApp, value: string): string {
|
|||||||
|
|
||||||
const widget = node.widgets?.find((w) => w.name === split[1]);
|
const widget = node.widgets?.find((w) => w.name === split[1]);
|
||||||
if (!widget) {
|
if (!widget) {
|
||||||
console.warn("Unable to find widget", split[1], "on node", split[0], node);
|
console.warn(
|
||||||
|
"Unable to find widget",
|
||||||
|
split[1],
|
||||||
|
"on node",
|
||||||
|
split[0],
|
||||||
|
node
|
||||||
|
);
|
||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,13 +92,19 @@ export function applyTextReplacements(app: ComfyApp, value: string): string {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addStylesheet(urlOrFile: string, relativeTo?: string): Promise<void> {
|
export async function addStylesheet(
|
||||||
|
urlOrFile: string,
|
||||||
|
relativeTo?: string
|
||||||
|
): Promise<void> {
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
let url;
|
let url;
|
||||||
if (urlOrFile.endsWith(".js")) {
|
if (urlOrFile.endsWith(".js")) {
|
||||||
url = urlOrFile.substr(0, urlOrFile.length - 2) + "css";
|
url = urlOrFile.substr(0, urlOrFile.length - 2) + "css";
|
||||||
} else {
|
} else {
|
||||||
url = new URL(urlOrFile, relativeTo ?? `${window.location.protocol}//${window.location.host}`).toString();
|
url = new URL(
|
||||||
|
urlOrFile,
|
||||||
|
relativeTo ?? `${window.location.protocol}//${window.location.host}`
|
||||||
|
).toString();
|
||||||
}
|
}
|
||||||
$el("link", {
|
$el("link", {
|
||||||
parent: document.head,
|
parent: document.head,
|
||||||
@@ -103,7 +117,6 @@ export async function addStylesheet(urlOrFile: string, relativeTo?: string): Pro
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param { string } filename
|
* @param { string } filename
|
||||||
* @param { Blob } blob
|
* @param { Blob } blob
|
||||||
@@ -147,7 +160,10 @@ export function prop(target, name, defaultValue, onChanged) {
|
|||||||
|
|
||||||
export function getStorageValue(id) {
|
export function getStorageValue(id) {
|
||||||
const clientId = api.clientId ?? api.initialClientId;
|
const clientId = api.clientId ?? api.initialClientId;
|
||||||
return (clientId && sessionStorage.getItem(`${id}:${clientId}`)) ?? localStorage.getItem(id);
|
return (
|
||||||
|
(clientId && sessionStorage.getItem(`${id}:${clientId}`)) ??
|
||||||
|
localStorage.getItem(id)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setStorageValue(id, value) {
|
export function setStorageValue(id, value) {
|
||||||
@@ -156,4 +172,4 @@ export function setStorageValue(id, value) {
|
|||||||
sessionStorage.setItem(`${id}:${clientId}`, value);
|
sessionStorage.setItem(`${id}:${clientId}`, value);
|
||||||
}
|
}
|
||||||
localStorage.setItem(id, value);
|
localStorage.setItem(id, value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import { api } from "./api"
|
import { api } from "./api";
|
||||||
import "./domWidget";
|
import "./domWidget";
|
||||||
import type { ComfyApp } from "./app";
|
import type { ComfyApp } from "./app";
|
||||||
import type { IWidget, LGraphNode } from "/types/litegraph";
|
import type { IWidget, LGraphNode } from "/types/litegraph";
|
||||||
import { ComfyNodeDef } from "/types/apiTypes";
|
import { ComfyNodeDef } from "/types/apiTypes";
|
||||||
|
|
||||||
export type ComfyWidgetConstructor = (
|
export type ComfyWidgetConstructor = (
|
||||||
node: LGraphNode, inputName: string, inputData: ComfyNodeDef, app?: ComfyApp, widgetName?: string) =>
|
node: LGraphNode,
|
||||||
{widget: IWidget, minWidth?: number; minHeight?: number };
|
inputName: string,
|
||||||
|
inputData: ComfyNodeDef,
|
||||||
|
app?: ComfyApp,
|
||||||
|
widgetName?: string
|
||||||
|
) => { widget: IWidget; minWidth?: number; minHeight?: number };
|
||||||
|
|
||||||
let controlValueRunBefore = false;
|
let controlValueRunBefore = false;
|
||||||
export function updateControlWidgetLabel(widget) {
|
export function updateControlWidgetLabel(widget) {
|
||||||
let replacement = "after";
|
let replacement = "after";
|
||||||
let find = "before";
|
let find = "before";
|
||||||
if (controlValueRunBefore) {
|
if (controlValueRunBefore) {
|
||||||
[find, replacement] = [replacement, find]
|
[find, replacement] = [replacement, find];
|
||||||
}
|
}
|
||||||
widget.label = (widget.label ?? widget.name).replace(find, replacement);
|
widget.label = (widget.label ?? widget.name).replace(find, replacement);
|
||||||
}
|
}
|
||||||
@@ -22,9 +25,14 @@ export function updateControlWidgetLabel(widget) {
|
|||||||
const IS_CONTROL_WIDGET = Symbol();
|
const IS_CONTROL_WIDGET = Symbol();
|
||||||
const HAS_EXECUTED = Symbol();
|
const HAS_EXECUTED = Symbol();
|
||||||
|
|
||||||
function getNumberDefaults(inputData: ComfyNodeDef, defaultStep, precision, enable_rounding) {
|
function getNumberDefaults(
|
||||||
|
inputData: ComfyNodeDef,
|
||||||
|
defaultStep,
|
||||||
|
precision,
|
||||||
|
enable_rounding
|
||||||
|
) {
|
||||||
let defaultVal = inputData[1]["default"];
|
let defaultVal = inputData[1]["default"];
|
||||||
let { min, max, step, round} = inputData[1];
|
let { min, max, step, round } = inputData[1];
|
||||||
|
|
||||||
if (defaultVal == undefined) defaultVal = 0;
|
if (defaultVal == undefined) defaultVal = 0;
|
||||||
if (min == undefined) min = 0;
|
if (min == undefined) min = 0;
|
||||||
@@ -33,30 +41,52 @@ function getNumberDefaults(inputData: ComfyNodeDef, defaultStep, precision, enab
|
|||||||
// precision is the number of decimal places to show.
|
// precision is the number of decimal places to show.
|
||||||
// by default, display the the smallest number of decimal places such that changes of size step are visible.
|
// by default, display the the smallest number of decimal places such that changes of size step are visible.
|
||||||
if (precision == undefined) {
|
if (precision == undefined) {
|
||||||
precision = Math.max(-Math.floor(Math.log10(step)),0);
|
precision = Math.max(-Math.floor(Math.log10(step)), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enable_rounding && (round == undefined || round === true)) {
|
if (enable_rounding && (round == undefined || round === true)) {
|
||||||
// by default, round the value to those decimal places shown.
|
// by default, round the value to those decimal places shown.
|
||||||
round = Math.round(1000000*Math.pow(0.1,precision))/1000000;
|
round = Math.round(1000000 * Math.pow(0.1, precision)) / 1000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { val: defaultVal, config: { min, max, step: 10.0 * step, round, precision } };
|
return {
|
||||||
|
val: defaultVal,
|
||||||
|
config: { min, max, step: 10.0 * step, round, precision },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addValueControlWidget(node, targetWidget, defaultValue = "randomize", values, widgetName, inputData: ComfyNodeDef) {
|
export function addValueControlWidget(
|
||||||
|
node,
|
||||||
|
targetWidget,
|
||||||
|
defaultValue = "randomize",
|
||||||
|
values,
|
||||||
|
widgetName,
|
||||||
|
inputData: ComfyNodeDef
|
||||||
|
) {
|
||||||
let name = inputData[1]?.control_after_generate;
|
let name = inputData[1]?.control_after_generate;
|
||||||
if(typeof name !== "string") {
|
if (typeof name !== "string") {
|
||||||
name = widgetName;
|
name = widgetName;
|
||||||
}
|
}
|
||||||
const widgets = addValueControlWidgets(node, targetWidget, defaultValue, {
|
const widgets = addValueControlWidgets(
|
||||||
addFilterList: false,
|
node,
|
||||||
controlAfterGenerateName: name
|
targetWidget,
|
||||||
}, inputData);
|
defaultValue,
|
||||||
|
{
|
||||||
|
addFilterList: false,
|
||||||
|
controlAfterGenerateName: name,
|
||||||
|
},
|
||||||
|
inputData
|
||||||
|
);
|
||||||
return widgets[0];
|
return widgets[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addValueControlWidgets(node, targetWidget, defaultValue = "randomize", options, inputData: ComfyNodeDef) {
|
export function addValueControlWidgets(
|
||||||
|
node,
|
||||||
|
targetWidget,
|
||||||
|
defaultValue = "randomize",
|
||||||
|
options,
|
||||||
|
inputData: ComfyNodeDef
|
||||||
|
) {
|
||||||
if (!defaultValue) defaultValue = "randomize";
|
if (!defaultValue) defaultValue = "randomize";
|
||||||
if (!options) options = {};
|
if (!options) options = {};
|
||||||
|
|
||||||
@@ -67,10 +97,10 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
|
|||||||
} else if (typeof inputData?.[1]?.[defaultName] === "string") {
|
} else if (typeof inputData?.[1]?.[defaultName] === "string") {
|
||||||
name = inputData?.[1]?.[defaultName];
|
name = inputData?.[1]?.[defaultName];
|
||||||
} else if (inputData?.[1]?.control_prefix) {
|
} else if (inputData?.[1]?.control_prefix) {
|
||||||
name = inputData?.[1]?.control_prefix + " " + name
|
name = inputData?.[1]?.control_prefix + " " + name;
|
||||||
}
|
}
|
||||||
return name;
|
return name;
|
||||||
}
|
};
|
||||||
|
|
||||||
const widgets = [];
|
const widgets = [];
|
||||||
const valueControl = node.addWidget(
|
const valueControl = node.addWidget(
|
||||||
@@ -120,16 +150,23 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
|
|||||||
const regex = new RegExp(filter.substring(1, filter.length - 1));
|
const regex = new RegExp(filter.substring(1, filter.length - 1));
|
||||||
check = (item) => regex.test(item);
|
check = (item) => regex.test(item);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error constructing RegExp filter for node " + node.id, filter, error);
|
console.error(
|
||||||
|
"Error constructing RegExp filter for node " + node.id,
|
||||||
|
filter,
|
||||||
|
error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!check) {
|
if (!check) {
|
||||||
const lower = filter.toLocaleLowerCase();
|
const lower = filter.toLocaleLowerCase();
|
||||||
check = (item) => item.toLocaleLowerCase().includes(lower);
|
check = (item) => item.toLocaleLowerCase().includes(lower);
|
||||||
}
|
}
|
||||||
values = values.filter(item => check(item));
|
values = values.filter((item) => check(item));
|
||||||
if (!values.length && targetWidget.options.values.length) {
|
if (!values.length && targetWidget.options.values.length) {
|
||||||
console.warn("Filter for node " + node.id + " has filtered out all items", filter);
|
console.warn(
|
||||||
|
"Filter for node " + node.id + " has filtered out all items",
|
||||||
|
filter
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let current_index = values.indexOf(targetWidget.value);
|
let current_index = values.indexOf(targetWidget.value);
|
||||||
@@ -141,8 +178,8 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
|
|||||||
break;
|
break;
|
||||||
case "increment-wrap":
|
case "increment-wrap":
|
||||||
current_index += 1;
|
current_index += 1;
|
||||||
if ( current_index >= current_length ) {
|
if (current_index >= current_length) {
|
||||||
current_index = 0;
|
current_index = 0;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "decrement":
|
case "decrement":
|
||||||
@@ -181,7 +218,10 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
|
|||||||
targetWidget.value -= targetWidget.options.step / 10;
|
targetWidget.value -= targetWidget.options.step / 10;
|
||||||
break;
|
break;
|
||||||
case "randomize":
|
case "randomize":
|
||||||
targetWidget.value = Math.floor(Math.random() * range) * (targetWidget.options.step / 10) + min;
|
targetWidget.value =
|
||||||
|
Math.floor(Math.random() * range) *
|
||||||
|
(targetWidget.options.step / 10) +
|
||||||
|
min;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -190,8 +230,7 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
|
|||||||
* ranges and set them to min or max.*/
|
* ranges and set them to min or max.*/
|
||||||
if (targetWidget.value < min) targetWidget.value = min;
|
if (targetWidget.value < min) targetWidget.value = min;
|
||||||
|
|
||||||
if (targetWidget.value > max)
|
if (targetWidget.value > max) targetWidget.value = max;
|
||||||
targetWidget.value = max;
|
|
||||||
targetWidget.callback(targetWidget.value);
|
targetWidget.callback(targetWidget.value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -213,20 +252,39 @@ export function addValueControlWidgets(node, targetWidget, defaultValue = "rando
|
|||||||
};
|
};
|
||||||
|
|
||||||
return widgets;
|
return widgets;
|
||||||
};
|
}
|
||||||
|
|
||||||
function seedWidget(node, inputName, inputData: ComfyNodeDef, app, widgetName) {
|
function seedWidget(node, inputName, inputData: ComfyNodeDef, app, widgetName) {
|
||||||
const seed = createIntWidget(node, inputName, inputData, app, true);
|
const seed = createIntWidget(node, inputName, inputData, app, true);
|
||||||
const seedControl = addValueControlWidget(node, seed.widget, "randomize", undefined, widgetName, inputData);
|
const seedControl = addValueControlWidget(
|
||||||
|
node,
|
||||||
|
seed.widget,
|
||||||
|
"randomize",
|
||||||
|
undefined,
|
||||||
|
widgetName,
|
||||||
|
inputData
|
||||||
|
);
|
||||||
|
|
||||||
seed.widget.linkedWidgets = [seedControl];
|
seed.widget.linkedWidgets = [seedControl];
|
||||||
return seed;
|
return seed;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createIntWidget(node, inputName, inputData: ComfyNodeDef, app, isSeedInput: boolean = false) {
|
function createIntWidget(
|
||||||
|
node,
|
||||||
|
inputName,
|
||||||
|
inputData: ComfyNodeDef,
|
||||||
|
app,
|
||||||
|
isSeedInput: boolean = false
|
||||||
|
) {
|
||||||
const control = inputData[1]?.control_after_generate;
|
const control = inputData[1]?.control_after_generate;
|
||||||
if (!isSeedInput && control) {
|
if (!isSeedInput && control) {
|
||||||
return seedWidget(node, inputName, inputData, app, typeof control === "string" ? control : undefined);
|
return seedWidget(
|
||||||
|
node,
|
||||||
|
inputName,
|
||||||
|
inputData,
|
||||||
|
app,
|
||||||
|
typeof control === "string" ? control : undefined
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let widgetType = isSlider(inputData[1]["display"], app);
|
let widgetType = isSlider(inputData[1]["display"], app);
|
||||||
@@ -275,10 +333,10 @@ function addMultilineWidget(node, name, opts, app) {
|
|||||||
|
|
||||||
function isSlider(display, app) {
|
function isSlider(display, app) {
|
||||||
if (app.ui.settings.getSettingValue("Comfy.DisableSliders")) {
|
if (app.ui.settings.getSettingValue("Comfy.DisableSliders")) {
|
||||||
return "number"
|
return "number";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (display==="slider") ? "slider" : "number"
|
return display === "slider" ? "slider" : "number";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initWidgets(app) {
|
export function initWidgets(app) {
|
||||||
@@ -288,7 +346,8 @@ export function initWidgets(app) {
|
|||||||
type: "combo",
|
type: "combo",
|
||||||
defaultValue: "after",
|
defaultValue: "after",
|
||||||
options: ["before", "after"],
|
options: ["before", "after"],
|
||||||
tooltip: "Controls when widget values are updated (randomize/increment/decrement), either before the prompt is queued or after.",
|
tooltip:
|
||||||
|
"Controls when widget values are updated (randomize/increment/decrement), either before the prompt is queued or after.",
|
||||||
onChange(value) {
|
onChange(value) {
|
||||||
controlValueRunBefore = value === "before";
|
controlValueRunBefore = value === "before";
|
||||||
for (const n of app.graph._nodes) {
|
for (const n of app.graph._nodes) {
|
||||||
@@ -313,21 +372,41 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
|||||||
"INT:seed": seedWidget,
|
"INT:seed": seedWidget,
|
||||||
"INT:noise_seed": seedWidget,
|
"INT:noise_seed": seedWidget,
|
||||||
FLOAT(node, inputName, inputData: ComfyNodeDef, app) {
|
FLOAT(node, inputName, inputData: ComfyNodeDef, app) {
|
||||||
let widgetType: "number" | "slider" = isSlider(inputData[1]["display"], app);
|
let widgetType: "number" | "slider" = isSlider(
|
||||||
let precision = app.ui.settings.getSettingValue("Comfy.FloatRoundingPrecision");
|
inputData[1]["display"],
|
||||||
let disable_rounding = app.ui.settings.getSettingValue("Comfy.DisableFloatRounding")
|
app
|
||||||
|
);
|
||||||
|
let precision = app.ui.settings.getSettingValue(
|
||||||
|
"Comfy.FloatRoundingPrecision"
|
||||||
|
);
|
||||||
|
let disable_rounding = app.ui.settings.getSettingValue(
|
||||||
|
"Comfy.DisableFloatRounding"
|
||||||
|
);
|
||||||
if (precision == 0) precision = undefined;
|
if (precision == 0) precision = undefined;
|
||||||
const { val, config } = getNumberDefaults(inputData, 0.5, precision, !disable_rounding);
|
const { val, config } = getNumberDefaults(
|
||||||
return { widget: node.addWidget(widgetType, inputName, val,
|
inputData,
|
||||||
function (v) {
|
0.5,
|
||||||
if (config.round) {
|
precision,
|
||||||
this.value = Math.round((v + Number.EPSILON)/config.round)*config.round;
|
!disable_rounding
|
||||||
if (this.value > config.max) this.value = config.max;
|
);
|
||||||
if (this.value < config.min) this.value = config.min;
|
return {
|
||||||
} else {
|
widget: node.addWidget(
|
||||||
this.value = v;
|
widgetType,
|
||||||
}
|
inputName,
|
||||||
}, config) };
|
val,
|
||||||
|
function (v) {
|
||||||
|
if (config.round) {
|
||||||
|
this.value =
|
||||||
|
Math.round((v + Number.EPSILON) / config.round) * config.round;
|
||||||
|
if (this.value > config.max) this.value = config.max;
|
||||||
|
if (this.value < config.min) this.value = config.min;
|
||||||
|
} else {
|
||||||
|
this.value = v;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
config
|
||||||
|
),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
INT(node, inputName, inputData: ComfyNodeDef, app) {
|
INT(node, inputName, inputData: ComfyNodeDef, app) {
|
||||||
return createIntWidget(node, inputName, inputData, app);
|
return createIntWidget(node, inputName, inputData, app);
|
||||||
@@ -336,12 +415,9 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
|||||||
let defaultVal = false;
|
let defaultVal = false;
|
||||||
let options = {};
|
let options = {};
|
||||||
if (inputData[1]) {
|
if (inputData[1]) {
|
||||||
if (inputData[1].default)
|
if (inputData[1].default) defaultVal = inputData[1].default;
|
||||||
defaultVal = inputData[1].default;
|
if (inputData[1].label_on) options["on"] = inputData[1].label_on;
|
||||||
if (inputData[1].label_on)
|
if (inputData[1].label_off) options["off"] = inputData[1].label_off;
|
||||||
options["on"] = inputData[1].label_on;
|
|
||||||
if (inputData[1].label_off)
|
|
||||||
options["off"] = inputData[1].label_off;
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
widget: node.addWidget(
|
widget: node.addWidget(
|
||||||
@@ -349,8 +425,8 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
|||||||
inputName,
|
inputName,
|
||||||
defaultVal,
|
defaultVal,
|
||||||
() => {},
|
() => {},
|
||||||
options,
|
options
|
||||||
)
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
STRING(node, inputName, inputData: ComfyNodeDef, app) {
|
STRING(node, inputName, inputData: ComfyNodeDef, app) {
|
||||||
@@ -359,12 +435,19 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
|||||||
|
|
||||||
let res;
|
let res;
|
||||||
if (multiline) {
|
if (multiline) {
|
||||||
res = addMultilineWidget(node, inputName, { defaultVal, ...inputData[1] }, app);
|
res = addMultilineWidget(
|
||||||
|
node,
|
||||||
|
inputName,
|
||||||
|
{ defaultVal, ...inputData[1] },
|
||||||
|
app
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
res = { widget: node.addWidget("text", inputName, defaultVal, () => {}, {}) };
|
res = {
|
||||||
|
widget: node.addWidget("text", inputName, defaultVal, () => {}, {}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if(inputData[1].dynamicPrompts != undefined)
|
if (inputData[1].dynamicPrompts != undefined)
|
||||||
res.widget.dynamicPrompts = inputData[1].dynamicPrompts;
|
res.widget.dynamicPrompts = inputData[1].dynamicPrompts;
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
@@ -375,18 +458,35 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
|||||||
if (inputData[1] && inputData[1].default) {
|
if (inputData[1] && inputData[1].default) {
|
||||||
defaultValue = inputData[1].default;
|
defaultValue = inputData[1].default;
|
||||||
}
|
}
|
||||||
const res = { widget: node.addWidget("combo", inputName, defaultValue, () => {}, { values: type }) };
|
const res = {
|
||||||
|
widget: node.addWidget("combo", inputName, defaultValue, () => {}, {
|
||||||
|
values: type,
|
||||||
|
}),
|
||||||
|
};
|
||||||
if (inputData[1]?.control_after_generate) {
|
if (inputData[1]?.control_after_generate) {
|
||||||
// TODO make combo handle a widget node type?
|
// TODO make combo handle a widget node type?
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
res.widget.linkedWidgets = addValueControlWidgets(node, res.widget, undefined, undefined, inputData);
|
res.widget.linkedWidgets = addValueControlWidgets(
|
||||||
|
node,
|
||||||
|
res.widget,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
inputData
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
IMAGEUPLOAD(node: LGraphNode, inputName: string, inputData: ComfyNodeDef, app) {
|
IMAGEUPLOAD(
|
||||||
|
node: LGraphNode,
|
||||||
|
inputName: string,
|
||||||
|
inputData: ComfyNodeDef,
|
||||||
|
app
|
||||||
|
) {
|
||||||
// TODO make image upload handle a custom node type?
|
// TODO make image upload handle a custom node type?
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const imageWidget = node.widgets.find((w) => w.name === (inputData[1]?.widget ?? "image"));
|
const imageWidget = node.widgets.find(
|
||||||
|
(w) => w.name === (inputData[1]?.widget ?? "image")
|
||||||
|
);
|
||||||
let uploadWidget;
|
let uploadWidget;
|
||||||
|
|
||||||
function showImage(name) {
|
function showImage(name) {
|
||||||
@@ -402,18 +502,20 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
|||||||
subfolder = name.substring(0, folder_separator);
|
subfolder = name.substring(0, folder_separator);
|
||||||
name = name.substring(folder_separator + 1);
|
name = name.substring(folder_separator + 1);
|
||||||
}
|
}
|
||||||
img.src = api.apiURL(`/view?filename=${encodeURIComponent(name)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`);
|
img.src = api.apiURL(
|
||||||
|
`/view?filename=${encodeURIComponent(name)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}${app.getRandParam()}`
|
||||||
|
);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
node.setSizeForImage?.();
|
node.setSizeForImage?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
var default_value = imageWidget.value;
|
var default_value = imageWidget.value;
|
||||||
Object.defineProperty(imageWidget, "value", {
|
Object.defineProperty(imageWidget, "value", {
|
||||||
set : function(value) {
|
set: function (value) {
|
||||||
this._real_value = value;
|
this._real_value = value;
|
||||||
},
|
},
|
||||||
|
|
||||||
get : function() {
|
get: function () {
|
||||||
if (!this._real_value) {
|
if (!this._real_value) {
|
||||||
return default_value;
|
return default_value;
|
||||||
}
|
}
|
||||||
@@ -428,11 +530,11 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
|||||||
|
|
||||||
value += real_value.filename;
|
value += real_value.filename;
|
||||||
|
|
||||||
if(real_value.type && real_value.type !== "input")
|
if (real_value.type && real_value.type !== "input")
|
||||||
value += ` [${real_value.type}]`;
|
value += ` [${real_value.type}]`;
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add our own callback to the combo widget to render an image when it changes
|
// Add our own callback to the combo widget to render an image when it changes
|
||||||
@@ -535,15 +637,15 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
node.pasteFile = function(file) {
|
node.pasteFile = function (file) {
|
||||||
if (file.type.startsWith("image/")) {
|
if (file.type.startsWith("image/")) {
|
||||||
const is_pasted = (file.name === "image.png") &&
|
const is_pasted =
|
||||||
(file.lastModified - Date.now() < 2000);
|
file.name === "image.png" && file.lastModified - Date.now() < 2000;
|
||||||
uploadFile(file, true, is_pasted);
|
uploadFile(file, true, is_pasted);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
return { widget: uploadWidget };
|
return { widget: uploadWidget };
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -57,7 +57,10 @@ export class ComfyWorkflowManager extends EventTarget {
|
|||||||
#bindExecutionEvents() {
|
#bindExecutionEvents() {
|
||||||
// TODO: on reload, set active prompt based on the latest ws message
|
// TODO: on reload, set active prompt based on the latest ws message
|
||||||
|
|
||||||
const emit = () => this.dispatchEvent(new CustomEvent("execute", { detail: this.activePrompt }));
|
const emit = () =>
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("execute", { detail: this.activePrompt })
|
||||||
|
);
|
||||||
let executing = null;
|
let executing = null;
|
||||||
api.addEventListener("execution_start", (e) => {
|
api.addEventListener("execution_start", (e) => {
|
||||||
this.#activePromptId = e.detail.prompt_id;
|
this.#activePromptId = e.detail.prompt_id;
|
||||||
@@ -106,14 +109,21 @@ export class ComfyWorkflowManager extends EventTarget {
|
|||||||
favorites = new Set();
|
favorites = new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflows = (await api.listUserData("workflows", true, true)).map((w) => {
|
const workflows = (await api.listUserData("workflows", true, true)).map(
|
||||||
let workflow = this.workflowLookup[w[0]];
|
(w) => {
|
||||||
if (!workflow) {
|
let workflow = this.workflowLookup[w[0]];
|
||||||
workflow = new ComfyWorkflow(this, w[0], w.slice(1), favorites.has(w[0]));
|
if (!workflow) {
|
||||||
this.workflowLookup[workflow.path] = workflow;
|
workflow = new ComfyWorkflow(
|
||||||
|
this,
|
||||||
|
w[0],
|
||||||
|
w.slice(1),
|
||||||
|
favorites.has(w[0])
|
||||||
|
);
|
||||||
|
this.workflowLookup[workflow.path] = workflow;
|
||||||
|
}
|
||||||
|
return workflow;
|
||||||
}
|
}
|
||||||
return workflow;
|
);
|
||||||
});
|
|
||||||
|
|
||||||
this.workflows = workflows;
|
this.workflows = workflows;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -124,7 +134,9 @@ export class ComfyWorkflowManager extends EventTarget {
|
|||||||
|
|
||||||
async saveWorkflowMetadata() {
|
async saveWorkflowMetadata() {
|
||||||
await api.storeUserData("workflows/.index.json", {
|
await api.storeUserData("workflows/.index.json", {
|
||||||
favorites: [...this.workflows.filter((w) => w.isFavorite).map((w) => w.path)],
|
favorites: [
|
||||||
|
...this.workflows.filter((w) => w.isFavorite).map((w) => w.path),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,13 +149,20 @@ export class ComfyWorkflowManager extends EventTarget {
|
|||||||
const found = this.workflows.find((w) => w.path === workflow);
|
const found = this.workflows.find((w) => w.path === workflow);
|
||||||
if (found) {
|
if (found) {
|
||||||
workflow = found;
|
workflow = found;
|
||||||
workflow.unsaved = !workflow || getStorageValue("Comfy.PreviousWorkflowUnsaved") === "true";
|
workflow.unsaved =
|
||||||
|
!workflow ||
|
||||||
|
getStorageValue("Comfy.PreviousWorkflowUnsaved") === "true";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(workflow instanceof ComfyWorkflow)) {
|
if (!(workflow instanceof ComfyWorkflow)) {
|
||||||
// Still not found, either reloading a deleted workflow or blank
|
// Still not found, either reloading a deleted workflow or blank
|
||||||
workflow = new ComfyWorkflow(this, workflow || "Unsaved Workflow" + (this.#unsavedCount++ ? ` (${this.#unsavedCount})` : ""));
|
workflow = new ComfyWorkflow(
|
||||||
|
this,
|
||||||
|
workflow ||
|
||||||
|
"Unsaved Workflow" +
|
||||||
|
(this.#unsavedCount++ ? ` (${this.#unsavedCount})` : "")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = this.openWorkflows.indexOf(workflow);
|
const index = this.openWorkflows.indexOf(workflow);
|
||||||
@@ -293,7 +312,9 @@ export class ComfyWorkflow {
|
|||||||
async getWorkflowData() {
|
async getWorkflowData() {
|
||||||
const resp = await api.getUserData("workflows/" + this.path);
|
const resp = await api.getUserData("workflows/" + this.path);
|
||||||
if (resp.status !== 200) {
|
if (resp.status !== 200) {
|
||||||
alert(`Error loading workflow file '${this.path}': ${resp.status} ${resp.statusText}`);
|
alert(
|
||||||
|
`Error loading workflow file '${this.path}': ${resp.status} ${resp.statusText}`
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return await resp.json();
|
return await resp.json();
|
||||||
@@ -301,7 +322,12 @@ export class ComfyWorkflow {
|
|||||||
|
|
||||||
load = async () => {
|
load = async () => {
|
||||||
if (this.isOpen) {
|
if (this.isOpen) {
|
||||||
await this.manager.app.loadGraphData(this.changeTracker.activeState, true, true, this);
|
await this.manager.app.loadGraphData(
|
||||||
|
this.changeTracker.activeState,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
this
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const data = await this.getWorkflowData();
|
const data = await this.getWorkflowData();
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
@@ -327,7 +353,12 @@ export class ComfyWorkflow {
|
|||||||
await this.manager.saveWorkflowMetadata();
|
await this.manager.saveWorkflowMetadata();
|
||||||
this.manager.dispatchEvent(new CustomEvent("favorite", { detail: this }));
|
this.manager.dispatchEvent(new CustomEvent("favorite", { detail: this }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert("Error favoriting workflow " + this.path + "\n" + (error.message ?? error));
|
alert(
|
||||||
|
"Error favoriting workflow " +
|
||||||
|
this.path +
|
||||||
|
"\n" +
|
||||||
|
(error.message ?? error)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,15 +367,29 @@ export class ComfyWorkflow {
|
|||||||
*/
|
*/
|
||||||
async rename(path) {
|
async rename(path) {
|
||||||
path = appendJsonExt(path);
|
path = appendJsonExt(path);
|
||||||
let resp = await api.moveUserData("workflows/" + this.path, "workflows/" + path);
|
let resp = await api.moveUserData(
|
||||||
|
"workflows/" + this.path,
|
||||||
|
"workflows/" + path
|
||||||
|
);
|
||||||
|
|
||||||
if (resp.status === 409) {
|
if (resp.status === 409) {
|
||||||
if (!confirm(`Workflow '${path}' already exists, do you want to overwrite it?`)) return resp;
|
if (
|
||||||
resp = await api.moveUserData("workflows/" + this.path, "workflows/" + path, { overwrite: true });
|
!confirm(
|
||||||
|
`Workflow '${path}' already exists, do you want to overwrite it?`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return resp;
|
||||||
|
resp = await api.moveUserData(
|
||||||
|
"workflows/" + this.path,
|
||||||
|
"workflows/" + path,
|
||||||
|
{ overwrite: true }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resp.status !== 200) {
|
if (resp.status !== 200) {
|
||||||
alert(`Error renaming workflow file '${this.path}': ${resp.status} ${resp.statusText}`);
|
alert(
|
||||||
|
`Error renaming workflow file '${this.path}': ${resp.status} ${resp.statusText}`
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,7 +412,10 @@ export class ComfyWorkflow {
|
|||||||
|
|
||||||
const old = localStorage.getItem("litegrapheditor_clipboard");
|
const old = localStorage.getItem("litegrapheditor_clipboard");
|
||||||
const graph = new LGraph(data);
|
const graph = new LGraph(data);
|
||||||
const canvas = new LGraphCanvas(null, graph, { skip_events: true, skip_render: true });
|
const canvas = new LGraphCanvas(null, graph, {
|
||||||
|
skip_events: true,
|
||||||
|
skip_render: true,
|
||||||
|
});
|
||||||
canvas.selectNodes();
|
canvas.selectNodes();
|
||||||
canvas.copyToClipboard();
|
canvas.copyToClipboard();
|
||||||
this.manager.app.canvas.pasteFromClipboard();
|
this.manager.app.canvas.pasteFromClipboard();
|
||||||
@@ -406,7 +454,10 @@ export class ComfyWorkflow {
|
|||||||
*/
|
*/
|
||||||
async #save(path, overwrite) {
|
async #save(path, overwrite) {
|
||||||
if (!path) {
|
if (!path) {
|
||||||
path = prompt("Save workflow as:", trimJsonExt(this.path) ?? this.name ?? "workflow");
|
path = prompt(
|
||||||
|
"Save workflow as:",
|
||||||
|
trimJsonExt(this.path) ?? this.name ?? "workflow"
|
||||||
|
);
|
||||||
if (!path) return;
|
if (!path) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,14 +465,27 @@ export class ComfyWorkflow {
|
|||||||
|
|
||||||
const p = await this.manager.app.graphToPrompt();
|
const p = await this.manager.app.graphToPrompt();
|
||||||
const json = JSON.stringify(p.workflow, null, 2);
|
const json = JSON.stringify(p.workflow, null, 2);
|
||||||
let resp = await api.storeUserData("workflows/" + path, json, { stringify: false, throwOnError: false, overwrite });
|
let resp = await api.storeUserData("workflows/" + path, json, {
|
||||||
|
stringify: false,
|
||||||
|
throwOnError: false,
|
||||||
|
overwrite,
|
||||||
|
});
|
||||||
if (resp.status === 409) {
|
if (resp.status === 409) {
|
||||||
if (!confirm(`Workflow '${path}' already exists, do you want to overwrite it?`)) return;
|
if (
|
||||||
resp = await api.storeUserData("workflows/" + path, json, { stringify: false });
|
!confirm(
|
||||||
|
`Workflow '${path}' already exists, do you want to overwrite it?`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
resp = await api.storeUserData("workflows/" + path, json, {
|
||||||
|
stringify: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resp.status !== 200) {
|
if (resp.status !== 200) {
|
||||||
alert(`Error saving workflow '${this.path}': ${resp.status} ${resp.statusText}`);
|
alert(
|
||||||
|
`Error saving workflow '${this.path}': ${resp.status} ${resp.statusText}`
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,107 +7,113 @@ const zQueueIndex = z.number();
|
|||||||
const zPromptId = z.string();
|
const zPromptId = z.string();
|
||||||
|
|
||||||
const zPromptItem = z.object({
|
const zPromptItem = z.object({
|
||||||
inputs: z.record(z.string(), z.any()),
|
inputs: z.record(z.string(), z.any()),
|
||||||
class_type: zNodeType,
|
class_type: zNodeType,
|
||||||
});
|
});
|
||||||
|
|
||||||
const zPrompt = z.array(zPromptItem);
|
const zPrompt = z.array(zPromptItem);
|
||||||
|
|
||||||
const zExtraPngInfo = z.object({
|
const zExtraPngInfo = z
|
||||||
|
.object({
|
||||||
workflow: zComfyWorkflow,
|
workflow: zComfyWorkflow,
|
||||||
}).passthrough();
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
const zExtraData = z.object({
|
const zExtraData = z.object({
|
||||||
extra_pnginfo: zExtraPngInfo,
|
extra_pnginfo: zExtraPngInfo,
|
||||||
client_id: z.string(),
|
client_id: z.string(),
|
||||||
});
|
});
|
||||||
const zOutputsToExecute = z.array(zNodeId);
|
const zOutputsToExecute = z.array(zNodeId);
|
||||||
|
|
||||||
const zExecutionStartMessage = z.tuple([
|
const zExecutionStartMessage = z.tuple([
|
||||||
z.literal("execution_start"),
|
z.literal("execution_start"),
|
||||||
z.object({
|
z.object({
|
||||||
prompt_id: zPromptId,
|
prompt_id: zPromptId,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const zExecutionCachedMessage = z.tuple([
|
const zExecutionCachedMessage = z.tuple([
|
||||||
z.literal("execution_cached"),
|
z.literal("execution_cached"),
|
||||||
z.object({
|
z.object({
|
||||||
prompt_id: zPromptId,
|
prompt_id: zPromptId,
|
||||||
nodes: z.array(zNodeId),
|
nodes: z.array(zNodeId),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const zExecutionInterruptedMessage = z.tuple([
|
const zExecutionInterruptedMessage = z.tuple([
|
||||||
z.literal("execution_interrupted"),
|
z.literal("execution_interrupted"),
|
||||||
z.object({
|
z.object({
|
||||||
// InterruptProcessingException
|
// InterruptProcessingException
|
||||||
prompt_id: zPromptId,
|
prompt_id: zPromptId,
|
||||||
node_id: zNodeId,
|
node_id: zNodeId,
|
||||||
node_type: zNodeType,
|
node_type: zNodeType,
|
||||||
executed: z.array(zNodeId),
|
executed: z.array(zNodeId),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const zExecutionErrorMessage = z.tuple([
|
const zExecutionErrorMessage = z.tuple([
|
||||||
z.literal("execution_error"),
|
z.literal("execution_error"),
|
||||||
z.object({
|
z.object({
|
||||||
prompt_id: zPromptId,
|
prompt_id: zPromptId,
|
||||||
node_id: zNodeId,
|
node_id: zNodeId,
|
||||||
node_type: zNodeType,
|
node_type: zNodeType,
|
||||||
executed: z.array(zNodeId),
|
executed: z.array(zNodeId),
|
||||||
|
|
||||||
exception_message: z.string(),
|
exception_message: z.string(),
|
||||||
exception_type: z.string(),
|
exception_type: z.string(),
|
||||||
traceback: z.string(),
|
traceback: z.string(),
|
||||||
current_inputs: z.any(),
|
current_inputs: z.any(),
|
||||||
current_outputs: z.any(),
|
current_outputs: z.any(),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const zStatusMessage = z.union([
|
const zStatusMessage = z.union([
|
||||||
zExecutionStartMessage,
|
zExecutionStartMessage,
|
||||||
zExecutionCachedMessage,
|
zExecutionCachedMessage,
|
||||||
zExecutionInterruptedMessage,
|
zExecutionInterruptedMessage,
|
||||||
zExecutionErrorMessage,
|
zExecutionErrorMessage,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const zStatus = z.object({
|
const zStatus = z.object({
|
||||||
status_str: z.enum(["success", "error"]),
|
status_str: z.enum(["success", "error"]),
|
||||||
completed: z.boolean(),
|
completed: z.boolean(),
|
||||||
messages: z.array(zStatusMessage),
|
messages: z.array(zStatusMessage),
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: this is a placeholder
|
// TODO: this is a placeholder
|
||||||
const zOutput = z.any();
|
const zOutput = z.any();
|
||||||
|
|
||||||
const zTaskPrompt = z.tuple([
|
const zTaskPrompt = z.tuple([
|
||||||
zQueueIndex,
|
zQueueIndex,
|
||||||
zPromptId,
|
zPromptId,
|
||||||
zPrompt,
|
zPrompt,
|
||||||
zExtraData,
|
zExtraData,
|
||||||
zOutputsToExecute,
|
zOutputsToExecute,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const zRunningTaskItem = z.object({
|
const zRunningTaskItem = z.object({
|
||||||
prompt: zTaskPrompt,
|
prompt: zTaskPrompt,
|
||||||
remove: z.object({
|
remove: z.object({
|
||||||
name: z.literal("Cancel"),
|
name: z.literal("Cancel"),
|
||||||
cb: z.function(),
|
cb: z.function(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const zPendingTaskItem = z.object({
|
const zPendingTaskItem = z.object({
|
||||||
prompt: zTaskPrompt,
|
prompt: zTaskPrompt,
|
||||||
});
|
});
|
||||||
|
|
||||||
const zHistoryTaskItem = z.object({
|
const zHistoryTaskItem = z.object({
|
||||||
prompt: zTaskPrompt,
|
prompt: zTaskPrompt,
|
||||||
status: zStatus.optional(),
|
status: zStatus.optional(),
|
||||||
outputs: z.record(zNodeId, zOutput),
|
outputs: z.record(zNodeId, zOutput),
|
||||||
});
|
});
|
||||||
|
|
||||||
const zTaskItem = z.union([zRunningTaskItem, zPendingTaskItem, zHistoryTaskItem]);
|
const zTaskItem = z.union([
|
||||||
|
zRunningTaskItem,
|
||||||
|
zPendingTaskItem,
|
||||||
|
zHistoryTaskItem,
|
||||||
|
]);
|
||||||
|
|
||||||
// `/queue`
|
// `/queue`
|
||||||
export type RunningTaskItem = z.infer<typeof zRunningTaskItem>;
|
export type RunningTaskItem = z.infer<typeof zRunningTaskItem>;
|
||||||
@@ -119,101 +125,100 @@ export type TaskItem = z.infer<typeof zTaskItem>;
|
|||||||
// TODO: validate `/history` `/queue` API endpoint responses.
|
// TODO: validate `/history` `/queue` API endpoint responses.
|
||||||
|
|
||||||
function inputSpec(spec: [ZodType, ZodType]): ZodType {
|
function inputSpec(spec: [ZodType, ZodType]): ZodType {
|
||||||
const [inputType, inputSpec] = spec;
|
const [inputType, inputSpec] = spec;
|
||||||
return z.union([
|
return z.union([z.tuple([inputType, inputSpec]), z.tuple([inputType])]);
|
||||||
z.tuple([inputType, inputSpec]),
|
|
||||||
z.tuple([inputType]),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const zIntInputSpec = inputSpec([
|
const zIntInputSpec = inputSpec([
|
||||||
z.literal("INT"),
|
z.literal("INT"),
|
||||||
z.object({
|
z.object({
|
||||||
min: z.number().optional(),
|
min: z.number().optional(),
|
||||||
max: z.number().optional(),
|
max: z.number().optional(),
|
||||||
step: z.number().optional(),
|
step: z.number().optional(),
|
||||||
default: z.number().optional(),
|
default: z.number().optional(),
|
||||||
forceInput: z.boolean().optional(),
|
forceInput: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const zFloatInputSpec = inputSpec([
|
const zFloatInputSpec = inputSpec([
|
||||||
z.literal("FLOAT"),
|
z.literal("FLOAT"),
|
||||||
z.object({
|
z.object({
|
||||||
min: z.number().optional(),
|
min: z.number().optional(),
|
||||||
max: z.number().optional(),
|
max: z.number().optional(),
|
||||||
step: z.number().optional(),
|
step: z.number().optional(),
|
||||||
round: z.number().optional(),
|
round: z.number().optional(),
|
||||||
default: z.number().optional(),
|
default: z.number().optional(),
|
||||||
forceInput: z.boolean().optional(),
|
forceInput: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const zBooleanInputSpec = inputSpec([
|
const zBooleanInputSpec = inputSpec([
|
||||||
z.literal("BOOLEAN"),
|
z.literal("BOOLEAN"),
|
||||||
z.object({
|
z.object({
|
||||||
label_on: z.string().optional(),
|
label_on: z.string().optional(),
|
||||||
label_off: z.string().optional(),
|
label_off: z.string().optional(),
|
||||||
default: z.boolean().optional(),
|
default: z.boolean().optional(),
|
||||||
forceInput: z.boolean().optional(),
|
forceInput: z.boolean().optional(),
|
||||||
})
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const zStringInputSpec = inputSpec([
|
const zStringInputSpec = inputSpec([
|
||||||
z.literal("STRING"),
|
z.literal("STRING"),
|
||||||
z.object({
|
z.object({
|
||||||
default: z.string().optional(),
|
default: z.string().optional(),
|
||||||
multiline: z.boolean().optional(),
|
multiline: z.boolean().optional(),
|
||||||
dynamicPrompts: z.boolean().optional(),
|
dynamicPrompts: z.boolean().optional(),
|
||||||
forceInput: z.boolean().optional(),
|
forceInput: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Dropdown Selection.
|
// Dropdown Selection.
|
||||||
const zComboInputSpec = inputSpec([
|
const zComboInputSpec = inputSpec([
|
||||||
z.array(z.any()),
|
z.array(z.any()),
|
||||||
z.object({
|
z.object({
|
||||||
default: z.any().optional(),
|
default: z.any().optional(),
|
||||||
control_after_generate: z.boolean().optional(),
|
control_after_generate: z.boolean().optional(),
|
||||||
image_upload: z.boolean().optional(),
|
image_upload: z.boolean().optional(),
|
||||||
forceInput: z.boolean().optional(),
|
forceInput: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const zCustomInputSpec = inputSpec([
|
const zCustomInputSpec = inputSpec([
|
||||||
z.string(),
|
z.string(),
|
||||||
z.object({
|
z.object({
|
||||||
default: z.any().optional(),
|
default: z.any().optional(),
|
||||||
forceInput: z.boolean().optional(),
|
forceInput: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const zInputSpec = z.union([
|
const zInputSpec = z.union([
|
||||||
zIntInputSpec,
|
zIntInputSpec,
|
||||||
zFloatInputSpec,
|
zFloatInputSpec,
|
||||||
zBooleanInputSpec,
|
zBooleanInputSpec,
|
||||||
zStringInputSpec,
|
zStringInputSpec,
|
||||||
zComboInputSpec,
|
zComboInputSpec,
|
||||||
zCustomInputSpec,
|
zCustomInputSpec,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const zComfyNodeDataType = z.string();
|
const zComfyNodeDataType = z.string();
|
||||||
const zComfyComboOutput = z.array(z.any());
|
const zComfyComboOutput = z.array(z.any());
|
||||||
const zComfyOutputSpec = z.array(z.union([zComfyNodeDataType, zComfyComboOutput]));
|
const zComfyOutputSpec = z.array(
|
||||||
|
z.union([zComfyNodeDataType, zComfyComboOutput])
|
||||||
|
);
|
||||||
|
|
||||||
const zComfyNodeDef = z.object({
|
const zComfyNodeDef = z.object({
|
||||||
input: z.object({
|
input: z.object({
|
||||||
required: z.record(zInputSpec).optional(),
|
required: z.record(zInputSpec).optional(),
|
||||||
optional: z.record(zInputSpec).optional(),
|
optional: z.record(zInputSpec).optional(),
|
||||||
}),
|
}),
|
||||||
output: zComfyOutputSpec,
|
output: zComfyOutputSpec,
|
||||||
output_is_list: z.array(z.boolean()),
|
output_is_list: z.array(z.boolean()),
|
||||||
output_name: z.array(z.string()),
|
output_name: z.array(z.string()),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
display_name: z.string(),
|
display_name: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
category: z.string(),
|
category: z.string(),
|
||||||
output_node: z.boolean(),
|
output_node: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// `/object_info`
|
// `/object_info`
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from "zod";
|
||||||
|
|
||||||
const nodeSlotSchema = z.object({
|
const nodeSlotSchema = z
|
||||||
|
.object({
|
||||||
BOOLEAN: z.string().optional(),
|
BOOLEAN: z.string().optional(),
|
||||||
CLIP: z.string(),
|
CLIP: z.string(),
|
||||||
CLIP_VISION: z.string(),
|
CLIP_VISION: z.string(),
|
||||||
@@ -25,10 +26,12 @@ const nodeSlotSchema = z.object({
|
|||||||
TAESD: z.string(),
|
TAESD: z.string(),
|
||||||
TIMESTEP_KEYFRAME: z.string().optional(),
|
TIMESTEP_KEYFRAME: z.string().optional(),
|
||||||
UPSCALE_MODEL: z.string().optional(),
|
UPSCALE_MODEL: z.string().optional(),
|
||||||
VAE: z.string()
|
VAE: z.string(),
|
||||||
}).passthrough();
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
const litegraphBaseSchema = z.object({
|
const litegraphBaseSchema = z
|
||||||
|
.object({
|
||||||
BACKGROUND_IMAGE: z.string(),
|
BACKGROUND_IMAGE: z.string(),
|
||||||
CLEAR_BACKGROUND_COLOR: z.string(),
|
CLEAR_BACKGROUND_COLOR: z.string(),
|
||||||
NODE_TITLE_COLOR: z.string(),
|
NODE_TITLE_COLOR: z.string(),
|
||||||
@@ -49,37 +52,40 @@ const litegraphBaseSchema = z.object({
|
|||||||
WIDGET_SECONDARY_TEXT_COLOR: z.string(),
|
WIDGET_SECONDARY_TEXT_COLOR: z.string(),
|
||||||
LINK_COLOR: z.string(),
|
LINK_COLOR: z.string(),
|
||||||
EVENT_LINK_COLOR: z.string(),
|
EVENT_LINK_COLOR: z.string(),
|
||||||
CONNECTING_LINK_COLOR: z.string()
|
CONNECTING_LINK_COLOR: z.string(),
|
||||||
}).passthrough();
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
const comfyBaseSchema = z.object({
|
const comfyBaseSchema = z.object({
|
||||||
["fg-color"]: z.string(),
|
["fg-color"]: z.string(),
|
||||||
["bg-color"]: z.string(),
|
["bg-color"]: z.string(),
|
||||||
["comfy-menu-bg"]: z.string(),
|
["comfy-menu-bg"]: z.string(),
|
||||||
["comfy-input-bg"]: z.string(),
|
["comfy-input-bg"]: z.string(),
|
||||||
["input-text"]: z.string(),
|
["input-text"]: z.string(),
|
||||||
["descrip-text"]: z.string(),
|
["descrip-text"]: z.string(),
|
||||||
["drag-text"]: z.string(),
|
["drag-text"]: z.string(),
|
||||||
["error-text"]: z.string(),
|
["error-text"]: z.string(),
|
||||||
["border-color"]: z.string(),
|
["border-color"]: z.string(),
|
||||||
["tr-even-bg-color"]: z.string(),
|
["tr-even-bg-color"]: z.string(),
|
||||||
["tr-odd-bg-color"]: z.string(),
|
["tr-odd-bg-color"]: z.string(),
|
||||||
["content-bg"]: z.string(),
|
["content-bg"]: z.string(),
|
||||||
["content-fg"]: z.string(),
|
["content-fg"]: z.string(),
|
||||||
["content-hover-bg"]: z.string(),
|
["content-hover-bg"]: z.string(),
|
||||||
["content-hover-fg"]: z.string(),
|
["content-hover-fg"]: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const colorsSchema = z.object({
|
const colorsSchema = z
|
||||||
|
.object({
|
||||||
node_slot: nodeSlotSchema,
|
node_slot: nodeSlotSchema,
|
||||||
litegraph_base: litegraphBaseSchema,
|
litegraph_base: litegraphBaseSchema,
|
||||||
comfy_base: comfyBaseSchema
|
comfy_base: comfyBaseSchema,
|
||||||
}).passthrough();
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
const paletteSchema = z.object({
|
const paletteSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
colors: colorsSchema
|
colors: colorsSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
const colorPalettesSchema = z.record(paletteSchema);
|
const colorPalettesSchema = z.record(paletteSchema);
|
||||||
|
|||||||
147
src/types/comfy.d.ts
vendored
147
src/types/comfy.d.ts
vendored
@@ -2,75 +2,90 @@ import { LGraphNode, IWidget } from "./litegraph";
|
|||||||
import { ComfyApp } from "../../scripts/app";
|
import { ComfyApp } from "../../scripts/app";
|
||||||
|
|
||||||
export interface ComfyExtension {
|
export interface ComfyExtension {
|
||||||
/**
|
/**
|
||||||
* The name of the extension
|
* The name of the extension
|
||||||
*/
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
/**
|
/**
|
||||||
* Allows any initialisation, e.g. loading resources. Called after the canvas is created but before nodes are added
|
* Allows any initialisation, e.g. loading resources. Called after the canvas is created but before nodes are added
|
||||||
* @param app The ComfyUI app instance
|
* @param app The ComfyUI app instance
|
||||||
*/
|
*/
|
||||||
init?(app: ComfyApp): Promise<void>;
|
init?(app: ComfyApp): Promise<void>;
|
||||||
/**
|
/**
|
||||||
* Allows any additonal setup, called after the application is fully set up and running
|
* Allows any additonal setup, called after the application is fully set up and running
|
||||||
* @param app The ComfyUI app instance
|
* @param app The ComfyUI app instance
|
||||||
*/
|
*/
|
||||||
setup?(app: ComfyApp): Promise<void>;
|
setup?(app: ComfyApp): Promise<void>;
|
||||||
/**
|
/**
|
||||||
* Called before nodes are registered with the graph
|
* Called before nodes are registered with the graph
|
||||||
* @param defs The collection of node definitions, add custom ones or edit existing ones
|
* @param defs The collection of node definitions, add custom ones or edit existing ones
|
||||||
* @param app The ComfyUI app instance
|
* @param app The ComfyUI app instance
|
||||||
*/
|
*/
|
||||||
addCustomNodeDefs?(defs: Record<string, ComfyObjectInfo>, app: ComfyApp): Promise<void>;
|
addCustomNodeDefs?(
|
||||||
/**
|
defs: Record<string, ComfyObjectInfo>,
|
||||||
* Allows the extension to add custom widgets
|
app: ComfyApp
|
||||||
* @param app The ComfyUI app instance
|
): Promise<void>;
|
||||||
* @returns An array of {[widget name]: widget data}
|
/**
|
||||||
*/
|
* Allows the extension to add custom widgets
|
||||||
getCustomWidgets?(
|
* @param app The ComfyUI app instance
|
||||||
app: ComfyApp
|
* @returns An array of {[widget name]: widget data}
|
||||||
): Promise<
|
*/
|
||||||
Record<string, (node, inputName, inputData, app) => { widget?: IWidget; minWidth?: number; minHeight?: number }>
|
getCustomWidgets?(
|
||||||
>;
|
app: ComfyApp
|
||||||
/**
|
): Promise<
|
||||||
* Allows the extension to add additional handling to the node before it is registered with LGraph
|
Record<
|
||||||
* @param nodeType The node class (not an instance)
|
string,
|
||||||
* @param nodeData The original node object info config object
|
(
|
||||||
* @param app The ComfyUI app instance
|
node,
|
||||||
*/
|
inputName,
|
||||||
beforeRegisterNodeDef?(nodeType: typeof LGraphNode, nodeData: ComfyObjectInfo, app: ComfyApp): Promise<void>;
|
inputData,
|
||||||
/**
|
app
|
||||||
* Allows the extension to register additional nodes with LGraph after standard nodes are added
|
) => { widget?: IWidget; minWidth?: number; minHeight?: number }
|
||||||
* @param app The ComfyUI app instance
|
>
|
||||||
*/
|
>;
|
||||||
registerCustomNodes?(app: ComfyApp): Promise<void>;
|
/**
|
||||||
/**
|
* Allows the extension to add additional handling to the node before it is registered with LGraph
|
||||||
* Allows the extension to modify a node that has been reloaded onto the graph.
|
* @param nodeType The node class (not an instance)
|
||||||
* If you break something in the backend and want to patch workflows in the frontend
|
* @param nodeData The original node object info config object
|
||||||
* This is the place to do this
|
* @param app The ComfyUI app instance
|
||||||
* @param node The node that has been loaded
|
*/
|
||||||
* @param app The ComfyUI app instance
|
beforeRegisterNodeDef?(
|
||||||
*/
|
nodeType: typeof LGraphNode,
|
||||||
loadedGraphNode?(node: LGraphNode, app: ComfyApp);
|
nodeData: ComfyObjectInfo,
|
||||||
/**
|
app: ComfyApp
|
||||||
* Allows the extension to run code after the constructor of the node
|
): Promise<void>;
|
||||||
* @param node The node that has been created
|
/**
|
||||||
* @param app The ComfyUI app instance
|
* Allows the extension to register additional nodes with LGraph after standard nodes are added
|
||||||
*/
|
* @param app The ComfyUI app instance
|
||||||
nodeCreated?(node: LGraphNode, app: ComfyApp);
|
*/
|
||||||
|
registerCustomNodes?(app: ComfyApp): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Allows the extension to modify a node that has been reloaded onto the graph.
|
||||||
|
* If you break something in the backend and want to patch workflows in the frontend
|
||||||
|
* This is the place to do this
|
||||||
|
* @param node The node that has been loaded
|
||||||
|
* @param app The ComfyUI app instance
|
||||||
|
*/
|
||||||
|
loadedGraphNode?(node: LGraphNode, app: ComfyApp);
|
||||||
|
/**
|
||||||
|
* Allows the extension to run code after the constructor of the node
|
||||||
|
* @param node The node that has been created
|
||||||
|
* @param app The ComfyUI app instance
|
||||||
|
*/
|
||||||
|
nodeCreated?(node: LGraphNode, app: ComfyApp);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ComfyObjectInfo = {
|
export type ComfyObjectInfo = {
|
||||||
name: string;
|
name: string;
|
||||||
display_name?: string;
|
display_name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
category: string;
|
category: string;
|
||||||
input?: {
|
input?: {
|
||||||
required?: Record<string, ComfyObjectInfoConfig>;
|
required?: Record<string, ComfyObjectInfoConfig>;
|
||||||
optional?: Record<string, ComfyObjectInfoConfig>;
|
optional?: Record<string, ComfyObjectInfoConfig>;
|
||||||
};
|
};
|
||||||
output?: string[];
|
output?: string[];
|
||||||
output_name: string[];
|
output_name: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ComfyObjectInfoConfig = [string | any[]] | [string | any[], any];
|
export type ComfyObjectInfoConfig = [string | any[]] | [string | any[], any];
|
||||||
|
|||||||
@@ -1,47 +1,56 @@
|
|||||||
import { z } from 'zod';
|
import { z } from "zod";
|
||||||
import { fromZodError } from 'zod-validation-error';
|
import { fromZodError } from "zod-validation-error";
|
||||||
|
|
||||||
const zComfyLink = z.tuple([
|
const zComfyLink = z.tuple([
|
||||||
z.number(), // Link id
|
z.number(), // Link id
|
||||||
z.number(), // Node id of source node
|
z.number(), // Node id of source node
|
||||||
z.number(), // Output slot# of source node
|
z.number(), // Output slot# of source node
|
||||||
z.number(), // Node id of destination node
|
z.number(), // Node id of destination node
|
||||||
z.number(), // Input slot# of destination node
|
z.number(), // Input slot# of destination node
|
||||||
z.string(), // Data type
|
z.string(), // Data type
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const zNodeOutput = z.object({
|
const zNodeOutput = z
|
||||||
|
.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
links: z.array(z.number()).nullable(),
|
links: z.array(z.number()).nullable(),
|
||||||
slot_index: z.number().optional(),
|
slot_index: z.number().optional(),
|
||||||
}).passthrough();
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
const zNodeInput = z.object({
|
const zNodeInput = z
|
||||||
|
.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
link: z.number().nullable(),
|
link: z.number().nullable(),
|
||||||
slot_index: z.number().optional(),
|
slot_index: z.number().optional(),
|
||||||
}).passthrough();
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
const zFlags = z.object({
|
const zFlags = z
|
||||||
|
.object({
|
||||||
collapsed: z.boolean().optional(),
|
collapsed: z.boolean().optional(),
|
||||||
pinned: z.boolean().optional(),
|
pinned: z.boolean().optional(),
|
||||||
allow_interaction: z.boolean().optional(),
|
allow_interaction: z.boolean().optional(),
|
||||||
horizontal: z.boolean().optional(),
|
horizontal: z.boolean().optional(),
|
||||||
skip_repeated_outputs: z.boolean().optional(),
|
skip_repeated_outputs: z.boolean().optional(),
|
||||||
}).passthrough();
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
const zProperties = z.object({
|
const zProperties = z
|
||||||
|
.object({
|
||||||
["Node name for S&R"]: z.string().optional(),
|
["Node name for S&R"]: z.string().optional(),
|
||||||
}).passthrough();
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
const zVector2 = z.union([
|
const zVector2 = z.union([
|
||||||
z.object({ 0: z.number(), 1: z.number() }),
|
z.object({ 0: z.number(), 1: z.number() }),
|
||||||
z.tuple([z.number(), z.number()]),
|
z.tuple([z.number(), z.number()]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const zComfyNode = z.object({
|
const zComfyNode = z
|
||||||
|
.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
pos: z.tuple([z.number(), z.number()]),
|
pos: z.tuple([z.number(), z.number()]),
|
||||||
@@ -52,20 +61,24 @@ const zComfyNode = z.object({
|
|||||||
inputs: z.array(zNodeInput).optional(),
|
inputs: z.array(zNodeInput).optional(),
|
||||||
outputs: z.array(zNodeOutput).optional(),
|
outputs: z.array(zNodeOutput).optional(),
|
||||||
properties: zProperties,
|
properties: zProperties,
|
||||||
widgets_values: z.array(z.any()).optional(), // This could contain mixed types
|
widgets_values: z.array(z.any()).optional(), // This could contain mixed types
|
||||||
color: z.string().optional(),
|
color: z.string().optional(),
|
||||||
bgcolor: z.string().optional(),
|
bgcolor: z.string().optional(),
|
||||||
}).passthrough();
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
const zGroup = z.object({
|
const zGroup = z
|
||||||
|
.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
bounding: z.tuple([z.number(), z.number(), z.number(), z.number()]),
|
bounding: z.tuple([z.number(), z.number(), z.number(), z.number()]),
|
||||||
color: z.string(),
|
color: z.string(),
|
||||||
font_size: z.number(),
|
font_size: z.number(),
|
||||||
locked: z.boolean().optional(),
|
locked: z.boolean().optional(),
|
||||||
}).passthrough();
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
const zInfo = z.object({
|
const zInfo = z
|
||||||
|
.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
author: z.string(),
|
author: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
@@ -73,24 +86,32 @@ const zInfo = z.object({
|
|||||||
created: z.string(),
|
created: z.string(),
|
||||||
modified: z.string(),
|
modified: z.string(),
|
||||||
software: z.string(),
|
software: z.string(),
|
||||||
}).passthrough();
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
const zDS = z.object({
|
const zDS = z
|
||||||
|
.object({
|
||||||
scale: z.number(),
|
scale: z.number(),
|
||||||
offset: zVector2,
|
offset: zVector2,
|
||||||
}).passthrough();
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
const zConfig = z.object({
|
const zConfig = z
|
||||||
|
.object({
|
||||||
links_ontop: z.boolean().optional(),
|
links_ontop: z.boolean().optional(),
|
||||||
align_to_grid: z.boolean().optional(),
|
align_to_grid: z.boolean().optional(),
|
||||||
}).passthrough();
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
const zExtra = z.object({
|
const zExtra = z
|
||||||
|
.object({
|
||||||
ds: zDS.optional(),
|
ds: zDS.optional(),
|
||||||
info: zInfo.optional(),
|
info: zInfo.optional(),
|
||||||
}).passthrough();
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
export const zComfyWorkflow = z.object({
|
export const zComfyWorkflow = z
|
||||||
|
.object({
|
||||||
last_node_id: z.number(),
|
last_node_id: z.number(),
|
||||||
last_link_id: z.number(),
|
last_link_id: z.number(),
|
||||||
nodes: z.array(zComfyNode),
|
nodes: z.array(zComfyNode),
|
||||||
@@ -99,7 +120,8 @@ export const zComfyWorkflow = z.object({
|
|||||||
config: zConfig.optional().nullable(),
|
config: zConfig.optional().nullable(),
|
||||||
extra: zExtra.optional().nullable(),
|
extra: zExtra.optional().nullable(),
|
||||||
version: z.number(),
|
version: z.number(),
|
||||||
}).passthrough();
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
export type NodeInput = z.infer<typeof zNodeInput>;
|
export type NodeInput = z.infer<typeof zNodeInput>;
|
||||||
export type NodeOutput = z.infer<typeof zNodeOutput>;
|
export type NodeOutput = z.infer<typeof zNodeOutput>;
|
||||||
@@ -107,15 +129,14 @@ export type ComfyLink = z.infer<typeof zComfyLink>;
|
|||||||
export type ComfyNode = z.infer<typeof zComfyNode>;
|
export type ComfyNode = z.infer<typeof zComfyNode>;
|
||||||
export type ComfyWorkflow = z.infer<typeof zComfyWorkflow>;
|
export type ComfyWorkflow = z.infer<typeof zComfyWorkflow>;
|
||||||
|
|
||||||
|
|
||||||
export async function parseComfyWorkflow(data: string): Promise<ComfyWorkflow> {
|
export async function parseComfyWorkflow(data: string): Promise<ComfyWorkflow> {
|
||||||
// Validate
|
// Validate
|
||||||
const result = await zComfyWorkflow.safeParseAsync(JSON.parse(data));
|
const result = await zComfyWorkflow.safeParseAsync(JSON.parse(data));
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
// TODO: Pretty print the error on UI modal.
|
// TODO: Pretty print the error on UI modal.
|
||||||
const error = fromZodError(result.error);
|
const error = fromZodError(result.error);
|
||||||
alert(`Invalid workflow against zod schema:\n${error}`);
|
alert(`Invalid workflow against zod schema:\n${error}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|||||||
2767
src/types/litegraph.d.ts
vendored
2767
src/types/litegraph.d.ts
vendored
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user