Merge pull request #28 from space-nuko/output-pane

img2img
This commit is contained in:
space-nuko
2023-05-08 15:30:58 -05:00
committed by GitHub
52 changed files with 7910 additions and 4072 deletions

2
gradio

Submodule gradio updated: 759fb3b1f2...bebfb72b35

View File

@@ -47,6 +47,7 @@
"@litegraph-ts/core": "workspace:*", "@litegraph-ts/core": "workspace:*",
"@litegraph-ts/nodes-basic": "workspace:*", "@litegraph-ts/nodes-basic": "workspace:*",
"@litegraph-ts/nodes-events": "workspace:*", "@litegraph-ts/nodes-events": "workspace:*",
"@litegraph-ts/nodes-logic": "workspace:*",
"@litegraph-ts/nodes-math": "workspace:*", "@litegraph-ts/nodes-math": "workspace:*",
"@litegraph-ts/nodes-strings": "workspace:*", "@litegraph-ts/nodes-strings": "workspace:*",
"@litegraph-ts/tsconfig": "workspace:*", "@litegraph-ts/tsconfig": "workspace:*",

359
pnpm-lock.yaml generated
View File

@@ -46,6 +46,9 @@ importers:
'@litegraph-ts/nodes-events': '@litegraph-ts/nodes-events':
specifier: workspace:* specifier: workspace:*
version: link:litegraph/packages/nodes-events version: link:litegraph/packages/nodes-events
'@litegraph-ts/nodes-logic':
specifier: workspace:*
version: link:litegraph/packages/nodes-logic
'@litegraph-ts/nodes-math': '@litegraph-ts/nodes-math':
specifier: workspace:* specifier: workspace:*
version: link:litegraph/packages/nodes-math version: link:litegraph/packages/nodes-math
@@ -141,7 +144,18 @@ importers:
specifier: ^0.25.8 specifier: ^0.25.8
version: 0.25.8(sass@1.61.0) version: 0.25.8(sass@1.61.0)
gradio/client/js: {} gradio/client/js:
dependencies:
ws:
specifier: ^8.13.0
version: 8.13.0
devDependencies:
'@types/ws':
specifier: ^8.5.4
version: 8.5.4
esbuild:
specifier: ^0.17.14
version: 0.17.18
gradio/js/_cdn-test: gradio/js/_cdn-test:
devDependencies: devDependencies:
@@ -246,12 +260,6 @@ importers:
postcss-prefix-selector: postcss-prefix-selector:
specifier: ^1.16.0 specifier: ^1.16.0
version: 1.16.0(postcss@8.4.21) version: 1.16.0(postcss@8.4.21)
svelte:
specifier: ^3.25.1
version: 3.58.0
svelte-i18n:
specifier: ^3.3.13
version: 3.3.13(svelte@3.58.0)
gradio/js/atoms: gradio/js/atoms:
dependencies: dependencies:
@@ -311,7 +319,7 @@ importers:
version: 4.0.2 version: 4.0.2
d3-shape: d3-shape:
specifier: ^3.1.0 specifier: ^3.1.0
version: 3.1.0 version: 3.2.0
devDependencies: devDependencies:
'@types/d3-dsv': '@types/d3-dsv':
specifier: ^3.0.0 specifier: ^3.0.0
@@ -339,46 +347,46 @@ importers:
dependencies: dependencies:
'@codemirror/autocomplete': '@codemirror/autocomplete':
specifier: ^6.3.0 specifier: ^6.3.0
version: 6.3.0(@codemirror/language@6.6.0)(@codemirror/state@6.1.2)(@codemirror/view@6.4.1)(@lezer/common@1.0.2) version: 6.6.1(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/common@1.0.2)
'@codemirror/commands': '@codemirror/commands':
specifier: ^6.1.2 specifier: ^6.1.2
version: 6.1.2 version: 6.2.4
'@codemirror/lang-css': '@codemirror/lang-css':
specifier: ^6.1.0 specifier: ^6.1.0
version: 6.1.0(@codemirror/view@6.4.1)(@lezer/common@1.0.2) version: 6.2.0(@codemirror/view@6.11.0)
'@codemirror/lang-html': '@codemirror/lang-html':
specifier: ^6.4.2 specifier: ^6.4.2
version: 6.4.2 version: 6.4.3
'@codemirror/lang-javascript': '@codemirror/lang-javascript':
specifier: ^6.1.4 specifier: ^6.1.4
version: 6.1.4 version: 6.1.7
'@codemirror/lang-json': '@codemirror/lang-json':
specifier: ^6.0.1 specifier: ^6.0.1
version: 6.0.1 version: 6.0.1
'@codemirror/lang-markdown': '@codemirror/lang-markdown':
specifier: ^6.1.0 specifier: ^6.1.0
version: 6.1.0 version: 6.1.1
'@codemirror/lang-python': '@codemirror/lang-python':
specifier: ^6.0.4 specifier: ^6.0.4
version: 6.0.4 version: 6.1.2(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/common@1.0.2)
'@codemirror/language': '@codemirror/language':
specifier: ^6.6.0 specifier: ^6.6.0
version: 6.6.0 version: 6.6.0
'@codemirror/legacy-modes': '@codemirror/legacy-modes':
specifier: ^6.3.1 specifier: ^6.3.1
version: 6.3.1 version: 6.3.2
'@codemirror/lint': '@codemirror/lint':
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0 version: 6.2.1
'@codemirror/search': '@codemirror/search':
specifier: ^6.2.2 specifier: ^6.2.2
version: 6.2.2 version: 6.4.0
'@codemirror/state': '@codemirror/state':
specifier: ^6.1.2 specifier: ^6.1.2
version: 6.1.2 version: 6.2.0
'@codemirror/view': '@codemirror/view':
specifier: ^6.4.1 specifier: ^6.4.1
version: 6.4.1 version: 6.11.0
'@gradio/atoms': '@gradio/atoms':
specifier: workspace:^0.0.1 specifier: workspace:^0.0.1
version: link:../atoms version: link:../atoms
@@ -393,16 +401,16 @@ importers:
version: 1.0.2 version: 1.0.2
'@lezer/highlight': '@lezer/highlight':
specifier: ^1.1.3 specifier: ^1.1.3
version: 1.1.3 version: 1.1.4
'@lezer/markdown': '@lezer/markdown':
specifier: ^1.0.2 specifier: ^1.0.2
version: 1.0.2 version: 1.0.2
cm6-theme-basic-dark: cm6-theme-basic-dark:
specifier: ^0.2.0 specifier: ^0.2.0
version: 0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.1.2)(@codemirror/view@6.4.1)(@lezer/highlight@1.1.3) version: 0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/highlight@1.1.4)
cm6-theme-basic-light: cm6-theme-basic-light:
specifier: ^0.2.0 specifier: ^0.2.0
version: 0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.1.2)(@codemirror/view@6.4.1)(@lezer/highlight@1.1.3) version: 0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/highlight@1.1.4)
codemirror: codemirror:
specifier: ^6.0.1 specifier: ^6.0.1
version: 6.0.1(@lezer/common@1.0.2) version: 6.0.1(@lezer/common@1.0.2)
@@ -721,19 +729,19 @@ importers:
version: 8.4.21 version: 8.4.21
postcss-load-config: postcss-load-config:
specifier: ^3.1.1 specifier: ^3.1.1
version: 3.1.1 version: 3.1.4(postcss@8.4.21)
svelte-check: svelte-check:
specifier: ^2.2.6 specifier: ^2.2.6
version: 2.2.6(postcss-load-config@3.1.1)(postcss@8.4.21)(svelte@3.58.0) version: 2.2.6(postcss-load-config@3.1.4)(postcss@8.4.21)(svelte@3.58.0)
svelte-preprocess: svelte-preprocess:
specifier: ^4.10.1 specifier: ^4.10.1
version: 4.10.1(postcss-load-config@3.1.1)(postcss@8.4.21)(svelte@3.58.0)(typescript@4.5.4) version: 4.10.1(postcss-load-config@3.1.4)(postcss@8.4.21)(svelte@3.58.0)(typescript@4.5.4)
tailwindcss: tailwindcss:
specifier: ^3.0.12 specifier: ^3.0.12
version: 3.3.1 version: 3.3.1
tslib: tslib:
specifier: ^2.3.1 specifier: ^2.3.1
version: 2.3.1 version: 2.5.0
typescript: typescript:
specifier: ~4.5.4 specifier: ~4.5.4
version: 4.5.4 version: 4.5.4
@@ -801,6 +809,22 @@ importers:
specifier: ^4.2.1 specifier: ^4.2.1
version: 4.3.1 version: 4.3.1
litegraph/packages/nodes-logic:
dependencies:
'@litegraph-ts/core':
specifier: workspace:*
version: link:../core
devDependencies:
'@litegraph-ts/tsconfig':
specifier: workspace:*
version: link:../tsconfig
typescript:
specifier: ^5.0.3
version: 5.0.3
vite:
specifier: ^4.2.1
version: 4.3.1
litegraph/packages/nodes-math: litegraph/packages/nodes-math:
dependencies: dependencies:
'@litegraph-ts/core': '@litegraph-ts/core':
@@ -1181,8 +1205,8 @@ packages:
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
dev: true dev: true
/@codemirror/autocomplete@6.3.0(@codemirror/language@6.6.0)(@codemirror/state@6.1.2)(@codemirror/view@6.4.1)(@lezer/common@1.0.2): /@codemirror/autocomplete@6.6.1(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/common@1.0.2):
resolution: {integrity: sha512-4jEvh3AjJZTDKazd10J6ZsCIqaYxDMCeua5ouQxY8hlFIml+nr7le0SgBhT3SIytFBmdzPK3AUhXGuW3T79nVg==} resolution: {integrity: sha512-RpsvnYOopnyNbZg487qoRD5bKg63KMMUVP5d8MQ4Luc7Mb6JBWTORovLi6cTvWaKlbmLW8Zd2dAJkIdrhBsXug==}
peerDependencies: peerDependencies:
'@codemirror/language': ^6.0.0 '@codemirror/language': ^6.0.0
'@codemirror/state': ^6.0.0 '@codemirror/state': ^6.0.0
@@ -1190,54 +1214,54 @@ packages:
'@lezer/common': ^1.0.0 '@lezer/common': ^1.0.0
dependencies: dependencies:
'@codemirror/language': 6.6.0 '@codemirror/language': 6.6.0
'@codemirror/state': 6.1.2 '@codemirror/state': 6.2.0
'@codemirror/view': 6.4.1 '@codemirror/view': 6.11.0
'@lezer/common': 1.0.2 '@lezer/common': 1.0.2
dev: false dev: false
/@codemirror/commands@6.1.2: /@codemirror/commands@6.2.4:
resolution: {integrity: sha512-sO3jdX1s0pam6lIdeSJLMN3DQ6mPEbM4yLvyKkdqtmd/UDwhXA5+AwFJ89rRXm6vTeOXBsE5cAmlos/t7MJdgg==} resolution: {integrity: sha512-42lmDqVH0ttfilLShReLXsDfASKLXzfyC36bzwcqzox9PlHulMcsUOfHXNo2X2aFMVNUoQ7j+d4q5bnfseYoOA==}
dependencies: dependencies:
'@codemirror/language': 6.6.0 '@codemirror/language': 6.6.0
'@codemirror/state': 6.1.2 '@codemirror/state': 6.2.0
'@codemirror/view': 6.4.1 '@codemirror/view': 6.11.0
'@lezer/common': 1.0.2 '@lezer/common': 1.0.2
dev: false dev: false
/@codemirror/lang-css@6.1.0(@codemirror/view@6.4.1)(@lezer/common@1.0.2): /@codemirror/lang-css@6.2.0(@codemirror/view@6.11.0):
resolution: {integrity: sha512-GYn4TyMvQLrkrhdisFh8HCTDAjPY/9pzwN12hG9UdrTUxRUMicF+8GS24sFEYaleaG1KZClIFLCj0Rol/WO24w==} resolution: {integrity: sha512-oyIdJM29AyRPM3+PPq1I2oIk8NpUfEN3kAM05XWDDs6o3gSneIKaVJifT2P+fqONLou2uIgXynFyMUDQvo/szA==}
dependencies: dependencies:
'@codemirror/autocomplete': 6.3.0(@codemirror/language@6.6.0)(@codemirror/state@6.1.2)(@codemirror/view@6.4.1)(@lezer/common@1.0.2) '@codemirror/autocomplete': 6.6.1(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/common@1.0.2)
'@codemirror/language': 6.6.0 '@codemirror/language': 6.6.0
'@codemirror/state': 6.1.2 '@codemirror/state': 6.2.0
'@lezer/common': 1.0.2
'@lezer/css': 1.1.1 '@lezer/css': 1.1.1
transitivePeerDependencies: transitivePeerDependencies:
- '@codemirror/view' - '@codemirror/view'
- '@lezer/common'
dev: false dev: false
/@codemirror/lang-html@6.4.2: /@codemirror/lang-html@6.4.3:
resolution: {integrity: sha512-bqCBASkteKySwtIbiV/WCtGnn/khLRbbiV5TE+d9S9eQJD7BA4c5dTRm2b3bVmSpilff5EYxvB4PQaZzM/7cNw==} resolution: {integrity: sha512-VKzQXEC8nL69Jg2hvAFPBwOdZNvL8tMFOrdFwWpU+wc6a6KEkndJ/19R5xSaglNX6v2bttm8uIEFYxdQDcIZVQ==}
dependencies: dependencies:
'@codemirror/autocomplete': 6.3.0(@codemirror/language@6.6.0)(@codemirror/state@6.1.2)(@codemirror/view@6.4.1)(@lezer/common@1.0.2) '@codemirror/autocomplete': 6.6.1(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/common@1.0.2)
'@codemirror/lang-css': 6.1.0(@codemirror/view@6.4.1)(@lezer/common@1.0.2) '@codemirror/lang-css': 6.2.0(@codemirror/view@6.11.0)
'@codemirror/lang-javascript': 6.1.4 '@codemirror/lang-javascript': 6.1.7
'@codemirror/language': 6.6.0 '@codemirror/language': 6.6.0
'@codemirror/state': 6.1.2 '@codemirror/state': 6.2.0
'@codemirror/view': 6.4.1 '@codemirror/view': 6.11.0
'@lezer/common': 1.0.2 '@lezer/common': 1.0.2
'@lezer/css': 1.1.1 '@lezer/css': 1.1.1
'@lezer/html': 1.3.4 '@lezer/html': 1.3.4
dev: false dev: false
/@codemirror/lang-javascript@6.1.4: /@codemirror/lang-javascript@6.1.7:
resolution: {integrity: sha512-OxLf7OfOZBTMRMi6BO/F72MNGmgOd9B0vetOLvHsDACFXayBzW8fm8aWnDM0yuy68wTK03MBf4HbjSBNRG5q7A==} resolution: {integrity: sha512-KXKqxlZ4W6t5I7i2ScmITUD3f/F5Cllk3kj0De9P9mFeYVfhOVOWuDLgYiLpk357u7Xh4dhqjJAnsNPPoTLghQ==}
dependencies: dependencies:
'@codemirror/autocomplete': 6.3.0(@codemirror/language@6.6.0)(@codemirror/state@6.1.2)(@codemirror/view@6.4.1)(@lezer/common@1.0.2) '@codemirror/autocomplete': 6.6.1(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/common@1.0.2)
'@codemirror/language': 6.6.0 '@codemirror/language': 6.6.0
'@codemirror/lint': 6.0.0 '@codemirror/lint': 6.2.1
'@codemirror/state': 6.1.2 '@codemirror/state': 6.2.0
'@codemirror/view': 6.4.1 '@codemirror/view': 6.11.0
'@lezer/common': 1.0.2 '@lezer/common': 1.0.2
'@lezer/javascript': 1.4.3 '@lezer/javascript': 1.4.3
dev: false dev: false
@@ -1249,65 +1273,70 @@ packages:
'@lezer/json': 1.0.0 '@lezer/json': 1.0.0
dev: false dev: false
/@codemirror/lang-markdown@6.1.0: /@codemirror/lang-markdown@6.1.1:
resolution: {integrity: sha512-HQDJg1Js19fPKKsI3Rp1X0J6mxyrRy2NX6+Evh0+/jGm6IZHL5ygMGKBYNWKXodoDQFvgdofNRG33gWOwV59Ag==} resolution: {integrity: sha512-n87Ms6Y5UYb1UkFu8sRzTLfq/yyF1y2AYiWvaVdbBQi5WDj1tFk5N+AKA+WC0Jcjc1VxvrCCM0iizjdYYi9sFQ==}
dependencies: dependencies:
'@codemirror/lang-html': 6.4.2 '@codemirror/lang-html': 6.4.3
'@codemirror/language': 6.6.0 '@codemirror/language': 6.6.0
'@codemirror/state': 6.1.2 '@codemirror/state': 6.2.0
'@codemirror/view': 6.4.1 '@codemirror/view': 6.11.0
'@lezer/common': 1.0.2 '@lezer/common': 1.0.2
'@lezer/markdown': 1.0.2 '@lezer/markdown': 1.0.2
dev: false dev: false
/@codemirror/lang-python@6.0.4: /@codemirror/lang-python@6.1.2(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/common@1.0.2):
resolution: {integrity: sha512-CuC7V6MVw4HshQuFaB1SMXHOSbKLnBnBXMzm9Zjb+uvkggyY8fXp79T9eYFzMn7fuadoPJcXyTcT/q/SRT7lvQ==} resolution: {integrity: sha512-nbQfifLBZstpt6Oo4XxA2LOzlSp4b/7Bc5cmodG1R+Cs5PLLCTUvsMNWDnziiCfTOG/SW1rVzXq/GbIr6WXlcw==}
dependencies: dependencies:
'@codemirror/autocomplete': 6.6.1(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/common@1.0.2)
'@codemirror/language': 6.6.0 '@codemirror/language': 6.6.0
'@lezer/python': 1.1.4 '@lezer/python': 1.1.5
transitivePeerDependencies:
- '@codemirror/state'
- '@codemirror/view'
- '@lezer/common'
dev: false dev: false
/@codemirror/language@6.6.0: /@codemirror/language@6.6.0:
resolution: {integrity: sha512-cwUd6lzt3MfNYOobdjf14ZkLbJcnv4WtndYaoBkbor/vF+rCNguMPK0IRtvZJG4dsWiaWPcK8x1VijhvSxnstg==} resolution: {integrity: sha512-cwUd6lzt3MfNYOobdjf14ZkLbJcnv4WtndYaoBkbor/vF+rCNguMPK0IRtvZJG4dsWiaWPcK8x1VijhvSxnstg==}
dependencies: dependencies:
'@codemirror/state': 6.1.2 '@codemirror/state': 6.2.0
'@codemirror/view': 6.4.1 '@codemirror/view': 6.11.0
'@lezer/common': 1.0.2 '@lezer/common': 1.0.2
'@lezer/highlight': 1.1.3 '@lezer/highlight': 1.1.4
'@lezer/lr': 1.3.4 '@lezer/lr': 1.3.4
style-mod: 4.0.3 style-mod: 4.0.3
dev: false dev: false
/@codemirror/legacy-modes@6.3.1: /@codemirror/legacy-modes@6.3.2:
resolution: {integrity: sha512-icXmCs4Mhst2F8mE0TNpmG6l7YTj1uxam3AbZaFaabINH5oWAdg2CfR/PVi+d/rqxJ+TuTnvkKK5GILHrNThtw==} resolution: {integrity: sha512-ki5sqNKWzKi5AKvpVE6Cna4Q+SgxYuYVLAZFSsMjGBWx5qSVa+D+xipix65GS3f2syTfAD9pXKMX4i4p49eneQ==}
dependencies: dependencies:
'@codemirror/language': 6.6.0 '@codemirror/language': 6.6.0
dev: false dev: false
/@codemirror/lint@6.0.0: /@codemirror/lint@6.2.1:
resolution: {integrity: sha512-nUUXcJW1Xp54kNs+a1ToPLK8MadO0rMTnJB8Zk4Z8gBdrN0kqV7uvUraU/T2yqg+grDNR38Vmy/MrhQN/RgwiA==} resolution: {integrity: sha512-y1muai5U/uUPAGRyHMx9mHuHLypPcHWxzlZGknp/U5Mdb5Ol8Q5ZLp67UqyTbNFJJ3unVxZ8iX3g1fMN79S1JQ==}
dependencies: dependencies:
'@codemirror/state': 6.1.2 '@codemirror/state': 6.2.0
'@codemirror/view': 6.4.1 '@codemirror/view': 6.11.0
crelt: 1.0.5 crelt: 1.0.5
dev: false dev: false
/@codemirror/search@6.2.2: /@codemirror/search@6.4.0:
resolution: {integrity: sha512-2pWY599zXk+lSoJ2iv9EuTO4gB7lhgBPLPwFb/zTbimFH4NmZSaKzJSV51okjABZ7/Rj0DYy5klWbIgaJh2LoQ==} resolution: {integrity: sha512-zMDgaBXah+nMLK2dHz9GdCnGbQu+oaGRXS1qviqNZkvOCv/whp5XZFyoikLp/23PM9RBcbuKUUISUmQHM1eRHw==}
dependencies: dependencies:
'@codemirror/state': 6.1.2 '@codemirror/state': 6.2.0
'@codemirror/view': 6.4.1 '@codemirror/view': 6.11.0
crelt: 1.0.5 crelt: 1.0.5
dev: false dev: false
/@codemirror/state@6.1.2: /@codemirror/state@6.2.0:
resolution: {integrity: sha512-Mxff85Hp5va+zuj+H748KbubXjrinX/k28lj43H14T2D0+4kuvEFIEIO7hCEcvBT8ubZyIelt9yGOjj2MWOEQA==} resolution: {integrity: sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA==}
dev: false dev: false
/@codemirror/view@6.4.1: /@codemirror/view@6.11.0:
resolution: {integrity: sha512-QdBpD6E5HYx6YFXXhqwrRyQ83w7CxWZnchM4QpWBVkkmV7/oJT8N+yz2KAi2iRaLObc/aOf7C2RCQTO2yswF8A==} resolution: {integrity: sha512-PRpPRkqMkAKKxEuiUBxapE0YR+wqs9At92ujbJo93PwTZ0jEJDzx9wahrDcXEhQ43Pe0RK9DdZMLWrt+QN80DA==}
dependencies: dependencies:
'@codemirror/state': 6.1.2 '@codemirror/state': 6.2.0
style-mod: 4.0.3 style-mod: 4.0.3
w3c-keyname: 2.2.6 w3c-keyname: 2.2.6
dev: false dev: false
@@ -1544,40 +1573,6 @@ packages:
'@floating-ui/core': 1.2.6 '@floating-ui/core': 1.2.6
dev: false dev: false
/@formatjs/ecma402-abstract@1.11.4:
resolution: {integrity: sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==}
dependencies:
'@formatjs/intl-localematcher': 0.2.25
tslib: 2.5.0
dev: false
/@formatjs/fast-memoize@1.2.1:
resolution: {integrity: sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==}
dependencies:
tslib: 2.5.0
dev: false
/@formatjs/icu-messageformat-parser@2.1.0:
resolution: {integrity: sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==}
dependencies:
'@formatjs/ecma402-abstract': 1.11.4
'@formatjs/icu-skeleton-parser': 1.3.6
tslib: 2.5.0
dev: false
/@formatjs/icu-skeleton-parser@1.3.6:
resolution: {integrity: sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==}
dependencies:
'@formatjs/ecma402-abstract': 1.11.4
tslib: 2.5.0
dev: false
/@formatjs/intl-localematcher@0.2.25:
resolution: {integrity: sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==}
dependencies:
tslib: 2.5.0
dev: false
/@humanwhocodes/config-array@0.11.8: /@humanwhocodes/config-array@0.11.8:
resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==} resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==}
engines: {node: '>=10.10.0'} engines: {node: '>=10.10.0'}
@@ -1862,12 +1857,12 @@ packages:
/@lezer/css@1.1.1: /@lezer/css@1.1.1:
resolution: {integrity: sha512-mSjx+unLLapEqdOYDejnGBokB5+AiJKZVclmud0MKQOKx3DLJ5b5VTCstgDDknR6iIV4gVrN6euzsCnj0A2gQA==} resolution: {integrity: sha512-mSjx+unLLapEqdOYDejnGBokB5+AiJKZVclmud0MKQOKx3DLJ5b5VTCstgDDknR6iIV4gVrN6euzsCnj0A2gQA==}
dependencies: dependencies:
'@lezer/highlight': 1.1.3 '@lezer/highlight': 1.1.4
'@lezer/lr': 1.3.4 '@lezer/lr': 1.3.4
dev: false dev: false
/@lezer/highlight@1.1.3: /@lezer/highlight@1.1.4:
resolution: {integrity: sha512-3vLKLPThO4td43lYRBygmMY18JN3CPh9w+XS2j8WC30vR4yZeFG4z1iFe4jXE43NtGqe//zHW5q8ENLlHvz9gw==} resolution: {integrity: sha512-IECkFmw2l7sFcYXrV8iT9GeY4W0fU4CxX0WMwhmhMIVjoDdD1Hr6q3G2NqVtLg/yVe5n7i4menG3tJ2r4eCrPQ==}
dependencies: dependencies:
'@lezer/common': 1.0.2 '@lezer/common': 1.0.2
dev: false dev: false
@@ -1876,21 +1871,21 @@ packages:
resolution: {integrity: sha512-HdJYMVZcT4YsMo7lW3ipL4NoyS2T67kMPuSVS5TgLGqmaCjEU/D6xv7zsa1ktvTK5lwk7zzF1e3eU6gBZIPm5g==} resolution: {integrity: sha512-HdJYMVZcT4YsMo7lW3ipL4NoyS2T67kMPuSVS5TgLGqmaCjEU/D6xv7zsa1ktvTK5lwk7zzF1e3eU6gBZIPm5g==}
dependencies: dependencies:
'@lezer/common': 1.0.2 '@lezer/common': 1.0.2
'@lezer/highlight': 1.1.3 '@lezer/highlight': 1.1.4
'@lezer/lr': 1.3.4 '@lezer/lr': 1.3.4
dev: false dev: false
/@lezer/javascript@1.4.3: /@lezer/javascript@1.4.3:
resolution: {integrity: sha512-k7Eo9z9B1supZ5cCD4ilQv/RZVN30eUQL+gGbr6ybrEY3avBAL5MDiYi2aa23Aj0A79ry4rJRvPAwE2TM8bd+A==} resolution: {integrity: sha512-k7Eo9z9B1supZ5cCD4ilQv/RZVN30eUQL+gGbr6ybrEY3avBAL5MDiYi2aa23Aj0A79ry4rJRvPAwE2TM8bd+A==}
dependencies: dependencies:
'@lezer/highlight': 1.1.3 '@lezer/highlight': 1.1.4
'@lezer/lr': 1.3.4 '@lezer/lr': 1.3.4
dev: false dev: false
/@lezer/json@1.0.0: /@lezer/json@1.0.0:
resolution: {integrity: sha512-zbAuUY09RBzCoCA3lJ1+ypKw5WSNvLqGMtasdW6HvVOqZoCpPr8eWrsGnOVWGKGn8Rh21FnrKRVlJXrGAVUqRw==} resolution: {integrity: sha512-zbAuUY09RBzCoCA3lJ1+ypKw5WSNvLqGMtasdW6HvVOqZoCpPr8eWrsGnOVWGKGn8Rh21FnrKRVlJXrGAVUqRw==}
dependencies: dependencies:
'@lezer/highlight': 1.1.3 '@lezer/highlight': 1.1.4
'@lezer/lr': 1.3.4 '@lezer/lr': 1.3.4
dev: false dev: false
@@ -1904,13 +1899,13 @@ packages:
resolution: {integrity: sha512-8CY0OoZ6V5EzPjSPeJ4KLVbtXdLBd8V6sRCooN5kHnO28ytreEGTyrtU/zUwo/XLRzGr/e1g44KlzKi3yWGB5A==} resolution: {integrity: sha512-8CY0OoZ6V5EzPjSPeJ4KLVbtXdLBd8V6sRCooN5kHnO28ytreEGTyrtU/zUwo/XLRzGr/e1g44KlzKi3yWGB5A==}
dependencies: dependencies:
'@lezer/common': 1.0.2 '@lezer/common': 1.0.2
'@lezer/highlight': 1.1.3 '@lezer/highlight': 1.1.4
dev: false dev: false
/@lezer/python@1.1.4: /@lezer/python@1.1.5:
resolution: {integrity: sha512-x82XgYxqqX0Yiw7uIemQJ3z2QyQme5BYpectkPfNg99OQrakqfwqVolqEVIrsj4QO9rVDLFZZ49J0Vbne7UbAA==} resolution: {integrity: sha512-h0DVr6IfrmKUbTc5PeetaC87IZYoHyn5JogsVYW5mRDpVRyEsvaLBMLyEN4Ufc2BKp1c9y2Pkr8ZNLxS8dTLsQ==}
dependencies: dependencies:
'@lezer/highlight': 1.1.3 '@lezer/highlight': 1.1.4
'@lezer/lr': 1.3.4 '@lezer/lr': 1.3.4
dev: false dev: false
@@ -2186,7 +2181,7 @@ packages:
/@types/concat-stream@1.6.1: /@types/concat-stream@1.6.1:
resolution: {integrity: sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==} resolution: {integrity: sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==}
dependencies: dependencies:
'@types/node': 8.10.66 '@types/node': 18.16.0
dev: false dev: false
/@types/cookie@0.5.1: /@types/cookie@0.5.1:
@@ -2223,7 +2218,7 @@ packages:
/@types/form-data@0.0.33: /@types/form-data@0.0.33:
resolution: {integrity: sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw==} resolution: {integrity: sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw==}
dependencies: dependencies:
'@types/node': 8.10.66 '@types/node': 18.16.0
dev: false dev: false
/@types/graceful-fs@4.1.6: /@types/graceful-fs@4.1.6:
@@ -2265,7 +2260,6 @@ packages:
/@types/node@18.16.0: /@types/node@18.16.0:
resolution: {integrity: sha512-BsAaKhB+7X+H4GnSjGhJG9Qi8Tw+inU9nJDwmD5CgOmBLEI6ArdhikpLX7DjbjDRDTbqZzU2LSQNZg8WGPiSZQ==} resolution: {integrity: sha512-BsAaKhB+7X+H4GnSjGhJG9Qi8Tw+inU9nJDwmD5CgOmBLEI6ArdhikpLX7DjbjDRDTbqZzU2LSQNZg8WGPiSZQ==}
dev: true
/@types/node@8.10.66: /@types/node@8.10.66:
resolution: {integrity: sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==} resolution: {integrity: sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==}
@@ -2297,6 +2291,12 @@ packages:
resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==}
dev: true dev: true
/@types/ws@8.5.4:
resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==}
dependencies:
'@types/node': 18.16.0
dev: true
/@types/yargs-parser@21.0.0: /@types/yargs-parser@21.0.0:
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
dev: true dev: true
@@ -2784,7 +2784,7 @@ packages:
wrap-ansi: 7.0.0 wrap-ansi: 7.0.0
dev: true dev: true
/cm6-theme-basic-dark@0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.1.2)(@codemirror/view@6.4.1)(@lezer/highlight@1.1.3): /cm6-theme-basic-dark@0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/highlight@1.1.4):
resolution: {integrity: sha512-+mNNJecRtxS/KkloMDCQF0oTrT6aFGRZTjnBcdT5UG1pcDO4Brq8l1+0KR/8dZ7hub2gOGOzoi3rGFD8GzlH7Q==} resolution: {integrity: sha512-+mNNJecRtxS/KkloMDCQF0oTrT6aFGRZTjnBcdT5UG1pcDO4Brq8l1+0KR/8dZ7hub2gOGOzoi3rGFD8GzlH7Q==}
peerDependencies: peerDependencies:
'@codemirror/language': ^6.0.0 '@codemirror/language': ^6.0.0
@@ -2793,12 +2793,12 @@ packages:
'@lezer/highlight': ^1.0.0 '@lezer/highlight': ^1.0.0
dependencies: dependencies:
'@codemirror/language': 6.6.0 '@codemirror/language': 6.6.0
'@codemirror/state': 6.1.2 '@codemirror/state': 6.2.0
'@codemirror/view': 6.4.1 '@codemirror/view': 6.11.0
'@lezer/highlight': 1.1.3 '@lezer/highlight': 1.1.4
dev: false dev: false
/cm6-theme-basic-light@0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.1.2)(@codemirror/view@6.4.1)(@lezer/highlight@1.1.3): /cm6-theme-basic-light@0.2.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/highlight@1.1.4):
resolution: {integrity: sha512-1prg2gv44sYfpHscP26uLT/ePrh0mlmVwMSoSd3zYKQ92Ab3jPRLzyCnpyOCQLJbK+YdNs4HvMRqMNYdy4pMhA==} resolution: {integrity: sha512-1prg2gv44sYfpHscP26uLT/ePrh0mlmVwMSoSd3zYKQ92Ab3jPRLzyCnpyOCQLJbK+YdNs4HvMRqMNYdy4pMhA==}
peerDependencies: peerDependencies:
'@codemirror/language': ^6.0.0 '@codemirror/language': ^6.0.0
@@ -2807,9 +2807,9 @@ packages:
'@lezer/highlight': ^1.0.0 '@lezer/highlight': ^1.0.0
dependencies: dependencies:
'@codemirror/language': 6.6.0 '@codemirror/language': 6.6.0
'@codemirror/state': 6.1.2 '@codemirror/state': 6.2.0
'@codemirror/view': 6.4.1 '@codemirror/view': 6.11.0
'@lezer/highlight': 1.1.3 '@lezer/highlight': 1.1.4
dev: false dev: false
/co@4.6.0: /co@4.6.0:
@@ -2824,13 +2824,13 @@ packages:
/codemirror@6.0.1(@lezer/common@1.0.2): /codemirror@6.0.1(@lezer/common@1.0.2):
resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==} resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==}
dependencies: dependencies:
'@codemirror/autocomplete': 6.3.0(@codemirror/language@6.6.0)(@codemirror/state@6.1.2)(@codemirror/view@6.4.1)(@lezer/common@1.0.2) '@codemirror/autocomplete': 6.6.1(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.11.0)(@lezer/common@1.0.2)
'@codemirror/commands': 6.1.2 '@codemirror/commands': 6.2.4
'@codemirror/language': 6.6.0 '@codemirror/language': 6.6.0
'@codemirror/lint': 6.0.0 '@codemirror/lint': 6.2.1
'@codemirror/search': 6.2.2 '@codemirror/search': 6.4.0
'@codemirror/state': 6.1.2 '@codemirror/state': 6.2.0
'@codemirror/view': 6.4.1 '@codemirror/view': 6.11.0
transitivePeerDependencies: transitivePeerDependencies:
- '@lezer/common' - '@lezer/common'
dev: false dev: false
@@ -3091,13 +3091,6 @@ packages:
d3-time-format: 4.1.0 d3-time-format: 4.1.0
dev: false dev: false
/d3-shape@3.1.0:
resolution: {integrity: sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==}
engines: {node: '>=12'}
dependencies:
d3-path: 3.1.0
dev: false
/d3-shape@3.2.0: /d3-shape@3.2.0:
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -4051,6 +4044,7 @@ packages:
/globalyzer@0.1.0: /globalyzer@0.1.0:
resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==}
dev: true
/globby@11.1.0: /globby@11.1.0:
resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
@@ -4066,6 +4060,7 @@ packages:
/globrex@0.1.2: /globrex@0.1.2:
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
dev: true
/graceful-fs@4.2.11: /graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
@@ -4211,15 +4206,6 @@ packages:
engines: {node: '>=12'} engines: {node: '>=12'}
dev: false dev: false
/intl-messageformat@9.13.0:
resolution: {integrity: sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==}
dependencies:
'@formatjs/ecma402-abstract': 1.11.4
'@formatjs/fast-memoize': 1.2.1
'@formatjs/icu-messageformat-parser': 2.1.0
tslib: 2.5.0
dev: false
/is-arrayish@0.2.1: /is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
dev: true dev: true
@@ -5116,6 +5102,7 @@ packages:
/mri@1.2.0: /mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true
/mrmime@1.0.1: /mrmime@1.0.1:
resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==}
@@ -5377,19 +5364,6 @@ packages:
camelcase-css: 2.0.1 camelcase-css: 2.0.1
postcss: 8.4.21 postcss: 8.4.21
/postcss-load-config@3.1.1:
resolution: {integrity: sha512-c/9XYboIbSEUZpiD1UQD0IKiUe8n9WHYV7YFe7X7J+ZwCsEKkUJSFWjS9hBU1RR9THR7jMXst8sxiqP0jjo2mg==}
engines: {node: '>= 10'}
peerDependencies:
ts-node: '>=9.0.0'
peerDependenciesMeta:
ts-node:
optional: true
dependencies:
lilconfig: 2.1.0
yaml: 1.10.2
dev: true
/postcss-load-config@3.1.4(postcss@8.4.21): /postcss-load-config@3.1.4(postcss@8.4.21):
resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
@@ -5709,6 +5683,7 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dependencies: dependencies:
mri: 1.2.0 mri: 1.2.0
dev: true
/safe-buffer@5.1.2: /safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
@@ -6016,7 +5991,7 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
/svelte-check@2.2.6(postcss-load-config@3.1.1)(postcss@8.4.21)(svelte@3.58.0): /svelte-check@2.2.6(postcss-load-config@3.1.4)(postcss@8.4.21)(svelte@3.58.0):
resolution: {integrity: sha512-oJux/afbmcZO+N+ADXB88h6XANLie8Y2rh2qBlhgfkpr2c3t/q/T0w2JWrHqagaDL8zeNwO8a8RVFBkrRox8gg==} resolution: {integrity: sha512-oJux/afbmcZO+N+ADXB88h6XANLie8Y2rh2qBlhgfkpr2c3t/q/T0w2JWrHqagaDL8zeNwO8a8RVFBkrRox8gg==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -6030,7 +6005,7 @@ packages:
sade: 1.8.1 sade: 1.8.1
source-map: 0.7.4 source-map: 0.7.4
svelte: 3.58.0 svelte: 3.58.0
svelte-preprocess: 4.10.1(postcss-load-config@3.1.1)(postcss@8.4.21)(svelte@3.58.0)(typescript@5.0.3) svelte-preprocess: 4.10.1(postcss-load-config@3.1.4)(postcss@8.4.21)(svelte@3.58.0)(typescript@5.0.3)
typescript: 5.0.3 typescript: 5.0.3
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
@@ -6095,22 +6070,7 @@ packages:
dependencies: dependencies:
svelte: 3.58.0 svelte: 3.58.0
/svelte-i18n@3.3.13(svelte@3.58.0): /svelte-preprocess@4.10.1(postcss-load-config@3.1.4)(postcss@8.4.21)(svelte@3.58.0)(typescript@4.5.4):
resolution: {integrity: sha512-RQM+ys4+Y9ztH//tX22H1UL2cniLNmIR+N4xmYygV6QpQ6EyQvloZiENRew8XrVzfvJ8HaE8NU6/yurLkl7z3g==}
engines: {node: '>= 11.15.0'}
hasBin: true
peerDependencies:
svelte: ^3.25.1
dependencies:
deepmerge: 4.3.1
estree-walker: 2.0.2
intl-messageformat: 9.13.0
sade: 1.8.1
svelte: 3.58.0
tiny-glob: 0.2.9
dev: false
/svelte-preprocess@4.10.1(postcss-load-config@3.1.1)(postcss@8.4.21)(svelte@3.58.0)(typescript@4.5.4):
resolution: {integrity: sha512-NSNloaylf+o9UeyUR2KvpdxrAyMdHl3U7rMnoP06/sG0iwJvlUM4TpMno13RaNqovh4AAoGsx1jeYcIyuGUXMw==} resolution: {integrity: sha512-NSNloaylf+o9UeyUR2KvpdxrAyMdHl3U7rMnoP06/sG0iwJvlUM4TpMno13RaNqovh4AAoGsx1jeYcIyuGUXMw==}
engines: {node: '>= 9.11.2'} engines: {node: '>= 9.11.2'}
requiresBuild: true requiresBuild: true
@@ -6156,14 +6116,14 @@ packages:
detect-indent: 6.1.0 detect-indent: 6.1.0
magic-string: 0.25.9 magic-string: 0.25.9
postcss: 8.4.21 postcss: 8.4.21
postcss-load-config: 3.1.1 postcss-load-config: 3.1.4(postcss@8.4.21)
sorcery: 0.10.0 sorcery: 0.10.0
strip-indent: 3.0.0 strip-indent: 3.0.0
svelte: 3.58.0 svelte: 3.58.0
typescript: 4.5.4 typescript: 4.5.4
dev: true dev: true
/svelte-preprocess@4.10.1(postcss-load-config@3.1.1)(postcss@8.4.21)(svelte@3.58.0)(typescript@5.0.3): /svelte-preprocess@4.10.1(postcss-load-config@3.1.4)(postcss@8.4.21)(svelte@3.58.0)(typescript@5.0.3):
resolution: {integrity: sha512-NSNloaylf+o9UeyUR2KvpdxrAyMdHl3U7rMnoP06/sG0iwJvlUM4TpMno13RaNqovh4AAoGsx1jeYcIyuGUXMw==} resolution: {integrity: sha512-NSNloaylf+o9UeyUR2KvpdxrAyMdHl3U7rMnoP06/sG0iwJvlUM4TpMno13RaNqovh4AAoGsx1jeYcIyuGUXMw==}
engines: {node: '>= 9.11.2'} engines: {node: '>= 9.11.2'}
requiresBuild: true requiresBuild: true
@@ -6209,7 +6169,7 @@ packages:
detect-indent: 6.1.0 detect-indent: 6.1.0
magic-string: 0.25.9 magic-string: 0.25.9
postcss: 8.4.21 postcss: 8.4.21
postcss-load-config: 3.1.1 postcss-load-config: 3.1.4(postcss@8.4.21)
sorcery: 0.10.0 sorcery: 0.10.0
strip-indent: 3.0.0 strip-indent: 3.0.0
svelte: 3.58.0 svelte: 3.58.0
@@ -6399,6 +6359,7 @@ packages:
dependencies: dependencies:
globalyzer: 0.1.0 globalyzer: 0.1.0
globrex: 0.1.2 globrex: 0.1.2
dev: true
/tiny-invariant@1.3.1: /tiny-invariant@1.3.1:
resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==}
@@ -6483,13 +6444,8 @@ packages:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
dev: true dev: true
/tslib@2.3.1:
resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==}
dev: true
/tslib@2.5.0: /tslib@2.5.0:
resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==}
dev: false
/tsutils@3.21.0(typescript@5.0.3): /tsutils@3.21.0(typescript@5.0.3):
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
@@ -7454,6 +7410,19 @@ packages:
signal-exit: 3.0.7 signal-exit: 3.0.7
dev: true dev: true
/ws@8.13.0:
resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
dev: false
/y18n@5.0.8: /y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'} engines: {node: '>=10'}

View File

@@ -1,17 +1,8 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { get } from "svelte/store";
import { Button } from "@gradio/button";
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp"; import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
import { Checkbox } from "@gradio/form"
import uiState from "$lib/stores/uiState";
import { ImageViewer } from "$lib/ImageViewer";
import { download } from "$lib/utils"
import { LGraph, LGraphNode } from "@litegraph-ts/core"; import { App, View } from "framework7-svelte"
import type { ComfyAPIStatus } from "$lib/api";
import queueState from "$lib/stores/queueState";
import { App, View, Toolbar, Page, Navbar, Link, BlockTitle, Block, List, ListItem } from "framework7-svelte"
import { f7, f7ready } from 'framework7-svelte'; import { f7, f7ready } from 'framework7-svelte';
@@ -26,6 +17,9 @@
import GraphPage from './mobile/routes/graph.svelte'; import GraphPage from './mobile/routes/graph.svelte';
import ListSubWorkflowsPage from './mobile/routes/list-subworkflows.svelte'; import ListSubWorkflowsPage from './mobile/routes/list-subworkflows.svelte';
import SubWorkflowPage from './mobile/routes/subworkflow.svelte'; import SubWorkflowPage from './mobile/routes/subworkflow.svelte';
import type { Framework7Parameters } from "framework7/types";
export let app: ComfyApp;
function onBackKeyDown(e) { function onBackKeyDown(e) {
if(f7.view.current.router.currentRoute.path == '/'){ if(f7.view.current.router.currentRoute.path == '/'){
@@ -39,6 +33,8 @@
} }
onMount(async () => { onMount(async () => {
await app.setup();
(window as any).app = app;
window.addEventListener("backbutton", onBackKeyDown, false); window.addEventListener("backbutton", onBackKeyDown, false);
window.addEventListener("popstate", onBackKeyDown, false); window.addEventListener("popstate", onBackKeyDown, false);
}); });
@@ -48,11 +44,14 @@
We need to pass them along with the F7 app parameters to <App> component We need to pass them along with the F7 app parameters to <App> component
*/ */
let f7params = { let f7params: Framework7Parameters = {
routes: [ routes: [
{ {
path: '/', path: '/',
component: HomePage, component: HomePage,
options: {
props: { app }
}
}, },
{ {
path: '/about/', path: '/about/',
@@ -65,14 +64,23 @@
{ {
path: '/graph/', path: '/graph/',
component: GraphPage, component: GraphPage,
options: {
props: { app }
}
}, },
{ {
path: '/subworkflows/', path: '/subworkflows/',
component: ListSubWorkflowsPage, component: ListSubWorkflowsPage,
options: {
props: { app }
}
}, },
{ {
path: '/subworkflows/:subworkflowID/', path: '/subworkflows/:subworkflowID/',
component: SubWorkflowPage, component: SubWorkflowPage,
options: {
props: { app }
}
}, },
], ],
popup: { popup: {
@@ -91,16 +99,19 @@
</script> </script>
{#if app} {#if app}
<App theme="auto" name="ComfyBox" {...f7params}> <App theme="auto" name="ComfyBox" {...f7params}>
<View <View
url="/" url="/"
main={true} main={true}
class="safe-areas" class="safe-areas"
masterDetailBreakpoint={768}, masterDetailBreakpoint={768},
browserHistory=true, browserHistory=true,
browserHistoryRoot="/mobile/" browserHistoryRoot="/mobile/"
> >
<GenToolbar/> <GenToolbar {app} />
</View> </View>
</App> </App>
<div class="canvas-wrapper pane-wrapper" style="display: none">
<canvas id="graph-canvas" />
</div>
{/if} {/if}

View File

@@ -1,4 +1,4 @@
import { LConnectionKind, LGraph, LGraphNode, type INodeSlot, type SlotIndex, LiteGraph, getStaticProperty, type LGraphAddNodeOptions } from "@litegraph-ts/core"; import { LConnectionKind, LGraph, LGraphNode, type INodeSlot, type SlotIndex, LiteGraph, getStaticProperty, type LGraphAddNodeOptions, LGraphCanvas } from "@litegraph-ts/core";
import GraphSync from "./GraphSync"; import GraphSync from "./GraphSync";
import EventEmitter from "events"; import EventEmitter from "events";
import type TypedEmitter from "typed-emitter"; import type TypedEmitter from "typed-emitter";
@@ -53,6 +53,17 @@ export default class ComfyGraph extends LGraph {
layoutState.nodeAdded(node) layoutState.nodeAdded(node)
this.graphSync.onNodeAdded(node); this.graphSync.onNodeAdded(node);
// All nodes whether they come from base litegraph or ComfyBox should
// have tags added to them. Can't override serialization for existing
// node types to add `tags` as anew field so putting it in properties is better.
if (node.properties.tags == null)
node.properties.tags = []
if ((node as any).canInheritSlotTypes && node.inputs.length > 1) {
node.color ||= LGraphCanvas.node_colors["green"].color;
node.bgColor ||= LGraphCanvas.node_colors["green"].bgColor;
}
if ("outputProperties" in node) { if ("outputProperties" in node) {
const widgetNode = node as ComfyWidgetNode; const widgetNode = node as ComfyWidgetNode;
for (const propName of widgetNode.outputProperties) { for (const propName of widgetNode.outputProperties) {

View File

@@ -11,7 +11,7 @@ export type SerializedGraphCanvasState = {
} }
export default class ComfyGraphCanvas extends LGraphCanvas { export default class ComfyGraphCanvas extends LGraphCanvas {
app: ComfyApp app: ComfyApp | null;
constructor( constructor(
app: ComfyApp, app: ComfyApp,
@@ -60,7 +60,8 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
let color = null; let color = null;
if (node.id === +state.runningNodeId) { if (node.id === +state.runningNodeId) {
color = "#0f0"; color = "#0f0";
} else if (this.app.dragOverNode && node.id === this.app.dragOverNode.id) { // this.app can be null inside the constructor if rendering is taking place already
} else if (this.app && this.app.dragOverNode && node.id === this.app.dragOverNode.id) {
color = "dodgerblue"; color = "dodgerblue";
} }

View File

@@ -13,6 +13,7 @@
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState"; import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { startDrag, stopDrag } from "$lib/utils" import { startDrag, stopDrag } from "$lib/utils"
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { isHidden } from "$lib/widgets/utils";
export let container: ContainerLayout | null = null; export let container: ContainerLayout | null = null;
export let zIndex: number = 0; export let zIndex: number = 0;
@@ -20,6 +21,7 @@
export let showHandles: boolean = false; export let showHandles: boolean = false;
export let edit: boolean = false; export let edit: boolean = false;
export let dragDisabled: boolean = false; export let dragDisabled: boolean = false;
export let isMobile: boolean = false;
let attrsChanged: Writable<boolean> | null = null; let attrsChanged: Writable<boolean> | null = null;
let children: IDragItem[] | null = null; let children: IDragItem[] | null = null;
@@ -72,20 +74,20 @@
on:finalize="{handleFinalize}" on:finalize="{handleFinalize}"
> >
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)} {#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
{@const hidden = item?.attrs?.hidden} {@const hidden = isHidden(item)}
<div class="animation-wrapper" <div class="animation-wrapper"
class:hidden={hidden} class:hidden={hidden}
animate:flip={{duration:flipDurationMs}} animate:flip={{duration:flipDurationMs}}
style={item?.attrs?.flexGrow ? `flex-grow: ${item.attrs.flexGrow}` : ""} style={item?.attrs?.flexGrow ? `flex-grow: ${item.attrs.flexGrow}` : ""}
> >
<WidgetContainer dragItem={item} zIndex={zIndex+1} /> <WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]} {#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
<div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/> <div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/>
{/if} {/if}
</div> </div>
{/each} {/each}
</div> </div>
{#if container.attrs.hidden && edit} {#if isHidden(container) && edit}
<div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/> <div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/>
{/if} {/if}
{#if showHandles} {#if showHandles}
@@ -97,7 +99,7 @@
<Block elem_classes={["gradio-accordion"]}> <Block elem_classes={["gradio-accordion"]}>
<Accordion label={container.attrs.title} open={container.attrs.openOnStartup}> <Accordion label={container.attrs.title} open={container.attrs.openOnStartup}>
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)} {#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
<WidgetContainer dragItem={item} zIndex={zIndex+1} /> <WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
{/each} {/each}
</Accordion> </Accordion>
</Block> </Block>
@@ -164,7 +166,8 @@
} }
:global(.label-wrap > span:not(.icon)) { :global(.label-wrap > span:not(.icon)) {
color: var(--block-title-text-color); /* color: var(--block-title-text-color); */
font-size: 16px;
} }
.handle { .handle {

View File

@@ -12,6 +12,7 @@
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState"; import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { startDrag, stopDrag } from "$lib/utils" import { startDrag, stopDrag } from "$lib/utils"
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { isHidden } from "$lib/widgets/utils";
export let container: ContainerLayout | null = null; export let container: ContainerLayout | null = null;
export let zIndex: number = 0; export let zIndex: number = 0;
@@ -19,6 +20,7 @@
export let showHandles: boolean = false; export let showHandles: boolean = false;
export let edit: boolean = false; export let edit: boolean = false;
export let dragDisabled: boolean = false; export let dragDisabled: boolean = false;
export let isMobile: boolean = false;
let attrsChanged: Writable<boolean> | null = null; let attrsChanged: Writable<boolean> | null = null;
let children: IDragItem[] | null = null; let children: IDragItem[] | null = null;
@@ -74,20 +76,20 @@
on:finalize="{handleFinalize}" on:finalize="{handleFinalize}"
> >
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)} {#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
{@const hidden = item?.attrs?.hidden} {@const hidden = isHidden(item)}
<div class="animation-wrapper" <div class="animation-wrapper"
class:hidden={hidden} class:hidden={hidden}
animate:flip={{duration:flipDurationMs}} animate:flip={{duration:flipDurationMs}}
style={item?.attrs?.flexGrow ? `flex-grow: ${item.attrs.flexGrow}` : ""} style={item?.attrs?.flexGrow ? `flex-grow: ${item.attrs.flexGrow}` : ""}
> >
<WidgetContainer dragItem={item} zIndex={zIndex+1} /> <WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]} {#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
<div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/> <div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/>
{/if} {/if}
</div> </div>
{/each} {/each}
</div> </div>
{#if container.attrs.hidden && edit} {#if isHidden(container) && edit}
<div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/> <div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/>
{/if} {/if}
{#if showHandles} {#if showHandles}
@@ -238,6 +240,10 @@
flex-grow: 100; flex-grow: 100;
} }
.handle-hidden {
background-color: #40404080;
}
.handle-widget:hover { .handle-widget:hover {
background-color: #add8e680; background-color: #add8e680;
} }

View File

@@ -21,9 +21,10 @@
import queueState from "$lib/stores/queueState"; import queueState from "$lib/stores/queueState";
import ComfyUnlockUIButton from "./ComfyUnlockUIButton.svelte"; import ComfyUnlockUIButton from "./ComfyUnlockUIButton.svelte";
import ComfyGraphView from "./ComfyGraphView.svelte"; import ComfyGraphView from "./ComfyGraphView.svelte";
import { download, jsonToJsObject } from "$lib/utils";
import notify from "$lib/notify";
export let app: ComfyApp = undefined; export let app: ComfyApp = undefined;
let imageViewer: ImageViewer;
let queue: ComfyQueue = undefined; let queue: ComfyQueue = undefined;
let mainElem: HTMLDivElement; let mainElem: HTMLDivElement;
let uiPane: ComfyUIPane = undefined; let uiPane: ComfyUIPane = undefined;
@@ -32,6 +33,7 @@
let resizeTimeout: NodeJS.Timeout | null; let resizeTimeout: NodeJS.Timeout | null;
let hasShownUIHelpToast: boolean = false; let hasShownUIHelpToast: boolean = false;
let uiTheme: string = ""; let uiTheme: string = "";
let fileInput: HTMLInputElement = undefined;
let debugLayout: boolean = false; let debugLayout: boolean = false;
@@ -100,8 +102,47 @@
if (!app?.lGraph) if (!app?.lGraph)
return; return;
const promptFilename = true; // TODO
let filename = "workflow.json";
if (promptFilename) {
filename = prompt("Save workflow as:", filename);
if (!filename) return;
if (!filename.toLowerCase().endsWith(".json")) {
filename += ".json";
}
}
else {
const date = new Date();
const formattedDate = date.toISOString().replace(/:/g, '-').replace(/\.\d{3}/g, '').replace('T', '_').replace("Z", "");
filename = `workflow-${formattedDate}.json`
}
const indent = 2
const json = JSON.stringify(app.serialize(), null, indent)
download(filename, json, "application/json")
}
function doLoad(): void {
if (!app?.lGraph || !fileInput)
return;
fileInput.click();
}
function loadWorkflow(): void {
app.handleFile(fileInput.files[0]);
fileInput.files = null;
}
function doSaveLocal(): void {
if (!app?.lGraph)
return;
app.saveStateToLocalStorage(); app.saveStateToLocalStorage();
toast.push("Saved to local storage.") notify("Saved to local storage.")
console.debug(jsonToJsObject(JSON.stringify(app.serialize(), null, 2)))
// //
// const date = new Date(); // const date = new Date();
// const formattedDate = date.toISOString().replace(/:/g, '-').replace(/\.\d{3}/g, '').replace('T', '_').replace("Z", ""); // const formattedDate = date.toISOString().replace(/:/g, '-').replace(/\.\d{3}/g, '').replace('T', '_').replace("Z", "");
@@ -109,13 +150,6 @@
// download(`workflow-${formattedDate}.json`, JSON.stringify(app.serialize()), "application/json") // download(`workflow-${formattedDate}.json`, JSON.stringify(app.serialize()), "application/json")
} }
function doReset(): void {
var confirmed = confirm("Are you sure you want to clear the current workflow?");
if (confirmed) {
app.reset();
}
}
async function doLoadDefault(): void { async function doLoadDefault(): void {
var confirmed = confirm("Are you sure you want to clear the current workflow and load the default graph?"); var confirmed = confirm("Are you sure you want to clear the current workflow and load the default graph?");
if (confirmed) { if (confirmed) {
@@ -123,9 +157,16 @@
} }
} }
function doClear(): void {
var confirmed = confirm("Are you sure you want to clear the current workflow?");
if (confirmed) {
app.clear();
}
}
$: if ($uiState.uiUnlocked && !hasShownUIHelpToast) { $: if ($uiState.uiUnlocked && !hasShownUIHelpToast) {
hasShownUIHelpToast = true; hasShownUIHelpToast = true;
toast.push("Right-click to open context menu.") notify("Right-click to open context menu.")
} }
if (debugLayout) { if (debugLayout) {
@@ -134,14 +175,6 @@
}) })
} }
app.api.addEventListener("status", (ev: CustomEvent) => {
queueState.statusUpdated(ev.detail as ComfyAPIStatus);
});
$: if (app.rootEl && !imageViewer) {
imageViewer = new ImageViewer(app.rootEl);
}
$: if (containerElem) { $: if (containerElem) {
const canvas = containerElem.querySelector<HTMLDivElement>("#graph-canvas") const canvas = containerElem.querySelector<HTMLDivElement>("#graph-canvas")
if (canvas) { if (canvas) {
@@ -169,7 +202,7 @@
}) })
async function doRefreshCombos() { async function doRefreshCombos() {
await app.refreshComboInNodes() await app.refreshComboInNodes(true)
} }
</script> </script>
@@ -222,8 +255,14 @@
<Button variant="secondary" on:click={doSave}> <Button variant="secondary" on:click={doSave}>
Save Save
</Button> </Button>
<Button variant="secondary" on:click={doReset}> <Button variant="secondary" on:click={doSaveLocal}>
Reset Save Local
</Button>
<Button variant="secondary" on:click={doLoad}>
Load
</Button>
<Button variant="secondary" on:click={doClear}>
Clear
</Button> </Button>
<Button variant="secondary" on:click={doLoadDefault}> <Button variant="secondary" on:click={doLoadDefault}>
Load Default Load Default
@@ -255,6 +294,7 @@
</div> </div>
</div> </div>
<LightboxModal /> <LightboxModal />
<input bind:this={fileInput} id="comfy-file-input" type="file" accept=".json" on:change={loadWorkflow} />
</div> </div>
<SvelteToast options={toastOptions} /> <SvelteToast options={toastOptions} />
@@ -358,4 +398,8 @@
span.left { span.left {
right: 0px; right: 0px;
} }
#comfy-file-input {
display: none;
}
</style> </style>

View File

@@ -1,6 +1,6 @@
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType } from "@litegraph-ts/core"; import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2, BuiltInSlotType } from "@litegraph-ts/core";
import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core"; import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core";
import ComfyAPI from "$lib/api" import ComfyAPI, { type ComfyAPIQueueStatus } from "$lib/api"
import defaultGraph from "$lib/defaultGraph" import defaultGraph from "$lib/defaultGraph"
import { getPngMetadata, importA1111 } from "$lib/pnginfo"; import { getPngMetadata, importA1111 } from "$lib/pnginfo";
import EventEmitter from "events"; import EventEmitter from "events";
@@ -9,6 +9,7 @@ import type TypedEmitter from "typed-emitter";
// Import nodes // Import nodes
import "@litegraph-ts/nodes-basic" import "@litegraph-ts/nodes-basic"
import "@litegraph-ts/nodes-events" import "@litegraph-ts/nodes-events"
import "@litegraph-ts/nodes-logic"
import "@litegraph-ts/nodes-math" import "@litegraph-ts/nodes-math"
import "@litegraph-ts/nodes-strings" import "@litegraph-ts/nodes-strings"
import "$lib/nodes/index" import "$lib/nodes/index"
@@ -27,7 +28,8 @@ import ComfyGraph from "$lib/ComfyGraph";
import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode"; import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
import { get } from "svelte/store"; import { get } from "svelte/store";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import { promptToGraphVis } from "$lib/utils"; import { promptToGraphVis, workflowToGraphVis } from "$lib/utils";
import notify from "$lib/notify";
export const COMFYBOX_SERIAL_VERSION = 1; export const COMFYBOX_SERIAL_VERSION = 1;
@@ -69,6 +71,27 @@ export type Progress = {
max: number max: number
} }
function isActiveBackendNode(node: ComfyGraphNode, tag: string | null): boolean {
if (!node.isBackendNode)
return false;
if (tag && !hasTag(node, tag)) {
console.debug("Skipping tagged node", tag, node.properties.tags, node)
return false;
}
if (node.mode === NodeMode.NEVER) {
// Don't serialize muted nodes
return false;
}
return true;
}
function hasTag(node: LGraphNode, tag: string): boolean {
return "tags" in node.properties && node.properties.tags.indexOf(tag) !== -1
}
export default class ComfyApp { export default class ComfyApp {
api: ComfyAPI; api: ComfyAPI;
rootEl: HTMLDivElement | null = null; rootEl: HTMLDivElement | null = null;
@@ -97,7 +120,9 @@ export default class ComfyApp {
return; return;
} }
this.rootEl = document.getElementById("main") as HTMLDivElement; this.setupColorScheme()
this.rootEl = document.getElementById("app") as HTMLDivElement;
this.canvasEl = document.getElementById("graph-canvas") as HTMLCanvasElement; this.canvasEl = document.getElementById("graph-canvas") as HTMLCanvasElement;
this.lGraph = new ComfyGraph(); this.lGraph = new ComfyGraph();
this.lCanvas = new ComfyGraphCanvas(this, this.canvasEl); this.lCanvas = new ComfyGraphCanvas(this, this.canvasEl);
@@ -137,17 +162,12 @@ export default class ComfyApp {
this.addPasteHandler(); this.addPasteHandler();
this.addKeyboardHandler(); this.addKeyboardHandler();
this.setupColorScheme()
// await this.#invokeExtensionsAsync("setup"); // await this.#invokeExtensionsAsync("setup");
// Ensure the canvas fills the window // Ensure the canvas fills the window
this.resizeCanvas(); this.resizeCanvas();
window.addEventListener("resize", this.resizeCanvas.bind(this)); window.addEventListener("resize", this.resizeCanvas.bind(this));
this.lGraph.start();
this.lGraph.eventBus.on("afterExecute", () => this.lCanvas.draw(true))
this.alreadySetup = true; this.alreadySetup = true;
return Promise.resolve(); return Promise.resolve();
@@ -295,6 +315,10 @@ export default class ComfyApp {
this.lGraph.setDirtyCanvas(true, false); this.lGraph.setDirtyCanvas(true, false);
}); });
this.api.addEventListener("status", (ev: CustomEvent) => {
queueState.statusUpdated(ev.detail as ComfyAPIQueueStatus);
});
this.api.addEventListener("executed", ({ detail }: CustomEvent) => { this.api.addEventListener("executed", ({ detail }: CustomEvent) => {
this.nodeOutputs[detail.node] = detail.output; this.nodeOutputs[detail.node] = detail.output;
const node = this.lGraph.getNodeById(detail.node) as ComfyGraphNode; const node = this.lGraph.getNodeById(detail.node) as ComfyGraphNode;
@@ -322,8 +346,8 @@ export default class ComfyApp {
private setupColorScheme() { private setupColorScheme() {
const setColor = (type: any, color: string) => { const setColor = (type: any, color: string) => {
this.lCanvas.link_type_colors[type] = color LGraphCanvas.DEFAULT_LINK_TYPE_COLORS[type] = color
this.lCanvas.default_connection_color_byType[type] = color LGraphCanvas.DEFAULT_CONNECTION_COLORS_BY_TYPE[type] = color
} }
// Distinguish frontend/backend connections // Distinguish frontend/backend connections
@@ -374,6 +398,9 @@ export default class ComfyApp {
this.lCanvas.deserialize(data.canvas) this.lCanvas.deserialize(data.canvas)
await this.refreshComboInNodes(); await this.refreshComboInNodes();
this.lGraph.start();
this.lGraph.eventBus.on("afterExecute", () => this.lCanvas.draw(true))
} }
async initDefaultGraph() { async initDefaultGraph() {
@@ -404,7 +431,7 @@ export default class ComfyApp {
} }
} }
reset() { clear() {
this.clean(); this.clean();
const blankGraph: SerializedLGraph = { const blankGraph: SerializedLGraph = {
@@ -438,33 +465,26 @@ export default class ComfyApp {
for (const node_ of this.lGraph.computeExecutionOrder<ComfyGraphNode>(false, null)) { for (const node_ of this.lGraph.computeExecutionOrder<ComfyGraphNode>(false, null)) {
const n = workflow.nodes.find((n) => n.id === node_.id); const n = workflow.nodes.find((n) => n.id === node_.id);
if (!node_.isBackendNode) { if (!isActiveBackendNode(node_, tag)) {
// console.debug("Not serializing node: ", node_.type)
continue; continue;
} }
const node = node_ as ComfyBackendNode; const node = node_ as ComfyBackendNode;
if (tag && node.tags.indexOf(tag) === -1) {
console.debug("Skipping tagged node", tag, node.tags)
continue;
}
if (node.mode === NodeMode.NEVER) {
// Don't serialize muted nodes
continue;
}
const inputs = {}; const inputs = {};
// Store all link values // Store input values passed by frontend-only nodes
if (node.inputs) { if (node.inputs) {
for (let i = 0; i < node.inputs.length; i++) { for (let i = 0; i < node.inputs.length; i++) {
const inp = node.inputs[i]; const inp = node.inputs[i];
const inputLink = node.getInputLink(i) const inputLink = node.getInputLink(i)
const inputNode = node.getInputNode(i) const inputNode = node.getInputNode(i)
if (inputNode && tag && "tags" in inputNode && (inputNode.tags as string[]).indexOf(tag) === -1) { // We don't check tags for non-backend nodes.
// Just check for node inactivity (so you can toggle groups of
// tagged frontend nodes on/off)
if (inputNode && inputNode.mode === NodeMode.NEVER) {
console.debug("Skipping inactive node", inputNode)
continue; continue;
} }
@@ -500,31 +520,44 @@ export default class ComfyApp {
} }
} }
// Store all links between nodes // Store links between backend-only and hybrid nodes
for (let i = 0; i < node.inputs.length; i++) { for (let i = 0; i < node.inputs.length; i++) {
let parent: ComfyGraphNode = node.getInputNode(i) as ComfyGraphNode; let parent: ComfyGraphNode = node.getInputNode(i) as ComfyGraphNode;
if (parent) { if (parent) {
const seen = {} const seen = {}
let link = node.getInputLink(i); let link = node.getInputLink(i);
const isValidParent = (parent: ComfyGraphNode) => { const isFrontendParent = (parent: ComfyGraphNode) => {
if (!parent || parent.isBackendNode) if (!parent || parent.isBackendNode)
return false; return false;
if ("tags" in parent && (parent.tags as string[]).indexOf(tag) === -1) if (tag && !hasTag(parent, tag))
return false; return false;
return true; return true;
} }
while (isValidParent(parent)) { // If there are frontend-only nodes between us and another
link = parent.getInputLink(link.origin_slot); // backend node, we have to traverse them first. This
if (link && !seen[link.id]) { // behavior is dependent on the type of node. Reroute nodes
seen[link.id] = true // will simply follow their single input, while branching
const inputNode = parent.getInputNode(link.origin_slot) as ComfyGraphNode; // nodes have conditional logic that determines which link
if (inputNode && "tags" in inputNode && tag && (inputNode.tags as string[]).indexOf(tag) === -1) { // to follow backwards.
console.debug("Skipping tagged parent node", tag, node.tags) while (isFrontendParent(parent)) {
const nextLink = parent.getUpstreamLink()
if (nextLink == null) {
console.warn("[graphToPrompt] No upstream link found in frontend node", parent)
break;
}
if (nextLink && !seen[nextLink.id]) {
seen[nextLink.id] = true
const inputNode = parent.graph.getNodeById(nextLink.origin_id) as ComfyGraphNode;
if (inputNode && tag && !hasTag(inputNode, tag)) {
console.debug("[graphToPrompt] Skipping tagged intermediate frontend node", tag, node.properties.tags)
parent = null; parent = null;
} }
else { else {
console.debug("[graphToPrompt] Traverse upstream link", parent.id, inputNode?.id, inputNode?.isBackendNode)
link = nextLink;
parent = inputNode; parent = inputNode;
} }
} else { } else {
@@ -533,9 +566,10 @@ export default class ComfyApp {
} }
if (link && parent && parent.isBackendNode) { if (link && parent && parent.isBackendNode) {
if ("tags" in parent && tag && (parent.tags as string[]).indexOf(tag) === -1) if (tag && !hasTag(parent, tag))
continue; continue;
console.debug("[graphToPrompt] final link", parent.id, node.id)
const input = node.inputs[i] const input = node.inputs[i]
// TODO can null be a legitimate value in some cases? // TODO can null be a legitimate value in some cases?
// Nodes like CLIPLoader will never have a value in the frontend, hence "null". // Nodes like CLIPLoader will never have a value in the frontend, hence "null".
@@ -552,14 +586,13 @@ export default class ComfyApp {
} }
// Remove inputs connected to removed nodes // Remove inputs connected to removed nodes
for (const nodeId in output) {
for (const o in output) { for (const inputName in output[nodeId].inputs) {
for (const i in output[o].inputs) { if (Array.isArray(output[nodeId].inputs[inputName])
if (Array.isArray(output[o].inputs[i]) && output[nodeId].inputs[inputName].length === 2
&& output[o].inputs[i].length === 2 && !output[output[nodeId].inputs[inputName][0]]) {
&& !output[output[o].inputs[i][0]]) { console.debug("Prune removed node link", nodeId, inputName, output[nodeId].inputs[inputName])
console.debug("Prune removed node link", o, i, output[o].inputs[i]) delete output[nodeId].inputs[inputName];
delete output[o].inputs[i];
} }
} }
} }
@@ -595,17 +628,15 @@ export default class ComfyApp {
} }
const p = await this.graphToPrompt(tag); const p = await this.graphToPrompt(tag);
console.debug(promptToGraphVis(p))
try { try {
await this.api.queuePrompt(num, p); await this.api.queuePrompt(num, p);
} catch (error) { } catch (error) {
// this.ui.dialog.show(error.response || error.toString()); // this.ui.dialog.show(error.response || error.toString());
const mes = error.response || error.toString() const mes = error.response || error.toString()
toast.push(`Error queuing prompt:\n${mes}`, { notify(`Error queuing prompt:\n${mes}`, null, "error")
theme: { console.error(promptToGraphVis(p))
'--toastBackground': 'var(--color-red-500)',
}
})
console.error("Error queuing prompt", mes, num, p) console.error("Error queuing prompt", mes, num, p)
break; break;
} }
@@ -634,16 +665,21 @@ export default class ComfyApp {
if (file.type === "image/png") { if (file.type === "image/png") {
const pngInfo = await getPngMetadata(file); const pngInfo = await getPngMetadata(file);
if (pngInfo) { if (pngInfo) {
if (pngInfo.workflow) { if (pngInfo.comfyBoxConfig) {
this.loadGraphData(JSON.parse(pngInfo.workflow)); this.deserialize(JSON.parse(pngInfo.comfyBoxConfig));
} else if (pngInfo.parameters) { } else if (pngInfo.parameters) {
importA1111(this.lGraph, pngInfo.parameters, this.api); throw "TODO import A111 import!"
// importA1111(this.lGraph, pngInfo.parameters, this.api);
}
else {
console.error("No metadata found in image file.", pngInfo)
notify("No metadata found in image file.")
} }
} }
} else if (file.type === "application/json" || file.name.endsWith(".json")) { } else if (file.type === "application/json" || file.name.endsWith(".json")) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
this.loadGraphData(JSON.parse(reader.result as string)); this.deserialize(JSON.parse(reader.result as string));
}; };
reader.readAsText(file); reader.readAsText(file);
} }
@@ -662,7 +698,7 @@ export default class ComfyApp {
/** /**
* Refresh combo list on whole nodes * Refresh combo list on whole nodes
*/ */
async refreshComboInNodes() { async refreshComboInNodes(flashUI: boolean = false) {
const defs = await this.api.getNodeDefs(); const defs = await this.api.getNodeDefs();
for (let nodeNum in this.lGraph._nodes) { for (let nodeNum in this.lGraph._nodes) {
@@ -680,11 +716,13 @@ export default class ComfyApp {
const inputNode = node.getInputNode(index) const inputNode = node.getInputNode(index)
if (inputNode && "doAutoConfig" in inputNode) { if (inputNode && "doAutoConfig" in inputNode) {
const comfyInputNode = inputNode as nodes.ComfyWidgetNode; const comfyComboNode = inputNode as nodes.ComfyComboNode;
comfyInputNode.doAutoConfig(comfyInput) comfyComboNode.doAutoConfig(comfyInput)
if (!comfyInput.config.values.includes(get(comfyInputNode.value))) { if (!comfyInput.config.values.includes(get(comfyComboNode.value))) {
comfyInputNode.setValue(comfyInput.config.defaultValue || comfyInput.config.values[0]) comfyComboNode.setValue(comfyInput.config.defaultValue || comfyInput.config.values[0])
} }
if (flashUI)
comfyComboNode.comboRefreshed.set(true)
} }
} }
} }

View File

@@ -142,7 +142,7 @@
value = spec.deserialize(value) value = spec.deserialize(value)
target.attrs[name] = value target.attrs[name] = value
target.attrsChanged.set(!get(target.attrsChanged)) target.attrsChanged.set(get(target.attrsChanged) + 1)
if (node && "propsChanged" in node) { if (node && "propsChanged" in node) {
const comfyNode = node as ComfyWidgetNode const comfyNode = node as ComfyWidgetNode
@@ -151,27 +151,39 @@
console.warn(spec) console.warn(spec)
if (spec.refreshPanelOnChange) { if (spec.refreshPanelOnChange) {
console.error("A! refresh") doRefreshPanel()
$refreshPanel += 1;
} }
} }
function getProperty(node: LGraphNode, spec: AttributesSpec) {
let value = node.properties[spec.name]
if (value == null)
value = spec.defaultValue
else if (spec.serialize)
value = spec.serialize(value)
console.debug("[ComfyProperties] getProperty", spec, value, node)
return value
}
function updateProperty(spec: AttributesSpec, value: any) { function updateProperty(spec: AttributesSpec, value: any) {
if (node == null || !spec.editable) if (node == null || !spec.editable)
return return
const name = spec.name const name = spec.name
console.warn("updateProperty", name, value) console.warn("[ComfyProperties] updateProperty", name, value)
if (spec.deserialize)
value = spec.deserialize(value)
node.properties[name] = value; node.properties[name] = value;
if ("propsChanged" in node) { if ("propsChanged" in node) {
const comfyNode = node as ComfyWidgetNode const comfyNode = node as ComfyWidgetNode
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1) comfyNode.notifyPropsChanged();
} }
if (spec.refreshPanelOnChange) if (spec.refreshPanelOnChange)
$refreshPanel += 1; doRefreshPanel()
} }
function getVar(node: LGraphNode, spec: AttributesSpec) { function getVar(node: LGraphNode, spec: AttributesSpec) {
@@ -201,8 +213,14 @@
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1) comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
} }
if (spec.refreshPanelOnChange) if (spec.refreshPanelOnChange) {
$refreshPanel += 1; doRefreshPanel()
}
}
function doRefreshPanel() {
console.warn("[ComfyProperties] doRefreshPanel")
$refreshPanel += 1;
} }
function updateWorkflowAttribute(spec: AttributesSpec, value: any) { function updateWorkflowAttribute(spec: AttributesSpec, value: any) {
@@ -214,6 +232,9 @@
$layoutState.attrs[name] = value $layoutState.attrs[name] = value
$layoutState = $layoutState $layoutState = $layoutState
if (spec.refreshPanelOnChange)
doRefreshPanel()
} }
</script> </script>
@@ -281,7 +302,7 @@
<div class="props-entry"> <div class="props-entry">
{#if spec.type === "string"} {#if spec.type === "string"}
<TextBox <TextBox
value={node.properties[spec.name] || spec.defaultValue} value={getProperty(node, spec)}
on:change={(e) => updateProperty(spec, e.detail)} on:change={(e) => updateProperty(spec, e.detail)}
on:input={(e) => updateProperty(spec, e.detail)} on:input={(e) => updateProperty(spec, e.detail)}
label={spec.name} label={spec.name}
@@ -290,7 +311,7 @@
/> />
{:else if spec.type === "boolean"} {:else if spec.type === "boolean"}
<Checkbox <Checkbox
value={node.properties[spec.name] || spec.defaultValue} value={getProperty(node, spec)}
label={spec.name} label={spec.name}
disabled={!$uiState.uiUnlocked || !spec.editable} disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateProperty(spec, e.detail)} on:change={(e) => updateProperty(spec, e.detail)}
@@ -298,7 +319,7 @@
{:else if spec.type === "number"} {:else if spec.type === "number"}
<ComfyNumberProperty <ComfyNumberProperty
name={spec.name} name={spec.name}
value={node.properties[spec.name] || spec.defaultValue} value={getProperty(node, spec)}
step={spec.step || 1} step={spec.step || 1}
min={spec.min || -1024} min={spec.min || -1024}
max={spec.max || 1024} max={spec.max || 1024}
@@ -308,7 +329,7 @@
{:else if spec.type === "enum"} {:else if spec.type === "enum"}
<ComfyComboProperty <ComfyComboProperty
name={spec.name} name={spec.name}
value={node.properties[spec.name] || spec.defaultValue} value={getProperty(node, spec)}
values={spec.values} values={spec.values}
disabled={!$uiState.uiUnlocked || !spec.editable} disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateProperty(spec, e.detail)} on:change={(e) => updateProperty(spec, e.detail)}

View File

@@ -1,15 +1,7 @@
<script lang="ts"> <script lang="ts">
import queueState from "$lib/stores/queueState"; import queueState from "$lib/stores/queueState";
import ProgressBar from "./ProgressBar.svelte"; import ProgressBar from "./ProgressBar.svelte";
import { getNodeInfo } from "$lib/utils"
function getNodeInfo(nodeId: number): string {
let app = (window as any).app;
if (!app)
return String(nodeId);
const title = app.lGraph.getNodeById(nodeId)?.title || String(nodeId);
return title + " (" + nodeId + ")"
}
const entries = [ const entries = [
{ {
@@ -89,7 +81,7 @@
<span>Node: {getNodeInfo($queueState.runningNodeId)}</span> <span>Node: {getNodeInfo($queueState.runningNodeId)}</span>
</div> </div>
<div> <div>
<ProgressBar value={$queueState.progress?.value} max={$queueState.progress?.max} /> <ProgressBar value={$queueState.progress?.value} max={$queueState.progress?.max} styles="height: 30px;" />
</div> </div>
{/if} {/if}
{#if typeof $queueState.queueRemaining === "number" && $queueState.queueRemaining > 0} {#if typeof $queueState.queueRemaining === "number" && $queueState.queueRemaining > 0}

View File

@@ -14,11 +14,14 @@
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState"; import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { startDrag, stopDrag } from "$lib/utils" import { startDrag, stopDrag } from "$lib/utils"
import type { Writable } from "svelte/store";
import { isHidden } from "$lib/widgets/utils";
export let container: ContainerLayout | null = null; export let container: ContainerLayout | null = null;
export let zIndex: number = 0; export let zIndex: number = 0;
export let classes: string[] = []; export let classes: string[] = [];
export let showHandles: boolean = false; export let showHandles: boolean = false;
export let isMobile: boolean = false
let attrsChanged: Writable<boolean> | null = null; let attrsChanged: Writable<boolean> | null = null;
$: if (container) { $: if (container) {
@@ -33,12 +36,14 @@
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1} {@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1}
{@const dragDisabled = zIndex === 0 || $layoutState.currentSelection.length > 2 || !$uiState.uiUnlocked} {@const dragDisabled = zIndex === 0 || $layoutState.currentSelection.length > 2 || !$uiState.uiUnlocked}
{#key $attrsChanged} {#key $attrsChanged}
{#if container.attrs.variant === "tabs"} {#if edit || !isHidden(container)}
<TabsContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} /> {#if container.attrs.variant === "tabs"}
{:else if container.attrs.variant === "accordion"} <TabsContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
<AccordionContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} /> {:else if container.attrs.variant === "accordion"}
{:else} <AccordionContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
<BlockContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} /> {:else}
<BlockContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
{/if}
{/if} {/if}
{/key} {/key}
{/if} {/if}

View File

@@ -23,15 +23,17 @@
<style> <style>
.progress { .progress {
width: 100%;
height: 100%;
text-align: center; text-align: center;
background-color: lightgrey; background-color: lightgrey;
padding: 3px; padding: 0px;
position: relative; position: relative;
} }
.bar { .bar {
height: 100%;
background-color: #B3D8A9; background-color: #B3D8A9;
height: 20px;
} }
.label { .label {

View File

@@ -13,6 +13,7 @@
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState"; import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { startDrag, stopDrag } from "$lib/utils" import { startDrag, stopDrag } from "$lib/utils"
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { isHidden } from "$lib/widgets/utils";
export let container: ContainerLayout | null = null; export let container: ContainerLayout | null = null;
export let zIndex: number = 0; export let zIndex: number = 0;
@@ -20,6 +21,7 @@
export let showHandles: boolean = false; export let showHandles: boolean = false;
export let edit: boolean = false; export let edit: boolean = false;
export let dragDisabled: boolean = false; export let dragDisabled: boolean = false;
export let isMobile: boolean = false;
let attrsChanged: Writable<boolean> | null = null; let attrsChanged: Writable<boolean> | null = null;
let children: IDragItem[] | null = null; let children: IDragItem[] | null = null;
@@ -85,7 +87,7 @@
on:finalize="{handleFinalize}" on:finalize="{handleFinalize}"
> >
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item, i(item.id)} {#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item, i(item.id)}
{@const hidden = item?.attrs?.hidden} {@const hidden = isHidden(item)}
{@const tabName = getTabName(container, i)} {@const tabName = getTabName(container, i)}
<div class="animation-wrapper" <div class="animation-wrapper"
class:hidden={hidden} class:hidden={hidden}
@@ -95,7 +97,7 @@
<label for={String(item.id)}> <label for={String(item.id)}>
<BlockTitle><strong>Tab {i+1}:</strong> {tabName}</BlockTitle> <BlockTitle><strong>Tab {i+1}:</strong> {tabName}</BlockTitle>
</label> </label>
<WidgetContainer dragItem={item} zIndex={zIndex+1} /> <WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]} {#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
<div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/> <div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/>
{/if} {/if}
@@ -103,7 +105,7 @@
</div> </div>
{/each} {/each}
</div> </div>
{#if container.attrs.hidden && edit} {#if isHidden(container) && edit}
<div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/> <div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/>
{/if} {/if}
{#if showHandles} {#if showHandles}
@@ -115,7 +117,7 @@
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item, i(item.id)} {#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item, i(item.id)}
{@const tabName = getTabName(container, i)} {@const tabName = getTabName(container, i)}
<TabItem name={tabName} on:select={() => console.log("tab " + i)}> <TabItem name={tabName} on:select={() => console.log("tab " + i)}>
<WidgetContainer dragItem={item} zIndex={zIndex+1} /> <WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
</TabItem> </TabItem>
{/each} {/each}
</Tabs> </Tabs>

View File

@@ -7,10 +7,13 @@
import Container from "./Container.svelte" import Container from "./Container.svelte"
import { type Writable } from "svelte/store" import { type Writable } from "svelte/store"
import type { ComfyWidgetNode } from "$lib/nodes"; import type { ComfyWidgetNode } from "$lib/nodes";
import { NodeMode } from "@litegraph-ts/core";
import { isHidden } from "$lib/widgets/utils";
export let dragItem: IDragItem | null = null; export let dragItem: IDragItem | null = null;
export let zIndex: number = 0; export let zIndex: number = 0;
export let classes: string[] = []; export let classes: string[] = [];
export let isMobile: boolean = false;
let container: ContainerLayout | null = null; let container: ContainerLayout | null = null;
let attrsChanged: Writable<boolean> | null = null; let attrsChanged: Writable<boolean> | null = null;
let propsChanged: Writable<number> | null = null; let propsChanged: Writable<number> | null = null;
@@ -59,21 +62,22 @@
{#if container} {#if container}
{#key $attrsChanged} {#key $attrsChanged}
<Container {container} {classes} {zIndex} {showHandles} /> <Container {container} {classes} {zIndex} {showHandles} {isMobile} />
{/key} {/key}
{:else if widget && widget.node} {:else if widget && widget.node}
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1} {@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1}
{@const hidden = isHidden(widget)}
{#key $attrsChanged} {#key $attrsChanged}
{#key $propsChanged} {#key $propsChanged}
<div class="widget {widget.attrs.classes} {getWidgetClass()}" <div class="widget {widget.attrs.classes} {getWidgetClass()}"
class:edit={edit} class:edit={edit}
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(widget.id)} class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(widget.id)}
class:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.node.id} class:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.node.id}
class:hidden={widget.attrs.hidden} class:hidden={hidden}
> >
<svelte:component this={widget.node.svelteComponentType} {widget} /> <svelte:component this={widget.node.svelteComponentType} {widget} {isMobile} />
</div> </div>
{#if widget.attrs.hidden && edit} {#if hidden && edit}
<div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/> <div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/>
{/if} {/if}
{#if showHandles} {#if showHandles}

View File

@@ -0,0 +1,130 @@
<script context="module">
let _id = 0;
</script>
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { BlockTitle } from "@gradio/atoms";
export let value: number = 0;
export let minimum: number = 0;
export let maximum: number = 100;
export let step: number = 1;
export let disabled: boolean = false;
export let label: string;
export let info: string | undefined = undefined;
export let show_label: boolean;
const id = `range_id_${_id++}`;
const dispatch = createEventDispatcher<{ change: number; release: number }>();
let inputValue = value;
function handle_input(e: Event) {
const element = e.currentTarget as HTMLInputElement;
let newValue = parseFloat(element.value);
if (isNaN(newValue)) {
newValue = minimum;
}
inputValue = Math.min(Math.max(inputValue, minimum), maximum);
value = inputValue;
dispatch("release", value);
}
function handle_release(e: MouseEvent) {
dispatch("release", value);
}
$: {
inputValue = value;
dispatch("change", value);
}
const clamp = () => {
dispatch("release", value);
value = Math.min(Math.max(value, minimum), maximum);
};
</script>
<div class="wrap">
<div class="head">
<label for={id}>
<BlockTitle {show_label} {info}>{label}</BlockTitle>
</label>
<input
data-testid="number-input"
type="number"
bind:value={inputValue}
on:input={handle_input}
min={minimum}
max={maximum}
on:blur={clamp}
{step}
{disabled}
on:pointerup={handle_release}
/>
</div>
</div>
<input
type="range"
{id}
name="cowbell"
bind:value
min={minimum}
max={maximum}
{step}
{disabled}
on:pointerup={handle_release}
on:pointerdown
on:pointermove
/>
<style lang="scss">
.wrap {
display: flex;
flex-direction: column;
width: 100%;
}
.head {
display: flex;
justify-content: space-between;
}
input[type="number"] {
display: block;
position: relative;
outline: none !important;
box-shadow: var(--input-shadow);
border: var(--input-border-width) solid var(--input-border-color);
border-radius: var(--input-radius);
background: var(--input-background-fill);
padding: var(--size-2) var(--size-2);
height: var(--size-6);
color: var(--body-text-color);
font-size: var(--input-text-size);
line-height: var(--line-sm);
text-align: center;
}
input:disabled {
-webkit-text-fill-color: var(--body-text-color);
-webkit-opacity: 1;
opacity: 1;
}
input[type="number"]:focus {
box-shadow: var(--input-shadow-focus);
border-color: var(--input-border-color-focus);
}
input::placeholder {
color: var(--input-placeholder-color);
}
input[type="range"] {
width: 100%;
accent-color: var(--slider-color);
}
input[disabled] {
cursor: not-allowed;
}
</style>

View File

@@ -0,0 +1 @@
export { default as Range } from "./Range.svelte"

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +1,17 @@
import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, BuiltInSlotType, type ITextWidget, type SerializedLGraphNode } from "@litegraph-ts/core";
import ComfyGraphNode from "./ComfyGraphNode";
import { Watch } from "@litegraph-ts/nodes-basic";
import type { SerializedPrompt } from "$lib/components/ComfyApp"; import type { SerializedPrompt } from "$lib/components/ComfyApp";
import { toast } from '@zerodevx/svelte-toast' import notify from "$lib/notify";
import type { GalleryOutput } from "./ComfyWidgetNodes"; import layoutState from "$lib/stores/layoutState";
import { get } from "svelte/store";
import queueState from "$lib/stores/queueState"; import queueState from "$lib/stores/queueState";
import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWidget, type SerializedLGraphNode, type SlotLayout } from "@litegraph-ts/core";
export interface ComfyQueueEventsProperties extends Record<any, any> { import { get } from "svelte/store";
} import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
import type { ComfyWidgetNode, GalleryOutput } from "./ComfyWidgetNodes";
export class ComfyQueueEvents extends ComfyGraphNode { export class ComfyQueueEvents extends ComfyGraphNode {
override properties: ComfyQueueEventsProperties = {
}
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
outputs: [ outputs: [
{ name: "beforeQueued", type: BuiltInSlotType.EVENT }, { name: "beforeQueued", type: BuiltInSlotType.EVENT },
{ name: "afterQueued", type: BuiltInSlotType.EVENT }, { name: "afterQueued", type: BuiltInSlotType.EVENT }
{ name: "prompt", type: "*" }
], ],
} }
@@ -55,24 +48,21 @@ LiteGraph.registerNodeType({
type: "actions/queue_events" type: "actions/queue_events"
}) })
export interface ComfyOnExecutedEventProperties extends Record<any, any> { export interface ComfyStoreImagesActionProperties extends ComfyGraphNodeProperties {
images: GalleryOutput | null, images: GalleryOutput | null
filename: string | null
} }
export class ComfyOnExecutedEvent extends ComfyGraphNode { export class ComfyStoreImagesAction extends ComfyGraphNode {
override properties: ComfyOnExecutedEventProperties = { override properties: ComfyStoreImagesActionProperties = {
images: null, images: null
filename: null
} }
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
inputs: [ inputs: [
{ name: "images", type: "IMAGE" } { name: "output", type: BuiltInSlotType.ACTION, options: { color_off: "rebeccapurple", color_on: "rebeccapurple" } }
], ],
outputs: [ outputs: [
{ name: "images", type: "OUTPUT" }, { name: "images", type: "OUTPUT" },
{ name: "onExecuted", type: BuiltInSlotType.EVENT },
], ],
} }
@@ -81,29 +71,30 @@ export class ComfyOnExecutedEvent extends ComfyGraphNode {
this.setOutputData(0, this.properties.images) this.setOutputData(0, this.properties.images)
} }
override receiveOutput(output: any) { override onAction(action: any, param: any) {
if (output && "images" in output) { if (action !== "store" || !param || !("images" in param))
this.setProperty("images", output as GalleryOutput) return;
this.setOutputData(0, this.properties.images)
this.triggerSlot(1, "bang") this.setProperty("images", param as GalleryOutput)
} this.setOutputData(0, this.properties.images)
} }
} }
LiteGraph.registerNodeType({ LiteGraph.registerNodeType({
class: ComfyOnExecutedEvent, class: ComfyStoreImagesAction,
title: "Comfy.OnExecutedEvent", title: "Comfy.StoreImagesAction",
desc: "Triggers a 'bang' event when a prompt output is received.", desc: "Stores images from an onExecuted callback",
type: "actions/on_executed" type: "actions/store_images"
}) })
export interface ComfyCopyActionProperties extends Record<any, any> { export interface ComfyCopyActionProperties extends ComfyGraphNodeProperties {
value: any value: any
} }
export class ComfyCopyAction extends ComfyGraphNode { export class ComfyCopyAction extends ComfyGraphNode {
override properties: ComfyCopyActionProperties = { override properties: ComfyCopyActionProperties = {
value: null value: null,
tags: []
} }
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
@@ -149,7 +140,7 @@ LiteGraph.registerNodeType({
type: "actions/copy" type: "actions/copy"
}) })
export interface ComfySwapActionProperties extends Record<any, any> { export interface ComfySwapActionProperties extends ComfyGraphNodeProperties {
} }
export class ComfySwapAction extends ComfyGraphNode { export class ComfySwapAction extends ComfyGraphNode {
@@ -163,16 +154,16 @@ export class ComfySwapAction extends ComfyGraphNode {
{ name: "swap", type: BuiltInSlotType.ACTION } { name: "swap", type: BuiltInSlotType.ACTION }
], ],
outputs: [ outputs: [
{ name: "B", type: "*" }, { name: "B", type: BuiltInSlotType.EVENT },
{ name: "A", type: "*" } { name: "A", type: BuiltInSlotType.EVENT }
], ],
} }
override onAction(action: any, param: any) { override onAction(action: any, param: any) {
const a = this.getInputData(0) const a = this.getInputData(0)
const b = this.getInputData(1) const b = this.getInputData(1)
this.setOutputData(0, a) this.triggerSlot(0, a)
this.setOutputData(1, b) this.triggerSlot(1, b)
}; };
} }
@@ -183,13 +174,14 @@ LiteGraph.registerNodeType({
type: "actions/swap" type: "actions/swap"
}) })
export interface ComfyNotifyActionProperties extends Record<any, any> { export interface ComfyNotifyActionProperties extends ComfyGraphNodeProperties {
message: string message: string
} }
export class ComfyNotifyAction extends ComfyGraphNode { export class ComfyNotifyAction extends ComfyGraphNode {
override properties: ComfyNotifyActionProperties = { override properties: ComfyNotifyActionProperties = {
message: "Nya." message: "Nya.",
tags: []
} }
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
@@ -202,7 +194,7 @@ export class ComfyNotifyAction extends ComfyGraphNode {
override onAction(action: any, param: any) { override onAction(action: any, param: any) {
const message = this.getInputData(0); const message = this.getInputData(0);
if (message) { if (message) {
toast.push(message); notify(message);
} }
}; };
} }
@@ -214,7 +206,7 @@ LiteGraph.registerNodeType({
type: "actions/notify" type: "actions/notify"
}) })
export interface ComfyExecuteSubgraphActionProperties extends Record<any, any> { export interface ComfyExecuteSubgraphActionProperties extends ComfyGraphNodeProperties {
tag: string | null, tag: string | null,
} }
@@ -253,3 +245,83 @@ LiteGraph.registerNodeType({
desc: "Runs a part of the graph based on a tag", desc: "Runs a part of the graph based on a tag",
type: "actions/execute_subgraph" type: "actions/execute_subgraph"
}) })
export interface ComfySetNodeModeActionProperties extends ComfyGraphNodeProperties {
targetTags: string,
enable: boolean,
}
export class ComfySetNodeModeAction extends ComfyGraphNode {
override properties: ComfySetNodeModeActionProperties = {
targetTags: "",
enable: false,
tags: []
}
static slotLayout: SlotLayout = {
inputs: [
{ name: "enabled", type: "boolean" },
{ name: "set", type: BuiltInSlotType.ACTION },
],
}
displayWidget: ITextWidget;
enableWidget: IToggleWidget;
constructor(title?: string) {
super(title)
this.displayWidget = this.addWidget("text", "Tags", this.properties.targetTags, "targetTags")
}
override onPropertyChanged(property: any, value: any) {
if (property === "enabled") {
this.enableWidget.value = value
}
}
override onAction(action: any, param: any) {
let enabled = this.getInputData(0)
if (typeof param === "object" && "enabled" in param)
enabled = param["enabled"]
const tags = this.properties.targetTags.split(",").map(s => s.trim());
for (const node of this.graph._nodes) {
if ("tags" in node.properties) {
const comfyNode = node as ComfyGraphNode;
const hasTag = tags.some(t => comfyNode.properties.tags.indexOf(t) != -1);
if (hasTag) {
let newMode: NodeMode;
if (enabled) {
newMode = NodeMode.ALWAYS;
} else {
newMode = NodeMode.NEVER;
}
console.warn("CHANGEMODE", newMode == NodeMode.ALWAYS ? "ALWAYS" : "NEVER", tags, node)
node.changeMode(newMode);
if ("notifyPropsChanged" in node)
(node as ComfyWidgetNode).notifyPropsChanged();
}
}
}
for (const entry of Object.values(get(layoutState).allItems)) {
if (entry.dragItem.type === "container") {
const container = entry.dragItem;
const hasTag = tags.some(t => container.attrs.tags.indexOf(t) != -1);
if (hasTag) {
container.attrs.hidden = !enabled;
}
container.attrsChanged.set(get(container.attrsChanged) + 1)
}
}
}
}
LiteGraph.registerNodeType({
class: ComfySetNodeModeAction,
title: "Comfy.SetNodeModeAction",
desc: "Sets a group of nodes/UI containers as enabled/disabled based on their tags (comma-separated)",
type: "actions/set_node_mode"
})

View File

@@ -2,7 +2,9 @@ import LGraphCanvas from "@litegraph-ts/core/src/LGraphCanvas";
import ComfyGraphNode from "./ComfyGraphNode"; import ComfyGraphNode from "./ComfyGraphNode";
import ComfyWidgets from "$lib/widgets" import ComfyWidgets from "$lib/widgets"
import type { ComfyWidgetNode } from "./ComfyWidgetNodes"; import type { ComfyWidgetNode } from "./ComfyWidgetNodes";
import type { SerializedLGraphNode } from "@litegraph-ts/core"; import { BuiltInSlotType, type SerializedLGraphNode } from "@litegraph-ts/core";
import type IComfyInputSlot from "$lib/IComfyInputSlot";
import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
/* /*
* Base class for any node with configuration sent by the backend. * Base class for any node with configuration sent by the backend.
@@ -26,15 +28,11 @@ export class ComfyBackendNode extends ComfyGraphNode {
// It just returns a hash like { "ui": { "images": results } } internally. // It just returns a hash like { "ui": { "images": results } } internally.
// So this will need to be hardcoded for now. // So this will need to be hardcoded for now.
if (["PreviewImage", "SaveImage"].indexOf(comfyClass) !== -1) { if (["PreviewImage", "SaveImage"].indexOf(comfyClass) !== -1) {
this.addOutput("output", "IMAGE"); this.addOutput("onExecuted", BuiltInSlotType.EVENT, { color_off: "rebeccapurple", color_on: "rebeccapurple" });
} }
} }
/* private static defaultInputConfigs: Record<string, Record<string, ComfyInputConfig>> = {}
* Tags this node belongs to
* Allows you to run subsections of the graph
*/
tags: string[] = []
private setup(nodeData: any) { private setup(nodeData: any) {
var inputs = nodeData["input"]["required"]; var inputs = nodeData["input"]["required"];
@@ -42,8 +40,11 @@ export class ComfyBackendNode extends ComfyGraphNode {
inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"]) inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"])
} }
const config = { minWidth: 1, minHeight: 1 }; ComfyBackendNode.defaultInputConfigs[this.type] = {}
for (const inputName in inputs) { for (const inputName in inputs) {
const config = { minWidth: 1, minHeight: 1 };
const inputData = inputs[inputName]; const inputData = inputs[inputName];
const type = inputData[0]; const type = inputData[0];
@@ -64,6 +65,9 @@ export class ComfyBackendNode extends ComfyGraphNode {
this.addInput(inputName, type); this.addInput(inputName, type);
} }
} }
if ("widgetNodeType" in config)
ComfyBackendNode.defaultInputConfigs[this.type][config.name] = config.config
} }
for (const o in nodeData["output"]) { for (const o in nodeData["output"]) {
@@ -72,39 +76,42 @@ export class ComfyBackendNode extends ComfyGraphNode {
this.addOutput(outputName, output); this.addOutput(outputName, output);
} }
const s = this.computeSize();
s[0] = Math.max(config.minWidth, s[0] * 1.5);
s[1] = Math.max(config.minHeight, s[1]);
this.size = s;
this.serialize_widgets = false; this.serialize_widgets = false;
// app.#invokeExtensionsAsync("nodeCreated", this); // app.#invokeExtensionsAsync("nodeCreated", this);
} }
override onSerialize(o: SerializedLGraphNode) { override onSerialize(o: SerializedLGraphNode) {
super.onSerialize(o); super.onSerialize(o);
(o as any).tags = this.tags for (const input of o.inputs) {
// strip user-identifying data, it will be reinstantiated later
if ((input as any).config != null) {
(input as any).config = {};
}
}
} }
override onConfigure(o: SerializedLGraphNode) { override onConfigure(o: SerializedLGraphNode) {
super.onConfigure(o); super.onConfigure(o);
this.tags = (o as any).tags || []
const configs = ComfyBackendNode.defaultInputConfigs[o.type]
for (let index = 0; index < this.inputs.length; index++) {
const input = this.inputs[index] as IComfyInputSlot
const config = configs[input.name]
if (config != null && index >= 0 && index < this.inputs.length) {
if (input.config == null || Object.keys(input.config).length !== Object.keys(config).length) {
console.debug("[ComfyBackendNode] restore input config", input, config)
input.config = config
}
}
else {
console.debug("[ComfyBackendNode] Missing input config in onConfigure()", input, configs)
input.config = {}
}
}
} }
override onExecuted(outputData: any) { override onExecuted(outputData: any) {
console.warn("onExecuted outputs", outputData) console.warn("onExecuted outputs", outputData)
for (let index = 0; index < this.outputs.length; index++) { this.triggerSlot(0, outputData)
const output = this.outputs[index]
if (output.type === "IMAGE") {
this.setOutputData(index, outputData)
for (const node of this.getOutputNodes(index)) {
console.warn(node)
if ("receiveOutput" in node) {
const widgetNode = node as ComfyGraphNode;
widgetNode.receiveOutput(outputData);
}
}
}
}
} }
} }

View File

@@ -1,7 +1,7 @@
import type { ComfyInputConfig } from "$lib/IComfyInputSlot"; import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
import type { SerializedPrompt } from "$lib/components/ComfyApp"; import type { SerializedPrompt } from "$lib/components/ComfyApp";
import type ComfyWidget from "$lib/components/widgets/ComfyWidget"; import type ComfyWidget from "$lib/components/widgets/ComfyWidget";
import { LGraph, LGraphNode, LiteGraph, type SerializedLGraphNode, type Vector2 } from "@litegraph-ts/core"; import { LGraph, LGraphNode, LLink, LiteGraph, NodeMode, type INodeInputSlot, type SerializedLGraphNode, type Vector2, type INodeOutputSlot, LConnectionKind, type SlotType, LGraphCanvas, getStaticPropertyOnInstance, type PropertyLayout, type SlotLayout } from "@litegraph-ts/core";
import type { SvelteComponentDev } from "svelte/internal"; import type { SvelteComponentDev } from "svelte/internal";
import type { ComfyWidgetNode } from "./ComfyWidgetNodes"; import type { ComfyWidgetNode } from "./ComfyWidgetNodes";
import type IComfyInputSlot from "$lib/IComfyInputSlot"; import type IComfyInputSlot from "$lib/IComfyInputSlot";
@@ -17,7 +17,15 @@ export type DefaultWidgetLayout = {
inputs?: Record<number, DefaultWidgetSpec>, inputs?: Record<number, DefaultWidgetSpec>,
} }
export interface ComfyGraphNodeProperties extends Record<string, any> {
tags: string[]
}
export default class ComfyGraphNode extends LGraphNode { export default class ComfyGraphNode extends LGraphNode {
override properties: ComfyGraphNodeProperties = {
tags: []
}
isBackendNode?: boolean; isBackendNode?: boolean;
beforeQueued?(subgraph: string | null): void; beforeQueued?(subgraph: string | null): void;
@@ -26,8 +34,198 @@ export default class ComfyGraphNode extends LGraphNode {
defaultWidgets?: DefaultWidgetLayout defaultWidgets?: DefaultWidgetLayout
/** Called when a backend node sends a ComfyUI output over a link */ /*
receiveOutput(output: any) { * If true, attempt to reconcile wildcard types in slots ("*")
* when a new input/output is connected
*
* Only set this to true if all output slots are wildcard typed in the
* static slotLayout property by default!
*/
canInheritSlotTypes: boolean = false;
/*
* If false, don't serialize user-set properties into the workflow.
* Useful for removing personal information from shared workflows.
*/
saveUserState: boolean = true;
/*
* Called to remove user-set properties from this node.
*/
stripUserState(o: SerializedLGraphNode) {
o.widgets_values = []
}
/*
* Traverses this node backwards in the graph in order to reach a connecting
* backend node, if any. For example, reroute nodes will simply follow their
* single input, while branching nodes have conditional logic that
* determines which link to follow backwards.
*/
getUpstreamLink(): LLink | null {
return null;
}
constructor(title?: string) {
super(title)
this.addProperty("tags", [], "array")
}
private inheritSlotTypes(type: LConnectionKind, isConnected: boolean) {
// Prevent multiple connections to different types when we have no input
if (isConnected && type === LConnectionKind.OUTPUT) {
// Ignore wildcard nodes as these will be updated to real types
const types = new Set(this.outputs.flatMap(o => o.links.map((l) => this.graph.links[l].type).filter((t) => t !== "*")));
if (types.size > 1) {
for (let j = 0; j < this.outputs.length; j++) {
for (let i = 0; i < this.outputs[j].links.length - 1; i++) {
const linkId = this.outputs[j].links[i];
const link = this.graph.links[linkId];
const node = this.graph.getNodeById(link.target_id);
node.disconnectInput(link.target_slot);
}
}
}
}
// Find root input
let currentNode: ComfyGraphNode = this;
let updateNodes: ComfyGraphNode[] = [];
let inputType: SlotType | null = null;
let inputNode = null;
while (currentNode) {
updateNodes.unshift(currentNode);
const link = currentNode.getUpstreamLink();
if (link !== null) {
const node = this.graph.getNodeById(link.origin_id) as ComfyGraphNode;
console.warn(node.type)
if (node.canInheritSlotTypes) {
console.log("REROUTE2", node)
if (node === this) {
// We've found a circle
currentNode.disconnectInput(link.target_slot);
currentNode = null;
}
else {
// Move the previous node
currentNode = node;
}
} else {
// We've found the end
inputNode = currentNode;
inputType = node.outputs[link.origin_slot]?.type ?? null;
break;
}
} else {
// This path has no input node
currentNode = null;
break;
}
}
// Find all outputs
const nodes: ComfyGraphNode[] = [this];
let outputType: SlotType | null = null;
while (nodes.length) {
currentNode = nodes.pop();
if (currentNode.outputs) {
for (let i = 0; i < currentNode.outputs.length; i++) {
const outputs = currentNode.outputs[i].links || [];
if (outputs.length) {
for (const linkId of outputs) {
const link = this.graph.links[linkId];
// When disconnecting sometimes the link is still registered
if (!link) continue;
const node = this.graph.getNodeById(link.target_id) as ComfyGraphNode;
if (node.canInheritSlotTypes) {
console.log("REROUTE", node)
// Follow reroute nodes
nodes.push(node);
updateNodes.push(node);
} else {
// We've found an output
const nodeOutType = node.inputs && node.inputs[link?.target_slot] && node.inputs[link.target_slot].type ? node.inputs[link.target_slot].type : null;
if (inputType && nodeOutType !== inputType) {
// The output doesnt match our input so disconnect it
node.disconnectInput(link.target_slot);
} else {
outputType = nodeOutType;
}
}
}
} else {
// No more outputs for this path
}
}
}
}
const displayType = inputType || outputType || "*";
const color = LGraphCanvas.DEFAULT_LINK_TYPE_COLORS[displayType];
// Update the types of each node
for (const node of updateNodes) {
// in lieu of static abstract properties
const slotLayout = getStaticPropertyOnInstance<SlotLayout>(node, "slotLayout");
if (!slotLayout)
continue
const layoutOutputs = slotLayout.outputs || []
for (let i = 0; i < node.outputs.length; i++) {
// Check if this output was defined as starting off as a
// wildcard. If for example it was something else like a string,
// it wouldn't make sense to change its type dynamically.
const isWildcardOutput = layoutOutputs.length > i && layoutOutputs[i].type === "*";
if (!isWildcardOutput) {
console.error("not wildcard", node.outputs[i], layoutOutputs[i])
continue;
}
// 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
node.outputs[i].type = inputType || "*";
(node as any).__outputType = displayType;
node.outputs[i].name = node.properties.showOutputText ? String(displayType) : "";
node.size = node.computeSize();
// TODO from ComfyReroute
if ("applyOrientation" in node && typeof node.applyOrientation === "function")
node.applyOrientation();
for (const l of node.outputs[i].links || []) {
const link = this.graph.links[l];
if (link) {
link.color = color;
}
}
}
}
if (inputNode) {
for (let i = 0; i < inputNode.inputs.length; i++) {
const link = this.graph.links[inputNode.inputs[i].link];
if (link) {
link.color = color;
}
}
}
}
override onConnectionsChange(
type: LConnectionKind,
slotIndex: number,
isConnected: boolean,
link: LLink,
ioSlot: (INodeInputSlot | INodeOutputSlot)
) {
if (this.canInheritSlotTypes) {
this.inheritSlotTypes(type, isConnected);
}
} }
override onResize(size: Vector2) { override onResize(size: Vector2) {
@@ -56,6 +254,11 @@ export default class ComfyGraphNode extends LGraphNode {
(serInput as any).defaultWidgetNode = null (serInput as any).defaultWidgetNode = null
} }
} }
(o as any).saveUserState = this.saveUserState
if (!this.saveUserState) {
this.stripUserState(o)
console.warn("[ComfyGraphNode] stripUserState", this, o)
}
} }
override onConfigure(o: SerializedLGraphNode) { override onConfigure(o: SerializedLGraphNode) {
@@ -71,5 +274,9 @@ export default class ComfyGraphNode extends LGraphNode {
comfyInput.defaultWidgetNode = widgetNode.class as any comfyInput.defaultWidgetNode = widgetNode.class as any
} }
} }
this.saveUserState = (o as any).saveUserState;
if (this.saveUserState == null)
this.saveUserState = true
} }
} }

View File

@@ -1,8 +1,8 @@
import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout, clamp, type PropertyLayout, type IComboWidget } from "@litegraph-ts/core"; import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout, clamp, type PropertyLayout, type IComboWidget } from "@litegraph-ts/core";
import ComfyGraphNode from "./ComfyGraphNode"; import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
import type { GalleryOutput } from "./ComfyWidgetNodes"; import type { GalleryOutput } from "./ComfyWidgetNodes";
export interface ComfyImageCacheNodeProperties extends Record<any, any> { export interface ComfyImageCacheNodeProperties extends ComfyGraphNodeProperties {
images: GalleryOutput | null, images: GalleryOutput | null,
index: number, index: number,
filenames: Record<number, { filename: string | null, status: ImageCacheState }>, filenames: Record<number, { filename: string | null, status: ImageCacheState }>,
@@ -18,6 +18,7 @@ type ImageCacheState = "none" | "uploading" | "failed" | "cached"
*/ */
export default class ComfyImageCacheNode extends ComfyGraphNode { export default class ComfyImageCacheNode extends ComfyGraphNode {
override properties: ComfyImageCacheNodeProperties = { override properties: ComfyImageCacheNodeProperties = {
tags: [],
images: null, images: null,
index: 0, index: 0,
filenames: {}, filenames: {},
@@ -29,7 +30,7 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
inputs: [ inputs: [
{ name: "images", type: "OUTPUT" }, { name: "images", type: "OUTPUT" },
{ name: "index", type: "number" }, { name: "index", type: "number" },
{ name: "store", type: BuiltInSlotType.ACTION }, { name: "store", type: BuiltInSlotType.ACTION, options: { color_off: "rebeccapurple", color_on: "rebeccapurple" } },
{ name: "clear", type: BuiltInSlotType.ACTION } { name: "clear", type: BuiltInSlotType.ACTION }
], ],
outputs: [ outputs: [
@@ -158,7 +159,7 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
else { else {
this.properties.filenames[newIndex] = { filename: null, status: "uploading" } this.properties.filenames[newIndex] = { filename: null, status: "uploading" }
this.onPropertyChanged("filenames", this.properties.filenames) this.onPropertyChanged("filenames", this.properties.filenames)
const url = "http://localhost:8188" // TODO make configurable const url = `http://${location.hostname}:8188` // TODO make configurable
const params = new URLSearchParams(data) const params = new URLSearchParams(data)
const promise = fetch(url + "/view?" + params) const promise = fetch(url + "/view?" + params)
@@ -204,7 +205,7 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
} }
} }
override onAction(action: any) { override onAction(action: any, param: any) {
if (action === "clear") { if (action === "clear") {
this.setProperty("images", null) this.setProperty("images", null)
this.setProperty("filenames", {}) this.setProperty("filenames", {})
@@ -213,12 +214,10 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
return return
} }
const link = this.getInputLink(0) if (param && "images" in param) {
if (link.data && "images" in link.data) {
this.setProperty("genNumber", this.properties.genNumber + 1) this.setProperty("genNumber", this.properties.genNumber + 1)
const output = link.data as GalleryOutput; const output = param as GalleryOutput;
if (this.properties.updateMode === "append" && this.properties.images != null) { if (this.properties.updateMode === "append" && this.properties.images != null) {
const newImages = this.properties.images.images.concat(output.images) const newImages = this.properties.images.images.concat(output.images)
@@ -226,7 +225,7 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
this.setProperty("images", this.properties.images) this.setProperty("images", this.properties.images)
} }
else { else {
this.setProperty("images", link.data as GalleryOutput) this.setProperty("images", param as GalleryOutput)
this.setProperty("filenames", {}) this.setProperty("filenames", {})
} }

View File

@@ -0,0 +1,161 @@
import { BuiltInSlotType, LiteGraph, NodeMode, type INodeInputSlot, type SlotLayout, type INodeOutputSlot, LLink, LConnectionKind, type ITextWidget } from "@litegraph-ts/core";
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
import { Watch } from "@litegraph-ts/nodes-basic";
export interface ComfyPickFirstNodeProperties extends ComfyGraphNodeProperties {
acceptNullLinkData: boolean
}
function nextLetter(s: string): string {
return s.replace(/([a-zA-Z])[^a-zA-Z]*$/, function(a) {
var c = a.charCodeAt(0);
switch (c) {
case 90: return 'A';
case 122: return 'a';
default: return String.fromCharCode(++c);
}
});
}
export default class ComfyPickFirstNode extends ComfyGraphNode {
override properties: ComfyPickFirstNodeProperties = {
tags: [],
acceptNullLinkData: false
}
static slotLayout: SlotLayout = {
inputs: [
{ name: "A", type: "*" },
{ name: "B", type: "*" },
],
outputs: [
{ name: "out", type: "*" }
],
}
override canInheritSlotTypes = true;
private selected: number = -1;
displayWidget: ITextWidget;
constructor(title?: string) {
super(title);
this.displayWidget = this.addWidget("text", "Value", "")
this.displayWidget.disabled = true;
}
override onDrawBackground(ctx: CanvasRenderingContext2D) {
if (this.flags.collapsed || this.selected === -1) {
return;
}
ctx.fillStyle = "#AFB";
var y = (this.selected) * LiteGraph.NODE_SLOT_HEIGHT + 6;
ctx.beginPath();
ctx.moveTo(50, y);
ctx.lineTo(50, y + LiteGraph.NODE_SLOT_HEIGHT);
ctx.lineTo(34, y + LiteGraph.NODE_SLOT_HEIGHT * 0.5);
ctx.fill();
};
override onConnectionsChange(
type: LConnectionKind,
slotIndex: number,
isConnected: boolean,
link: LLink,
ioSlot: (INodeInputSlot | INodeOutputSlot)
) {
super.onConnectionsChange(type, slotIndex, isConnected, link, ioSlot);
if (type !== LConnectionKind.INPUT)
return;
if (isConnected) {
if (link != null && slotIndex === this.inputs.length - 1) {
// Add a new input
const lastInputName = this.inputs[this.inputs.length - 1].name
const inputName = nextLetter(lastInputName);
this.addInput(inputName, this.inputs[0].type)
}
}
else {
if (this.getInputLink(this.inputs.length - 1) != null)
return;
// Remove empty inputs
for (let i = this.inputs.length - 2; i > 0; i--) {
if (i <= 0)
break;
if (this.getInputLink(i) == null)
this.removeInput(i)
else
break;
}
let name = "A"
for (let i = 0; i < this.inputs.length; i++) {
this.inputs[i].name = name;
name = nextLetter(name);
}
}
}
private isValidLink(link: LLink | null): boolean {
if (!link)
return false;
const node = this.graph.getNodeById(link.origin_id);
// Links to deactivated nodes won't count.
if (!node || node.mode !== NodeMode.ALWAYS)
return false;
if ((node as ComfyGraphNode).isBackendNode) {
// Backend nodes won't set data, we can safely assume they're valid.
return true;
}
else {
return link.data != null || this.properties.acceptNullLinkData;
}
}
override getUpstreamLink(): LLink | null {
for (let index = 0; index < this.inputs.length; index++) {
const link = this.getInputLink(index);
if (this.isValidLink(link)) {
return link;
}
}
return null;
}
override onExecute() {
for (let index = 0; index < this.inputs.length; index++) {
const link = this.getInputLink(index);
if (this.isValidLink(link)) {
// Copy frontend-only inputs.
const node = this.getInputNode(index);
if (node != null) {
this.selected = index;
if (!(node as any).isBackendNode) {
this.displayWidget.value = Watch.toString(link.data)
this.setOutputData(0, link.data)
}
return
}
}
}
this.selected = -1;
this.setOutputData(0, null)
}
}
LiteGraph.registerNodeType({
class: ComfyPickFirstNode,
title: "Comfy.PickFirst",
desc: "Picks the first active input connected to this node (top to bottom)",
type: "utils/pick_first"
})

View File

@@ -1,7 +1,7 @@
import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout } from "@litegraph-ts/core"; import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, NodeMode } from "@litegraph-ts/core";
import ComfyGraphNode from "./ComfyGraphNode"; import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
export interface ComfyRerouteProperties extends Record<any, any> { export interface ComfyRerouteProperties extends ComfyGraphNodeProperties {
showOutputText: boolean; showOutputText: boolean;
horizontal: boolean; horizontal: boolean;
} }
@@ -22,6 +22,7 @@ export default class ComfyReroute extends ComfyGraphNode {
override collapsable: boolean = false; override collapsable: boolean = false;
override properties: ComfyRerouteProperties = { override properties: ComfyRerouteProperties = {
tags: [],
showOutputText: ComfyReroute.defaultVisibility, showOutputText: ComfyReroute.defaultVisibility,
horizontal: false horizontal: false
} }
@@ -47,124 +48,20 @@ export default class ComfyReroute extends ComfyGraphNode {
} }
} }
override onConnectionsChange(type: LConnectionKind, slotIndex: number, isConnected: boolean, _link: LLink) { override getUpstreamLink(): LLink | null {
const link = this.getInputLink(0)
const node = this.getInputNode(0)
if (link && node && node.mode === NodeMode.ALWAYS)
return link;
return null;
}
override canInheritSlotTypes = true;
override onConnectionsChange(type: LConnectionKind, slotIndex: number, isConnected: boolean, link: LLink, ioSlot: (INodeInputSlot | INodeOutputSlot)) {
this.applyOrientation(); this.applyOrientation();
// Prevent multiple connections to different types when we have no input super.onConnectionsChange(type, slotIndex, isConnected, link, ioSlot);
if (isConnected && type === LConnectionKind.OUTPUT) {
// Ignore wildcard nodes as these will be updated to real types
const types = new Set(this.outputs[0].links.map((l) => this.graph.links[l].type).filter((t) => t !== "*"));
if (types.size > 1) {
for (let i = 0; i < this.outputs[0].links.length - 1; i++) {
const linkId = this.outputs[0].links[i];
const link = this.graph.links[linkId];
const node = this.graph.getNodeById(link.target_id);
node.disconnectInput(link.target_slot);
}
}
}
// Find root input
let currentNode: ComfyReroute = this;
let updateNodes: ComfyReroute[] = [];
let inputType: SlotType | null = null;
let inputNode = null;
while (currentNode) {
updateNodes.unshift(currentNode);
const linkId = currentNode.inputs[0].link;
if (linkId !== null) {
const link = this.graph.links[linkId];
const node = this.graph.getNodeById(link.origin_id);
console.warn(node.type)
if (node.class === ComfyReroute) {
console.log("REROUTE2")
if (node === this) {
// We've found a circle
currentNode.disconnectInput(link.target_slot);
currentNode = null;
}
else {
// Move the previous node
currentNode = node as ComfyReroute;
}
} else {
// We've found the end
inputNode = currentNode;
inputType = node.outputs[link.origin_slot]?.type ?? null;
break;
}
} else {
// This path has no input node
currentNode = null;
break;
}
}
// Find all outputs
const nodes: ComfyReroute[] = [this];
let outputType: SlotType | null = null;
while (nodes.length) {
currentNode = nodes.pop();
const outputs = (currentNode.outputs ? currentNode.outputs[0].links : []) || [];
if (outputs.length) {
for (const linkId of outputs) {
const link = this.graph.links[linkId];
// When disconnecting sometimes the link is still registered
if (!link) continue;
const node = this.graph.getNodeById(link.target_id);
if (node.class === ComfyReroute) {
console.log("REROUTE")
// Follow reroute nodes
nodes.push(node as ComfyReroute);
updateNodes.push(node as ComfyReroute);
} else {
// We've found an output
const nodeOutType = node.inputs && node.inputs[link?.target_slot] && node.inputs[link.target_slot].type ? node.inputs[link.target_slot].type : null;
if (inputType && nodeOutType !== inputType) {
// The output doesnt match our input so disconnect it
node.disconnectInput(link.target_slot);
} else {
outputType = nodeOutType;
}
}
}
} else {
// No more outputs for this path
}
}
const displayType = inputType || outputType || "*";
const color = LGraphCanvas.link_type_colors[displayType];
// Update the types of each node
for (const node of updateNodes) {
// 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
node.outputs[0].type = inputType || "*";
(node as any).__outputType = displayType;
node.outputs[0].name = node.properties.showOutputText ? String(displayType) : "";
node.size = node.computeSize();
if ("applyOrientation" in node && typeof node.applyOrientation === "function")
node.applyOrientation();
for (const l of node.outputs[0].links || []) {
const link = this.graph.links[l];
if (link) {
link.color = color;
}
}
}
if (inputNode) {
const link = this.graph.links[inputNode.inputs[0].link];
if (link) {
link.color = color;
}
}
}; };
override clone(): LGraphNode { override clone(): LGraphNode {

View File

@@ -1,12 +1,13 @@
import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core"; import { BuiltInSlotType, LConnectionKind, LLink, LiteGraph, NodeMode, type INodeInputSlot, type SlotLayout, type INodeOutputSlot } from "@litegraph-ts/core";
import ComfyGraphNode from "./ComfyGraphNode"; import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
export interface ComfySelectorProperties extends Record<any, any> { export interface ComfySelectorProperties extends ComfyGraphNodeProperties {
value: any value: any
} }
export default class ComfySelector extends ComfyGraphNode { export default class ComfySelector extends ComfyGraphNode {
override properties: ComfySelectorProperties = { override properties: ComfySelectorProperties = {
tags: [],
value: null value: null
} }
@@ -23,12 +24,29 @@ export default class ComfySelector extends ComfyGraphNode {
], ],
} }
override canInheritSlotTypes = true;
private selected: number = 0; private selected: number = 0;
constructor(title?: string) { constructor(title?: string) {
super(title); super(title);
} }
override getUpstreamLink(): LLink | null {
var sel = this.getInputData(0);
if (sel == null || sel.constructor !== Number)
sel = 0;
this.selected = sel = Math.round(sel) % (this.inputs.length - 1);
var link = this.getInputLink(sel + 1);
var node = this.getInputNode(sel + 1);
if (link != null && node != null && node.mode === NodeMode.ALWAYS)
return link;
return null
}
override onDrawBackground(ctx: CanvasRenderingContext2D) { override onDrawBackground(ctx: CanvasRenderingContext2D) {
if (this.flags.collapsed) { if (this.flags.collapsed) {
return; return;
@@ -61,12 +79,13 @@ LiteGraph.registerNodeType({
type: "utils/selector" type: "utils/selector"
}) })
export interface ComfySelectorTwoProperties extends Record<any, any> { export interface ComfySelectorTwoProperties extends ComfyGraphNodeProperties {
value: any value: any
} }
export class ComfySelectorTwo extends ComfyGraphNode { export class ComfySelectorTwo extends ComfyGraphNode {
override properties: ComfySelectorTwoProperties = { override properties: ComfySelectorTwoProperties = {
tags: [],
value: null value: null
} }
@@ -81,12 +100,41 @@ export class ComfySelectorTwo extends ComfyGraphNode {
], ],
} }
override canInheritSlotTypes = true;
private selected: number = 0; private selected: number = 0;
constructor(title?: string) { constructor(title?: string) {
super(title); super(title);
} }
override getUpstreamLink(): LLink | null {
var sel = this.getInputData(0);
if (sel == null || sel.constructor !== Boolean)
sel = 0;
this.selected = sel ? 0 : 1;
var link = this.getInputLink(this.selected + 1);
var node = this.getInputNode(this.selected + 1);
if (link != null && node != null && node.mode === NodeMode.ALWAYS)
return link
return null;
}
override onConnectionsChange(
type: LConnectionKind,
slotIndex: number,
isConnected: boolean,
link: LLink,
ioSlot: (INodeInputSlot | INodeOutputSlot)
) {
if (type === LConnectionKind.INPUT) {
}
}
override onDrawBackground(ctx: CanvasRenderingContext2D) { override onDrawBackground(ctx: CanvasRenderingContext2D) {
if (this.flags.collapsed) { if (this.flags.collapsed) {
return; return;
@@ -107,6 +155,8 @@ export class ComfySelectorTwo extends ComfyGraphNode {
this.selected = sel ? 0 : 1; this.selected = sel ? 0 : 1;
var v = this.getInputData(this.selected + 1); var v = this.getInputData(this.selected + 1);
if (v !== undefined) { if (v !== undefined) {
const link = this.getInputLink(this.selected + 1);
const node = this.getInputNode(this.selected + 1);
this.setOutputData(0, v); this.setOutputData(0, v);
} }
} }

View File

@@ -1,10 +1,10 @@
import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core"; import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core";
import ComfyGraphNode, { type DefaultWidgetLayout } from "./ComfyGraphNode"; import ComfyGraphNode, { type ComfyGraphNodeProperties, type DefaultWidgetLayout } from "./ComfyGraphNode";
import { clamp } from "$lib/utils"; import { clamp } from "$lib/utils";
import ComboWidget from "$lib/widgets/ComboWidget.svelte"; import ComboWidget from "$lib/widgets/ComboWidget.svelte";
import { ComfyComboNode } from "./ComfyWidgetNodes"; import { ComfyComboNode } from "./ComfyWidgetNodes";
export interface ComfyValueControlProperties extends Record<any, any> { export interface ComfyValueControlProperties extends ComfyGraphNodeProperties {
value: any, value: any,
action: "fixed" | "increment" | "decrement" | "randomize", action: "fixed" | "increment" | "decrement" | "randomize",
min: number, min: number,
@@ -16,6 +16,7 @@ const INT_MAX = 1125899906842624;
export default class ComfyValueControl extends ComfyGraphNode { export default class ComfyValueControl extends ComfyGraphNode {
override properties: ComfyValueControlProperties = { override properties: ComfyValueControlProperties = {
tags: [],
value: null, value: null,
action: "fixed", action: "fixed",
min: -INT_MAX, min: -INT_MAX,
@@ -33,7 +34,8 @@ export default class ComfyValueControl extends ComfyGraphNode {
{ name: "step", type: "number" } { name: "step", type: "number" }
], ],
outputs: [ outputs: [
{ name: "value", type: "*" } { name: "value", type: "*" },
{ name: "changed", type: BuiltInSlotType.EVENT }
], ],
} }
@@ -49,6 +51,11 @@ export default class ComfyValueControl extends ComfyGraphNode {
} }
} }
delayChangeEvent: boolean = true;
private _aboutToChange: number = 0;
private _aboutToChangeValue: any = null;
constructor(title?: string) { constructor(title?: string) {
super(title); super(title);
} }
@@ -58,6 +65,16 @@ export default class ComfyValueControl extends ComfyGraphNode {
this.setProperty("min", this.getInputData(3)) this.setProperty("min", this.getInputData(3))
this.setProperty("max", this.getInputData(4)) this.setProperty("max", this.getInputData(4))
this.setProperty("step", this.getInputData(5) || 1) this.setProperty("step", this.getInputData(5) || 1)
if (this._aboutToChange > 0) {
this._aboutToChange -= 1;
if (this._aboutToChange <= 0) {
const value = this._aboutToChangeValue;
this._aboutToChange = 0;
this._aboutToChangeValue = null;
this.triggerSlot(1, value)
}
}
} }
override onAction(action: any, param: any) { override onAction(action: any, param: any) {
@@ -95,6 +112,14 @@ export default class ComfyValueControl extends ComfyGraphNode {
this.setProperty("value", v) this.setProperty("value", v)
this.setOutputData(0, v) this.setOutputData(0, v)
if (this.delayChangeEvent) {
this._aboutToChange = 2;
this._aboutToChangeValue = v;
}
else {
this.triggerSlot(1, v)
}
console.debug("ValueControl", v, this.properties) console.debug("ValueControl", v, this.properties)
}; };
} }

View File

@@ -1,21 +1,39 @@
import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, LGraph, type INodeInputSlot, type ITextWidget, type INodeOutputSlot, type SerializedLGraphNode, BuiltInSlotType, type PropertyLayout, type IComboWidget } from "@litegraph-ts/core"; import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, LGraph, type INodeInputSlot, type ITextWidget, type INodeOutputSlot, type SerializedLGraphNode, BuiltInSlotType, type PropertyLayout, type IComboWidget, NodeMode, type INumberWidget } from "@litegraph-ts/core";
import ComfyGraphNode from "./ComfyGraphNode"; import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
import type { SvelteComponentDev } from "svelte/internal";
import { Watch } from "@litegraph-ts/nodes-basic";
import type IComfyInputSlot from "$lib/IComfyInputSlot";
import { writable, type Unsubscriber, type Writable, get } from "svelte/store";
import { clamp, convertComfyOutputToGradio, range } from "$lib/utils"
import layoutState from "$lib/stores/layoutState";
import type { FileData as GradioFileData } from "@gradio/upload";
import queueState from "$lib/stores/queueState";
import ComboWidget from "$lib/widgets/ComboWidget.svelte"; import ComboWidget from "$lib/widgets/ComboWidget.svelte";
import RangeWidget from "$lib/widgets/RangeWidget.svelte"; import RangeWidget from "$lib/widgets/RangeWidget.svelte";
import TextWidget from "$lib/widgets/TextWidget.svelte"; import TextWidget from "$lib/widgets/TextWidget.svelte";
import GalleryWidget from "$lib/widgets/GalleryWidget.svelte"; import GalleryWidget from "$lib/widgets/GalleryWidget.svelte";
import ButtonWidget from "$lib/widgets/ButtonWidget.svelte"; import ButtonWidget from "$lib/widgets/ButtonWidget.svelte";
import CheckboxWidget from "$lib/widgets/CheckboxWidget.svelte"; import CheckboxWidget from "$lib/widgets/CheckboxWidget.svelte";
import type { SvelteComponentDev } from "svelte/internal"; import RadioWidget from "$lib/widgets/RadioWidget.svelte";
import { Watch } from "@litegraph-ts/nodes-basic";
import type IComfyInputSlot from "$lib/IComfyInputSlot";
import { writable, type Unsubscriber, type Writable, get } from "svelte/store";
import { clamp, range } from "$lib/utils"
import layoutState from "$lib/stores/layoutState";
import type { FileData as GradioFileData } from "@gradio/upload";
import queueState from "$lib/stores/queueState";
export interface ComfyWidgetProperties extends Record<string, any> { /*
* NOTE: If you want to add a new widget but it has the same input/output type
* as another one of the existing widgets, best to create a new "variant" of
* that widget instead.
*
* - Go to layoutState, look for `ALL_ATTRIBUTES,` insert or find a "variant"
* attribute and set `validNodeTypes` to the type of the litegraph node
* - Add a new entry in the `values` array, like "knob" or "dial" for ComfySliderWidget
* - Add an {#if widget.attrs.variant === <...>} statement in the corresponding Svelte component
*
* Also, BEWARE of calling setOutputData() and triggerSlot() on the same frame!
* You will have to either implement an internal delay on the event triggering
* or use an Event Delay node to ensure the output slot data can propagate to
* the rest of the graph first (see `delayChangedEvent` for details)
*/
export interface ComfyWidgetProperties extends ComfyGraphNodeProperties {
defaultValue: any defaultValue: any
} }
@@ -39,6 +57,18 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
copyFromInputLink: boolean = true; copyFromInputLink: boolean = true;
/**
* If true wait until next frame update to trigger the changed event.
* Reason is, if the event is triggered immediately then other stuff that wants to run
* their own onExecute on the output value won't have completed yet.
*/
delayChangedEvent: boolean = true;
private _aboutToChange: number = 0;
private _aboutToChangeValue: any = null;
abstract defaultValue: T;
/** Names of properties to add as inputs */ /** Names of properties to add as inputs */
// shownInputProperties: string[] = [] // shownInputProperties: string[] = []
@@ -91,6 +121,12 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
return Watch.toString(value) return Watch.toString(value)
} }
override changeMode(modeTo: NodeMode): boolean {
const result = super.changeMode(modeTo);
this.notifyPropsChanged();
return result;
}
private onValueUpdated(value: any) { private onValueUpdated(value: any) {
console.debug("[Widget] valueUpdated", this, value) console.debug("[Widget] valueUpdated", this, value)
this.displayWidget.value = this.formatValue(value) this.displayWidget.value = this.formatValue(value)
@@ -98,13 +134,23 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
if (this.outputIndex !== null && this.outputs.length >= this.outputIndex) { if (this.outputIndex !== null && this.outputs.length >= this.outputIndex) {
this.setOutputData(this.outputIndex, get(this.value)) this.setOutputData(this.outputIndex, get(this.value))
} }
if (this.changedIndex !== null && this.outputs.length >= this.changedIndex) { if (this.changedIndex !== null && this.outputs.length >= this.changedIndex) {
const changedOutput = this.outputs[this.changedIndex] if (!this.delayChangedEvent)
if (changedOutput.type === BuiltInSlotType.EVENT) this.triggerChangeEvent(get(this.value))
this.triggerSlot(this.changedIndex, "changed") else {
this._aboutToChange = 2; // wait 1.5-2 frames, in case we're already in the middle of one
this._aboutToChangeValue = get(this.value);
}
} }
} }
private triggerChangeEvent(value: any) {
const changedOutput = this.outputs[this.changedIndex]
if (changedOutput.type === BuiltInSlotType.EVENT)
this.triggerSlot(this.changedIndex, value)
}
setValue(value: any) { setValue(value: any) {
this.value.set(value) this.value.set(value)
} }
@@ -118,7 +164,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
/* /*
* Logic to run if this widget can be treated as output (slider, combo, text) * Logic to run if this widget can be treated as output (slider, combo, text)
*/ */
override onExecute() { override onExecute(param: any, options: object) {
if (this.copyFromInputLink) { if (this.copyFromInputLink) {
if (this.inputs.length >= this.inputIndex) { if (this.inputs.length >= this.inputIndex) {
const data = this.getInputData(this.inputIndex) const data = this.getInputData(this.inputIndex)
@@ -134,6 +180,18 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
const data = this.shownOutputProperties[propName] const data = this.shownOutputProperties[propName]
this.setOutputData(data.index, this.properties[propName]) this.setOutputData(data.index, this.properties[propName])
} }
// Fire a pending change event after one full step of the graph has
// finished processing
if (this._aboutToChange > 0) {
this._aboutToChange -= 1
if (this._aboutToChange <= 0) {
const value = this._aboutToChangeValue;
this._aboutToChange = 0;
this._aboutToChangeValue = null;
this.triggerChangeEvent(value);
}
}
} }
onConnectOutput( onConnectOutput(
@@ -169,16 +227,27 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
console.debug("Property copy", input, this.properties) console.debug("Property copy", input, this.properties)
this.setValue(get(this.value)) this.setValue(get(this.value))
this.propsChanged.set(get(this.propsChanged) + 1) this.notifyPropsChanged();
} }
onConnectionsChange( notifyPropsChanged() {
const layoutEntry = layoutState.findLayoutEntryForNode(this.id)
if (layoutEntry && layoutEntry.parent) {
layoutEntry.parent.attrsChanged.set(get(layoutEntry.parent.attrsChanged) + 1)
}
console.debug("propsChanged", this)
this.propsChanged.set(get(this.propsChanged) + 1)
}
override onConnectionsChange(
type: LConnectionKind, type: LConnectionKind,
slotIndex: number, slotIndex: number,
isConnected: boolean, isConnected: boolean,
link: LLink, link: LLink,
ioSlot: (INodeOutputSlot | INodeInputSlot) ioSlot: (INodeOutputSlot | INodeInputSlot)
): void { ): void {
super.onConnectionsChange(type, slotIndex, isConnected, link, ioSlot);
this.clampConfig(); this.clampConfig();
} }
@@ -198,21 +267,27 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
} }
// Force reactivity change so the frontend can be updated with the new props // Force reactivity change so the frontend can be updated with the new props
this.propsChanged.set(get(this.propsChanged) + 1) this.notifyPropsChanged();
} }
clampOneConfig(input: IComfyInputSlot) { } clampOneConfig(input: IComfyInputSlot) { }
override onSerialize(o: SerializedLGraphNode) { override onSerialize(o: SerializedLGraphNode) {
super.onSerialize(o);
(o as any).comfyValue = get(this.value); (o as any).comfyValue = get(this.value);
(o as any).shownOutputProperties = this.shownOutputProperties (o as any).shownOutputProperties = this.shownOutputProperties
super.onSerialize(o);
} }
override onConfigure(o: SerializedLGraphNode) { override onConfigure(o: SerializedLGraphNode) {
this.value.set((o as any).comfyValue); this.value.set((o as any).comfyValue);
this.shownOutputProperties = (o as any).shownOutputProperties; this.shownOutputProperties = (o as any).shownOutputProperties;
} }
override stripUserState(o: SerializedLGraphNode) {
super.stripUserState(o);
(o as any).comfyValue = this.defaultValue;
o.properties.defaultValue = null;
}
} }
export interface ComfySliderProperties extends ComfyWidgetProperties { export interface ComfySliderProperties extends ComfyWidgetProperties {
@@ -224,6 +299,7 @@ export interface ComfySliderProperties extends ComfyWidgetProperties {
export class ComfySliderNode extends ComfyWidgetNode<number> { export class ComfySliderNode extends ComfyWidgetNode<number> {
override properties: ComfySliderProperties = { override properties: ComfySliderProperties = {
tags: [],
defaultValue: 0, defaultValue: 0,
min: 0, min: 0,
max: 10, max: 10,
@@ -232,6 +308,7 @@ export class ComfySliderNode extends ComfyWidgetNode<number> {
} }
override svelteComponentType = RangeWidget override svelteComponentType = RangeWidget
override defaultValue = 0;
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
inputs: [ inputs: [
@@ -287,6 +364,7 @@ export interface ComfyComboProperties extends ComfyWidgetProperties {
export class ComfyComboNode extends ComfyWidgetNode<string> { export class ComfyComboNode extends ComfyWidgetNode<string> {
override properties: ComfyComboProperties = { override properties: ComfyComboProperties = {
tags: [],
defaultValue: "A", defaultValue: "A",
values: ["A", "B", "C", "D"] values: ["A", "B", "C", "D"]
} }
@@ -303,9 +381,14 @@ export class ComfyComboNode extends ComfyWidgetNode<string> {
} }
override svelteComponentType = ComboWidget override svelteComponentType = ComboWidget
override defaultValue = "A";
override saveUserState = false;
comboRefreshed: Writable<boolean>;
constructor(name?: string) { constructor(name?: string) {
super(name, "A") super(name, "A")
this.comboRefreshed = writable(false)
} }
onConnectOutput( onConnectOutput(
@@ -346,13 +429,20 @@ export class ComfyComboNode extends ComfyWidgetNode<string> {
} }
override clampOneConfig(input: IComfyInputSlot) { override clampOneConfig(input: IComfyInputSlot) {
if (input.config.values.indexOf(this.properties.value) === -1) { if (!input.config.values)
this.setValue("")
else if (input.config.values.indexOf(this.properties.value) === -1) {
if (input.config.values.length === 0) if (input.config.values.length === 0)
this.setValue("") this.setValue("")
else else
this.setValue(input.config.defaultValue || input.config.values[0]) this.setValue(input.config.defaultValue || input.config.values[0])
} }
} }
override stripUserState(o: SerializedLGraphNode) {
super.stripUserState(o);
o.properties.values = []
}
} }
LiteGraph.registerNodeType({ LiteGraph.registerNodeType({
@@ -368,6 +458,7 @@ export interface ComfyTextProperties extends ComfyWidgetProperties {
export class ComfyTextNode extends ComfyWidgetNode<string> { export class ComfyTextNode extends ComfyWidgetNode<string> {
override properties: ComfyTextProperties = { override properties: ComfyTextProperties = {
tags: [],
defaultValue: "", defaultValue: "",
multiline: false multiline: false
} }
@@ -384,6 +475,7 @@ export class ComfyTextNode extends ComfyWidgetNode<string> {
} }
override svelteComponentType = TextWidget override svelteComponentType = TextWidget
override defaultValue = "";
constructor(name?: string) { constructor(name?: string) {
super(name, "") super(name, "")
@@ -425,6 +517,7 @@ export interface ComfyGalleryProperties extends ComfyWidgetProperties {
export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> { export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
override properties: ComfyGalleryProperties = { override properties: ComfyGalleryProperties = {
tags: [],
defaultValue: [], defaultValue: [],
index: 0, index: 0,
updateMode: "replace" updateMode: "replace"
@@ -433,7 +526,7 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
inputs: [ inputs: [
{ name: "images", type: "OUTPUT" }, { name: "images", type: "OUTPUT" },
{ name: "store", type: BuiltInSlotType.ACTION }, { name: "store", type: BuiltInSlotType.ACTION, options: { color_off: "rebeccapurple", color_on: "rebeccapurple" } },
{ name: "clear", type: BuiltInSlotType.ACTION } { name: "clear", type: BuiltInSlotType.ACTION }
], ],
outputs: [ outputs: [
@@ -446,7 +539,9 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
] ]
override svelteComponentType = GalleryWidget override svelteComponentType = GalleryWidget
override defaultValue = []
override copyFromInputLink = false; override copyFromInputLink = false;
override saveUserState = false;
override outputIndex = null; override outputIndex = null;
override changedIndex = null; override changedIndex = null;
@@ -472,12 +567,11 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
this.setValue([]) this.setValue([])
} }
else if (action === "store") { else if (action === "store") {
const link = this.getInputLink(0) if (param && "images" in param) {
if (link.data && "images" in link.data) { const data = param as GalleryOutput
const data = link.data as GalleryOutput
console.debug("[ComfyGalleryNode] Received output!", data) console.debug("[ComfyGalleryNode] Received output!", data)
const galleryItems: GradioFileData[] = this.convertItems(link.data) const galleryItems: GradioFileData[] = convertComfyOutputToGradio(data)
if (this.properties.updateMode === "append") { if (this.properties.updateMode === "append") {
const currentValue = get(this.value) const currentValue = get(this.value)
@@ -495,18 +589,6 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
return `Images: ${value?.length || 0}` return `Images: ${value?.length || 0}`
} }
private convertItems(output: GalleryOutput): GradioFileData[] {
return output.images.map(r => {
// TODO configure backend URL
const url = "http://localhost:8188/view?"
const params = new URLSearchParams(r)
return {
name: null,
data: url + params
}
});
}
override setValue(value: any) { override setValue(value: any) {
if (Array.isArray(value)) { if (Array.isArray(value)) {
super.setValue(value) super.setValue(value)
@@ -535,6 +617,7 @@ export interface ComfyButtonProperties extends ComfyWidgetProperties {
export class ComfyButtonNode extends ComfyWidgetNode<boolean> { export class ComfyButtonNode extends ComfyWidgetNode<boolean> {
override properties: ComfyButtonProperties = { override properties: ComfyButtonProperties = {
tags: [],
defaultValue: false, defaultValue: false,
param: "bang" param: "bang"
} }
@@ -546,8 +629,13 @@ export class ComfyButtonNode extends ComfyWidgetNode<boolean> {
] ]
} }
override outputIndex = 1;
override svelteComponentType = ButtonWidget; override svelteComponentType = ButtonWidget;
override defaultValue = false;
override outputIndex = 1;
constructor(name?: string) {
super(name, false)
}
override setValue(value: any) { override setValue(value: any) {
super.setValue(Boolean(value)) super.setValue(Boolean(value))
@@ -558,10 +646,6 @@ export class ComfyButtonNode extends ComfyWidgetNode<boolean> {
this.triggerSlot(0, this.properties.param); this.triggerSlot(0, this.properties.param);
this.setValue(false) // TODO onRelease this.setValue(false) // TODO onRelease
} }
constructor(name?: string) {
super(name, false)
}
} }
LiteGraph.registerNodeType({ LiteGraph.registerNodeType({
@@ -576,6 +660,7 @@ export interface ComfyCheckboxProperties extends ComfyWidgetProperties {
export class ComfyCheckboxNode extends ComfyWidgetNode<boolean> { export class ComfyCheckboxNode extends ComfyWidgetNode<boolean> {
override properties: ComfyCheckboxProperties = { override properties: ComfyCheckboxProperties = {
tags: [],
defaultValue: false, defaultValue: false,
} }
@@ -587,13 +672,14 @@ export class ComfyCheckboxNode extends ComfyWidgetNode<boolean> {
} }
override svelteComponentType = CheckboxWidget; override svelteComponentType = CheckboxWidget;
override defaultValue = false;
override setValue(value: any) { override setValue(value: any) {
value = Boolean(value) value = Boolean(value)
const changed = value != get(this.value); const changed = value != get(this.value);
super.setValue(Boolean(value)) super.setValue(Boolean(value))
if (changed) if (changed)
this.triggerSlot(1) this.triggerSlot(1, value)
} }
constructor(name?: string) { constructor(name?: string) {
@@ -607,3 +693,61 @@ LiteGraph.registerNodeType({
desc: "Checkbox that stores a boolean value", desc: "Checkbox that stores a boolean value",
type: "ui/checkbox" type: "ui/checkbox"
}) })
export interface ComfyRadioProperties extends ComfyWidgetProperties {
choices: string[]
}
export class ComfyRadioNode extends ComfyWidgetNode<string> {
override properties: ComfyRadioProperties = {
tags: [],
choices: ["Choice A", "Choice B", "Choice C"],
defaultValue: "Choice A",
}
static slotLayout: SlotLayout = {
outputs: [
{ name: "value", type: "string" },
{ name: "index", type: "number" },
{ name: "changed", type: BuiltInSlotType.EVENT },
]
}
override svelteComponentType = RadioWidget;
override defaultValue = "";
override changedIndex = 2;
indexWidget: INumberWidget;
index = 0;
constructor(name?: string) {
super(name, "Choice A")
this.indexWidget = this.addWidget("number", "Index", this.index)
this.indexWidget.disabled = true;
}
override onExecute(param: any, options: object) {
super.onExecute(param, options);
this.setOutputData(1, this.index)
}
override setValue(value: string) {
const index = this.properties.choices.indexOf(value)
if (index == -1)
return;
this.index = index;
this.indexWidget.value = index;
this.setOutputData(1, this.index)
super.setValue(value)
}
}
LiteGraph.registerNodeType({
class: ComfyRadioNode,
title: "UI.Radio",
desc: "Radio that outputs a string and index",
type: "ui/radio"
})

View File

@@ -1,6 +1,7 @@
export { default as ComfyReroute } from "./ComfyReroute" export { default as ComfyReroute } from "./ComfyReroute"
export { ComfyWidgetNode, ComfySliderNode, ComfyComboNode, ComfyTextNode } from "./ComfyWidgetNodes" export { ComfyWidgetNode, ComfySliderNode, ComfyComboNode, ComfyTextNode } from "./ComfyWidgetNodes"
export { ComfyQueueEvents, ComfyCopyAction, ComfySwapAction, ComfyNotifyAction, ComfyOnExecutedEvent, ComfyExecuteSubgraphAction } from "./ComfyActionNodes" export { ComfyQueueEvents, ComfyCopyAction, ComfySwapAction, ComfyNotifyAction, ComfyStoreImagesAction, ComfyExecuteSubgraphAction } from "./ComfyActionNodes"
export { default as ComfyPickFirstNode } from "./ComfyPickFirstNode"
export { default as ComfyValueControl } from "./ComfyValueControl" export { default as ComfyValueControl } from "./ComfyValueControl"
export { default as ComfySelector } from "./ComfySelector" export { default as ComfySelector } from "./ComfySelector"
export { default as ComfyImageCacheNode } from "./ComfyImageCacheNode" export { default as ComfyImageCacheNode } from "./ComfyImageCacheNode"

40
src/lib/notify.ts Normal file
View File

@@ -0,0 +1,40 @@
import { toast } from "@zerodevx/svelte-toast";
import type { SvelteToastOptions } from "@zerodevx/svelte-toast/stores";
import { f7 } from "framework7-svelte"
let notification;
function notifyf7(text: string, title?: string) {
if (!f7)
return;
if (!notification) {
notification = f7.notification.create({
title: title,
titleRightText: 'now',
// subtitle: 'Notification with close on click',
text: text,
closeOnClick: true,
closeTimeout: 3000,
});
}
// Open it
notification.open();
}
function notifyToast(text: string, type?: string) {
const options: SvelteToastOptions = {}
if (type === "error") {
options.theme = {
'--toastBackground': 'var(--color-red-500)',
}
}
toast.push(text, options);
}
export default function notify(text: string, title?: string, type?: string) {
notifyf7(text, title);
notifyToast(text, title);
}

View File

@@ -0,0 +1,48 @@
import { debounce } from '$lib/utils';
import { get, writable } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store';
export type InterfaceState = {
// Show a large indicator of the currently editing number value for mobile
// use (sliders).
pointerNearTop: boolean,
pointerNearLeft: boolean,
showIndicator: boolean,
indicatorValue: any,
}
type InterfaceStateOps = {
showIndicator: (pointerX: number, pointerY: number, value: any) => void,
}
export type WritableInterfaceStateStore = Writable<InterfaceState> & InterfaceStateOps;
const store: Writable<InterfaceState> = writable(
{
pointerNearTop: false,
pointerNearLeft: false,
showIndicator: false,
indicatorValue: null,
})
const debounceDrag = debounce(() => { store.update(s => { s.showIndicator = false; return s }) }, 1000)
function showIndicator(pointerX: number, pointerY: number, value: any) {
if (!window)
return;
const state = get(store)
let middleWidth = window.innerWidth / 2;
let middleHeight = window.innerHeight / 2;
const pointerNearLeft = pointerX < middleWidth;
const pointerNearTop = pointerY < middleHeight;
store.update(s => { return { ...s, pointerNearTop, pointerNearLeft, showIndicator: true, indicatorValue: value } });
debounceDrag();
}
const interfaceStateStore: WritableInterfaceStateStore =
{
...store,
showIndicator
}
export default interfaceStateStore;

View File

@@ -1,7 +1,7 @@
import { get, writable } from 'svelte/store'; import { get, writable } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store'; import type { Readable, Writable } from 'svelte/store';
import type ComfyApp from "$lib/components/ComfyApp" import type ComfyApp from "$lib/components/ComfyApp"
import type { LGraphNode, IWidget, LGraph } from "@litegraph-ts/core" import { type LGraphNode, type IWidget, type LGraph, NodeMode } from "@litegraph-ts/core"
import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action'; import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
import type { ComfyWidgetNode } from '$lib/nodes'; import type { ComfyWidgetNode } from '$lib/nodes';
@@ -116,6 +116,12 @@ export type Attributes = {
*/ */
containerVariant?: "block" | "hidden", containerVariant?: "block" | "hidden",
/*
* Tags for hiding containers with
* For WidgetLayouts this will be ignored, it will use node.properties.tags instead
*/
tags: string[],
/* /*
* If true, don't show this component in the UI * If true, don't show this component in the UI
*/ */
@@ -142,6 +148,12 @@ export type Attributes = {
*/ */
variant?: string, variant?: string,
/*
* What state to set this widget to in the frontend if its corresponding
* node is disabled in the graph.
*/
nodeDisabledState: "visible" | "disabled" | "hidden",
/*********************************************/ /*********************************************/
/* Special attributes for widgets/containers */ /* Special attributes for widgets/containers */
/*********************************************/ /*********************************************/
@@ -154,6 +166,9 @@ export type Attributes = {
buttonSize?: "large" | "small" buttonSize?: "large" | "small"
} }
/*
* Defines something that can be edited in the properties side panel.
*/
export type AttributesSpec = { export type AttributesSpec = {
/* /*
* ID necessary for svelte's keyed each, autoset at the top level in this source file. * ID necessary for svelte's keyed each, autoset at the top level in this source file.
@@ -256,9 +271,13 @@ export type AttributesCategorySpec = {
export type AttributesSpecList = AttributesCategorySpec[] export type AttributesSpecList = AttributesCategorySpec[]
const serializeStringArray = (arg: string[]) => arg.join(",") const serializeStringArray = (arg: string[]) => {
if (arg == null)
arg = []
return arg.join(",")
}
const deserializeStringArray = (arg: string) => { const deserializeStringArray = (arg: string) => {
if (arg === "") if (arg === "" || arg == null)
return [] return []
return arg.split(",").map(s => s.trim()) return arg.split(",").map(s => s.trim())
} }
@@ -308,6 +327,13 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
defaultValue: 100, defaultValue: 100,
editable: true editable: true
}, },
{
name: "height",
type: "string",
location: "widget",
defaultValue: "auto",
editable: true
},
{ {
name: "classes", name: "classes",
type: "string", type: "string",
@@ -315,6 +341,15 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
defaultValue: "", defaultValue: "",
editable: true, editable: true,
}, },
{
name: "nodeDisabledState",
type: "enum",
location: "widget",
editable: true,
values: ["visible", "disabled", "hidden"],
defaultValue: "disabled",
canShow: (di: IDragItem) => di.type === "widget"
},
// Container variants // Container variants
{ {
@@ -366,22 +401,65 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
values: ["large", "small"], values: ["large", "small"],
defaultValue: "large" defaultValue: "large"
}, },
// Gallery
{
name: "variant",
type: "enum",
location: "widget",
editable: true,
validNodeTypes: ["ui/gallery"],
values: ["gallery", "image"],
defaultValue: "gallery",
refreshPanelOnChange: true
},
] ]
}, },
{ {
categoryName: "behavior", categoryName: "behavior",
specs: [ specs: [
// Node variables // Node variables
{
name: "saveUserState",
type: "boolean",
location: "nodeVars",
editable: true,
defaultValue: true,
},
{
name: "mode",
type: "enum",
location: "nodeVars",
editable: true,
values: ["ALWAYS", "NEVER"],
defaultValue: "ALWAYS",
serialize: (s) => s === NodeMode.ALWAYS ? "ALWAYS" : "NEVER",
deserialize: (m) => m === "ALWAYS" ? NodeMode.ALWAYS : NodeMode.NEVER
},
// Node properties
{ {
name: "tags", name: "tags",
type: "string", type: "string",
location: "nodeVars", location: "nodeProps",
editable: true, editable: true,
defaultValue: [], defaultValue: [],
serialize: serializeStringArray, serialize: serializeStringArray,
deserialize: deserializeStringArray deserialize: deserializeStringArray
}, },
// Container tags are contained in the widget attributes
{
name: "tags",
type: "string",
location: "widget",
editable: true,
defaultValue: [],
serialize: serializeStringArray,
deserialize: deserializeStringArray,
canShow: (di: IDragItem) => di.type === "container"
},
// Range // Range
{ {
name: "min", name: "min",
@@ -424,7 +502,7 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
defaultValue: "bang" defaultValue: "bang"
}, },
// gallery // Gallery
{ {
name: "updateMode", name: "updateMode",
type: "enum", type: "enum",
@@ -435,6 +513,18 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
defaultValue: "replace" defaultValue: "replace"
}, },
// Radio
{
name: "choices",
type: "string",
location: "nodeProps",
editable: true,
validNodeTypes: ["ui/radio"],
defaultValue: ["Choice A", "Choice B", "Choice C"],
serialize: serializeStringArray,
deserialize: deserializeStringArray,
},
// Workflow // Workflow
{ {
name: "defaultSubgraph", name: "defaultSubgraph",
@@ -450,14 +540,23 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
// This is needed so the specs can be iterated with svelte's keyed #each. // This is needed so the specs can be iterated with svelte's keyed #each.
let i = 0; let i = 0;
for (const cat of Object.values(ALL_ATTRIBUTES)) { for (const cat of Object.values(ALL_ATTRIBUTES)) {
for (const val of Object.values(cat.specs)) { for (const spec of Object.values(cat.specs)) {
val.id = i; spec.id = i;
i += 1; i += 1;
} }
} }
export { ALL_ATTRIBUTES }; export { ALL_ATTRIBUTES };
const defaultWidgetAttributes: Attributes = {} as any
for (const cat of Object.values(ALL_ATTRIBUTES)) {
for (const spec of Object.values(cat.specs)) {
if (spec.location === "widget" && spec.defaultValue != null) {
defaultWidgetAttributes[spec.name] = spec.defaultValue;
}
}
}
/* /*
* Something that can be dragged around in the frontend - a widget or a container. * Something that can be dragged around in the frontend - a widget or a container.
*/ */
@@ -487,7 +586,7 @@ export interface IDragItem {
* Hackish thing to indicate to Svelte that an attribute changed. * Hackish thing to indicate to Svelte that an attribute changed.
* TODO Use Writeable<Attributes> instead! * TODO Use Writeable<Attributes> instead!
*/ */
attrsChanged: Writable<boolean> attrsChanged: Writable<number>
} }
/* /*
@@ -521,7 +620,8 @@ type LayoutStateOps = {
groupItems: (dragItems: IDragItem[], attrs?: Partial<Attributes>) => ContainerLayout, groupItems: (dragItems: IDragItem[], attrs?: Partial<Attributes>) => ContainerLayout,
ungroup: (container: ContainerLayout) => void, ungroup: (container: ContainerLayout) => void,
getCurrentSelection: () => IDragItem[], getCurrentSelection: () => IDragItem[],
findLayoutForNode: (nodeId: number) => IDragItem | null; findLayoutEntryForNode: (nodeId: number) => DragItemEntry | null,
findLayoutForNode: (nodeId: number) => IDragItem | null,
serialize: () => SerializedLayoutState, serialize: () => SerializedLayoutState,
deserialize: (data: SerializedLayoutState, graph: LGraph) => void, deserialize: (data: SerializedLayoutState, graph: LGraph) => void,
initDefaultLayout: () => void, initDefaultLayout: () => void,
@@ -568,13 +668,10 @@ function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes>
const dragItem: ContainerLayout = { const dragItem: ContainerLayout = {
type: "container", type: "container",
id: `${state.currentId++}`, id: `${state.currentId++}`,
attrsChanged: writable(false), attrsChanged: writable(0),
attrs: { attrs: {
...defaultWidgetAttributes,
title: "Container", title: "Container",
direction: "vertical",
classes: "",
containerVariant: "block",
flexGrow: 100,
...attrs ...attrs
} }
} }
@@ -595,12 +692,11 @@ function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partia
type: "widget", type: "widget",
id: `${state.currentId++}`, id: `${state.currentId++}`,
node: node, node: node,
attrsChanged: writable(false), attrsChanged: writable(0),
attrs: { attrs: {
...defaultWidgetAttributes,
title: widgetName, title: widgetName,
direction: "horizontal", nodeDisabledState: "disabled",
classes: "",
flexGrow: 100,
...attrs ...attrs
} }
} }
@@ -771,16 +867,23 @@ function ungroup(container: ContainerLayout) {
store.set(state) store.set(state)
} }
function findLayoutForNode(nodeId: number): WidgetLayout | null { function findLayoutEntryForNode(nodeId: number): DragItemEntry | null {
const state = get(store) const state = get(store)
const found = Object.entries(state.allItems).find(pair => const found = Object.entries(state.allItems).find(pair =>
pair[1].dragItem.type === "widget" pair[1].dragItem.type === "widget"
&& (pair[1].dragItem as WidgetLayout).node.id === nodeId) && (pair[1].dragItem as WidgetLayout).node.id === nodeId)
if (found) if (found)
return found[1].dragItem as WidgetLayout return found[1]
return null; return null;
} }
function findLayoutForNode(nodeId: number): WidgetLayout | null {
const found = findLayoutEntryForNode(nodeId);
if (!found)
return null;
return found.dragItem as WidgetLayout
}
function initDefaultLayout() { function initDefaultLayout() {
store.set({ store.set({
root: null, root: null,
@@ -790,7 +893,10 @@ function initDefaultLayout() {
currentSelection: [], currentSelection: [],
currentSelectionNodes: [], currentSelectionNodes: [],
isMenuOpen: false, isMenuOpen: false,
isConfiguring: false isConfiguring: false,
attrs: {
defaultSubgraph: ""
}
}) })
const root = addContainer(null, { direction: "horizontal", title: "" }); const root = addContainer(null, { direction: "horizontal", title: "" });
@@ -859,8 +965,8 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) {
const dragItem: IDragItem = { const dragItem: IDragItem = {
type: entry.dragItem.type, type: entry.dragItem.type,
id: entry.dragItem.id, id: entry.dragItem.id,
attrs: entry.dragItem.attrs, attrs: { ...defaultWidgetAttributes, ...entry.dragItem.attrs },
attrsChanged: writable(false) attrsChanged: writable(0)
}; };
const dragEntry: DragItemEntry = { const dragEntry: DragItemEntry = {
@@ -929,6 +1035,7 @@ const layoutStateStore: WritableLayoutStateStore =
nodeRemoved, nodeRemoved,
getCurrentSelection, getCurrentSelection,
groupItems, groupItems,
findLayoutEntryForNode,
findLayoutForNode, findLayoutForNode,
ungroup, ungroup,
initDefaultLayout, initDefaultLayout,

View File

@@ -1,11 +1,9 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store'; import type { Readable, Writable } from 'svelte/store';
import type ComfyApp from "$lib/components/ComfyApp"
export type UIEditMode = "widgets" | "containers" | "layout"; export type UIEditMode = "widgets" | "containers" | "layout";
export type UIState = { export type UIState = {
app: ComfyApp,
nodesLocked: boolean, nodesLocked: boolean,
graphLocked: boolean, graphLocked: boolean,
autoAddUI: boolean, autoAddUI: boolean,
@@ -16,7 +14,6 @@ export type UIState = {
export type WritableUIStateStore = Writable<UIState>; export type WritableUIStateStore = Writable<UIState>;
const store: WritableUIStateStore = writable( const store: WritableUIStateStore = writable(
{ {
app: null,
graphLocked: false, graphLocked: false,
nodesLocked: false, nodesLocked: false,
autoAddUI: true, autoAddUI: true,

View File

@@ -6,6 +6,7 @@ import { get } from "svelte/store"
import layoutState from "$lib/stores/layoutState" import layoutState from "$lib/stores/layoutState"
import type { SvelteComponentDev } from "svelte/internal"; import type { SvelteComponentDev } from "svelte/internal";
import type { SerializedLGraph } from "@litegraph-ts/core"; import type { SerializedLGraph } from "@litegraph-ts/core";
import type { GalleryOutput } from "./nodes/ComfyWidgetNodes";
export function clamp(n: number, min: number, max: number): number { export function clamp(n: number, min: number, max: number): number {
return Math.min(Math.max(n, min), max) return Math.min(Math.max(n, min), max)
@@ -91,7 +92,7 @@ export function promptToGraphVis(prompt: SerializedPrompt): string {
} }
else { else {
// Value // Value
out += `"${id}-${inpName}-${typeof i}" -> "${outNode.title}"\n` out += `"${id}-${inpName}-${i}" -> "${outNode.title}"\n`
} }
} }
} }
@@ -99,3 +100,48 @@ export function promptToGraphVis(prompt: SerializedPrompt): string {
out += "}" out += "}"
return out return out
} }
export function getNodeInfo(nodeId: number): string {
let app = (window as any).app;
if (!app)
return String(nodeId);
const title = app.lGraph.getNodeById(nodeId)?.title || String(nodeId);
return title + " (" + nodeId + ")"
}
export const debounce = (callback: Function, wait = 250) => {
let timeout: NodeJS.Timeout | null = null;
return (...args: Array<unknown>) => {
const next = () => callback(...args);
if (timeout) clearTimeout(timeout);
timeout = setTimeout(next, wait);
};
};
export function convertComfyOutputToGradio(output: GalleryOutput): GradioFileData[] {
return output.images.map(r => {
// TODO configure backend URL
const url = `http://${location.hostname}:8188` // TODO make configurable
const params = new URLSearchParams(r)
return {
name: null,
data: url + "/view?" + params
}
});
}
export function jsonToJsObject(json: string): string {
// Try to parse, to see if it's real JSON
JSON.parse(json);
const regex = /\"([^"]+)\":/g;
const hyphenRegex = /-([a-z])/g;
return json.replace(regex, match => {
return match
.replace(hyphenRegex, g => g[1].toUpperCase())
.replace(regex, "$1:");
});
}

View File

@@ -4,7 +4,9 @@
import { type WidgetLayout } from "$lib/stores/layoutState"; import { type WidgetLayout } from "$lib/stores/layoutState";
import { Button } from "@gradio/button"; import { Button } from "@gradio/button";
import { get, type Writable, writable } from "svelte/store"; import { get, type Writable, writable } from "svelte/store";
import { isDisabled } from "./utils"
export let widget: WidgetLayout | null = null; export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyButtonNode | null = null; let node: ComfyButtonNode | null = null;
let nodeValue: Writable<boolean> | null = null; let nodeValue: Writable<boolean> | null = null;
let attrsChanged: Writable<boolean> | null = null; let attrsChanged: Writable<boolean> | null = null;
@@ -21,6 +23,7 @@
function onClick(e: MouseEvent) { function onClick(e: MouseEvent) {
node.onClick(); node.onClick();
navigator.vibrate(20)
} }
const style = { const style = {
@@ -30,9 +33,9 @@
<div class="wrapper gradio-button"> <div class="wrapper gradio-button">
{#key $attrsChanged} {#key $attrsChanged}
{#if node !== null} {#if widget !== null && node !== null}
<Button <Button
disabled={widget.attrs.disabled} disabled={isDisabled(widget)}
on:click={onClick} on:click={onClick}
variant={widget.attrs.buttonVariant || "primary"} variant={widget.attrs.buttonVariant || "primary"}
size={widget.attrs.buttonSize === "small" ? "sm" : "lg"} size={widget.attrs.buttonSize === "small" ? "sm" : "lg"}

View File

@@ -1,11 +1,13 @@
<script lang="ts"> <script lang="ts">
import type { ComfyCheckboxNode } from "$lib/nodes/ComfyWidgetNodes"; import type { ComfyCheckboxNode } from "$lib/nodes/ComfyWidgetNodes";
import { type WidgetLayout } from "$lib/stores/layoutState"; import { type WidgetLayout } from "$lib/stores/layoutState";
import { Block } from "@gradio/atoms"; import { Block } from "@gradio/atoms";
import { Checkbox } from "@gradio/form"; import { Checkbox } from "@gradio/form";
import { get, type Writable, writable } from "svelte/store"; import { get, type Writable, writable } from "svelte/store";
import { isDisabled } from "./utils"
export let widget: WidgetLayout | null = null; export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyCheckboxNode | null = null; let node: ComfyCheckboxNode | null = null;
let nodeValue: Writable<boolean> | null = null; let nodeValue: Writable<boolean> | null = null;
let attrsChanged: Writable<boolean> | null = null; let attrsChanged: Writable<boolean> | null = null;
@@ -19,6 +21,10 @@
attrsChanged = widget.attrsChanged; attrsChanged = widget.attrsChanged;
} }
}; };
function onSelect() {
navigator.vibrate(20)
}
</script> </script>
<div class="wrapper gradio-checkbox"> <div class="wrapper gradio-checkbox">
@@ -26,7 +32,12 @@
{#key $attrsChanged} {#key $attrsChanged}
{#if node !== null} {#if node !== null}
<Block> <Block>
<Checkbox disabled={widget.attrs.disabled} label={widget.attrs.title} bind:value={$nodeValue} /> <Checkbox
disabled={isDisabled(widget)}
label={widget.attrs.title}
bind:value={$nodeValue}
on:select={onSelect}
/>
</Block> </Block>
{/if} {/if}
{/key} {/key}

View File

@@ -4,13 +4,18 @@
import type { ComfyComboNode } from "$lib/nodes/index"; import type { ComfyComboNode } from "$lib/nodes/index";
import { type WidgetLayout } from "$lib/stores/layoutState"; import { type WidgetLayout } from "$lib/stores/layoutState";
import { get, type Writable } from "svelte/store"; import { get, type Writable } from "svelte/store";
import { isDisabled } from "./utils"
export let widget: WidgetLayout | null = null; export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyComboNode | null = null; let node: ComfyComboNode | null = null;
let nodeValue: Writable<string> | null = null; let nodeValue: Writable<string> | null = null;
let propsChanged: Writable<number> | null = null; let propsChanged: Writable<number> | null = null;
let comboRefreshed: Writable<boolean> | null = null;
let wasComboRefreshed: boolean = false;
let option: any let option: any
export let debug: boolean = false; export let debug: boolean = false;
let input: HTMLInputElement | null = null
$: widget && setNodeValue(widget); $: widget && setNodeValue(widget);
@@ -32,6 +37,9 @@
node = widget.node as ComfyComboNode node = widget.node as ComfyComboNode
nodeValue = node.value; nodeValue = node.value;
propsChanged = node.propsChanged; propsChanged = node.propsChanged;
comboRefreshed = node.comboRefreshed;
if ($comboRefreshed)
flashOnRefreshed();
setOption($nodeValue) // don't react on option setOption($nodeValue) // don't react on option
} }
} }
@@ -44,6 +52,12 @@
$nodeValue = option.value; $nodeValue = option.value;
} }
$: $comboRefreshed && flashOnRefreshed();
function flashOnRefreshed() {
setTimeout(() => ($comboRefreshed = false), 1000);
}
function getLinkValue() { function getLinkValue() {
if (!node) if (!node)
return "???"; return "???";
@@ -53,42 +67,48 @@
return links[0].data return links[0].data
} }
let lastPropsChanged: number = 0; function onFocus() {
let werePropsChanged: boolean = false; navigator.vibrate(20)
}
$: if ($propsChanged !== lastPropsChanged) { function onSelect() {
werePropsChanged = true; if (input)
lastPropsChanged = $propsChanged; input.blur();
setTimeout(() => (werePropsChanged = false), 2000); navigator.vibrate(20)
} }
</script> </script>
<div class="wrapper comfy-combo" class:updated={werePropsChanged}> <div class="wrapper comfy-combo" class:updated={$comboRefreshed}>
{#key $propsChanged} {#key $propsChanged}
{#if node !== null && nodeValue !== null} {#key $comboRefreshed}
<label> {#if node !== null && nodeValue !== null}
{#if widget.attrs.title !== ""} <label>
<BlockTitle show_label={true}>{widget.attrs.title}</BlockTitle> {#if widget.attrs.title !== ""}
{/if} <BlockTitle show_label={true}>{widget.attrs.title}</BlockTitle>
<Select {/if}
bind:value={option} <Select
items={node.properties.values} bind:value={option}
disabled={widget.attrs.disabled || node.properties.values.length === 0} items={node.properties.values}
clearable={false} disabled={isDisabled(widget) || node.properties.values.length === 0}
showChevron={true} clearable={false}
on:change showChevron={true}
on:select inputAttributes={{ autocomplete: 'off' }}
on:filter bind:input
on:blur on:change
/> on:focus={onFocus}
{#if debug} on:select={onSelect}
<div>Value: {option?.value}</div> on:filter
<div>Items: {node.properties.values}</div> on:blur
<div>NodeValue: {$nodeValue}</div> />
<div>LinkValue: {getLinkValue()}</div> {#if debug}
{/if} <div>Value: {option?.value}</div>
</label> <div>Items: {node.properties.values}</div>
{/if} <div>NodeValue: {$nodeValue}</div>
<div>LinkValue: {getLinkValue()}</div>
{/if}
</label>
{/if}
{/key}
{/key} {/key}
</div> </div>
@@ -116,7 +136,9 @@
:global(.svelte-select) { :global(.svelte-select) {
width: auto; width: auto;
max-width: 30rem; max-width: 16rem;
--font-size: 13px;
--height: 32px;
} }
:global(.svelte-select-list) { :global(.svelte-select-list) {

View File

@@ -1,16 +1,19 @@
<script lang="ts"> <script lang="ts">
import { ImageViewer } from "$lib/ImageViewer"; import { ImageViewer } from "$lib/ImageViewer";
import { Block } from "@gradio/atoms"; import { Block, BlockLabel, Empty } from "@gradio/atoms";
import { Gallery } from "@gradio/gallery"; import { Gallery } from "@gradio/gallery";
import { Image } from "@gradio/icons";
import type { Styles } from "@gradio/utils"; import type { Styles } from "@gradio/utils";
import type { WidgetLayout } from "$lib/stores/layoutState"; import type { WidgetLayout } from "$lib/stores/layoutState";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import type { ComfyGalleryNode } from "$lib/nodes/ComfyWidgetNodes"; import type { ComfyGalleryNode } from "$lib/nodes/ComfyWidgetNodes";
import type { FileData as GradioFileData } from "@gradio/upload"; import type { FileData as GradioFileData } from "@gradio/upload";
import type { SelectData as GradioSelectData } from "@gradio/utils"; import type { SelectData as GradioSelectData } from "@gradio/utils";
import { clamp } from "$lib/utils"; import { clamp } from "$lib/utils";
import { f7 } from "framework7-svelte";
export let widget: WidgetLayout | null = null; export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyGalleryNode | null = null; let node: ComfyGalleryNode | null = null;
let nodeValue: Writable<GradioFileData[]> | null = null; let nodeValue: Writable<GradioFileData[]> | null = null;
let propsChanged: Writable<number> | null = null; let propsChanged: Writable<number> | null = null;
@@ -24,28 +27,94 @@
nodeValue = node.value; nodeValue = node.value;
propsChanged = node.propsChanged; propsChanged = node.propsChanged;
const len = $nodeValue.length if ($nodeValue != null) {
if (node.properties.index < 0 || node.properties.index >= len) { if (node.properties.index < 0 || node.properties.index >= $nodeValue.length) {
node.setProperty("index", clamp(node.properties.index, 0, len)) node.setProperty("index", clamp(node.properties.index, 0, $nodeValue))
}
} }
} }
}; };
let style: Styles = { let style: Styles = {
// grid_cols: [2], grid_cols: [4],
grid: [3], grid_rows: [4],
// object_fit: "cover", // object_fit: "cover",
} }
let element: HTMLDivElement; let element: HTMLDivElement;
let mobileLightbox = null;
function showMobileLightbox(event: Event) {
if (!f7)
return
if (mobileLightbox) {
mobileLightbox.destroy();
mobileLightbox = null;
}
const source = (event.target || event.srcElement) as HTMLImageElement;
const galleryElem = source.closest<HTMLDivElement>("div.block")
console.debug("[ImageViewer] showModal", event, source, galleryElem);
if (!galleryElem || ImageViewer.all_gallery_buttons(galleryElem).length === 0) {
console.error("No buttons found on gallery element!", galleryElem)
return;
}
const allGalleryButtons = ImageViewer.all_gallery_buttons(galleryElem);
const selectedSource = source.src
const images = allGalleryButtons.map(button => {
return {
url: (button.children[0] as HTMLImageElement).src,
caption: "Image"
}
})
mobileLightbox = f7.photoBrowser.create({
photos: images,
thumbs: images.map(i => i.url),
type: 'popup',
});
mobileLightbox.open()
event.stopPropagation()
}
function setupImageForMobileLightbox(e: HTMLImageElement) {
if (e.dataset.modded === "true")
return;
e.dataset.modded = "true";
e.style.cursor = "pointer";
e.style.userSelect = "none";
var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1
// For Firefox, listening on click first switched to next image then shows the lightbox.
// If you know how to fix this without switching to mousedown event, please.
// For other browsers the event is click to make it possiblr to drag picture.
var event = isFirefox ? 'mousedown' : 'click'
e.addEventListener(event, (evt) => {
evt.preventDefault()
showMobileLightbox(evt)
}, true);
}
function onSelect(e: CustomEvent<GradioSelectData>) { function onSelect(e: CustomEvent<GradioSelectData>) {
// Setup lightbox // Setup lightbox
// Wait for gradio gallery to show the large preview image, if no timeout then // Wait for gradio gallery to show the large preview image, if no timeout then
// the event might fire too early // the event might fire too early
const callback = isMobile ? setupImageForMobileLightbox
: ImageViewer.instance.setupImageForLightbox.bind(ImageViewer.instance)
setTimeout(() => { setTimeout(() => {
const images = element.querySelectorAll<HTMLImageElement>('div.block div > img') const images = element.querySelectorAll<HTMLImageElement>('div.block div > img')
if (images != null) { if (images != null) {
images.forEach(ImageViewer.instance.setupImageForLightbox.bind(ImageViewer.instance)); images.forEach(callback);
} }
ImageViewer.instance.updateOnBackgroundChange(); ImageViewer.instance.updateOnBackgroundChange();
}, 200) }, 200)
@@ -53,25 +122,44 @@
// Update index // Update index
node.setProperty("index", e.detail.index as number) node.setProperty("index", e.detail.index as number)
} }
</script> </script>
<div class="wrapper comfy-gallery-widget gradio-gallery" bind:this={element}>
{#if widget && node && nodeValue} {#if widget && node && nodeValue && $nodeValue != null}
<Block variant="solid" padding={false}> {#if widget.attrs.variant === "image"}
<div class="padding"> <div class="wrapper comfy-image-widget" style="height: {widget.attrs.height || 'auto'}" bind:this={element}>
<Gallery <Block variant="solid" padding={false}>
bind:value={$nodeValue} {#if widget.attrs.title}
label={widget.attrs.title} <BlockLabel
show_label={widget.attrs.title !== ""} show_label={true}
{style} Icon={Image}
root={""} label={widget.attrs.title || "Image"}
root_url={""} />
on:select={onSelect} {/if}
/> {#if $nodeValue.length > 0}
</div> <img src={$nodeValue[$nodeValue.length-1].data}/>
</Block> {:else}
<Empty size="large" unpadded_box={true}><Image /></Empty>
{/if}
</Block>
</div>
{:else}
<div class="wrapper comfy-gallery-widget gradio-gallery" bind:this={element}>
<Block variant="solid" padding={false}>
<div class="padding">
<Gallery
bind:value={$nodeValue}
label={widget.attrs.title}
show_label={widget.attrs.title !== ""}
{style}
root={""}
root_url={""}
on:select={onSelect}
/>
</div>
</Block>
</div>
{/if} {/if}
</div> {/if}
<style lang="scss"> <style lang="scss">
.wrapper { .wrapper {
@@ -80,13 +168,28 @@
:global(> .block) { :global(> .block) {
border-radius: 0px !important; border-radius: 0px !important;
} }
:global(button.thumbnail-lg) {
width: var(--size-32);
}
&.comfy-image-widget {
aspect-ratio: 1/1;
:global(> .block) {
height: 100%;
:global(img) {
width: 100%;
height: 100%;
object-fit: contain;
}
}
}
} }
.padding { .padding {
height: 30rem; height: 30rem;
} }
.wrapper :global(button.thumbnail-lg) {
width: var(--size-32);
}
</style> </style>

View File

@@ -0,0 +1,64 @@
<script lang="ts">
import type { ComfyRadioNode } from "$lib/nodes/ComfyWidgetNodes";
import { type WidgetLayout } from "$lib/stores/layoutState";
import { Block } from "@gradio/atoms";
import { Radio } from "@gradio/form";
import { get, type Writable, writable } from "svelte/store";
import { isDisabled } from "./utils"
import type { SelectData } from "@gradio/utils";
import { clamp } from "$lib/utils";
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyRadioNode | null = null;
let nodeValue: Writable<string> | null = null;
let propsChanged: Writable<number> | null = null;
let attrsChanged: Writable<number> | null = null;
$: widget && setNodeValue(widget);
function setNodeValue(widget: WidgetLayout) {
if (widget) {
node = widget.node as ComfyRadioNode
nodeValue = node.value;
attrsChanged = widget.attrsChanged;
}
};
$: node && $propsChanged && clampIndex();
function clampIndex() {
node.index = clamp(node.index, 0, node.properties.choices?.length || 0)
}
function onSelect(e: CustomEvent<SelectData>) {
node.setValue(e.detail.value)
node.index = e.detail.index as number
navigator.vibrate(20)
}
</script>
<div class="wrapper gradio-radio">
<div class="inner">
{#key $propsChanged}
{#key $attrsChanged}
{#if node !== null && node.properties.choices}
<Block type="fieldset">
<Radio
elem_id="radio"
choices={node.properties.choices}
disabled={isDisabled(widget)}
label={widget.attrs.title}
show_label={widget.attrs.title && widget.attrs.title !== ""}
value={$nodeValue}
on:select={onSelect}
/>
</Block>
{/if}
{/key}
{/key}
</div>
</div>
<style lang="scss">
</style>

View File

@@ -1,13 +1,18 @@
<script lang="ts"> <script lang="ts">
import type { ComfySliderNode } from "$lib/nodes/index"; import type { ComfySliderNode } from "$lib/nodes/index";
import { type WidgetLayout } from "$lib/stores/layoutState"; import { type WidgetLayout } from "$lib/stores/layoutState";
import { Range } from "@gradio/form"; import { Range } from "$lib/components/gradio/form";
import { get, type Writable } from "svelte/store"; import { get, type Writable } from "svelte/store";
import { debounce } from "$lib/utils";
import interfaceState from "$lib/stores/interfaceState";
import { isDisabled } from "./utils"
export let widget: WidgetLayout | null = null; export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfySliderNode | null = null; let node: ComfySliderNode | null = null;
let nodeValue: Writable<number> | null = null; let nodeValue: Writable<number> | null = null;
let propsChanged: Writable<number> | null = null; let propsChanged: Writable<number> | null = null;
let option: number | null = null; let option: number | null = null;
let isDragging: boolean = false;
$: widget && setNodeValue(widget); $: widget && setNodeValue(widget);
@@ -18,6 +23,7 @@
propsChanged = node.propsChanged; propsChanged = node.propsChanged;
setOption($nodeValue); // don't react on option setOption($nodeValue); // don't react on option
} }
isDragging = false;
}; };
// I don't know why but this is necessary to watch for changes to node // I don't know why but this is necessary to watch for changes to node
@@ -33,14 +39,34 @@
} }
function onRelease(e: Event) { function onRelease(e: Event) {
if (nodeValue && option) { if (nodeValue && option != null) {
$nodeValue = option $nodeValue = option
} }
} }
let gradient: string = "" function setBackgroundSize(input: HTMLInputElement) {
input.style.setProperty("--background-size", `${getBackgroundSize(input)}%`);
}
function getBackgroundSize(input: HTMLInputElement) {
const min = +input.min || 0;
const max = +input.max || 100;
const value = +input.value;
return (value - min) / (max - min) * 100;
}
function updateSliderForMobile() {
const target = elem.querySelector<HTMLInputElement>("input[type=range]");
setBackgroundSize(target);
}
let elem: HTMLDivElement = null; let elem: HTMLDivElement = null;
$: if (elem) {
updateSliderForMobile()
}
$: if (elem && node !== null && option !== null && (!$propsChanged || $propsChanged)) { $: if (elem && node !== null && option !== null && (!$propsChanged || $propsChanged)) {
const slider = elem.querySelector("input[type='range']") as any const slider = elem.querySelector("input[type='range']") as any
//const range_selectors = "[id$='_clone']:is(input[type='range'])"; //const range_selectors = "[id$='_clone']:is(input[type='range'])";
@@ -50,27 +76,95 @@
const style = elem.style; const style = elem.style;
style.setProperty('--ae-slider-bg-overlay', 'repeating-linear-gradient( 90deg, transparent, transparent '+tsp+', var(--ae-input-border-color) '+tsp+', var(--ae-input-border-color) '+fsp+' )'); style.setProperty('--ae-slider-bg-overlay', 'repeating-linear-gradient( 90deg, transparent, transparent '+tsp+', var(--ae-input-border-color) '+tsp+', var(--ae-input-border-color) '+fsp+' )');
} }
function onPointerDown(e: PointerEvent) {
if (!isMobile)
return;
interfaceState.showIndicator(e.clientX, e.clientY, option);
}
let canVibrate = true;
let lastDisplayValue = null;
function onPointerMove(e: PointerEvent) {
if (!isMobile)
return;
interfaceState.showIndicator(e.clientX, e.clientY, option);
if (canVibrate && lastDisplayValue != option) {
lastDisplayValue = option;
canVibrate = false;
setTimeout(() => { canVibrate = true }, 30)
navigator.vibrate(10)
}
}
</script> </script>
<div class="wrapper gradio-slider" bind:this={elem}> <div class="wrapper gradio-slider" class:mobile={isMobile} bind:this={elem}>
{#if node !== null && option !== null} {#if node !== null && option !== null}
<Range <Range
bind:value={option} bind:value={option}
disabled={widget.attrs.disabled} disabled={isDisabled(widget)}
minimum={node.properties.min} minimum={node.properties.min}
maximum={node.properties.max} maximum={node.properties.max}
step={node.properties.step} step={node.properties.step}
label={widget.attrs.title} label={widget.attrs.title}
show_label={true} show_label={true}
on:release={onRelease} on:release={onRelease}
on:change on:change={updateSliderForMobile}
on:pointerdown={onPointerDown}
on:pointermove={onPointerMove}
/> />
{/if} {/if}
</div> </div>
<style> <style lang="scss">
.wrapper { .wrapper {
padding: 2px; padding: 2px;
width: 100%; width: 100%;
:global(input[type=number]) {
text-overflow: ellipsis;
}
&.mobile {
// Prevent swiping on the slider track from accidentally changing the value
:global(input[type="range"]) {
pointer-events: none;
-webkit-appearance: none;
appearance: none;
cursor: default;
height: 0.6rem;
padding: initial;
border: initial;
margin: 0.8rem 0;
width: 100%;
background: linear-gradient(to right, var(--color-blue-600), var(--color-blue-600)), #D7D7D7;
background-size: var(--background-size, 0%) 100%;
background-repeat: no-repeat;
border-radius: 1rem;
border: 1px solid var(--neutral-400);
&::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
pointer-events: all;
width: 1.75rem;
height: 1.75rem;
border-radius: 50%;
background: var(--color-blue-600);
cursor: pointer;
border: 2px solid var(--neutral-100);
box-shadow: 0px 0px 0px 1px var(--neutral-400);
}
:global(input[type=number]) {
font-size: 16px;
height: var(--size-6);
}
}
}
} }
</style> </style>

View File

@@ -3,7 +3,9 @@
import type { ComfyComboNode } from "$lib/nodes/index"; import type { ComfyComboNode } from "$lib/nodes/index";
import { type WidgetLayout } from "$lib/stores/layoutState"; import { type WidgetLayout } from "$lib/stores/layoutState";
import { get, type Writable } from "svelte/store"; import { get, type Writable } from "svelte/store";
import { isDisabled } from "./utils"
export let widget: WidgetLayout | null = null; export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyComboNode | null = null; let node: ComfyComboNode | null = null;
let nodeValue: Writable<string> | null = null; let nodeValue: Writable<string> | null = null;
let propsChanged: Writable<number> | null = null; let propsChanged: Writable<number> | null = null;
@@ -32,7 +34,7 @@
<TextBox <TextBox
bind:value={$nodeValue} bind:value={$nodeValue}
label={widget.attrs.title} label={widget.attrs.title}
disabled={widget.attrs.disabled} disabled={isDisabled(widget)}
lines={node.properties.multiline ? 5 : 1} lines={node.properties.multiline ? 5 : 1}
max_lines={node.properties.multiline ? 5 : 1} max_lines={node.properties.multiline ? 5 : 1}
show_label={widget.attrs.title !== ""} show_label={widget.attrs.title !== ""}

26
src/lib/widgets/utils.ts Normal file
View File

@@ -0,0 +1,26 @@
import type { IDragItem } from "$lib/stores/layoutState";
import layoutState from "$lib/stores/layoutState";
import { NodeMode } from "@litegraph-ts/core";
import { get } from "svelte/store";
export function isDisabled(widget: IDragItem) {
if (widget.attrs.disabled)
return true;
if (widget.type === "widget") {
return widget.attrs.nodeDisabledState === "disabled" && widget.node.mode === NodeMode.NEVER
}
return false;
}
export function isHidden(widget: IDragItem) {
if (widget.attrs.hidden)
return true;
if (widget.type === "widget") {
return widget.attrs.nodeDisabledState === "hidden" && widget.node.mode === NodeMode.NEVER
}
return false;
}

View File

@@ -2,12 +2,23 @@ import AppMobile from './AppMobile.svelte';
import Framework7 from 'framework7/lite-bundle'; import Framework7 from 'framework7/lite-bundle';
import Framework7Svelte from 'framework7-svelte'; import Framework7Svelte from 'framework7-svelte';
import { f7 } from 'framework7-svelte'; import { f7 } from 'framework7-svelte';
import ComfyApp from '$lib/components/ComfyApp';
import uiState from '$lib/stores/uiState';
import { LiteGraph } from '@litegraph-ts/core';
Framework7.use(Framework7Svelte); Framework7.use(Framework7Svelte);
LiteGraph.dialog_close_on_mouse_leave = false;
LiteGraph.search_hide_on_mouse_leave = false;
LiteGraph.pointerevents_method = "pointer";
const comfyApp = new ComfyApp();
uiState.update(s => { s.app = comfyApp; return s; })
const app = new AppMobile({ const app = new AppMobile({
target: document.getElementById('app'), target: document.getElementById('app'),
props: { app: comfyApp }
}) })
export default app; export default app;

View File

@@ -1,38 +1,104 @@
<script lang="ts"> <script lang="ts">
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp"; import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
import notify from "$lib/notify";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import queueState from "$lib/stores/queueState";
import { getNodeInfo } from "$lib/utils"
import { Link, Toolbar } from "framework7-svelte" import { Link, Toolbar } from "framework7-svelte"
import { f7 } from "framework7-svelte" import ProgressBar from "$lib/components/ProgressBar.svelte";
import Indicator from "./Indicator.svelte";
import interfaceState from "$lib/stores/interfaceState";
import LightboxModal from "$lib/components/LightboxModal.svelte";
export let subworkflowID: number = -1; export let subworkflowID: number = -1;
let app: ComfyApp = undefined; export let app: ComfyApp = undefined;
let fileInput: HTMLInputElement = undefined;
$: if (!app)
app = $uiState.app
function queuePrompt() { function queuePrompt() {
app.queuePrompt(0, 1); app.queuePrompt(0, 1);
showNotification(); notify("Prompt was queued", "Queued");
} }
let notification; function doLoad(): void {
const showNotification = () => { if (!app?.lGraph || !fileInput)
if (!notification) { return;
notification = f7.notification.create({
title: 'Queued', fileInput.click();
titleRightText: 'now',
// subtitle: 'Notification with close on click',
text: 'Prompt was queued',
closeOnClick: true,
closeTimeout: 3000,
});
}
// Open it
notification.open();
} }
function loadWorkflow(): void {
app.handleFile(fileInput.files[0]);
fileInput.files = null;
}
</script> </script>
<div class="bottom">
{#if $queueState.runningNodeId || $queueState.progress}
<div class="node-name">
<span>Node: {getNodeInfo($queueState.runningNodeId)}</span>
</div>
<div class="progress-bar">
<ProgressBar value={$queueState.progress?.value} max={$queueState.progress?.max} />
</div>
{/if}
{#if typeof $queueState.queueRemaining === "number" && $queueState.queueRemaining > 0}
<div class="queue-remaining in-progress">
<div>
Queued prompts: {$queueState.queueRemaining}.
</div>
</div>
{/if}
</div>
<Toolbar bottom> <Toolbar bottom>
<Link on:click={queuePrompt}>Queue Prompt</Link> <Link on:click={queuePrompt}>Queue Prompt</Link>
<Link on:click={() => app.refreshComboInNodes()}>🔄</Link>
<Link on:click={doLoad}>Load</Link>
<input bind:this={fileInput} id="comfy-file-input" type="file" accept=".json" on:change={loadWorkflow} />
</Toolbar> </Toolbar>
{#if $interfaceState.showIndicator}
<Indicator value={$interfaceState.indicatorValue} />
{/if}
<style lang="scss">
#comfy-file-input {
display: none;
}
.bottom {
display: flex;
flex-direction: row;
position: absolute;
text-align: center;
width: 100%;
height: 2rem;
bottom: calc(var(--f7-toolbar-height) + var(--f7-safe-area-bottom));
z-index: var(--layer-top);
background-color: grey;
.node-name {
flex-grow: 1;
background-color: var(--color-red-300);
padding: 0.2em;
display: flex;
justify-content: center;
align-items: center;
}
.progress-bar {
flex-grow: 10;
background-color: var(--color-red-300);
display: flex;
justify-content: center;
align-items: center;
}
.queue-remaining {
flex-grow: 1;
padding: 0.2em;
&.in-progress {
background-color: var(--secondary-300);
}
}
}
</style>

View File

@@ -0,0 +1,45 @@
<script lang="ts">
import interfaceState from "$lib/stores/interfaceState";
export let value: any = null;
</script>
<div style="position: relative; z-index: 10;">
<div class="indicator"
class:top={true}
class:bottom={false}
class:left={!$interfaceState.pointerNearLeft || !$interfaceState.pointerNearTop}
class:right={$interfaceState.pointerNearLeft && $interfaceState.pointerNearTop}>
<span>
{value}
</span>
</div>
</div>
<style lang="scss">
.indicator {
position: fixed;
align-content: left;
padding: 1rem;
font-size: xxx-large;
background: var(--neutral-300);
color: var(--neutral-800);
border-radius: 1rem;
border: 0.2rem solid var(--neutral-400);
z-index: var(--layer-top) !important;
&.top {
top: calc(1rem + var(--f7-navbar-height) + var(--f7-safe-area-top));
}
&.bottom {
bottom: 5rem
}
&.left {
left: 1rem;
}
&.right {
right: 1rem;
}
}
</style>

View File

@@ -6,13 +6,10 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import uiState from "$lib/stores/uiState" import uiState from "$lib/stores/uiState"
let app: ComfyApp | null = null; export let app: ComfyApp | null = null;
let lCanvas: LGraphCanvas | null = null; let lCanvas: ComfyGraphCanvas | null = null;
let canvasEl: HTMLCanvasElement | null = null; let canvasEl: HTMLCanvasElement | null = null;
$: if (!app)
app = $uiState.app
function resizeCanvas() { function resizeCanvas() {
canvasEl.width = canvasEl.parentElement.offsetWidth; canvasEl.width = canvasEl.parentElement.offsetWidth;
canvasEl.height = canvasEl.parentElement.offsetHeight; canvasEl.height = canvasEl.parentElement.offsetHeight;
@@ -21,13 +18,11 @@
lCanvas.draw(true, true); lCanvas.draw(true, true);
} }
$: if (app && canvasEl) { $: if (app != null && app.lGraph && canvasEl != null) {
if (!lCanvas) { if (!lCanvas) {
lCanvas = new ComfyGraphCanvas(app, canvasEl); lCanvas = new ComfyGraphCanvas(app, canvasEl);
lCanvas.allow_interaction = false; lCanvas.allow_interaction = false;
LiteGraph.dialog_close_on_mouse_leave = false; app.lGraph.eventBus.on("afterExecute", () => lCanvas.draw(true))
LiteGraph.search_hide_on_mouse_leave = false;
LiteGraph.pointerevents_method = "pointer";
} }
resizeCanvas(); resizeCanvas();
} }
@@ -46,5 +41,10 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: #333; background-color: #333;
> canvas {
// Don't try to scroll the page when scrolling on canvas
touch-action: none;
}
} }
</style> </style>

View File

@@ -14,22 +14,7 @@
import queueState from "$lib/stores/queueState"; import queueState from "$lib/stores/queueState";
import { Page, Navbar, Link, BlockTitle, Block, List, ListItem } from "framework7-svelte" import { Page, Navbar, Link, BlockTitle, Block, List, ListItem } from "framework7-svelte"
let app: ComfyApp | null = null; export let app: ComfyApp | null = null;
onMount(async () => {
if (app)
return
app = $uiState.app = new ComfyApp();
app.api.addEventListener("status", (ev: CustomEvent) => {
queueState.statusUpdated(ev.detail as ComfyAPIStatus);
});
await app.setup();
(window as any).app = app;
});
</script> </script>
@@ -49,8 +34,4 @@
<i class="icon icon-f7" slot="media" /> <i class="icon icon-f7" slot="media" />
</ListItem> </ListItem>
</List> </List>
<div class="canvas-wrapper pane-wrapper" style="display: none">
<canvas id="graph-canvas" />
</div>
</Page> </Page>

View File

@@ -1,19 +1,8 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import ComfyApp from "$lib/components/ComfyApp";
import { get } from "svelte/store";
import { Pane, Splitpanes } from 'svelte-splitpanes';
import { Button } from "@gradio/button";
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
import { Checkbox } from "@gradio/form"
import uiState from "$lib/stores/uiState";
import { ImageViewer } from "$lib/ImageViewer";
import { download } from "$lib/utils"
import { LGraph, LGraphNode } from "@litegraph-ts/core";
import type { ComfyAPIStatus } from "$lib/api";
import queueState from "$lib/stores/queueState";
import { Page, Navbar, Link, BlockTitle, Block, List, ListItem } from "framework7-svelte" import { Page, Navbar, Link, BlockTitle, Block, List, ListItem } from "framework7-svelte"
export let app: ComfyApp;
</script> </script>
<Page name="subworkflows"> <Page name="subworkflows">

View File

@@ -1,26 +1,30 @@
<script lang="ts"> <script lang="ts">
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp"; import layoutState, { type IDragItem } from "$lib/stores/layoutState";
import uiState from "$lib/stores/uiState";
import queueState from "$lib/stores/queueState";
import { Page, Navbar, Link, BlockTitle, Block, List, ListItem, Toolbar } from "framework7-svelte" import { Page, Navbar, Link, BlockTitle, Block, List, ListItem, Toolbar } from "framework7-svelte"
import WidgetContainer from "$lib/components/WidgetContainer.svelte";
import type ComfyApp from "$lib/components/ComfyApp";
export let subworkflowID: number = -1; export let subworkflowID: number = -1;
let app: ComfyApp = undefined; export let app: ComfyApp
$: if (!app)
app = $uiState.app
</script> </script>
<Page name="subworkflow"> <Page name="subworkflow">
<Navbar title="Workflow {subworkflowID}" backLink="Back" /> <Navbar title="Workflow {subworkflowID}" backLink="Back" />
<div>Workflow!</div> <div class="container">
<WidgetContainer bind:dragItem={$layoutState.root} isMobile={true} classes={["root-container", "mobile"]} />
</div>
</Page> </Page>
<style> <style lang="scss">
.is-executing { .container {
border: 5px dashed var(--color-green-600) !important; overflow-x: hidden;
}
// TODO generalize this to all properties!
:global(.root-container.mobile > .block > .v-pane) {
flex-direction: column !important;
} }
</style> </style>

View File

@@ -2,4 +2,7 @@
body { body {
overflow: hidden; overflow: hidden;
// Disable pull to refresh
overscroll-behavior-y: contain;
} }

View File

@@ -173,14 +173,17 @@ body {
background: var(--ae-panel-bg-color) !important; background: var(--ae-panel-bg-color) !important;
} }
.container { // Add blocks for components not located in a physical container
.z-index0, .z-index1, .z-index2 { // TODO make it work with accordions/tabs
> .block > .v-pane > .animation-wrapper > .widget:not(.edit) { // .container {
padding: var(--ae-inside-padding-size) !important; // .z-index0, .z-index1, .z-index2 {
border: 1px solid var(--ae-panel-border-color) !important; // > .block > .v-pane > .animation-wrapper > .widget:not(.edit) {
} // padding: var(--ae-inside-padding-size) !important;
} // border: 1px solid var(--ae-panel-border-color) !important;
} // }
// }
// }
.widget:has(> .gradio-button) { .widget:has(> .gradio-button) {
height: 100%; height: 100%;
@@ -390,8 +393,8 @@ div.block.padded {
fieldset.block.padded fieldset.block.padded
{ {
background-color: var(--ae-panel-bg-color) !important; background-color: var(--ae-panel-bg-color) !important;
/*border-width: var(--ae-border-width) !important;*/ border-width: var(--ae-border-width) !important;
/*border-color: var(--ae-panel-border-color) !important;*/ border-color: var(--ae-panel-border-color) !important;
border-radius: var(--ae-panel-border-radius) !important; border-radius: var(--ae-panel-border-radius) !important;
} }
@@ -440,7 +443,7 @@ div.gradio-row>.form{
} }
.wrap.svelte-1p9xokt.svelte-1p9xokt.svelte-1p9xokt label, .wrap.svelte-1p9xokt.svelte-1p9xokt.svelte-1p9xokt label,
.wrap.svelte-1qxcj04.svelte-1qxcj04.svelte-1qxcj04 label, .gradio-radio label,
button.tool.secondary, button.tool.secondary,
button.secondary, button.secondary,
.gradio-dropdown label .wrap, .gradio-dropdown label .wrap,
@@ -495,6 +498,7 @@ button.secondary{
white-space: break-spaces !important; white-space: break-spaces !important;
} }
.gradio-radio fieldset > .wrap > label,
[type=text], [type=text],
[type=email], [type=email],
[type=url], [type=url],
@@ -510,8 +514,8 @@ button.secondary{
[multiple], [multiple],
textarea, textarea,
select { select {
line-height: 1.5rem; line-height: 1.5rem !important;
padding: 4px 8px; padding: 4px 8px !important;
} }
button.tool.secondary, button.tool.secondary,
@@ -571,12 +575,6 @@ input[type=email] {
align-self: flex-start; align-self: flex-start;
} }
span.svelte-1gfkn6j:not(.has-info) {
margin-top: 1px;
margin-left: 1px;
margin-bottom: var(--ae-inside-padding-size);
}
/* input column alignment */ /* input column alignment */
label.block{ label.block{
display: flex; display: flex;
@@ -873,6 +871,11 @@ input[type=range]::-ms-fill-upper {
} }
} }
.comfy-image-widget > .block {
background: var(--ae-main-bg-color);
border-color: var(--ae-panel-border-color);
}
.comfy-toggle-button { .comfy-toggle-button {
> .lg { > .lg {
border-color: var(--ae-subpanel-border-color) !important; border-color: var(--ae-subpanel-border-color) !important;