mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-16 17:48:13 -05:00
Compare commits
514 Commits
v4.2.8rc2
...
v4.2.9.dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6af5a22d3a | ||
|
|
8cf4321010 | ||
|
|
aa7f2b096a | ||
|
|
bf0824b56d | ||
|
|
056c56d322 | ||
|
|
afc6f83d72 | ||
|
|
c776ac3af2 | ||
|
|
b7b3683bef | ||
|
|
fb26b6824a | ||
|
|
63d8ad912f | ||
|
|
bbd7d7fc17 | ||
|
|
6507a78182 | ||
|
|
22f46517f4 | ||
|
|
45596e1f94 | ||
|
|
6de0dbe854 | ||
|
|
011827fa29 | ||
|
|
fc6d244071 | ||
|
|
cd3da886d6 | ||
|
|
c013c55d92 | ||
|
|
cd3dd7db0d | ||
|
|
1fdcce9429 | ||
|
|
181e40926d | ||
|
|
c62ede5878 | ||
|
|
a2ad5f1a9a | ||
|
|
ff74a5356f | ||
|
|
92dc30dace | ||
|
|
3af577b210 | ||
|
|
d0464330f7 | ||
|
|
dd3ef4a80f | ||
|
|
0ced891944 | ||
|
|
10a5452df9 | ||
|
|
cb97969bbc | ||
|
|
71e742e238 | ||
|
|
fadd20fb8e | ||
|
|
01b9ca78e4 | ||
|
|
2baf825f34 | ||
|
|
1fa8048509 | ||
|
|
a000ad75f6 | ||
|
|
aefb2339bb | ||
|
|
a4f8671f86 | ||
|
|
73530ba54f | ||
|
|
685eb9927d | ||
|
|
ee57302fc3 | ||
|
|
c1fb9cdb93 | ||
|
|
aa6d441552 | ||
|
|
25d8d4c2e9 | ||
|
|
427ea6da5c | ||
|
|
d9f4266630 | ||
|
|
96f6e9e683 | ||
|
|
f10248e3f5 | ||
|
|
6a21f5fde1 | ||
|
|
ff20dd509a | ||
|
|
78a59b5b78 | ||
|
|
46bfbbbc87 | ||
|
|
a6d73d0773 | ||
|
|
6578e8bef8 | ||
|
|
0596d25e07 | ||
|
|
86e8ce9139 | ||
|
|
5aa2957da4 | ||
|
|
82f0cb2c8c | ||
|
|
fa48145cbc | ||
|
|
6d1edc330d | ||
|
|
97c0d3f6be | ||
|
|
a79a25ad63 | ||
|
|
6a8ceef404 | ||
|
|
3539670d93 | ||
|
|
c54bc32ef6 | ||
|
|
fee293e289 | ||
|
|
747eef9ccc | ||
|
|
7d2df399ed | ||
|
|
68fad5cdcc | ||
|
|
b4d656c203 | ||
|
|
3136d89d52 | ||
|
|
27e829b955 | ||
|
|
e03e870d5b | ||
|
|
9465ff450b | ||
|
|
92906a9575 | ||
|
|
77f206abe4 | ||
|
|
44a3f61580 | ||
|
|
0a2afed08b | ||
|
|
9b3b961105 | ||
|
|
9b1828e1aa | ||
|
|
5101873f49 | ||
|
|
c612f18114 | ||
|
|
7e400d876f | ||
|
|
677dddcfc9 | ||
|
|
0792b9175e | ||
|
|
e4829f80af | ||
|
|
bb760f3eb4 | ||
|
|
388c65287b | ||
|
|
12cd41e05c | ||
|
|
7765c03949 | ||
|
|
3daa80c57f | ||
|
|
5dbbef4ebd | ||
|
|
db33b3f7b5 | ||
|
|
8ffcf2a6be | ||
|
|
2e7ae6a07e | ||
|
|
fea1711f0c | ||
|
|
2a3546db97 | ||
|
|
285c266612 | ||
|
|
426ad54c53 | ||
|
|
fc75f7919f | ||
|
|
6c6b1aaff6 | ||
|
|
c319d653ac | ||
|
|
d887e474e7 | ||
|
|
da7b52d6ba | ||
|
|
b5aa308593 | ||
|
|
0b7ceb3bb6 | ||
|
|
3a70cefda2 | ||
|
|
4b609251e1 | ||
|
|
0839eac0f7 | ||
|
|
5f2a7feeee | ||
|
|
982535eb92 | ||
|
|
0c2b8edc8d | ||
|
|
f78f4ca25f | ||
|
|
d6b3e6c07d | ||
|
|
071ff8e74a | ||
|
|
1ea8aafca1 | ||
|
|
533dd221f8 | ||
|
|
2b325c6683 | ||
|
|
3845b1b3e6 | ||
|
|
cea7890a67 | ||
|
|
c38fe8025d | ||
|
|
f1de95349c | ||
|
|
2950775fa7 | ||
|
|
cb293fd7ac | ||
|
|
43b3fab6be | ||
|
|
d4b0dbce49 | ||
|
|
137b810669 | ||
|
|
c172657324 | ||
|
|
97c966b04f | ||
|
|
7178fc6253 | ||
|
|
4adb2eabf5 | ||
|
|
9f2c815e13 | ||
|
|
1435557d1d | ||
|
|
96abf687f6 | ||
|
|
636d9a7209 | ||
|
|
3b36eb0223 | ||
|
|
388c97bff0 | ||
|
|
b1cb018695 | ||
|
|
df78dd7953 | ||
|
|
0dc344a22e | ||
|
|
350d7f6f14 | ||
|
|
11059ee2d4 | ||
|
|
c90d3f3bb9 | ||
|
|
7f6d439fd1 | ||
|
|
783a78f069 | ||
|
|
0ff031950d | ||
|
|
d7e8f3d756 | ||
|
|
4668ea449b | ||
|
|
30d318d021 | ||
|
|
de96f97e5f | ||
|
|
57c0a2dfb1 | ||
|
|
cd4e464bde | ||
|
|
49e48c3eb7 | ||
|
|
edd3b3bce9 | ||
|
|
f8bfb66108 | ||
|
|
3b6a76cbf3 | ||
|
|
e0b60e4320 | ||
|
|
2159319035 | ||
|
|
b170fc232e | ||
|
|
594da60f2f | ||
|
|
6a432f6518 | ||
|
|
eb8eacfec6 | ||
|
|
c8d04d42e2 | ||
|
|
d39c9de81e | ||
|
|
a27d39b9ff | ||
|
|
6b385614f0 | ||
|
|
3ae7250ef7 | ||
|
|
a42d0ce1d2 | ||
|
|
d9131f7563 | ||
|
|
bdce958f29 | ||
|
|
3c86f1e979 | ||
|
|
894b8a29b9 | ||
|
|
8436a44973 | ||
|
|
f9f9ec3688 | ||
|
|
5a98d7a1f6 | ||
|
|
f9bc96e497 | ||
|
|
56350ff5dc | ||
|
|
6c1139340c | ||
|
|
641b1a7e6f | ||
|
|
674a3f462f | ||
|
|
2283186d3a | ||
|
|
340af1fe50 | ||
|
|
9378656d78 | ||
|
|
5f0413e222 | ||
|
|
c3e47515b1 | ||
|
|
5dcef6fa0d | ||
|
|
31ac02cd93 | ||
|
|
ab16976084 | ||
|
|
8e2b7845e1 | ||
|
|
3973bce342 | ||
|
|
f63847a504 | ||
|
|
07e3529948 | ||
|
|
03e1c60694 | ||
|
|
d766ed71fc | ||
|
|
ae68ef142a | ||
|
|
20f55768c4 | ||
|
|
c4fad4456e | ||
|
|
78f5ec44ad | ||
|
|
e14ba86942 | ||
|
|
d4e7720f6b | ||
|
|
a3f0e7e1cb | ||
|
|
30a696c476 | ||
|
|
66d6c64e16 | ||
|
|
d15be9b57c | ||
|
|
e5da902fd0 | ||
|
|
fc558094c2 | ||
|
|
ad9312e989 | ||
|
|
8e1a70b008 | ||
|
|
17f88cd5ad | ||
|
|
298f1919fa | ||
|
|
4d20cc11d4 | ||
|
|
14f249a2f0 | ||
|
|
b9746a6c2c | ||
|
|
94f298a6f4 | ||
|
|
8d3a8178da | ||
|
|
cad4212fe8 | ||
|
|
cff28dfaa9 | ||
|
|
70d7509fcc | ||
|
|
cf83af7a27 | ||
|
|
5c5a405c0f | ||
|
|
0208e4b232 | ||
|
|
e940754795 | ||
|
|
dc9fa1a735 | ||
|
|
08591fbf6d | ||
|
|
74db71bb5d | ||
|
|
60dbe798a5 | ||
|
|
0e676605fe | ||
|
|
3f781016f6 | ||
|
|
17cd2f6b02 | ||
|
|
99102a1b34 | ||
|
|
8d72e7d9e8 | ||
|
|
0b6b6f97ad | ||
|
|
fb2f6382b1 | ||
|
|
1ddea87c35 | ||
|
|
ea02323095 | ||
|
|
49733091c7 | ||
|
|
cf833fd6e2 | ||
|
|
ba5cf07ab8 | ||
|
|
d15321a373 | ||
|
|
de597a5eb4 | ||
|
|
e5f5cbdf5c | ||
|
|
7d4342bbff | ||
|
|
7f8a1d8d20 | ||
|
|
65353ac1e1 | ||
|
|
7f9a31ca4a | ||
|
|
592eb2886c | ||
|
|
c220dd8987 | ||
|
|
a263beb0d5 | ||
|
|
46b7c510eb | ||
|
|
f405e472ea | ||
|
|
7bdfd3ef5f | ||
|
|
778ee2c679 | ||
|
|
e70339ff3e | ||
|
|
88c57a9750 | ||
|
|
137252128b | ||
|
|
d4297b1345 | ||
|
|
6059bc7b47 | ||
|
|
c3ff3eb51f | ||
|
|
0b7751c413 | ||
|
|
d7f1c30624 | ||
|
|
3f4d7dbeea | ||
|
|
19b6ae2907 | ||
|
|
769f96ff9f | ||
|
|
fdaf75faa4 | ||
|
|
1380bb7ae6 | ||
|
|
9483c8cc29 | ||
|
|
2ef8a8cf5a | ||
|
|
d296ec1932 | ||
|
|
444ad3dae1 | ||
|
|
8cdcc71378 | ||
|
|
e8bc06cfd3 | ||
|
|
67a0a024e9 | ||
|
|
bd2c46c267 | ||
|
|
5acb27f350 | ||
|
|
7271b12d0f | ||
|
|
4a79467a33 | ||
|
|
5501bb87a3 | ||
|
|
561610e296 | ||
|
|
b76609ef18 | ||
|
|
070b78501b | ||
|
|
50df4f4ab6 | ||
|
|
9bbf430125 | ||
|
|
384a90958a | ||
|
|
0e4a25b029 | ||
|
|
4a44e171fd | ||
|
|
9bc57a6f59 | ||
|
|
4341ed7ab4 | ||
|
|
97ce72c542 | ||
|
|
a2c78a57a7 | ||
|
|
044a713dc9 | ||
|
|
b8479c5fe2 | ||
|
|
4e5d056824 | ||
|
|
118278b372 | ||
|
|
8e8c255f3f | ||
|
|
1575bee401 | ||
|
|
249bbfc883 | ||
|
|
3993ae410f | ||
|
|
edf040e3d2 | ||
|
|
66fd077ee7 | ||
|
|
b93462ebb6 | ||
|
|
aae60d0cdc | ||
|
|
d4da00e607 | ||
|
|
0c539ff00b | ||
|
|
5983cbf26c | ||
|
|
c513d6e3af | ||
|
|
9d57c0e631 | ||
|
|
a1923a8966 | ||
|
|
d988e18731 | ||
|
|
51008da2dd | ||
|
|
6ccc1f5672 | ||
|
|
4a556f84e0 | ||
|
|
2f21a2220d | ||
|
|
91a420b13e | ||
|
|
c27da3581b | ||
|
|
961dfbce93 | ||
|
|
df39c825ae | ||
|
|
3f6ee1b7a4 | ||
|
|
908e504a6f | ||
|
|
f2fa41afc5 | ||
|
|
440ff40ad5 | ||
|
|
5c15458e15 | ||
|
|
be5b474f1e | ||
|
|
cee178c2b6 | ||
|
|
27657f8b7a | ||
|
|
e0cde3815a | ||
|
|
09d0421de4 | ||
|
|
47b94d563c | ||
|
|
0b5d20c9f0 | ||
|
|
80e7e1293a | ||
|
|
3a82b0cbc1 | ||
|
|
a27cbc13b6 | ||
|
|
a8f962eb3f | ||
|
|
7f40d23f19 | ||
|
|
918354cd9d | ||
|
|
eef9278ee6 | ||
|
|
2c32e2e5c1 | ||
|
|
6f05654db5 | ||
|
|
1d31b6902f | ||
|
|
5a7d615e64 | ||
|
|
1dbf9e4ed4 | ||
|
|
5dcc6ee203 | ||
|
|
84a4e6ae3f | ||
|
|
f283bfd68f | ||
|
|
6e5ff7b79c | ||
|
|
7c3800d03f | ||
|
|
941db90518 | ||
|
|
0d9ecf0f90 | ||
|
|
9c77023a11 | ||
|
|
b55378c63c | ||
|
|
946c2a49ab | ||
|
|
b823c31ec6 | ||
|
|
ec6361e5cb | ||
|
|
0c26d28278 | ||
|
|
c5172d4c5a | ||
|
|
89de04775e | ||
|
|
b4c3c940b5 | ||
|
|
aee2aad959 | ||
|
|
5ca48a8a5f | ||
|
|
1806aa187b | ||
|
|
7824cb7a1a | ||
|
|
9807a896f4 | ||
|
|
19866f057d | ||
|
|
ec4eae3c9c | ||
|
|
bea0cba038 | ||
|
|
48ee75af9c | ||
|
|
929c593d2f | ||
|
|
221f32eca7 | ||
|
|
c3acc15e8b | ||
|
|
1b653278fc | ||
|
|
cc9062ee46 | ||
|
|
91c0feb0ad | ||
|
|
ae60292ac8 | ||
|
|
a6ca17b19d | ||
|
|
6a4a5ece74 | ||
|
|
9b81860307 | ||
|
|
5f4a3928d2 | ||
|
|
b703884763 | ||
|
|
32da98ab8f | ||
|
|
bd5a85bf70 | ||
|
|
d045f24014 | ||
|
|
2aad3f89c3 | ||
|
|
dd54d19f00 | ||
|
|
0ed6591d8c | ||
|
|
712e090134 | ||
|
|
8fc2a1d1cf | ||
|
|
cc15c1593e | ||
|
|
9997d3abda | ||
|
|
031471e785 | ||
|
|
2e860c6791 | ||
|
|
d071a9e17d | ||
|
|
ed53d33321 | ||
|
|
382bc6d978 | ||
|
|
dab42e258c | ||
|
|
81556410bb | ||
|
|
1f2dfd473c | ||
|
|
8f0f51be2c | ||
|
|
7179e250ed | ||
|
|
5bec091fd6 | ||
|
|
2c5896cb0c | ||
|
|
93ff252dc0 | ||
|
|
ac52224455 | ||
|
|
4087cad23d | ||
|
|
e936b1ff8f | ||
|
|
b7f9c5e221 | ||
|
|
fc5467150e | ||
|
|
4dcab357a0 | ||
|
|
695e464255 | ||
|
|
9999b60c3b | ||
|
|
e7df53e260 | ||
|
|
844590a571 | ||
|
|
9622beaa0d | ||
|
|
007e2553a8 | ||
|
|
15ad4e3f5e | ||
|
|
be5094fcb4 | ||
|
|
a20a861680 | ||
|
|
396d0a4bc0 | ||
|
|
ca9314e077 | ||
|
|
4b848798e7 | ||
|
|
083bcbc77d | ||
|
|
e8cdc9ae62 | ||
|
|
8abfa759a4 | ||
|
|
f6a324b633 | ||
|
|
f083be9391 | ||
|
|
091e2fb751 | ||
|
|
d8539daf1f | ||
|
|
7ec059f5fa | ||
|
|
4f2ecdefd2 | ||
|
|
e8891a1988 | ||
|
|
37d2607f34 | ||
|
|
0e7b10d3d9 | ||
|
|
1f85888638 | ||
|
|
c1f9a129fa | ||
|
|
7ccc5ba398 | ||
|
|
5e1a6ae334 | ||
|
|
3f6cf638f9 | ||
|
|
46e062a828 | ||
|
|
cc3a0b5d6c | ||
|
|
775479ab7b | ||
|
|
6b9e0e6d63 | ||
|
|
83a5c87f5e | ||
|
|
84fde74331 | ||
|
|
a517e29b39 | ||
|
|
ccceba7565 | ||
|
|
5fc7a03669 | ||
|
|
8864ad1b50 | ||
|
|
f2989885fb | ||
|
|
19c66e5c76 | ||
|
|
8a6690a57c | ||
|
|
2cc60f253a | ||
|
|
cb69872dd3 | ||
|
|
ba66d7c9a6 | ||
|
|
9fe727c9f8 | ||
|
|
58c656224f | ||
|
|
c51253f5f6 | ||
|
|
6c1d1588fc | ||
|
|
95d6183a6c | ||
|
|
f4c9facdaf | ||
|
|
a274e6f165 | ||
|
|
154e3e6f64 | ||
|
|
2c3ac972e5 | ||
|
|
2e2e072b0b | ||
|
|
9d8dd2bf66 | ||
|
|
9047f6db30 | ||
|
|
5ab345ee63 | ||
|
|
d8a83acd3a | ||
|
|
1f58e5756b | ||
|
|
744acb8f07 | ||
|
|
ae7228d821 | ||
|
|
99d8b3a7bf | ||
|
|
3fbe65bbcf | ||
|
|
f5d879d8e7 | ||
|
|
cbc5a4f8e6 | ||
|
|
37ac7d8ed5 | ||
|
|
bee3fa339d | ||
|
|
c171fe2b96 | ||
|
|
1fa8032fdb | ||
|
|
5c0676bcc2 | ||
|
|
cefd9a027c | ||
|
|
1bce156de1 | ||
|
|
cd4f63f2fd | ||
|
|
3c7140cbf3 | ||
|
|
b71ba63b5a | ||
|
|
d540e2c0d3 | ||
|
|
d79fafc5f5 | ||
|
|
9e93fa2092 | ||
|
|
392e9b4882 | ||
|
|
231e5ec94a | ||
|
|
e5bb6f9693 | ||
|
|
da7dee44c6 | ||
|
|
83144f4fe3 | ||
|
|
c451f52ea3 | ||
|
|
8a2c78f2e1 | ||
|
|
bcc78bde9b | ||
|
|
054bb6fe0a | ||
|
|
4f4aa6d92e | ||
|
|
eac51ac6f5 | ||
|
|
9f349a7c0a | ||
|
|
918afa5b15 | ||
|
|
eb1113f95c | ||
|
|
4f4ba7b462 | ||
|
|
2298be0e6b | ||
|
|
63494dfca7 | ||
|
|
36a1d39454 | ||
|
|
a6f6d5c400 | ||
|
|
e85f221aca | ||
|
|
d4797e37dc | ||
|
|
3e7923d072 | ||
|
|
a85d69ce3d | ||
|
|
96db006c99 | ||
|
|
8ca57d03d8 | ||
|
|
6c404ce5f8 | ||
|
|
584e07182b |
@@ -1,20 +1,22 @@
|
||||
# Invoke in Docker
|
||||
|
||||
- Ensure that Docker can use the GPU on your system
|
||||
- This documentation assumes Linux, but should work similarly under Windows with WSL2
|
||||
First things first:
|
||||
|
||||
- Ensure that Docker can use your [NVIDIA][nvidia docker docs] or [AMD][amd docker docs] GPU.
|
||||
- This document assumes a Linux system, but should work similarly under Windows with WSL2.
|
||||
- We don't recommend running Invoke in Docker on macOS at this time. It works, but very slowly.
|
||||
|
||||
## Quickstart :lightning:
|
||||
## Quickstart
|
||||
|
||||
No `docker compose`, no persistence, just a simple one-liner using the official images:
|
||||
No `docker compose`, no persistence, single command, using the official images:
|
||||
|
||||
**CUDA:**
|
||||
**CUDA (NVIDIA GPU):**
|
||||
|
||||
```bash
|
||||
docker run --runtime=nvidia --gpus=all --publish 9090:9090 ghcr.io/invoke-ai/invokeai
|
||||
```
|
||||
|
||||
**ROCm:**
|
||||
**ROCm (AMD GPU):**
|
||||
|
||||
```bash
|
||||
docker run --device /dev/kfd --device /dev/dri --publish 9090:9090 ghcr.io/invoke-ai/invokeai:main-rocm
|
||||
@@ -22,12 +24,20 @@ docker run --device /dev/kfd --device /dev/dri --publish 9090:9090 ghcr.io/invok
|
||||
|
||||
Open `http://localhost:9090` in your browser once the container finishes booting, install some models, and generate away!
|
||||
|
||||
> [!TIP]
|
||||
> To persist your data (including downloaded models) outside of the container, add a `--volume/-v` flag to the above command, e.g.: `docker run --volume /some/local/path:/invokeai <...the rest of the command>`
|
||||
### Data persistence
|
||||
|
||||
To persist your generated images and downloaded models outside of the container, add a `--volume/-v` flag to the above command, e.g.:
|
||||
|
||||
```bash
|
||||
docker run --volume /some/local/path:/invokeai {...etc...}
|
||||
```
|
||||
|
||||
`/some/local/path/invokeai` will contain all your data.
|
||||
It can *usually* be reused between different installs of Invoke. Tread with caution and read the release notes!
|
||||
|
||||
## Customize the container
|
||||
|
||||
We ship the `run.sh` script, which is a convenient wrapper around `docker compose` for cases where custom image build args are needed. Alternatively, the familiar `docker compose` commands work just as well.
|
||||
The included `run.sh` script is a convenience wrapper around `docker compose`. It can be helpful for passing additional build arguments to `docker compose`. Alternatively, the familiar `docker compose` commands work just as well.
|
||||
|
||||
```bash
|
||||
cd docker
|
||||
@@ -38,11 +48,14 @@ cp .env.sample .env
|
||||
|
||||
It will take a few minutes to build the image the first time. Once the application starts up, open `http://localhost:9090` in your browser to invoke!
|
||||
|
||||
>[!TIP]
|
||||
>When using the `run.sh` script, the container will continue running after Ctrl+C. To shut it down, use the `docker compose down` command.
|
||||
|
||||
## Docker setup in detail
|
||||
|
||||
#### Linux
|
||||
|
||||
1. Ensure builkit is enabled in the Docker daemon settings (`/etc/docker/daemon.json`)
|
||||
1. Ensure buildkit is enabled in the Docker daemon settings (`/etc/docker/daemon.json`)
|
||||
2. Install the `docker compose` plugin using your package manager, or follow a [tutorial](https://docs.docker.com/compose/install/linux/#install-using-the-repository).
|
||||
- The deprecated `docker-compose` (hyphenated) CLI probably won't work. Update to a recent version.
|
||||
3. Ensure docker daemon is able to access the GPU.
|
||||
@@ -98,25 +111,7 @@ GPU_DRIVER=cuda
|
||||
|
||||
Any environment variables supported by InvokeAI can be set here. See the [Configuration docs](https://invoke-ai.github.io/InvokeAI/features/CONFIGURATION/) for further detail.
|
||||
|
||||
## Even More Customizing!
|
||||
---
|
||||
|
||||
See the `docker-compose.yml` file. The `command` instruction can be uncommented and used to run arbitrary startup commands. Some examples below.
|
||||
|
||||
### Reconfigure the runtime directory
|
||||
|
||||
Can be used to download additional models from the supported model list
|
||||
|
||||
In conjunction with `INVOKEAI_ROOT` can be also used to initialize a runtime directory
|
||||
|
||||
```yaml
|
||||
command:
|
||||
- invokeai-configure
|
||||
- --yes
|
||||
```
|
||||
|
||||
Or install models:
|
||||
|
||||
```yaml
|
||||
command:
|
||||
- invokeai-model-install
|
||||
```
|
||||
[nvidia docker docs]: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
|
||||
[amd docker docs]: https://rocm.docs.amd.com/projects/install-on-linux/en/latest/how-to/docker.html
|
||||
|
||||
@@ -11,6 +11,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
|
||||
Batch,
|
||||
BatchStatus,
|
||||
CancelByBatchIDsResult,
|
||||
CancelByOriginResult,
|
||||
ClearResult,
|
||||
EnqueueBatchResult,
|
||||
PruneResult,
|
||||
@@ -105,6 +106,19 @@ async def cancel_by_batch_ids(
|
||||
return ApiDependencies.invoker.services.session_queue.cancel_by_batch_ids(queue_id=queue_id, batch_ids=batch_ids)
|
||||
|
||||
|
||||
@session_queue_router.put(
|
||||
"/{queue_id}/cancel_by_origin",
|
||||
operation_id="cancel_by_origin",
|
||||
responses={200: {"model": CancelByBatchIDsResult}},
|
||||
)
|
||||
async def cancel_by_origin(
|
||||
queue_id: str = Path(description="The queue id to perform this operation on"),
|
||||
origin: str = Query(description="The origin to cancel all queue items for"),
|
||||
) -> CancelByOriginResult:
|
||||
"""Immediately cancels all queue items with the given origin"""
|
||||
return ApiDependencies.invoker.services.session_queue.cancel_by_origin(queue_id=queue_id, origin=origin)
|
||||
|
||||
|
||||
@session_queue_router.put(
|
||||
"/{queue_id}/clear",
|
||||
operation_id="clear",
|
||||
|
||||
@@ -26,13 +26,10 @@ from invokeai.app.services.style_preset_records.style_preset_records_common impo
|
||||
)
|
||||
|
||||
|
||||
class StylePresetUpdateFormData(BaseModel):
|
||||
class StylePresetFormData(BaseModel):
|
||||
name: str = Field(description="Preset name")
|
||||
positive_prompt: str = Field(description="Positive prompt")
|
||||
negative_prompt: str = Field(description="Negative prompt")
|
||||
|
||||
|
||||
class StylePresetCreateFormData(StylePresetUpdateFormData):
|
||||
type: PresetType = Field(description="Preset type")
|
||||
|
||||
|
||||
@@ -95,9 +92,10 @@ async def update_style_preset(
|
||||
|
||||
try:
|
||||
parsed_data = json.loads(data)
|
||||
validated_data = StylePresetUpdateFormData(**parsed_data)
|
||||
validated_data = StylePresetFormData(**parsed_data)
|
||||
|
||||
name = validated_data.name
|
||||
type = validated_data.type
|
||||
positive_prompt = validated_data.positive_prompt
|
||||
negative_prompt = validated_data.negative_prompt
|
||||
|
||||
@@ -105,7 +103,7 @@ async def update_style_preset(
|
||||
raise HTTPException(status_code=400, detail="Invalid preset data")
|
||||
|
||||
preset_data = PresetData(positive_prompt=positive_prompt, negative_prompt=negative_prompt)
|
||||
changes = StylePresetChanges(name=name, preset_data=preset_data)
|
||||
changes = StylePresetChanges(name=name, preset_data=preset_data, type=type)
|
||||
|
||||
style_preset_image = ApiDependencies.invoker.services.style_preset_image_files.get_url(style_preset_id)
|
||||
style_preset = ApiDependencies.invoker.services.style_preset_records.update(
|
||||
@@ -145,7 +143,7 @@ async def create_style_preset(
|
||||
|
||||
try:
|
||||
parsed_data = json.loads(data)
|
||||
validated_data = StylePresetCreateFormData(**parsed_data)
|
||||
validated_data = StylePresetFormData(**parsed_data)
|
||||
|
||||
name = validated_data.name
|
||||
type = validated_data.type
|
||||
|
||||
@@ -6,13 +6,19 @@ import cv2
|
||||
import numpy
|
||||
from PIL import Image, ImageChops, ImageFilter, ImageOps
|
||||
|
||||
from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
|
||||
from invokeai.app.invocations.baseinvocation import (
|
||||
BaseInvocation,
|
||||
Classification,
|
||||
invocation,
|
||||
invocation_output,
|
||||
)
|
||||
from invokeai.app.invocations.constants import IMAGE_MODES
|
||||
from invokeai.app.invocations.fields import (
|
||||
ColorField,
|
||||
FieldDescriptions,
|
||||
ImageField,
|
||||
InputField,
|
||||
OutputField,
|
||||
WithBoard,
|
||||
WithMetadata,
|
||||
)
|
||||
@@ -1007,3 +1013,62 @@ class MaskFromIDInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
image_dto = context.images.save(image=mask, image_category=ImageCategory.MASK)
|
||||
|
||||
return ImageOutput.build(image_dto)
|
||||
|
||||
|
||||
@invocation_output("canvas_v2_mask_and_crop_output")
|
||||
class CanvasV2MaskAndCropOutput(ImageOutput):
|
||||
offset_x: int = OutputField(description="The x offset of the image, after cropping")
|
||||
offset_y: int = OutputField(description="The y offset of the image, after cropping")
|
||||
|
||||
|
||||
@invocation(
|
||||
"canvas_v2_mask_and_crop",
|
||||
title="Canvas V2 Mask and Crop",
|
||||
tags=["image", "mask", "id"],
|
||||
category="image",
|
||||
version="1.0.0",
|
||||
classification=Classification.Prototype,
|
||||
)
|
||||
class CanvasV2MaskAndCropInvocation(BaseInvocation, WithMetadata, WithBoard):
|
||||
"""Handles Canvas V2 image output masking and cropping"""
|
||||
|
||||
source_image: ImageField | None = InputField(
|
||||
default=None,
|
||||
description="The source image onto which the masked generated image is pasted. If omitted, the masked generated image is returned with transparency.",
|
||||
)
|
||||
generated_image: ImageField = InputField(description="The image to apply the mask to")
|
||||
mask: ImageField = InputField(description="The mask to apply")
|
||||
mask_blur: int = InputField(default=0, ge=0, description="The amount to blur the mask by")
|
||||
|
||||
def _prepare_mask(self, mask: Image.Image) -> Image.Image:
|
||||
mask_array = numpy.array(mask)
|
||||
kernel = numpy.ones((self.mask_blur, self.mask_blur), numpy.uint8)
|
||||
dilated_mask_array = cv2.erode(mask_array, kernel, iterations=3)
|
||||
dilated_mask = Image.fromarray(dilated_mask_array)
|
||||
if self.mask_blur > 0:
|
||||
mask = dilated_mask.filter(ImageFilter.GaussianBlur(self.mask_blur))
|
||||
return ImageOps.invert(mask.convert("L"))
|
||||
|
||||
def invoke(self, context: InvocationContext) -> CanvasV2MaskAndCropOutput:
|
||||
mask = self._prepare_mask(context.images.get_pil(self.mask.image_name))
|
||||
|
||||
if self.source_image:
|
||||
generated_image = context.images.get_pil(self.generated_image.image_name)
|
||||
source_image = context.images.get_pil(self.source_image.image_name)
|
||||
source_image.paste(generated_image, (0, 0), mask)
|
||||
image_dto = context.images.save(image=source_image)
|
||||
else:
|
||||
generated_image = context.images.get_pil(self.generated_image.image_name)
|
||||
generated_image.putalpha(mask)
|
||||
image_dto = context.images.save(image=generated_image)
|
||||
|
||||
# bbox = image.getbbox()
|
||||
# image = image.crop(bbox)
|
||||
|
||||
return CanvasV2MaskAndCropOutput(
|
||||
image=ImageField(image_name=image_dto.image_name),
|
||||
offset_x=0,
|
||||
offset_y=0,
|
||||
width=image_dto.width,
|
||||
height=image_dto.height,
|
||||
)
|
||||
|
||||
@@ -88,6 +88,7 @@ class QueueItemEventBase(QueueEventBase):
|
||||
|
||||
item_id: int = Field(description="The ID of the queue item")
|
||||
batch_id: str = Field(description="The ID of the queue batch")
|
||||
origin: str | None = Field(default=None, description="The origin of the batch")
|
||||
|
||||
|
||||
class InvocationEventBase(QueueItemEventBase):
|
||||
@@ -95,8 +96,6 @@ class InvocationEventBase(QueueItemEventBase):
|
||||
|
||||
session_id: str = Field(description="The ID of the session (aka graph execution state)")
|
||||
queue_id: str = Field(description="The ID of the queue")
|
||||
item_id: int = Field(description="The ID of the queue item")
|
||||
batch_id: str = Field(description="The ID of the queue batch")
|
||||
session_id: str = Field(description="The ID of the session (aka graph execution state)")
|
||||
invocation: AnyInvocation = Field(description="The ID of the invocation")
|
||||
invocation_source_id: str = Field(description="The ID of the prepared invocation's source node")
|
||||
@@ -114,6 +113,7 @@ class InvocationStartedEvent(InvocationEventBase):
|
||||
queue_id=queue_item.queue_id,
|
||||
item_id=queue_item.item_id,
|
||||
batch_id=queue_item.batch_id,
|
||||
origin=queue_item.origin,
|
||||
session_id=queue_item.session_id,
|
||||
invocation=invocation,
|
||||
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
|
||||
@@ -147,6 +147,7 @@ class InvocationDenoiseProgressEvent(InvocationEventBase):
|
||||
queue_id=queue_item.queue_id,
|
||||
item_id=queue_item.item_id,
|
||||
batch_id=queue_item.batch_id,
|
||||
origin=queue_item.origin,
|
||||
session_id=queue_item.session_id,
|
||||
invocation=invocation,
|
||||
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
|
||||
@@ -184,6 +185,7 @@ class InvocationCompleteEvent(InvocationEventBase):
|
||||
queue_id=queue_item.queue_id,
|
||||
item_id=queue_item.item_id,
|
||||
batch_id=queue_item.batch_id,
|
||||
origin=queue_item.origin,
|
||||
session_id=queue_item.session_id,
|
||||
invocation=invocation,
|
||||
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
|
||||
@@ -216,6 +218,7 @@ class InvocationErrorEvent(InvocationEventBase):
|
||||
queue_id=queue_item.queue_id,
|
||||
item_id=queue_item.item_id,
|
||||
batch_id=queue_item.batch_id,
|
||||
origin=queue_item.origin,
|
||||
session_id=queue_item.session_id,
|
||||
invocation=invocation,
|
||||
invocation_source_id=queue_item.session.prepared_source_mapping[invocation.id],
|
||||
@@ -253,6 +256,7 @@ class QueueItemStatusChangedEvent(QueueItemEventBase):
|
||||
queue_id=queue_item.queue_id,
|
||||
item_id=queue_item.item_id,
|
||||
batch_id=queue_item.batch_id,
|
||||
origin=queue_item.origin,
|
||||
session_id=queue_item.session_id,
|
||||
status=queue_item.status,
|
||||
error_type=queue_item.error_type,
|
||||
@@ -279,12 +283,14 @@ class BatchEnqueuedEvent(QueueEventBase):
|
||||
description="The number of invocations initially requested to be enqueued (may be less than enqueued if queue was full)"
|
||||
)
|
||||
priority: int = Field(description="The priority of the batch")
|
||||
origin: str | None = Field(default=None, description="The origin of the batch")
|
||||
|
||||
@classmethod
|
||||
def build(cls, enqueue_result: EnqueueBatchResult) -> "BatchEnqueuedEvent":
|
||||
return cls(
|
||||
queue_id=enqueue_result.queue_id,
|
||||
batch_id=enqueue_result.batch.batch_id,
|
||||
origin=enqueue_result.batch.origin,
|
||||
enqueued=enqueue_result.enqueued,
|
||||
requested=enqueue_result.requested,
|
||||
priority=enqueue_result.priority,
|
||||
|
||||
@@ -6,6 +6,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
|
||||
Batch,
|
||||
BatchStatus,
|
||||
CancelByBatchIDsResult,
|
||||
CancelByOriginResult,
|
||||
CancelByQueueIDResult,
|
||||
ClearResult,
|
||||
EnqueueBatchResult,
|
||||
@@ -95,6 +96,11 @@ class SessionQueueBase(ABC):
|
||||
"""Cancels all queue items with matching batch IDs"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cancel_by_origin(self, queue_id: str, origin: str) -> CancelByOriginResult:
|
||||
"""Cancels all queue items with the given batch origin"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
|
||||
"""Cancels all queue items with matching queue ID"""
|
||||
|
||||
@@ -77,6 +77,7 @@ BatchDataCollection: TypeAlias = list[list[BatchDatum]]
|
||||
|
||||
class Batch(BaseModel):
|
||||
batch_id: str = Field(default_factory=uuid_string, description="The ID of the batch")
|
||||
origin: str | None = Field(default=None, description="The origin of this batch.")
|
||||
data: Optional[BatchDataCollection] = Field(default=None, description="The batch data collection.")
|
||||
graph: Graph = Field(description="The graph to initialize the session with")
|
||||
workflow: Optional[WorkflowWithoutID] = Field(
|
||||
@@ -195,6 +196,7 @@ class SessionQueueItemWithoutGraph(BaseModel):
|
||||
status: QUEUE_ITEM_STATUS = Field(default="pending", description="The status of this queue item")
|
||||
priority: int = Field(default=0, description="The priority of this queue item")
|
||||
batch_id: str = Field(description="The ID of the batch associated with this queue item")
|
||||
origin: str | None = Field(default=None, description="The origin of this queue item. ")
|
||||
session_id: str = Field(
|
||||
description="The ID of the session associated with this queue item. The session doesn't exist in graph_executions until the queue item is executed."
|
||||
)
|
||||
@@ -294,6 +296,7 @@ class SessionQueueStatus(BaseModel):
|
||||
class BatchStatus(BaseModel):
|
||||
queue_id: str = Field(..., description="The ID of the queue")
|
||||
batch_id: str = Field(..., description="The ID of the batch")
|
||||
origin: str | None = Field(..., description="The origin of the batch")
|
||||
pending: int = Field(..., description="Number of queue items with status 'pending'")
|
||||
in_progress: int = Field(..., description="Number of queue items with status 'in_progress'")
|
||||
completed: int = Field(..., description="Number of queue items with status 'complete'")
|
||||
@@ -328,6 +331,12 @@ class CancelByBatchIDsResult(BaseModel):
|
||||
canceled: int = Field(..., description="Number of queue items canceled")
|
||||
|
||||
|
||||
class CancelByOriginResult(BaseModel):
|
||||
"""Result of canceling by list of batch ids"""
|
||||
|
||||
canceled: int = Field(..., description="Number of queue items canceled")
|
||||
|
||||
|
||||
class CancelByQueueIDResult(CancelByBatchIDsResult):
|
||||
"""Result of canceling by queue id"""
|
||||
|
||||
@@ -433,6 +442,7 @@ class SessionQueueValueToInsert(NamedTuple):
|
||||
field_values: Optional[str] # field_values json
|
||||
priority: int # priority
|
||||
workflow: Optional[str] # workflow json
|
||||
origin: str | None
|
||||
|
||||
|
||||
ValuesToInsert: TypeAlias = list[SessionQueueValueToInsert]
|
||||
@@ -453,6 +463,7 @@ def prepare_values_to_insert(queue_id: str, batch: Batch, priority: int, max_new
|
||||
json.dumps(field_values, default=to_jsonable_python) if field_values else None, # field_values (json)
|
||||
priority, # priority
|
||||
json.dumps(workflow, default=to_jsonable_python) if workflow else None, # workflow (json)
|
||||
batch.origin, # origin
|
||||
)
|
||||
)
|
||||
return values_to_insert
|
||||
|
||||
@@ -10,6 +10,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
|
||||
Batch,
|
||||
BatchStatus,
|
||||
CancelByBatchIDsResult,
|
||||
CancelByOriginResult,
|
||||
CancelByQueueIDResult,
|
||||
ClearResult,
|
||||
EnqueueBatchResult,
|
||||
@@ -127,8 +128,8 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
|
||||
self.__cursor.executemany(
|
||||
"""--sql
|
||||
INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO session_queue (queue_id, session, session_id, batch_id, field_values, priority, workflow, origin)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
values_to_insert,
|
||||
)
|
||||
@@ -417,11 +418,7 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
)
|
||||
self.__conn.commit()
|
||||
if current_queue_item is not None and current_queue_item.batch_id in batch_ids:
|
||||
batch_status = self.get_batch_status(queue_id=queue_id, batch_id=current_queue_item.batch_id)
|
||||
queue_status = self.get_queue_status(queue_id=queue_id)
|
||||
self.__invoker.services.events.emit_queue_item_status_changed(
|
||||
current_queue_item, batch_status, queue_status
|
||||
)
|
||||
self._set_queue_item_status(current_queue_item.item_id, "canceled")
|
||||
except Exception:
|
||||
self.__conn.rollback()
|
||||
raise
|
||||
@@ -429,6 +426,46 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
self.__lock.release()
|
||||
return CancelByBatchIDsResult(canceled=count)
|
||||
|
||||
def cancel_by_origin(self, queue_id: str, origin: str) -> CancelByOriginResult:
|
||||
try:
|
||||
current_queue_item = self.get_current(queue_id)
|
||||
self.__lock.acquire()
|
||||
where = """--sql
|
||||
WHERE
|
||||
queue_id == ?
|
||||
AND origin == ?
|
||||
AND status != 'canceled'
|
||||
AND status != 'completed'
|
||||
AND status != 'failed'
|
||||
"""
|
||||
params = (queue_id, origin)
|
||||
self.__cursor.execute(
|
||||
f"""--sql
|
||||
SELECT COUNT(*)
|
||||
FROM session_queue
|
||||
{where};
|
||||
""",
|
||||
params,
|
||||
)
|
||||
count = self.__cursor.fetchone()[0]
|
||||
self.__cursor.execute(
|
||||
f"""--sql
|
||||
UPDATE session_queue
|
||||
SET status = 'canceled'
|
||||
{where};
|
||||
""",
|
||||
params,
|
||||
)
|
||||
self.__conn.commit()
|
||||
if current_queue_item is not None and current_queue_item.origin == origin:
|
||||
self._set_queue_item_status(current_queue_item.item_id, "canceled")
|
||||
except Exception:
|
||||
self.__conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
self.__lock.release()
|
||||
return CancelByOriginResult(canceled=count)
|
||||
|
||||
def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
|
||||
try:
|
||||
current_queue_item = self.get_current(queue_id)
|
||||
@@ -541,7 +578,8 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
started_at,
|
||||
session_id,
|
||||
batch_id,
|
||||
queue_id
|
||||
queue_id,
|
||||
origin
|
||||
FROM session_queue
|
||||
WHERE queue_id = ?
|
||||
"""
|
||||
@@ -621,7 +659,7 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
self.__lock.acquire()
|
||||
self.__cursor.execute(
|
||||
"""--sql
|
||||
SELECT status, count(*)
|
||||
SELECT status, count(*), origin
|
||||
FROM session_queue
|
||||
WHERE
|
||||
queue_id = ?
|
||||
@@ -633,6 +671,7 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
result = cast(list[sqlite3.Row], self.__cursor.fetchall())
|
||||
total = sum(row[1] for row in result)
|
||||
counts: dict[str, int] = {row[0]: row[1] for row in result}
|
||||
origin = result[0]["origin"] if result else None
|
||||
except Exception:
|
||||
self.__conn.rollback()
|
||||
raise
|
||||
@@ -641,6 +680,7 @@ class SqliteSessionQueue(SessionQueueBase):
|
||||
|
||||
return BatchStatus(
|
||||
batch_id=batch_id,
|
||||
origin=origin,
|
||||
queue_id=queue_id,
|
||||
pending=counts.get("pending", 0),
|
||||
in_progress=counts.get("in_progress", 0),
|
||||
|
||||
@@ -17,6 +17,7 @@ from invokeai.app.services.shared.sqlite_migrator.migrations.migration_11 import
|
||||
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_12 import build_migration_12
|
||||
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_13 import build_migration_13
|
||||
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_14 import build_migration_14
|
||||
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_15 import build_migration_15
|
||||
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_impl import SqliteMigrator
|
||||
|
||||
|
||||
@@ -51,6 +52,7 @@ def init_db(config: InvokeAIAppConfig, logger: Logger, image_files: ImageFileSto
|
||||
migrator.register_migration(build_migration_12(app_config=config))
|
||||
migrator.register_migration(build_migration_13())
|
||||
migrator.register_migration(build_migration_14())
|
||||
migrator.register_migration(build_migration_15())
|
||||
migrator.run_migrations()
|
||||
|
||||
return db
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import sqlite3
|
||||
|
||||
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
|
||||
|
||||
|
||||
class Migration15Callback:
|
||||
def __call__(self, cursor: sqlite3.Cursor) -> None:
|
||||
self._add_origin_col(cursor)
|
||||
|
||||
def _add_origin_col(self, cursor: sqlite3.Cursor) -> None:
|
||||
"""
|
||||
- Adds `origin` column to the session queue table.
|
||||
"""
|
||||
|
||||
cursor.execute("ALTER TABLE session_queue ADD COLUMN origin TEXT;")
|
||||
|
||||
|
||||
def build_migration_15() -> Migration:
|
||||
"""
|
||||
Build the migration from database version 14 to 15.
|
||||
|
||||
This migration does the following:
|
||||
- Adds `origin` column to the session queue table.
|
||||
"""
|
||||
migration_15 = Migration(
|
||||
from_version=14,
|
||||
to_version=15,
|
||||
callback=Migration15Callback(),
|
||||
)
|
||||
|
||||
return migration_15
|
||||
@@ -32,6 +32,7 @@ class PresetType(str, Enum, metaclass=MetaEnum):
|
||||
class StylePresetChanges(BaseModel, extra="forbid"):
|
||||
name: Optional[str] = Field(default=None, description="The style preset's new name.")
|
||||
preset_data: Optional[PresetData] = Field(default=None, description="The updated data for style preset.")
|
||||
type: Optional[PresetType] = Field(description="The updated type of the style preset")
|
||||
|
||||
|
||||
class StylePresetWithoutId(BaseModel):
|
||||
|
||||
@@ -12,6 +12,10 @@ module.exports = {
|
||||
'i18next/no-literal-string': 'error',
|
||||
// https://eslint.org/docs/latest/rules/no-console
|
||||
'no-console': 'error',
|
||||
// https://eslint.org/docs/latest/rules/no-promise-executor-return
|
||||
'no-promise-executor-return': 'error',
|
||||
// https://eslint.org/docs/latest/rules/require-await
|
||||
'require-await': 'error',
|
||||
},
|
||||
overrides: [
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PropsWithChildren, memo, useEffect } from 'react';
|
||||
import { modelChanged } from '../src/features/parameters/store/generationSlice';
|
||||
import { modelChanged } from '../src/features/controlLayers/store/canvasV2Slice';
|
||||
import { useAppDispatch } from '../src/app/store/storeHooks';
|
||||
import { useGlobalModifiersInit } from '@invoke-ai/ui-library';
|
||||
/**
|
||||
@@ -10,7 +10,9 @@ export const ReduxInit = memo((props: PropsWithChildren) => {
|
||||
const dispatch = useAppDispatch();
|
||||
useGlobalModifiersInit();
|
||||
useEffect(() => {
|
||||
dispatch(modelChanged({ key: 'test_model', hash: 'some_hash', name: 'some name', base: 'sd-1', type: 'main' }));
|
||||
dispatch(
|
||||
modelChanged({ model: { key: 'test_model', hash: 'some_hash', name: 'some name', base: 'sd-1', type: 'main' } })
|
||||
);
|
||||
}, []);
|
||||
|
||||
return props.children;
|
||||
|
||||
@@ -9,6 +9,8 @@ const config: KnipConfig = {
|
||||
'src/services/api/schema.ts',
|
||||
'src/features/nodes/types/v1/**',
|
||||
'src/features/nodes/types/v2/**',
|
||||
// TODO(psyche): maybe we can clean up these utils after canvas v2 release
|
||||
'src/features/controlLayers/konva/util.ts',
|
||||
],
|
||||
ignoreBinaries: ['only-allow'],
|
||||
paths: {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"build": "pnpm run lint && vite build",
|
||||
"typegen": "node scripts/typegen.js",
|
||||
"preview": "vite preview",
|
||||
"lint:knip": "knip",
|
||||
"lint:knip": "knip --tags=-knipignore",
|
||||
"lint:dpdm": "dpdm --no-warning --no-tree --transform --exit-code circular:1 src/main.tsx",
|
||||
"lint:eslint": "eslint --max-warnings=0 .",
|
||||
"lint:prettier": "prettier --check .",
|
||||
@@ -52,17 +52,17 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/react-use-size": "^2.1.0",
|
||||
"@dagrejs/dagre": "^1.1.3",
|
||||
"@dagrejs/graphlib": "^2.2.3",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fontsource-variable/inter": "^5.0.20",
|
||||
"@invoke-ai/ui-library": "^0.0.29",
|
||||
"@invoke-ai/ui-library": "^0.0.31",
|
||||
"@nanostores/react": "^0.7.3",
|
||||
"@reduxjs/toolkit": "2.2.3",
|
||||
"@roarr/browser-log-writer": "^1.3.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"chakra-react-select": "^4.9.1",
|
||||
"compare-versions": "^6.1.1",
|
||||
"dateformat": "^5.0.3",
|
||||
@@ -74,6 +74,8 @@
|
||||
"jsondiffpatch": "^0.6.0",
|
||||
"konva": "^9.3.14",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lru-cache": "^11.0.0",
|
||||
"nanoid": "^5.0.7",
|
||||
"nanostores": "^0.11.2",
|
||||
"new-github-issue-url": "^1.0.0",
|
||||
"overlayscrollbars": "^2.10.0",
|
||||
@@ -88,7 +90,6 @@
|
||||
"react-hotkeys-hook": "4.5.0",
|
||||
"react-i18next": "^14.1.3",
|
||||
"react-icons": "^5.2.1",
|
||||
"react-konva": "^18.2.10",
|
||||
"react-redux": "9.1.2",
|
||||
"react-resizable-panels": "^2.0.23",
|
||||
"react-select": "5.8.0",
|
||||
@@ -102,9 +103,9 @@
|
||||
"roarr": "^7.21.1",
|
||||
"serialize-error": "^11.0.3",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"stable-hash": "^0.0.4",
|
||||
"use-debounce": "^10.0.2",
|
||||
"use-device-pixel-ratio": "^1.1.2",
|
||||
"use-image": "^1.1.1",
|
||||
"uuid": "^10.0.0",
|
||||
"zod": "^3.23.8",
|
||||
"zod-validation-error": "^3.3.1"
|
||||
|
||||
102
invokeai/frontend/web/pnpm-lock.yaml
generated
102
invokeai/frontend/web/pnpm-lock.yaml
generated
@@ -5,9 +5,6 @@ settings:
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
'@chakra-ui/react-use-size':
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0(react@18.3.1)
|
||||
'@dagrejs/dagre':
|
||||
specifier: ^1.1.3
|
||||
version: 1.1.3
|
||||
@@ -27,8 +24,8 @@ dependencies:
|
||||
specifier: ^5.0.20
|
||||
version: 5.0.20
|
||||
'@invoke-ai/ui-library':
|
||||
specifier: ^0.0.29
|
||||
version: 0.0.29(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1)
|
||||
specifier: ^0.0.31
|
||||
version: 0.0.31(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1)
|
||||
'@nanostores/react':
|
||||
specifier: ^0.7.3
|
||||
version: 0.7.3(nanostores@0.11.2)(react@18.3.1)
|
||||
@@ -38,6 +35,9 @@ dependencies:
|
||||
'@roarr/browser-log-writer':
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
async-mutex:
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0
|
||||
chakra-react-select:
|
||||
specifier: ^4.9.1
|
||||
version: 4.9.1(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.13.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
|
||||
@@ -71,6 +71,12 @@ dependencies:
|
||||
lodash-es:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
lru-cache:
|
||||
specifier: ^11.0.0
|
||||
version: 11.0.0
|
||||
nanoid:
|
||||
specifier: ^5.0.7
|
||||
version: 5.0.7
|
||||
nanostores:
|
||||
specifier: ^0.11.2
|
||||
version: 0.11.2
|
||||
@@ -113,9 +119,6 @@ dependencies:
|
||||
react-icons:
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1(react@18.3.1)
|
||||
react-konva:
|
||||
specifier: ^18.2.10
|
||||
version: 18.2.10(konva@9.3.14)(react-dom@18.3.1)(react@18.3.1)
|
||||
react-redux:
|
||||
specifier: 9.1.2
|
||||
version: 9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1)
|
||||
@@ -155,15 +158,15 @@ dependencies:
|
||||
socket.io-client:
|
||||
specifier: ^4.7.5
|
||||
version: 4.7.5
|
||||
stable-hash:
|
||||
specifier: ^0.0.4
|
||||
version: 0.0.4
|
||||
use-debounce:
|
||||
specifier: ^10.0.2
|
||||
version: 10.0.2(react@18.3.1)
|
||||
use-device-pixel-ratio:
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2(react@18.3.1)
|
||||
use-image:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1(react-dom@18.3.1)(react@18.3.1)
|
||||
uuid:
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0
|
||||
@@ -3561,8 +3564,8 @@ packages:
|
||||
prettier: 3.3.3
|
||||
dev: true
|
||||
|
||||
/@invoke-ai/ui-library@0.0.29(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-7SYOaiEEKk9iHk0hg2R2yVxiuV3I1x6bDEv0R3Y2tCH/Aq5XDG2tR+d7SQAPqf5+za3S+qfNFjbjl7GvEMwqmA==}
|
||||
/@invoke-ai/ui-library@0.0.31(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-7LtOUN/bcGHc8jCRd2m22DvP2eeogqwM/shdXQpLH5RY2FzWJNXlWdVT4hIPGDu7znnk3xvXlZvo6tiGSjbnCQ==}
|
||||
peerDependencies:
|
||||
'@fontsource-variable/inter': ^5.0.16
|
||||
react: ^18.2.0
|
||||
@@ -5202,12 +5205,6 @@ packages:
|
||||
'@types/react': 18.3.3
|
||||
dev: true
|
||||
|
||||
/@types/react-reconciler@0.28.8:
|
||||
resolution: {integrity: sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==}
|
||||
dependencies:
|
||||
'@types/react': 18.3.3
|
||||
dev: false
|
||||
|
||||
/@types/react-transition-group@4.4.10:
|
||||
resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==}
|
||||
dependencies:
|
||||
@@ -5903,6 +5900,12 @@ packages:
|
||||
tslib: 2.6.3
|
||||
dev: true
|
||||
|
||||
/async-mutex@0.5.0:
|
||||
resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==}
|
||||
dependencies:
|
||||
tslib: 2.6.3
|
||||
dev: false
|
||||
|
||||
/attr-accept@2.2.2:
|
||||
resolution: {integrity: sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -8384,15 +8387,6 @@ packages:
|
||||
set-function-name: 2.0.2
|
||||
dev: true
|
||||
|
||||
/its-fine@1.2.5(react@18.3.1):
|
||||
resolution: {integrity: sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==}
|
||||
peerDependencies:
|
||||
react: '>=18.0'
|
||||
dependencies:
|
||||
'@types/react-reconciler': 0.28.8
|
||||
react: 18.3.1
|
||||
dev: false
|
||||
|
||||
/jackspeak@2.3.6:
|
||||
resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -8710,6 +8704,11 @@ packages:
|
||||
engines: {node: 14 || >=16.14}
|
||||
dev: true
|
||||
|
||||
/lru-cache@11.0.0:
|
||||
resolution: {integrity: sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==}
|
||||
engines: {node: 20 || >=22}
|
||||
dev: false
|
||||
|
||||
/lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
dependencies:
|
||||
@@ -8997,6 +8996,12 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/nanoid@5.0.7:
|
||||
resolution: {integrity: sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==}
|
||||
engines: {node: ^18 || >=20}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/nanostores@0.11.2:
|
||||
resolution: {integrity: sha512-6bucNxMJA5rNV554WQl+MWGng0QVMzlRgpKTHHfIbVLrhQ+yRXBychV9ECGVuuUfCMQPjfIG9bj8oJFZ9hYP/Q==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
@@ -9801,33 +9806,6 @@ packages:
|
||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||
dev: true
|
||||
|
||||
/react-konva@18.2.10(konva@9.3.14)(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==}
|
||||
peerDependencies:
|
||||
konva: ^8.0.1 || ^7.2.5 || ^9.0.0
|
||||
react: '>=18.0.0'
|
||||
react-dom: '>=18.0.0'
|
||||
dependencies:
|
||||
'@types/react-reconciler': 0.28.8
|
||||
its-fine: 1.2.5(react@18.3.1)
|
||||
konva: 9.3.14
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
react-reconciler: 0.29.2(react@18.3.1)
|
||||
scheduler: 0.23.2
|
||||
dev: false
|
||||
|
||||
/react-reconciler@0.29.2(react@18.3.1):
|
||||
resolution: {integrity: sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
peerDependencies:
|
||||
react: ^18.3.1
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
react: 18.3.1
|
||||
scheduler: 0.23.2
|
||||
dev: false
|
||||
|
||||
/react-redux@9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1):
|
||||
resolution: {integrity: sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==}
|
||||
peerDependencies:
|
||||
@@ -10625,6 +10603,10 @@ packages:
|
||||
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
||||
dev: true
|
||||
|
||||
/stable-hash@0.0.4:
|
||||
resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==}
|
||||
dev: false
|
||||
|
||||
/stack-generator@2.0.10:
|
||||
resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==}
|
||||
dependencies:
|
||||
@@ -11330,16 +11312,6 @@ packages:
|
||||
react: 18.3.1
|
||||
dev: false
|
||||
|
||||
/use-image@1.1.1(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-n4YO2k8AJG/BcDtxmBx8Aa+47kxY5m335dJiCQA5tTeVU4XdhrhqR6wT0WISRXwdMEOv5CSjqekDZkEMiiWaYQ==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
dev: false
|
||||
|
||||
/use-isomorphic-layout-effect@1.1.2(@types/react@18.3.3)(react@18.3.1):
|
||||
resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==}
|
||||
peerDependencies:
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
"aboutDesc": "Using Invoke for work? Check out:",
|
||||
"aboutHeading": "Own Your Creative Power",
|
||||
"accept": "Accept",
|
||||
"apply": "Apply",
|
||||
"add": "Add",
|
||||
"advanced": "Advanced",
|
||||
"ai": "ai",
|
||||
@@ -115,6 +116,7 @@
|
||||
"githubLabel": "Github",
|
||||
"goTo": "Go to",
|
||||
"hotkeysLabel": "Hotkeys",
|
||||
"loadingImage": "Loading Image",
|
||||
"imageFailedToLoad": "Unable to Load Image",
|
||||
"img2img": "Image To Image",
|
||||
"inpaint": "inpaint",
|
||||
@@ -325,6 +327,10 @@
|
||||
"canceled": "Canceled",
|
||||
"completedIn": "Completed in",
|
||||
"batch": "Batch",
|
||||
"origin": "Origin",
|
||||
"originCanvas": "Canvas",
|
||||
"originWorkflows": "Workflows",
|
||||
"originOther": "Other",
|
||||
"batchFieldValues": "Batch Field Values",
|
||||
"item": "Item",
|
||||
"session": "Session",
|
||||
@@ -1095,7 +1101,6 @@
|
||||
"confirmOnDelete": "Confirm On Delete",
|
||||
"developer": "Developer",
|
||||
"displayInProgress": "Display Progress Images",
|
||||
"enableImageDebugging": "Enable Image Debugging",
|
||||
"enableInformationalPopovers": "Enable Informational Popovers",
|
||||
"informationalPopoversDisabled": "Informational Popovers Disabled",
|
||||
"informationalPopoversDisabledDesc": "Informational popovers have been disabled. Enable them in Settings.",
|
||||
@@ -1562,7 +1567,7 @@
|
||||
"copyToClipboard": "Copy to Clipboard",
|
||||
"cursorPosition": "Cursor Position",
|
||||
"darkenOutsideSelection": "Darken Outside Selection",
|
||||
"discardAll": "Discard All",
|
||||
"discardAll": "Discard All & Cancel Pending Generations",
|
||||
"discardCurrent": "Discard Current",
|
||||
"downloadAsImage": "Download As Image",
|
||||
"enableMask": "Enable Mask",
|
||||
@@ -1640,16 +1645,32 @@
|
||||
"storeNotInitialized": "Store is not initialized"
|
||||
},
|
||||
"controlLayers": {
|
||||
"generateMode": "Generate",
|
||||
"generateModeDesc": "Create individual images. Generated images are added directly to the gallery.",
|
||||
"composeMode": "Compose",
|
||||
"composeModeDesc": "Compose your work iterative. Generated images are added back to the canvas.",
|
||||
"autoSave": "Auto-save to Gallery",
|
||||
"resetCanvas": "Reset Canvas",
|
||||
"resetAll": "Reset All",
|
||||
"deleteAll": "Delete All",
|
||||
"clearCaches": "Clear Caches",
|
||||
"recalculateRects": "Recalculate Rects",
|
||||
"clipToBbox": "Clip Strokes to Bbox",
|
||||
"addLayer": "Add Layer",
|
||||
"duplicate": "Duplicate",
|
||||
"moveToFront": "Move to Front",
|
||||
"moveToBack": "Move to Back",
|
||||
"moveForward": "Move Forward",
|
||||
"moveBackward": "Move Backward",
|
||||
"brushSize": "Brush Size",
|
||||
"width": "Width",
|
||||
"zoom": "Zoom",
|
||||
"resetView": "Reset View",
|
||||
"controlLayers": "Control Layers",
|
||||
"globalMaskOpacity": "Global Mask Opacity",
|
||||
"autoNegative": "Auto Negative",
|
||||
"enableAutoNegative": "Enable Auto Negative",
|
||||
"disableAutoNegative": "Disable Auto Negative",
|
||||
"deletePrompt": "Delete Prompt",
|
||||
"resetRegion": "Reset Region",
|
||||
"debugLayers": "Debug Layers",
|
||||
@@ -1658,23 +1679,86 @@
|
||||
"addPositivePrompt": "Add $t(common.positivePrompt)",
|
||||
"addNegativePrompt": "Add $t(common.negativePrompt)",
|
||||
"addIPAdapter": "Add $t(common.ipAdapter)",
|
||||
"regionalGuidance": "Regional Guidance",
|
||||
"regionalGuidanceLayer": "$t(controlLayers.regionalGuidance) $t(unifiedCanvas.layer)",
|
||||
"raster": "Raster",
|
||||
"rasterLayer_one": "Raster Layer",
|
||||
"controlLayer_one": "Control Layer",
|
||||
"inpaintMask_one": "Inpaint Mask",
|
||||
"regionalGuidance_one": "Regional Guidance",
|
||||
"ipAdapter_one": "IP Adapter",
|
||||
"rasterLayer_other": "Raster Layers",
|
||||
"controlLayer_other": "Control Layers",
|
||||
"inpaintMask_other": "Inpaint Masks",
|
||||
"regionalGuidance_other": "Regional Guidance",
|
||||
"ipAdapter_other": "IP Adapters",
|
||||
"opacity": "Opacity",
|
||||
"regionalGuidance_withCount_hidden": "Regional Guidance ({{count}} hidden)",
|
||||
"controlAdapters_withCount_hidden": "Control Adapters ({{count}} hidden)",
|
||||
"controlLayers_withCount_hidden": "Control Layers ({{count}} hidden)",
|
||||
"rasterLayers_withCount_hidden": "Raster Layers ({{count}} hidden)",
|
||||
"ipAdapters_withCount_hidden": "IP Adapters ({{count}} hidden)",
|
||||
"inpaintMasks_withCount_hidden": "Inpaint Masks ({{count}} hidden)",
|
||||
"regionalGuidance_withCount_visible": "Regional Guidance ({{count}})",
|
||||
"controlAdapters_withCount_visible": "Control Adapters ({{count}})",
|
||||
"controlLayers_withCount_visible": "Control Layers ({{count}})",
|
||||
"rasterLayers_withCount_visible": "Raster Layers ({{count}})",
|
||||
"ipAdapters_withCount_visible": "IP Adapters ({{count}})",
|
||||
"inpaintMasks_withCount_visible": "Inpaint Masks ({{count}})",
|
||||
"globalControlAdapter": "Global $t(controlnet.controlAdapter_one)",
|
||||
"globalControlAdapterLayer": "Global $t(controlnet.controlAdapter_one) $t(unifiedCanvas.layer)",
|
||||
"globalIPAdapter": "Global $t(common.ipAdapter)",
|
||||
"globalIPAdapterLayer": "Global $t(common.ipAdapter) $t(unifiedCanvas.layer)",
|
||||
"globalInitialImage": "Global Initial Image",
|
||||
"globalInitialImageLayer": "$t(controlLayers.globalInitialImage) $t(unifiedCanvas.layer)",
|
||||
"layer": "Layer",
|
||||
"opacityFilter": "Opacity Filter",
|
||||
"clearProcessor": "Clear Processor",
|
||||
"resetProcessor": "Reset Processor to Defaults",
|
||||
"noLayersAdded": "No Layers Added",
|
||||
"layers_one": "Layer",
|
||||
"layers_other": "Layers"
|
||||
"layers_other": "Layers",
|
||||
"objects_zero": "empty",
|
||||
"objects_one": "{{count}} object",
|
||||
"objects_other": "{{count}} objects",
|
||||
"convertToControlLayer": "Convert to Control Layer",
|
||||
"convertToRasterLayer": "Convert to Raster Layer",
|
||||
"transparency": "Transparency",
|
||||
"enableTransparencyEffect": "Enable Transparency Effect",
|
||||
"disableTransparencyEffect": "Disable Transparency Effect",
|
||||
"hidingType": "Hiding {{type}}",
|
||||
"showingType": "Showing {{type}}",
|
||||
"dynamicGrid": "Dynamic Grid",
|
||||
"logDebugInfo": "Log Debug Info",
|
||||
"fill": {
|
||||
"fillStyle": "Fill Style",
|
||||
"solid": "Solid",
|
||||
"grid": "Grid",
|
||||
"crosshatch": "Crosshatch",
|
||||
"vertical": "Vertical",
|
||||
"horizontal": "Horizontal",
|
||||
"diagonal": "Diagonal"
|
||||
},
|
||||
"tool": {
|
||||
"brush": "Brush",
|
||||
"eraser": "Eraser",
|
||||
"rectangle": "Rectangle",
|
||||
"bbox": "Bbox",
|
||||
"move": "Move",
|
||||
"view": "View",
|
||||
"transform": "Transform",
|
||||
"colorPicker": "Color Picker"
|
||||
},
|
||||
"filter": {
|
||||
"filter": "Filter",
|
||||
"filters": "Filters",
|
||||
"filterType": "Filter Type",
|
||||
"preview": "Preview",
|
||||
"apply": "Apply",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
},
|
||||
"upscaling": {
|
||||
"upscale": "Upscale",
|
||||
"creativity": "Creativity",
|
||||
"exceedsMaxSize": "Upscale settings exceed max size limit",
|
||||
"exceedsMaxSizeDetails": "Max upscale limit is {{maxUpscaleDimension}}x{{maxUpscaleDimension}} pixels. Please try a smaller image or decrease your scale selection.",
|
||||
@@ -1723,6 +1807,7 @@
|
||||
"positivePrompt": "Positive Prompt",
|
||||
"preview": "Preview",
|
||||
"private": "Private",
|
||||
"promptTemplateCleared": "Prompt Template Cleared",
|
||||
"searchByName": "Search by name",
|
||||
"shared": "Shared",
|
||||
"sharedTemplates": "Shared Templates",
|
||||
@@ -1758,5 +1843,30 @@
|
||||
"upscaling": "Upscaling",
|
||||
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)"
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"enableLogging": "Enable Logging",
|
||||
"logLevel": {
|
||||
"logLevel": "Log Level",
|
||||
"trace": "Trace",
|
||||
"debug": "Debug",
|
||||
"info": "Info",
|
||||
"warn": "Warn",
|
||||
"error": "Error",
|
||||
"fatal": "Fatal"
|
||||
},
|
||||
"logNamespaces": {
|
||||
"logNamespaces": "Log Namespaces",
|
||||
"gallery": "Gallery",
|
||||
"models": "Models",
|
||||
"config": "Config",
|
||||
"canvas": "Canvas",
|
||||
"generation": "Generation",
|
||||
"workflows": "Workflows",
|
||||
"system": "System",
|
||||
"events": "Events",
|
||||
"queue": "Queue",
|
||||
"metadata": "Metadata"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -929,7 +929,7 @@
|
||||
"missingInvocationTemplate": "Modello di invocazione mancante",
|
||||
"missingFieldTemplate": "Modello di campo mancante",
|
||||
"singleFieldType": "{{name}} (Singola)",
|
||||
"imageAccessError": "Impossibile trovare l'immagine {{image_name}}, ripristino delle impostazioni predefinite",
|
||||
"imageAccessError": "Impossibile trovare l'immagine {{image_name}}, ripristino ai valori predefiniti",
|
||||
"boardAccessError": "Impossibile trovare la bacheca {{board_id}}, ripristino ai valori predefiniti",
|
||||
"modelAccessError": "Impossibile trovare il modello {{key}}, ripristino ai valori predefiniti"
|
||||
},
|
||||
@@ -1782,7 +1782,13 @@
|
||||
"updatePromptTemplate": "Aggiorna il modello di prompt",
|
||||
"type": "Tipo",
|
||||
"promptTemplatesDesc2": "Utilizza la stringa segnaposto <Pre>{{placeholder}}</Pre> per specificare dove inserire il tuo prompt nel modello.",
|
||||
"importTemplates": "Importa modelli di prompt",
|
||||
"importTemplatesDesc": "Il formato deve essere un CSV con colonne 'name' e 'prompt' o 'positive_prompt' e 'negative_prompt' incluse, oppure un file JSON con chiavi 'name' e 'prompt' o 'positive_prompt' e 'negative_prompt"
|
||||
"importTemplates": "Importa modelli di prompt (CSV/JSON)",
|
||||
"exportDownloaded": "Esportazione completata",
|
||||
"exportFailed": "Impossibile generare e scaricare il file CSV",
|
||||
"exportPromptTemplates": "Esporta i miei modelli di prompt (CSV)",
|
||||
"positivePromptColumn": "'prompt' o 'positive_prompt'",
|
||||
"noTemplates": "Nessun modello",
|
||||
"acceptedColumnsKeys": "Colonne/chiavi accettate:",
|
||||
"templateActions": "Azioni modello"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,8 @@
|
||||
"enabled": "Включено",
|
||||
"disabled": "Отключено",
|
||||
"comparingDesc": "Сравнение двух изображений",
|
||||
"comparing": "Сравнение"
|
||||
"comparing": "Сравнение",
|
||||
"dontShowMeThese": "Не показывай мне это"
|
||||
},
|
||||
"gallery": {
|
||||
"galleryImageSize": "Размер изображений",
|
||||
@@ -153,7 +154,11 @@
|
||||
"showArchivedBoards": "Показать архивированные доски",
|
||||
"searchImages": "Поиск по метаданным",
|
||||
"displayBoardSearch": "Отобразить поиск досок",
|
||||
"displaySearch": "Отобразить поиск"
|
||||
"displaySearch": "Отобразить поиск",
|
||||
"exitBoardSearch": "Выйти из поиска досок",
|
||||
"go": "Перейти",
|
||||
"exitSearch": "Выйти из поиска",
|
||||
"jump": "Пыгнуть"
|
||||
},
|
||||
"hotkeys": {
|
||||
"keyboardShortcuts": "Горячие клавиши",
|
||||
@@ -376,6 +381,10 @@
|
||||
"toggleViewer": {
|
||||
"title": "Переключить просмотр изображений",
|
||||
"desc": "Переключение между средством просмотра изображений и рабочей областью для текущей вкладки."
|
||||
},
|
||||
"postProcess": {
|
||||
"desc": "Обработайте текущее изображение с помощью выбранной модели постобработки",
|
||||
"title": "Обработать изображение"
|
||||
}
|
||||
},
|
||||
"modelManager": {
|
||||
@@ -589,7 +598,10 @@
|
||||
"infillColorValue": "Цвет заливки",
|
||||
"globalSettings": "Глобальные настройки",
|
||||
"globalNegativePromptPlaceholder": "Глобальный негативный запрос",
|
||||
"globalPositivePromptPlaceholder": "Глобальный запрос"
|
||||
"globalPositivePromptPlaceholder": "Глобальный запрос",
|
||||
"postProcessing": "Постобработка (Shift + U)",
|
||||
"processImage": "Обработка изображения",
|
||||
"sendToUpscale": "Отправить на увеличение"
|
||||
},
|
||||
"settings": {
|
||||
"models": "Модели",
|
||||
@@ -623,7 +635,9 @@
|
||||
"intermediatesCleared_many": "Очищено {{count}} промежуточных",
|
||||
"clearIntermediatesDesc1": "Очистка промежуточных элементов приведет к сбросу состояния Canvas и ControlNet.",
|
||||
"intermediatesClearedFailed": "Проблема очистки промежуточных",
|
||||
"reloadingIn": "Перезагрузка через"
|
||||
"reloadingIn": "Перезагрузка через",
|
||||
"informationalPopoversDisabled": "Информационные всплывающие окна отключены",
|
||||
"informationalPopoversDisabledDesc": "Информационные всплывающие окна были отключены. Включите их в Настройках."
|
||||
},
|
||||
"toast": {
|
||||
"uploadFailed": "Загрузка не удалась",
|
||||
@@ -694,7 +708,9 @@
|
||||
"sessionRef": "Сессия: {{sessionId}}",
|
||||
"outOfMemoryError": "Ошибка нехватки памяти",
|
||||
"outOfMemoryErrorDesc": "Ваши текущие настройки генерации превышают возможности системы. Пожалуйста, измените настройки и повторите попытку.",
|
||||
"somethingWentWrong": "Что-то пошло не так"
|
||||
"somethingWentWrong": "Что-то пошло не так",
|
||||
"importFailed": "Импорт неудачен",
|
||||
"importSuccessful": "Импорт успешен"
|
||||
},
|
||||
"tooltip": {
|
||||
"feature": {
|
||||
@@ -1017,7 +1033,8 @@
|
||||
"composition": "Только композиция",
|
||||
"hed": "HED",
|
||||
"beginEndStepPercentShort": "Начало/конец %",
|
||||
"setControlImageDimensionsForce": "Скопируйте размер в Ш/В (игнорируйте модель)"
|
||||
"setControlImageDimensionsForce": "Скопируйте размер в Ш/В (игнорируйте модель)",
|
||||
"depthAnythingSmallV2": "Small V2"
|
||||
},
|
||||
"boards": {
|
||||
"autoAddBoard": "Авто добавление Доски",
|
||||
@@ -1042,7 +1059,7 @@
|
||||
"downloadBoard": "Скачать доску",
|
||||
"deleteBoard": "Удалить доску",
|
||||
"deleteBoardAndImages": "Удалить доску и изображения",
|
||||
"deletedBoardsCannotbeRestored": "Удаленные доски не подлежат восстановлению",
|
||||
"deletedBoardsCannotbeRestored": "Удаленные доски не могут быть восстановлены. Выбор «Удалить только доску» переведет изображения в состояние без категории.",
|
||||
"assetsWithCount_one": "{{count}} ассет",
|
||||
"assetsWithCount_few": "{{count}} ассета",
|
||||
"assetsWithCount_many": "{{count}} ассетов",
|
||||
@@ -1057,7 +1074,11 @@
|
||||
"boards": "Доски",
|
||||
"addPrivateBoard": "Добавить личную доску",
|
||||
"private": "Личные доски",
|
||||
"shared": "Общие доски"
|
||||
"shared": "Общие доски",
|
||||
"hideBoards": "Скрыть доски",
|
||||
"viewBoards": "Просмотреть доски",
|
||||
"noBoards": "Нет досок {{boardType}}",
|
||||
"deletedPrivateBoardsCannotbeRestored": "Удаленные доски не могут быть восстановлены. Выбор «Удалить только доску» переведет изображения в приватное состояние без категории для создателя изображения."
|
||||
},
|
||||
"dynamicPrompts": {
|
||||
"seedBehaviour": {
|
||||
@@ -1417,6 +1438,30 @@
|
||||
"paragraphs": [
|
||||
"Метод, с помощью которого применяется текущий IP-адаптер."
|
||||
]
|
||||
},
|
||||
"structure": {
|
||||
"paragraphs": [
|
||||
"Структура контролирует, насколько точно выходное изображение будет соответствовать макету оригинала. Низкая структура допускает значительные изменения, в то время как высокая структура строго сохраняет исходную композицию и макет."
|
||||
],
|
||||
"heading": "Структура"
|
||||
},
|
||||
"scale": {
|
||||
"paragraphs": [
|
||||
"Масштаб управляет размером выходного изображения и основывается на кратном разрешении входного изображения. Например, при увеличении в 2 раза изображения 1024x1024 на выходе получится 2048 x 2048."
|
||||
],
|
||||
"heading": "Масштаб"
|
||||
},
|
||||
"creativity": {
|
||||
"paragraphs": [
|
||||
"Креативность контролирует степень свободы, предоставляемой модели при добавлении деталей. При низкой креативности модель остается близкой к оригинальному изображению, в то время как высокая креативность позволяет вносить больше изменений. При использовании подсказки высокая креативность увеличивает влияние подсказки."
|
||||
],
|
||||
"heading": "Креативность"
|
||||
},
|
||||
"upscaleModel": {
|
||||
"heading": "Модель увеличения",
|
||||
"paragraphs": [
|
||||
"Модель увеличения масштаба масштабирует изображение до выходного размера перед добавлением деталей. Можно использовать любую поддерживаемую модель масштабирования, но некоторые из них специализированы для различных видов изображений, например фотографий или линейных рисунков."
|
||||
]
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
@@ -1693,7 +1738,78 @@
|
||||
"canvasTab": "$t(ui.tabs.canvas) $t(common.tab)",
|
||||
"queueTab": "$t(ui.tabs.queue) $t(common.tab)",
|
||||
"modelsTab": "$t(ui.tabs.models) $t(common.tab)",
|
||||
"queue": "Очередь"
|
||||
"queue": "Очередь",
|
||||
"upscaling": "Увеличение",
|
||||
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)"
|
||||
}
|
||||
},
|
||||
"upscaling": {
|
||||
"exceedsMaxSize": "Параметры масштабирования превышают максимальный размер",
|
||||
"exceedsMaxSizeDetails": "Максимальный предел масштабирования составляет {{maxUpscaleDimension}}x{{maxUpscaleDimension}} пикселей. Пожалуйста, попробуйте использовать меньшее изображение или уменьшите масштаб.",
|
||||
"structure": "Структура",
|
||||
"missingTileControlNetModel": "Не установлены подходящие модели ControlNet",
|
||||
"missingUpscaleInitialImage": "Отсутствует увеличиваемое изображение",
|
||||
"missingUpscaleModel": "Отсутствует увеличивающая модель",
|
||||
"creativity": "Креативность",
|
||||
"upscaleModel": "Модель увеличения",
|
||||
"scale": "Масштаб",
|
||||
"mainModelDesc": "Основная модель (архитектура SD1.5 или SDXL)",
|
||||
"upscaleModelDesc": "Модель увеличения (img2img)",
|
||||
"postProcessingModel": "Модель постобработки",
|
||||
"tileControlNetModelDesc": "Модель ControlNet для выбранной архитектуры основной модели",
|
||||
"missingModelsWarning": "Зайдите в <LinkComponent>Менеджер моделей</LinkComponent> чтоб установить необходимые модели:",
|
||||
"postProcessingMissingModelWarning": "Посетите <LinkComponent>Менеджер моделей</LinkComponent>, чтобы установить модель постобработки (img2img)."
|
||||
},
|
||||
"stylePresets": {
|
||||
"noMatchingTemplates": "Нет подходящих шаблонов",
|
||||
"promptTemplatesDesc1": "Шаблоны подсказок добавляют текст к подсказкам, которые вы пишете в окне подсказок.",
|
||||
"sharedTemplates": "Общие шаблоны",
|
||||
"templateDeleted": "Шаблон запроса удален",
|
||||
"toggleViewMode": "Переключить режим просмотра",
|
||||
"type": "Тип",
|
||||
"unableToDeleteTemplate": "Не получилось удалить шаблон запроса",
|
||||
"viewModeTooltip": "Вот как будет выглядеть ваш запрос с выбранным шаблоном. Чтобы его отредактировать, щелкните в любом месте текстового поля.",
|
||||
"viewList": "Просмотреть список шаблонов",
|
||||
"active": "Активно",
|
||||
"choosePromptTemplate": "Выберите шаблон запроса",
|
||||
"defaultTemplates": "Стандартные шаблоны",
|
||||
"deleteImage": "Удалить изображение",
|
||||
"deleteTemplate": "Удалить шаблон",
|
||||
"deleteTemplate2": "Вы уверены, что хотите удалить этот шаблон? Это нельзя отменить.",
|
||||
"editTemplate": "Редактировать шаблон",
|
||||
"exportPromptTemplates": "Экспорт моих шаблонов запроса (CSV)",
|
||||
"exportDownloaded": "Экспорт скачан",
|
||||
"exportFailed": "Невозможно сгенерировать и загрузить CSV",
|
||||
"flatten": "Объединить выбранный шаблон с текущим запросом",
|
||||
"acceptedColumnsKeys": "Принимаемые столбцы/ключи:",
|
||||
"positivePromptColumn": "'prompt' или 'positive_prompt'",
|
||||
"insertPlaceholder": "Вставить заполнитель",
|
||||
"name": "Имя",
|
||||
"negativePrompt": "Негативный запрос",
|
||||
"promptTemplatesDesc3": "Если вы не используете заполнитель, шаблон будет добавлен в конец запроса.",
|
||||
"positivePrompt": "Позитивный запрос",
|
||||
"preview": "Предпросмотр",
|
||||
"private": "Приватный",
|
||||
"templateActions": "Действия с шаблоном",
|
||||
"updatePromptTemplate": "Обновить шаблон запроса",
|
||||
"uploadImage": "Загрузить изображение",
|
||||
"useForTemplate": "Использовать для шаблона запроса",
|
||||
"clearTemplateSelection": "Очистить выбор шаблона",
|
||||
"copyTemplate": "Копировать шаблон",
|
||||
"createPromptTemplate": "Создать шаблон запроса",
|
||||
"importTemplates": "Импортировать шаблоны запроса (CSV/JSON)",
|
||||
"nameColumn": "'name'",
|
||||
"negativePromptColumn": "'negative_prompt'",
|
||||
"myTemplates": "Мои шаблоны",
|
||||
"noTemplates": "Нет шаблонов",
|
||||
"promptTemplatesDesc2": "Используйте строку-заполнитель <Pre>{{placeholder}}</Pre>, чтобы указать место, куда должен быть включен ваш запрос в шаблоне.",
|
||||
"searchByName": "Поиск по имени",
|
||||
"shared": "Общий"
|
||||
},
|
||||
"upsell": {
|
||||
"inviteTeammates": "Пригласите членов команды",
|
||||
"professional": "Профессионал",
|
||||
"professionalUpsell": "Доступно в профессиональной версии Invoke. Нажмите здесь или посетите invoke.com/pricing для получения более подробной информации.",
|
||||
"shareAccess": "Поделиться доступом"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ async function generateTypes(schema) {
|
||||
process.stdout.write(`\nOK!\r\n`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
function main() {
|
||||
const encoding = 'utf-8';
|
||||
|
||||
if (process.stdin.isTTY) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/ap
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import type { PartialAppConfig } from 'app/types/invokeai';
|
||||
import ImageUploadOverlay from 'common/components/ImageUploadOverlay';
|
||||
import { useScopeFocusWatcher } from 'common/hooks/interactionScopes';
|
||||
import { useClearStorage } from 'common/hooks/useClearStorage';
|
||||
import { useFullscreenDropzone } from 'common/hooks/useFullscreenDropzone';
|
||||
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
|
||||
@@ -13,12 +14,13 @@ import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardMo
|
||||
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
|
||||
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
|
||||
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
|
||||
import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog';
|
||||
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
|
||||
import { configChanged } from 'features/system/store/configSlice';
|
||||
import { languageSelector } from 'features/system/store/systemSelectors';
|
||||
import InvokeTabs from 'features/ui/components/InvokeTabs';
|
||||
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
||||
import { AppContent } from 'features/ui/components/AppContent';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import { useGetAndLoadLibraryWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import i18n from 'i18n';
|
||||
@@ -39,7 +41,7 @@ interface Props {
|
||||
action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters';
|
||||
};
|
||||
selectedWorkflowId?: string;
|
||||
destination?: InvokeTabName | undefined;
|
||||
destination?: TabName | undefined;
|
||||
}
|
||||
|
||||
const App = ({ config = DEFAULT_CONFIG, selectedImage, selectedWorkflowId, destination }: Props) => {
|
||||
@@ -93,6 +95,7 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage, selectedWorkflowId, desti
|
||||
|
||||
useStarterModelsToast();
|
||||
useSyncQueueStatus();
|
||||
useScopeFocusWatcher();
|
||||
|
||||
return (
|
||||
<ErrorBoundary onReset={handleReset} FallbackComponent={AppErrorBoundaryFallback}>
|
||||
@@ -105,7 +108,7 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage, selectedWorkflowId, desti
|
||||
{...dropzone.getRootProps()}
|
||||
>
|
||||
<input {...dropzone.getInputProps()} />
|
||||
<InvokeTabs />
|
||||
<AppContent />
|
||||
<AnimatePresence>
|
||||
{dropzone.isDragActive && isHandlingUpload && (
|
||||
<ImageUploadOverlay dropzone={dropzone} setIsHandlingUpload={setIsHandlingUpload} />
|
||||
@@ -116,6 +119,7 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage, selectedWorkflowId, desti
|
||||
<ChangeBoardModal />
|
||||
<DynamicPromptsModal />
|
||||
<StylePresetModal />
|
||||
<ClearQueueConfirmationsAlertDialog />
|
||||
<PreselectedImage selectedImage={selectedImage} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ import type { PartialAppConfig } from 'app/types/invokeai';
|
||||
import Loading from 'common/components/Loading/Loading';
|
||||
import AppDndContext from 'features/dnd/components/AppDndContext';
|
||||
import type { WorkflowCategory } from 'features/nodes/types/workflow';
|
||||
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import type { PropsWithChildren, ReactNode } from 'react';
|
||||
import React, { lazy, memo, useEffect, useMemo } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
@@ -45,7 +45,7 @@ interface Props extends PropsWithChildren {
|
||||
action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters';
|
||||
};
|
||||
selectedWorkflowId?: string;
|
||||
destination?: InvokeTabName;
|
||||
destination?: TabName;
|
||||
customStarUi?: CustomStarUi;
|
||||
socketOptions?: Partial<ManagerOptions & SocketOptions>;
|
||||
isDebugging?: boolean;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react';
|
||||
import { $authToken } from 'app/store/nanostores/authToken';
|
||||
import { $baseUrl } from 'app/store/nanostores/baseUrl';
|
||||
import { $isDebugging } from 'app/store/nanostores/isDebugging';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import type { MapStore } from 'nanostores';
|
||||
import { atom, map } from 'nanostores';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
@@ -18,14 +18,19 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export type AppSocket = Socket<ServerToClientEvents, ClientToServerEvents>;
|
||||
|
||||
export const $socket = atom<AppSocket | null>(null);
|
||||
export const $socketOptions = map<Partial<ManagerOptions & SocketOptions>>({});
|
||||
|
||||
const $isSocketInitialized = atom<boolean>(false);
|
||||
export const $isConnected = atom<boolean>(false);
|
||||
|
||||
/**
|
||||
* Initializes the socket.io connection and sets up event listeners.
|
||||
*/
|
||||
export const useSocketIO = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { dispatch, getState } = useAppStore();
|
||||
const baseUrl = useStore($baseUrl);
|
||||
const authToken = useStore($authToken);
|
||||
const addlSocketOptions = useStore($socketOptions);
|
||||
@@ -61,8 +66,9 @@ export const useSocketIO = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io(socketUrl, socketOptions);
|
||||
setEventListeners({ dispatch, socket });
|
||||
const socket: AppSocket = io(socketUrl, socketOptions);
|
||||
$socket.set(socket);
|
||||
setEventListeners({ socket, dispatch, getState, setIsConnected: $isConnected.set });
|
||||
socket.connect();
|
||||
|
||||
if ($isDebugging.get() || import.meta.env.MODE === 'development') {
|
||||
@@ -84,5 +90,5 @@ export const useSocketIO = () => {
|
||||
socket.disconnect();
|
||||
$isSocketInitialized.set(false);
|
||||
};
|
||||
}, [dispatch, socketOptions, socketUrl]);
|
||||
}, [dispatch, getState, socketOptions, socketUrl]);
|
||||
};
|
||||
|
||||
@@ -15,21 +15,21 @@ export const BASE_CONTEXT = {};
|
||||
|
||||
export const $logger = atom<Logger>(Roarr.child(BASE_CONTEXT));
|
||||
|
||||
export type LoggerNamespace =
|
||||
| 'images'
|
||||
| 'models'
|
||||
| 'config'
|
||||
| 'canvas'
|
||||
| 'generation'
|
||||
| 'nodes'
|
||||
| 'system'
|
||||
| 'socketio'
|
||||
| 'session'
|
||||
| 'queue'
|
||||
| 'dnd'
|
||||
| 'controlLayers';
|
||||
export const zLogNamespace = z.enum([
|
||||
'canvas',
|
||||
'config',
|
||||
'events',
|
||||
'gallery',
|
||||
'generation',
|
||||
'metadata',
|
||||
'models',
|
||||
'system',
|
||||
'queue',
|
||||
'workflows',
|
||||
]);
|
||||
export type LogNamespace = z.infer<typeof zLogNamespace>;
|
||||
|
||||
export const logger = (namespace: LoggerNamespace) => $logger.get().child({ namespace });
|
||||
export const logger = (namespace: LogNamespace) => $logger.get().child({ namespace });
|
||||
|
||||
export const zLogLevel = z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']);
|
||||
export type LogLevel = z.infer<typeof zLogLevel>;
|
||||
|
||||
@@ -3,27 +3,34 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { ROARR, Roarr } from 'roarr';
|
||||
|
||||
import type { LoggerNamespace } from './logger';
|
||||
import type { LogNamespace } from './logger';
|
||||
import { $logger, BASE_CONTEXT, LOG_LEVEL_MAP, logger } from './logger';
|
||||
|
||||
export const useLogger = (namespace: LoggerNamespace) => {
|
||||
const consoleLogLevel = useAppSelector((s) => s.system.consoleLogLevel);
|
||||
const shouldLogToConsole = useAppSelector((s) => s.system.shouldLogToConsole);
|
||||
export const useLogger = (namespace: LogNamespace) => {
|
||||
const logLevel = useAppSelector((s) => s.system.logLevel);
|
||||
const logNamespaces = useAppSelector((s) => s.system.logNamespaces);
|
||||
const logIsEnabled = useAppSelector((s) => s.system.logIsEnabled);
|
||||
|
||||
// The provided Roarr browser log writer uses localStorage to config logging to console
|
||||
useEffect(() => {
|
||||
if (shouldLogToConsole) {
|
||||
if (logIsEnabled) {
|
||||
// Enable console log output
|
||||
localStorage.setItem('ROARR_LOG', 'true');
|
||||
|
||||
// Use a filter to show only logs of the given level
|
||||
localStorage.setItem('ROARR_FILTER', `context.logLevel:>=${LOG_LEVEL_MAP[consoleLogLevel]}`);
|
||||
let filter = `context.logLevel:>=${LOG_LEVEL_MAP[logLevel]}`;
|
||||
if (logNamespaces.length > 0) {
|
||||
filter += ` AND (${logNamespaces.map((ns) => `context.namespace:${ns}`).join(' OR ')})`;
|
||||
} else {
|
||||
filter += ' AND context.namespace:undefined';
|
||||
}
|
||||
localStorage.setItem('ROARR_FILTER', filter);
|
||||
} else {
|
||||
// Disable console log output
|
||||
localStorage.setItem('ROARR_LOG', 'false');
|
||||
}
|
||||
ROARR.write = createLogWriter();
|
||||
}, [consoleLogLevel, shouldLogToConsole]);
|
||||
}, [logLevel, logIsEnabled, logNamespaces]);
|
||||
|
||||
// Update the module-scoped logger context as needed
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
|
||||
export const enqueueRequested = createAction<{
|
||||
tabName: InvokeTabName;
|
||||
tabName: TabName;
|
||||
prepend: boolean;
|
||||
}>('app/enqueueRequested');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createDraftSafeSelectorCreator, createSelectorCreator, lruMemoize } from '@reduxjs/toolkit';
|
||||
import type { GetSelectorsOptions } from '@reduxjs/toolkit/dist/entities/state_selectors';
|
||||
import type { RootState } from 'app/store/store';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
/**
|
||||
@@ -19,3 +20,5 @@ export const getSelectorsOptions: GetSelectorsOptions = {
|
||||
argsMemoize: lruMemoize,
|
||||
}),
|
||||
};
|
||||
|
||||
export const createMemoizedAppSelector = createMemoizedSelector.withTypes<RootState>();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { PersistError, RehydrateError } from 'redux-remember';
|
||||
import { serializeError } from 'serialize-error';
|
||||
|
||||
@@ -41,6 +40,6 @@ export const errorHandler = (err: PersistError | RehydrateError) => {
|
||||
} else if (err instanceof RehydrateError) {
|
||||
log.error({ error: serializeError(err) }, 'Problem rehydrating state');
|
||||
} else {
|
||||
log.error({ error: parseify(err) }, 'Problem in persistence layer');
|
||||
log.error({ error: serializeError(err) }, 'Problem in persistence layer');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { UnknownAction } from '@reduxjs/toolkit';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { isAnyGraphBuilt } from 'features/nodes/store/actions';
|
||||
import { appInfoApi } from 'services/api/endpoints/appInfo';
|
||||
import type { Graph } from 'services/api/types';
|
||||
import { socketGeneratorProgress } from 'services/events/actions';
|
||||
|
||||
export const actionSanitizer = <A extends UnknownAction>(action: A): A => {
|
||||
if (isAnyGraphBuilt(action)) {
|
||||
@@ -24,13 +22,5 @@ export const actionSanitizer = <A extends UnknownAction>(action: A): A => {
|
||||
};
|
||||
}
|
||||
|
||||
if (socketGeneratorProgress.match(action)) {
|
||||
const sanitized = deepClone(action);
|
||||
if (sanitized.payload.data.progress_image) {
|
||||
sanitized.payload.data.progress_image.dataURL = '<Progress image omitted>';
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
return action;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { TypedStartListening } from '@reduxjs/toolkit';
|
||||
import { createListenerMiddleware } from '@reduxjs/toolkit';
|
||||
import { addAdHocPostProcessingRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
|
||||
import { addCommitStagingAreaImageListener } from 'app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener';
|
||||
import { addStagingListeners } from 'app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener';
|
||||
import { addAnyEnqueuedListener } from 'app/store/middleware/listenerMiddleware/listeners/anyEnqueued';
|
||||
import { addAppConfigReceivedListener } from 'app/store/middleware/listenerMiddleware/listeners/appConfigReceived';
|
||||
import { addAppStartedListener } from 'app/store/middleware/listenerMiddleware/listeners/appStarted';
|
||||
@@ -9,17 +9,6 @@ import { addBatchEnqueuedListener } from 'app/store/middleware/listenerMiddlewar
|
||||
import { addDeleteBoardAndImagesFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted';
|
||||
import { addBoardIdSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/boardIdSelected';
|
||||
import { addBulkDownloadListeners } from 'app/store/middleware/listenerMiddleware/listeners/bulkDownload';
|
||||
import { addCanvasCopiedToClipboardListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard';
|
||||
import { addCanvasDownloadedAsImageListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage';
|
||||
import { addCanvasImageToControlNetListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasImageToControlNet';
|
||||
import { addCanvasMaskSavedToGalleryListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMaskSavedToGallery';
|
||||
import { addCanvasMaskToControlNetListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMaskToControlNet';
|
||||
import { addCanvasMergedListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasMerged';
|
||||
import { addCanvasSavedToGalleryListener } from 'app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery';
|
||||
import { addControlAdapterPreprocessor } from 'app/store/middleware/listenerMiddleware/listeners/controlAdapterPreprocessor';
|
||||
import { addControlNetAutoProcessListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetAutoProcess';
|
||||
import { addControlNetImageProcessedListener } from 'app/store/middleware/listenerMiddleware/listeners/controlNetImageProcessed';
|
||||
import { addEnqueueRequestedCanvasListener } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedCanvas';
|
||||
import { addEnqueueRequestedLinear } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear';
|
||||
import { addEnqueueRequestedNodes } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes';
|
||||
import { addGalleryImageClickedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked';
|
||||
@@ -37,16 +26,7 @@ import { addModelSelectedListener } from 'app/store/middleware/listenerMiddlewar
|
||||
import { addModelsLoadedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelsLoaded';
|
||||
import { addDynamicPromptsListener } from 'app/store/middleware/listenerMiddleware/listeners/promptChanged';
|
||||
import { addSetDefaultSettingsListener } from 'app/store/middleware/listenerMiddleware/listeners/setDefaultSettings';
|
||||
import { addSocketConnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected';
|
||||
import { addSocketDisconnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketDisconnected';
|
||||
import { addGeneratorProgressEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketGeneratorProgress';
|
||||
import { addInvocationCompleteEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationComplete';
|
||||
import { addInvocationErrorEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationError';
|
||||
import { addInvocationStartedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketInvocationStarted';
|
||||
import { addModelInstallEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketModelInstall';
|
||||
import { addModelLoadEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketModelLoad';
|
||||
import { addSocketQueueItemStatusChangedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketio/socketQueueItemStatusChanged';
|
||||
import { addStagingAreaImageSavedListener } from 'app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved';
|
||||
import { addSocketConnectedEventListener } from 'app/store/middleware/listenerMiddleware/listeners/socketConnected';
|
||||
import { addUpdateAllNodesRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested';
|
||||
import { addWorkflowLoadRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested';
|
||||
import type { AppDispatch, RootState } from 'app/store/store';
|
||||
@@ -83,7 +63,6 @@ addGalleryImageClickedListener(startAppListening);
|
||||
addGalleryOffsetChangedListener(startAppListening);
|
||||
|
||||
// User Invoked
|
||||
addEnqueueRequestedCanvasListener(startAppListening);
|
||||
addEnqueueRequestedNodes(startAppListening);
|
||||
addEnqueueRequestedLinear(startAppListening);
|
||||
addEnqueueRequestedUpscale(startAppListening);
|
||||
@@ -91,31 +70,22 @@ addAnyEnqueuedListener(startAppListening);
|
||||
addBatchEnqueuedListener(startAppListening);
|
||||
|
||||
// Canvas actions
|
||||
addCanvasSavedToGalleryListener(startAppListening);
|
||||
addCanvasMaskSavedToGalleryListener(startAppListening);
|
||||
addCanvasImageToControlNetListener(startAppListening);
|
||||
addCanvasMaskToControlNetListener(startAppListening);
|
||||
addCanvasDownloadedAsImageListener(startAppListening);
|
||||
addCanvasCopiedToClipboardListener(startAppListening);
|
||||
addCanvasMergedListener(startAppListening);
|
||||
addStagingAreaImageSavedListener(startAppListening);
|
||||
addCommitStagingAreaImageListener(startAppListening);
|
||||
// addCanvasSavedToGalleryListener(startAppListening);
|
||||
// addCanvasMaskSavedToGalleryListener(startAppListening);
|
||||
// addCanvasImageToControlNetListener(startAppListening);
|
||||
// addCanvasMaskToControlNetListener(startAppListening);
|
||||
// addCanvasDownloadedAsImageListener(startAppListening);
|
||||
// addCanvasCopiedToClipboardListener(startAppListening);
|
||||
// addCanvasMergedListener(startAppListening);
|
||||
// addStagingAreaImageSavedListener(startAppListening);
|
||||
// addCommitStagingAreaImageListener(startAppListening);
|
||||
addStagingListeners(startAppListening);
|
||||
|
||||
// Socket.IO
|
||||
addGeneratorProgressEventListener(startAppListening);
|
||||
addInvocationCompleteEventListener(startAppListening);
|
||||
addInvocationErrorEventListener(startAppListening);
|
||||
addInvocationStartedEventListener(startAppListening);
|
||||
addSocketConnectedEventListener(startAppListening);
|
||||
addSocketDisconnectedEventListener(startAppListening);
|
||||
addModelLoadEventListener(startAppListening);
|
||||
addModelInstallEventListener(startAppListening);
|
||||
addSocketQueueItemStatusChangedEventListener(startAppListening);
|
||||
addBulkDownloadListeners(startAppListening);
|
||||
|
||||
// ControlNet
|
||||
addControlNetImageProcessedListener(startAppListening);
|
||||
addControlNetAutoProcessListener(startAppListening);
|
||||
// Gallery bulk download
|
||||
addBulkDownloadListeners(startAppListening);
|
||||
|
||||
// Boards
|
||||
addImageAddedToBoardFulfilledListener(startAppListening);
|
||||
@@ -148,4 +118,4 @@ addAdHocPostProcessingRequestedListener(startAppListening);
|
||||
addDynamicPromptsListener(startAppListening);
|
||||
|
||||
addSetDefaultSettingsListener(startAppListening);
|
||||
addControlAdapterPreprocessor(startAppListening);
|
||||
// addControlAdapterPreprocessor(startAppListening);
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import type { SerializableObject } from 'common/types';
|
||||
import { buildAdHocPostProcessingGraph } from 'features/nodes/util/graph/buildAdHocPostProcessingGraph';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
import { queueApi } from 'services/api/endpoints/queue';
|
||||
import type { BatchConfig, ImageDTO } from 'services/api/types';
|
||||
|
||||
const log = logger('queue');
|
||||
|
||||
export const adHocPostProcessingRequested = createAction<{ imageDTO: ImageDTO }>(`upscaling/postProcessingRequested`);
|
||||
|
||||
export const addAdHocPostProcessingRequestedListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: adHocPostProcessingRequested,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
const log = logger('session');
|
||||
|
||||
const { imageDTO } = action.payload;
|
||||
const state = getState();
|
||||
|
||||
@@ -39,9 +39,9 @@ export const addAdHocPostProcessingRequestedListener = (startAppListening: AppSt
|
||||
|
||||
const enqueueResult = await req.unwrap();
|
||||
req.reset();
|
||||
log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued'));
|
||||
log.debug({ enqueueResult } as SerializableObject, t('queue.graphQueued'));
|
||||
} catch (error) {
|
||||
log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue'));
|
||||
log.error({ enqueueBatchArg } as SerializableObject, t('queue.graphFailedToQueue'));
|
||||
|
||||
if (error instanceof Object && 'status' in error && error.status === 403) {
|
||||
return;
|
||||
|
||||
@@ -23,7 +23,7 @@ export const addArchivedOrDeletedBoardListener = (startAppListening: AppStartLis
|
||||
*/
|
||||
startAppListening({
|
||||
matcher: matchAnyBoardDeleted,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const state = getState();
|
||||
const deletedBoardId = action.meta.arg.originalArgs;
|
||||
const { autoAddBoardId, selectedBoardId } = state.gallery;
|
||||
@@ -44,7 +44,7 @@ export const addArchivedOrDeletedBoardListener = (startAppListening: AppStartLis
|
||||
// If we archived a board, it may end up hidden. If it's selected or the auto-add board, we should reset those.
|
||||
startAppListening({
|
||||
matcher: boardsApi.endpoints.updateBoard.matchFulfilled,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const state = getState();
|
||||
const { shouldShowArchivedBoards } = state.gallery;
|
||||
|
||||
@@ -61,7 +61,7 @@ export const addArchivedOrDeletedBoardListener = (startAppListening: AppStartLis
|
||||
// When we hide archived boards, if the selected or the auto-add board is archived, we should reset those.
|
||||
startAppListening({
|
||||
actionCreator: shouldShowArchivedBoardsChanged,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const shouldShowArchivedBoards = action.payload;
|
||||
|
||||
// We only need to take action if we have just hidden archived boards.
|
||||
@@ -100,7 +100,7 @@ export const addArchivedOrDeletedBoardListener = (startAppListening: AppStartLis
|
||||
*/
|
||||
startAppListening({
|
||||
matcher: boardsApi.endpoints.listAllBoards.matchFulfilled,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const boards = action.payload;
|
||||
const state = getState();
|
||||
const { selectedBoardId, autoAddBoardId } = state.gallery;
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
import { isAnyOf } from '@reduxjs/toolkit';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import {
|
||||
canvasBatchIdsReset,
|
||||
commitStagingAreaImage,
|
||||
discardStagedImages,
|
||||
resetCanvas,
|
||||
setInitialCanvasImage,
|
||||
} from 'features/canvas/store/canvasSlice';
|
||||
rasterLayerAdded,
|
||||
sessionStagingAreaImageAccepted,
|
||||
sessionStagingAreaReset,
|
||||
} from 'features/controlLayers/store/canvasV2Slice';
|
||||
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
import { queueApi } from 'services/api/endpoints/queue';
|
||||
import { $lastCanvasProgressEvent } from 'services/events/setEventListeners';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
const matcher = isAnyOf(commitStagingAreaImage, discardStagedImages, resetCanvas, setInitialCanvasImage);
|
||||
const log = logger('canvas');
|
||||
|
||||
export const addCommitStagingAreaImageListener = (startAppListening: AppStartListening) => {
|
||||
export const addStagingListeners = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
matcher,
|
||||
effect: async (_, { dispatch, getState }) => {
|
||||
const log = logger('canvas');
|
||||
const state = getState();
|
||||
const { batchIds } = state.canvas;
|
||||
|
||||
actionCreator: sessionStagingAreaReset,
|
||||
effect: async (_, { dispatch }) => {
|
||||
try {
|
||||
const req = dispatch(
|
||||
queueApi.endpoints.cancelByBatchIds.initiate({ batch_ids: batchIds }, { fixedCacheKey: 'cancelByBatchIds' })
|
||||
queueApi.endpoints.cancelByBatchOrigin.initiate(
|
||||
{ origin: 'canvas' },
|
||||
{ fixedCacheKey: 'cancelByBatchOrigin' }
|
||||
)
|
||||
);
|
||||
const { canceled } = await req.unwrap();
|
||||
req.reset();
|
||||
|
||||
$lastCanvasProgressEvent.set(null);
|
||||
|
||||
if (canceled > 0) {
|
||||
log.debug(`Canceled ${canceled} canvas batches`);
|
||||
toast({
|
||||
@@ -36,7 +39,6 @@ export const addCommitStagingAreaImageListener = (startAppListening: AppStartLis
|
||||
status: 'success',
|
||||
});
|
||||
}
|
||||
dispatch(canvasBatchIdsReset());
|
||||
} catch {
|
||||
log.error('Failed to cancel canvas batches');
|
||||
toast({
|
||||
@@ -47,4 +49,26 @@ export const addCommitStagingAreaImageListener = (startAppListening: AppStartLis
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
actionCreator: sessionStagingAreaImageAccepted,
|
||||
effect: (action, api) => {
|
||||
const { index } = action.payload;
|
||||
const state = api.getState();
|
||||
const stagingAreaImage = state.canvasV2.session.stagedImages[index];
|
||||
|
||||
assert(stagingAreaImage, 'No staged image found to accept');
|
||||
const { x, y } = state.canvasV2.bbox.rect;
|
||||
|
||||
const { imageDTO, offsetX, offsetY } = stagingAreaImage;
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
const overrides: Partial<CanvasRasterLayerState> = {
|
||||
position: { x: x + offsetX, y: y + offsetY },
|
||||
objects: [imageObject],
|
||||
};
|
||||
|
||||
api.dispatch(rasterLayerAdded({ overrides, isSelected: true }));
|
||||
api.dispatch(sessionStagingAreaReset());
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { queueApi, selectQueueStatus } from 'services/api/endpoints/queue';
|
||||
export const addAnyEnqueuedListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
matcher: queueApi.endpoints.enqueueBatch.matchFulfilled,
|
||||
effect: async (_, { dispatch, getState }) => {
|
||||
effect: (_, { dispatch, getState }) => {
|
||||
const { data } = selectQueueStatus(getState());
|
||||
|
||||
if (!data || data.processor.is_started) {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { setInfillMethod } from 'features/parameters/store/generationSlice';
|
||||
import { setInfillMethod } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { shouldUseNSFWCheckerChanged, shouldUseWatermarkerChanged } from 'features/system/store/systemSlice';
|
||||
import { appInfoApi } from 'services/api/endpoints/appInfo';
|
||||
|
||||
export const addAppConfigReceivedListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
matcher: appInfoApi.endpoints.getAppConfig.matchFulfilled,
|
||||
effect: async (action, { getState, dispatch }) => {
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const { infill_methods = [], nsfw_methods = [], watermarking_methods = [] } = action.payload;
|
||||
const infillMethod = getState().generation.infillMethod;
|
||||
const infillMethod = getState().canvasV2.compositing.infillMethod;
|
||||
|
||||
if (!infill_methods.includes(infillMethod)) {
|
||||
// if there is no infill method, set it to the first one
|
||||
|
||||
@@ -6,7 +6,7 @@ export const appStarted = createAction('app/appStarted');
|
||||
export const addAppStartedListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: appStarted,
|
||||
effect: async (action, { unsubscribe, cancelActiveListeners }) => {
|
||||
effect: (action, { unsubscribe, cancelActiveListeners }) => {
|
||||
// this should only run once
|
||||
cancelActiveListeners();
|
||||
unsubscribe();
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import type { SerializableObject } from 'common/types';
|
||||
import { zPydanticValidationError } from 'features/system/store/zodSchemas';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
import { truncate, upperFirst } from 'lodash-es';
|
||||
import { serializeError } from 'serialize-error';
|
||||
import { queueApi } from 'services/api/endpoints/queue';
|
||||
|
||||
const log = logger('queue');
|
||||
|
||||
export const addBatchEnqueuedListener = (startAppListening: AppStartListening) => {
|
||||
// success
|
||||
startAppListening({
|
||||
matcher: queueApi.endpoints.enqueueBatch.matchFulfilled,
|
||||
effect: async (action) => {
|
||||
const response = action.payload;
|
||||
effect: (action) => {
|
||||
const enqueueResult = action.payload;
|
||||
const arg = action.meta.arg.originalArgs;
|
||||
logger('queue').debug({ enqueueResult: parseify(response) }, 'Batch enqueued');
|
||||
log.debug({ enqueueResult } as SerializableObject, 'Batch enqueued');
|
||||
|
||||
toast({
|
||||
id: 'QUEUE_BATCH_SUCCEEDED',
|
||||
title: t('queue.batchQueued'),
|
||||
status: 'success',
|
||||
description: t('queue.batchQueuedDesc', {
|
||||
count: response.enqueued,
|
||||
count: enqueueResult.enqueued,
|
||||
direction: arg.prepend ? t('queue.front') : t('queue.back'),
|
||||
}),
|
||||
});
|
||||
@@ -31,9 +34,9 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) =
|
||||
// error
|
||||
startAppListening({
|
||||
matcher: queueApi.endpoints.enqueueBatch.matchRejected,
|
||||
effect: async (action) => {
|
||||
effect: (action) => {
|
||||
const response = action.payload;
|
||||
const arg = action.meta.arg.originalArgs;
|
||||
const batchConfig = action.meta.arg.originalArgs;
|
||||
|
||||
if (!response) {
|
||||
toast({
|
||||
@@ -42,7 +45,7 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) =
|
||||
status: 'error',
|
||||
description: t('common.unknownError'),
|
||||
});
|
||||
logger('queue').error({ batchConfig: parseify(arg), error: parseify(response) }, t('queue.batchFailedToQueue'));
|
||||
log.error({ batchConfig } as SerializableObject, t('queue.batchFailedToQueue'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -68,7 +71,7 @@ export const addBatchEnqueuedListener = (startAppListening: AppStartListening) =
|
||||
description: t('common.unknownError'),
|
||||
});
|
||||
}
|
||||
logger('queue').error({ batchConfig: parseify(arg), error: parseify(response) }, t('queue.batchFailedToQueue'));
|
||||
log.error({ batchConfig, error: serializeError(response) } as SerializableObject, t('queue.batchFailedToQueue'));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { resetCanvas } from 'features/canvas/store/canvasSlice';
|
||||
import { controlAdaptersReset } from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||
import { allLayersDeleted } from 'features/controlLayers/store/controlLayersSlice';
|
||||
import { getImageUsage } from 'features/deleteImageModal/store/selectors';
|
||||
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
@@ -9,39 +6,22 @@ import { imagesApi } from 'services/api/endpoints/images';
|
||||
export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.deleteBoardAndImages.matchFulfilled,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const { deleted_images } = action.payload;
|
||||
|
||||
// Remove all deleted images from the UI
|
||||
|
||||
let wasCanvasReset = false;
|
||||
let wasNodeEditorReset = false;
|
||||
let wereControlAdaptersReset = false;
|
||||
let wereControlLayersReset = false;
|
||||
|
||||
const { canvas, nodes, controlAdapters, controlLayers } = getState();
|
||||
const { nodes, canvasV2 } = getState();
|
||||
|
||||
deleted_images.forEach((image_name) => {
|
||||
const imageUsage = getImageUsage(canvas, nodes.present, controlAdapters, controlLayers.present, image_name);
|
||||
|
||||
if (imageUsage.isCanvasImage && !wasCanvasReset) {
|
||||
dispatch(resetCanvas());
|
||||
wasCanvasReset = true;
|
||||
}
|
||||
const imageUsage = getImageUsage(nodes.present, canvasV2, image_name);
|
||||
|
||||
if (imageUsage.isNodesImage && !wasNodeEditorReset) {
|
||||
dispatch(nodeEditorReset());
|
||||
wasNodeEditorReset = true;
|
||||
}
|
||||
|
||||
if (imageUsage.isControlImage && !wereControlAdaptersReset) {
|
||||
dispatch(controlAdaptersReset());
|
||||
wereControlAdaptersReset = true;
|
||||
}
|
||||
|
||||
if (imageUsage.isControlLayerImage && !wereControlLayersReset) {
|
||||
dispatch(allLayersDeleted());
|
||||
wereControlLayersReset = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import { ExternalLink } from '@invoke-ai/ui-library';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import {
|
||||
socketBulkDownloadComplete,
|
||||
socketBulkDownloadError,
|
||||
socketBulkDownloadStarted,
|
||||
} from 'services/events/actions';
|
||||
|
||||
const log = logger('images');
|
||||
const log = logger('gallery');
|
||||
|
||||
export const addBulkDownloadListeners = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.bulkDownloadImages.matchFulfilled,
|
||||
effect: async (action) => {
|
||||
effect: (action) => {
|
||||
log.debug(action.payload, 'Bulk download requested');
|
||||
|
||||
// If we have an item name, we are processing the bulk download locally and should use it as the toast id to
|
||||
@@ -33,7 +27,7 @@ export const addBulkDownloadListeners = (startAppListening: AppStartListening) =
|
||||
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.bulkDownloadImages.matchRejected,
|
||||
effect: async () => {
|
||||
effect: () => {
|
||||
log.debug('Bulk download request failed');
|
||||
|
||||
// There isn't any toast to update if we get this event.
|
||||
@@ -44,55 +38,4 @@ export const addBulkDownloadListeners = (startAppListening: AppStartListening) =
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
actionCreator: socketBulkDownloadStarted,
|
||||
effect: async (action) => {
|
||||
// This should always happen immediately after the bulk download request, so we don't need to show a toast here.
|
||||
log.debug(action.payload.data, 'Bulk download preparation started');
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
actionCreator: socketBulkDownloadComplete,
|
||||
effect: async (action) => {
|
||||
log.debug(action.payload.data, 'Bulk download preparation completed');
|
||||
|
||||
const { bulk_download_item_name } = action.payload.data;
|
||||
|
||||
// TODO(psyche): This URL may break in in some environments (e.g. Nvidia workbench) but we need to test it first
|
||||
const url = `/api/v1/images/download/${bulk_download_item_name}`;
|
||||
|
||||
toast({
|
||||
id: bulk_download_item_name,
|
||||
title: t('gallery.bulkDownloadReady', 'Download ready'),
|
||||
status: 'success',
|
||||
description: (
|
||||
<ExternalLink
|
||||
label={t('gallery.clickToDownload', 'Click here to download')}
|
||||
href={url}
|
||||
download={bulk_download_item_name}
|
||||
/>
|
||||
),
|
||||
duration: null,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
actionCreator: socketBulkDownloadError,
|
||||
effect: async (action) => {
|
||||
log.debug(action.payload.data, 'Bulk download preparation failed');
|
||||
|
||||
const { bulk_download_item_name } = action.payload.data;
|
||||
|
||||
toast({
|
||||
id: bulk_download_item_name,
|
||||
title: t('gallery.bulkDownloadFailed'),
|
||||
status: 'error',
|
||||
description: action.payload.data.error,
|
||||
duration: null,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { $logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { canvasCopiedToClipboard } from 'features/canvas/store/actions';
|
||||
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
||||
import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
|
||||
export const addCanvasCopiedToClipboardListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: canvasCopiedToClipboard,
|
||||
effect: async (action, { getState }) => {
|
||||
const moduleLog = $logger.get().child({ namespace: 'canvasCopiedToClipboardListener' });
|
||||
const state = getState();
|
||||
|
||||
try {
|
||||
const blob = getBaseLayerBlob(state);
|
||||
|
||||
copyBlobToClipboard(blob);
|
||||
} catch (err) {
|
||||
moduleLog.error(String(err));
|
||||
toast({
|
||||
id: 'CANVAS_COPY_FAILED',
|
||||
title: t('toast.problemCopyingCanvas'),
|
||||
description: t('toast.problemCopyingCanvasDesc'),
|
||||
status: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
toast({
|
||||
id: 'CANVAS_COPY_SUCCEEDED',
|
||||
title: t('toast.canvasCopiedClipboard'),
|
||||
status: 'success',
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import { $logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { canvasDownloadedAsImage } from 'features/canvas/store/actions';
|
||||
import { downloadBlob } from 'features/canvas/util/downloadBlob';
|
||||
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
|
||||
export const addCanvasDownloadedAsImageListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: canvasDownloadedAsImage,
|
||||
effect: async (action, { getState }) => {
|
||||
const moduleLog = $logger.get().child({ namespace: 'canvasSavedToGalleryListener' });
|
||||
const state = getState();
|
||||
|
||||
let blob;
|
||||
try {
|
||||
blob = await getBaseLayerBlob(state);
|
||||
} catch (err) {
|
||||
moduleLog.error(String(err));
|
||||
toast({
|
||||
id: 'CANVAS_DOWNLOAD_FAILED',
|
||||
title: t('toast.problemDownloadingCanvas'),
|
||||
description: t('toast.problemDownloadingCanvasDesc'),
|
||||
status: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
downloadBlob(blob, 'canvas.png');
|
||||
toast({ id: 'CANVAS_DOWNLOAD_SUCCEEDED', title: t('toast.canvasDownloaded'), status: 'success' });
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { canvasImageToControlAdapter } from 'features/canvas/store/actions';
|
||||
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
||||
import { controlAdapterImageChanged } from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
|
||||
export const addCanvasImageToControlNetListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: canvasImageToControlAdapter,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
const log = logger('canvas');
|
||||
const state = getState();
|
||||
const { id } = action.payload;
|
||||
|
||||
let blob: Blob;
|
||||
try {
|
||||
blob = await getBaseLayerBlob(state, true);
|
||||
} catch (err) {
|
||||
log.error(String(err));
|
||||
toast({
|
||||
id: 'PROBLEM_SAVING_CANVAS',
|
||||
title: t('toast.problemSavingCanvas'),
|
||||
description: t('toast.problemSavingCanvasDesc'),
|
||||
status: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { autoAddBoardId } = state.gallery;
|
||||
|
||||
const imageDTO = await dispatch(
|
||||
imagesApi.endpoints.uploadImage.initiate({
|
||||
file: new File([blob], 'savedCanvas.png', {
|
||||
type: 'image/png',
|
||||
}),
|
||||
image_category: 'control',
|
||||
is_intermediate: true,
|
||||
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
|
||||
crop_visible: false,
|
||||
postUploadAction: {
|
||||
type: 'TOAST',
|
||||
title: t('toast.canvasSentControlnetAssets'),
|
||||
},
|
||||
})
|
||||
).unwrap();
|
||||
|
||||
const { image_name } = imageDTO;
|
||||
|
||||
dispatch(
|
||||
controlAdapterImageChanged({
|
||||
id,
|
||||
controlImage: image_name,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { canvasMaskSavedToGallery } from 'features/canvas/store/actions';
|
||||
import { getCanvasData } from 'features/canvas/util/getCanvasData';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
|
||||
export const addCanvasMaskSavedToGalleryListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: canvasMaskSavedToGallery,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
const log = logger('canvas');
|
||||
const state = getState();
|
||||
|
||||
const canvasBlobsAndImageData = await getCanvasData(
|
||||
state.canvas.layerState,
|
||||
state.canvas.boundingBoxCoordinates,
|
||||
state.canvas.boundingBoxDimensions,
|
||||
state.canvas.isMaskEnabled,
|
||||
state.canvas.shouldPreserveMaskedArea
|
||||
);
|
||||
|
||||
if (!canvasBlobsAndImageData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { maskBlob } = canvasBlobsAndImageData;
|
||||
|
||||
if (!maskBlob) {
|
||||
log.error('Problem getting mask layer blob');
|
||||
toast({
|
||||
id: 'PROBLEM_SAVING_MASK',
|
||||
title: t('toast.problemSavingMask'),
|
||||
description: t('toast.problemSavingMaskDesc'),
|
||||
status: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { autoAddBoardId } = state.gallery;
|
||||
|
||||
dispatch(
|
||||
imagesApi.endpoints.uploadImage.initiate({
|
||||
file: new File([maskBlob], 'canvasMaskImage.png', {
|
||||
type: 'image/png',
|
||||
}),
|
||||
image_category: 'mask',
|
||||
is_intermediate: false,
|
||||
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
|
||||
crop_visible: true,
|
||||
postUploadAction: {
|
||||
type: 'TOAST',
|
||||
title: t('toast.maskSavedAssets'),
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { canvasMaskToControlAdapter } from 'features/canvas/store/actions';
|
||||
import { getCanvasData } from 'features/canvas/util/getCanvasData';
|
||||
import { controlAdapterImageChanged } from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
|
||||
export const addCanvasMaskToControlNetListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: canvasMaskToControlAdapter,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
const log = logger('canvas');
|
||||
const state = getState();
|
||||
const { id } = action.payload;
|
||||
const canvasBlobsAndImageData = await getCanvasData(
|
||||
state.canvas.layerState,
|
||||
state.canvas.boundingBoxCoordinates,
|
||||
state.canvas.boundingBoxDimensions,
|
||||
state.canvas.isMaskEnabled,
|
||||
state.canvas.shouldPreserveMaskedArea
|
||||
);
|
||||
|
||||
if (!canvasBlobsAndImageData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { maskBlob } = canvasBlobsAndImageData;
|
||||
|
||||
if (!maskBlob) {
|
||||
log.error('Problem getting mask layer blob');
|
||||
toast({
|
||||
id: 'PROBLEM_IMPORTING_MASK',
|
||||
title: t('toast.problemImportingMask'),
|
||||
description: t('toast.problemImportingMaskDesc'),
|
||||
status: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { autoAddBoardId } = state.gallery;
|
||||
|
||||
const imageDTO = await dispatch(
|
||||
imagesApi.endpoints.uploadImage.initiate({
|
||||
file: new File([maskBlob], 'canvasMaskImage.png', {
|
||||
type: 'image/png',
|
||||
}),
|
||||
image_category: 'mask',
|
||||
is_intermediate: true,
|
||||
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
|
||||
crop_visible: false,
|
||||
postUploadAction: {
|
||||
type: 'TOAST',
|
||||
title: t('toast.maskSentControlnetAssets'),
|
||||
},
|
||||
})
|
||||
).unwrap();
|
||||
|
||||
const { image_name } = imageDTO;
|
||||
|
||||
dispatch(
|
||||
controlAdapterImageChanged({
|
||||
id,
|
||||
controlImage: image_name,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
import { $logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { canvasMerged } from 'features/canvas/store/actions';
|
||||
import { $canvasBaseLayer } from 'features/canvas/store/canvasNanostore';
|
||||
import { setMergedCanvas } from 'features/canvas/store/canvasSlice';
|
||||
import { getFullBaseLayerBlob } from 'features/canvas/util/getFullBaseLayerBlob';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
|
||||
export const addCanvasMergedListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: canvasMerged,
|
||||
effect: async (action, { dispatch }) => {
|
||||
const moduleLog = $logger.get().child({ namespace: 'canvasCopiedToClipboardListener' });
|
||||
const blob = await getFullBaseLayerBlob();
|
||||
|
||||
if (!blob) {
|
||||
moduleLog.error('Problem getting base layer blob');
|
||||
toast({
|
||||
id: 'PROBLEM_MERGING_CANVAS',
|
||||
title: t('toast.problemMergingCanvas'),
|
||||
description: t('toast.problemMergingCanvasDesc'),
|
||||
status: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const canvasBaseLayer = $canvasBaseLayer.get();
|
||||
|
||||
if (!canvasBaseLayer) {
|
||||
moduleLog.error('Problem getting canvas base layer');
|
||||
toast({
|
||||
id: 'PROBLEM_MERGING_CANVAS',
|
||||
title: t('toast.problemMergingCanvas'),
|
||||
description: t('toast.problemMergingCanvasDesc'),
|
||||
status: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const baseLayerRect = canvasBaseLayer.getClientRect({
|
||||
relativeTo: canvasBaseLayer.getParent() ?? undefined,
|
||||
});
|
||||
|
||||
const imageDTO = await dispatch(
|
||||
imagesApi.endpoints.uploadImage.initiate({
|
||||
file: new File([blob], 'mergedCanvas.png', {
|
||||
type: 'image/png',
|
||||
}),
|
||||
image_category: 'general',
|
||||
is_intermediate: true,
|
||||
postUploadAction: {
|
||||
type: 'TOAST',
|
||||
title: t('toast.canvasMerged'),
|
||||
},
|
||||
})
|
||||
).unwrap();
|
||||
|
||||
// TODO: I can't figure out how to do the type narrowing in the `take()` so just brute forcing it here
|
||||
const { image_name } = imageDTO;
|
||||
|
||||
dispatch(
|
||||
setMergedCanvas({
|
||||
kind: 'image',
|
||||
layer: 'base',
|
||||
imageName: image_name,
|
||||
...baseLayerRect,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,53 +0,0 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { canvasSavedToGallery } from 'features/canvas/store/actions';
|
||||
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
|
||||
export const addCanvasSavedToGalleryListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: canvasSavedToGallery,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
const log = logger('canvas');
|
||||
const state = getState();
|
||||
|
||||
let blob;
|
||||
try {
|
||||
blob = await getBaseLayerBlob(state);
|
||||
} catch (err) {
|
||||
log.error(String(err));
|
||||
toast({
|
||||
id: 'CANVAS_SAVE_FAILED',
|
||||
title: t('toast.problemSavingCanvas'),
|
||||
description: t('toast.problemSavingCanvasDesc'),
|
||||
status: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { autoAddBoardId } = state.gallery;
|
||||
|
||||
dispatch(
|
||||
imagesApi.endpoints.uploadImage.initiate({
|
||||
file: new File([blob], 'savedCanvas.png', {
|
||||
type: 'image/png',
|
||||
}),
|
||||
image_category: 'general',
|
||||
is_intermediate: false,
|
||||
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
|
||||
crop_visible: true,
|
||||
postUploadAction: {
|
||||
type: 'TOAST',
|
||||
title: t('toast.canvasSavedGallery'),
|
||||
},
|
||||
metadata: {
|
||||
_canvas_objects: parseify(state.canvas.layerState.objects),
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,194 +0,0 @@
|
||||
import { isAnyOf } from '@reduxjs/toolkit';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import type { AppDispatch } from 'app/store/store';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import {
|
||||
caLayerImageChanged,
|
||||
caLayerModelChanged,
|
||||
caLayerProcessedImageChanged,
|
||||
caLayerProcessorConfigChanged,
|
||||
caLayerProcessorPendingBatchIdChanged,
|
||||
caLayerRecalled,
|
||||
isControlAdapterLayer,
|
||||
} from 'features/controlLayers/store/controlLayersSlice';
|
||||
import { CA_PROCESSOR_DATA } from 'features/controlLayers/util/controlAdapters';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { getImageDTO } from 'services/api/endpoints/images';
|
||||
import { queueApi } from 'services/api/endpoints/queue';
|
||||
import type { BatchConfig } from 'services/api/types';
|
||||
import { socketInvocationComplete } from 'services/events/actions';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
const matcher = isAnyOf(
|
||||
caLayerImageChanged,
|
||||
caLayerProcessedImageChanged,
|
||||
caLayerProcessorConfigChanged,
|
||||
caLayerModelChanged,
|
||||
caLayerRecalled
|
||||
);
|
||||
|
||||
const DEBOUNCE_MS = 300;
|
||||
const log = logger('session');
|
||||
|
||||
/**
|
||||
* Simple helper to cancel a batch and reset the pending batch ID
|
||||
*/
|
||||
const cancelProcessorBatch = async (dispatch: AppDispatch, layerId: string, batchId: string) => {
|
||||
const req = dispatch(queueApi.endpoints.cancelByBatchIds.initiate({ batch_ids: [batchId] }));
|
||||
log.trace({ batchId }, 'Cancelling existing preprocessor batch');
|
||||
try {
|
||||
await req.unwrap();
|
||||
} catch {
|
||||
// no-op
|
||||
} finally {
|
||||
req.reset();
|
||||
// Always reset the pending batch ID - the cancel req could fail if the batch doesn't exist
|
||||
dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: null }));
|
||||
}
|
||||
};
|
||||
|
||||
export const addControlAdapterPreprocessor = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
matcher,
|
||||
effect: async (action, { dispatch, getState, getOriginalState, cancelActiveListeners, delay, take, signal }) => {
|
||||
const layerId = caLayerRecalled.match(action) ? action.payload.id : action.payload.layerId;
|
||||
const state = getState();
|
||||
const originalState = getOriginalState();
|
||||
|
||||
// Cancel any in-progress instances of this listener
|
||||
cancelActiveListeners();
|
||||
log.trace('Control Layer CA auto-process triggered');
|
||||
|
||||
// Delay before starting actual work
|
||||
await delay(DEBOUNCE_MS);
|
||||
|
||||
const layer = state.controlLayers.present.layers.filter(isControlAdapterLayer).find((l) => l.id === layerId);
|
||||
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We should only process if the processor settings or image have changed
|
||||
const originalLayer = originalState.controlLayers.present.layers
|
||||
.filter(isControlAdapterLayer)
|
||||
.find((l) => l.id === layerId);
|
||||
const originalImage = originalLayer?.controlAdapter.image;
|
||||
const originalConfig = originalLayer?.controlAdapter.processorConfig;
|
||||
|
||||
const image = layer.controlAdapter.image;
|
||||
const processedImage = layer.controlAdapter.processedImage;
|
||||
const config = layer.controlAdapter.processorConfig;
|
||||
|
||||
if (isEqual(config, originalConfig) && isEqual(image, originalImage) && processedImage) {
|
||||
// Neither config nor image have changed, we can bail
|
||||
return;
|
||||
}
|
||||
|
||||
if (!image || !config) {
|
||||
// - If we have no image, we have nothing to process
|
||||
// - If we have no processor config, we have nothing to process
|
||||
// Clear the processed image and bail
|
||||
dispatch(caLayerProcessedImageChanged({ layerId, imageDTO: null }));
|
||||
return;
|
||||
}
|
||||
|
||||
// At this point, the user has stopped fiddling with the processor settings and there is a processor selected.
|
||||
|
||||
// If there is a pending processor batch, cancel it.
|
||||
if (layer.controlAdapter.processorPendingBatchId) {
|
||||
cancelProcessorBatch(dispatch, layerId, layer.controlAdapter.processorPendingBatchId);
|
||||
}
|
||||
|
||||
// TODO(psyche): I can't get TS to be happy, it thinkgs `config` is `never` but it should be inferred from the generic... I'll just cast it for now
|
||||
const processorNode = CA_PROCESSOR_DATA[config.type].buildNode(image, config as never);
|
||||
const enqueueBatchArg: BatchConfig = {
|
||||
prepend: true,
|
||||
batch: {
|
||||
graph: {
|
||||
nodes: {
|
||||
[processorNode.id]: {
|
||||
...processorNode,
|
||||
// Control images are always intermediate - do not save to gallery
|
||||
is_intermediate: true,
|
||||
},
|
||||
},
|
||||
edges: [],
|
||||
},
|
||||
runs: 1,
|
||||
},
|
||||
};
|
||||
|
||||
// Kick off the processor batch
|
||||
const req = dispatch(
|
||||
queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, {
|
||||
fixedCacheKey: 'enqueueBatch',
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
const enqueueResult = await req.unwrap();
|
||||
// TODO(psyche): Update the pydantic models, pretty sure we will _always_ have a batch_id here, but the model says it's optional
|
||||
assert(enqueueResult.batch.batch_id, 'Batch ID not returned from queue');
|
||||
dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: enqueueResult.batch.batch_id }));
|
||||
log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued'));
|
||||
|
||||
// Wait for the processor node to complete
|
||||
const [invocationCompleteAction] = await take(
|
||||
(action): action is ReturnType<typeof socketInvocationComplete> =>
|
||||
socketInvocationComplete.match(action) &&
|
||||
action.payload.data.batch_id === enqueueResult.batch.batch_id &&
|
||||
action.payload.data.invocation_source_id === processorNode.id
|
||||
);
|
||||
|
||||
// We still have to check the output type
|
||||
assert(
|
||||
invocationCompleteAction.payload.data.result.type === 'image_output',
|
||||
`Processor did not return an image output, got: ${invocationCompleteAction.payload.data.result}`
|
||||
);
|
||||
const { image_name } = invocationCompleteAction.payload.data.result.image;
|
||||
|
||||
const imageDTO = await getImageDTO(image_name);
|
||||
assert(imageDTO, "Failed to fetch processor output's image DTO");
|
||||
|
||||
// Whew! We made it. Update the layer with the processed image
|
||||
log.debug({ layerId, imageDTO }, 'ControlNet image processed');
|
||||
dispatch(caLayerProcessedImageChanged({ layerId, imageDTO }));
|
||||
dispatch(caLayerProcessorPendingBatchIdChanged({ layerId, batchId: null }));
|
||||
} catch (error) {
|
||||
if (signal.aborted) {
|
||||
// The listener was canceled - we need to cancel the pending processor batch, if there is one (could have changed by now).
|
||||
const pendingBatchId = getState()
|
||||
.controlLayers.present.layers.filter(isControlAdapterLayer)
|
||||
.find((l) => l.id === layerId)?.controlAdapter.processorPendingBatchId;
|
||||
if (pendingBatchId) {
|
||||
cancelProcessorBatch(dispatch, layerId, pendingBatchId);
|
||||
}
|
||||
log.trace('Control Adapter preprocessor cancelled');
|
||||
} else {
|
||||
// Some other error condition...
|
||||
log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue'));
|
||||
|
||||
if (error instanceof Object) {
|
||||
if ('data' in error && 'status' in error) {
|
||||
if (error.status === 403) {
|
||||
dispatch(caLayerImageChanged({ layerId, imageDTO: null }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toast({
|
||||
id: 'GRAPH_QUEUE_FAILED',
|
||||
title: t('queue.graphFailedToQueue'),
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
req.reset();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
import type { AnyListenerPredicate } from '@reduxjs/toolkit';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import type { RootState } from 'app/store/store';
|
||||
import { controlAdapterImageProcessed } from 'features/controlAdapters/store/actions';
|
||||
import {
|
||||
controlAdapterAutoConfigToggled,
|
||||
controlAdapterImageChanged,
|
||||
controlAdapterModelChanged,
|
||||
controlAdapterProcessorParamsChanged,
|
||||
controlAdapterProcessortTypeChanged,
|
||||
selectControlAdapterById,
|
||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
|
||||
|
||||
type AnyControlAdapterParamChangeAction =
|
||||
| ReturnType<typeof controlAdapterProcessorParamsChanged>
|
||||
| ReturnType<typeof controlAdapterModelChanged>
|
||||
| ReturnType<typeof controlAdapterImageChanged>
|
||||
| ReturnType<typeof controlAdapterProcessortTypeChanged>
|
||||
| ReturnType<typeof controlAdapterAutoConfigToggled>;
|
||||
|
||||
const predicate: AnyListenerPredicate<RootState> = (action, state, prevState) => {
|
||||
const isActionMatched =
|
||||
controlAdapterProcessorParamsChanged.match(action) ||
|
||||
controlAdapterModelChanged.match(action) ||
|
||||
controlAdapterImageChanged.match(action) ||
|
||||
controlAdapterProcessortTypeChanged.match(action) ||
|
||||
controlAdapterAutoConfigToggled.match(action);
|
||||
|
||||
if (!isActionMatched) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { id } = action.payload;
|
||||
const prevCA = selectControlAdapterById(prevState.controlAdapters, id);
|
||||
const ca = selectControlAdapterById(state.controlAdapters, id);
|
||||
if (!prevCA || !isControlNetOrT2IAdapter(prevCA) || !ca || !isControlNetOrT2IAdapter(ca)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (controlAdapterAutoConfigToggled.match(action)) {
|
||||
// do not process if the user just disabled auto-config
|
||||
if (prevCA.shouldAutoConfig === true) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const { controlImage, processorType, shouldAutoConfig } = ca;
|
||||
if (controlAdapterModelChanged.match(action) && !shouldAutoConfig) {
|
||||
// do not process if the action is a model change but the processor settings are dirty
|
||||
return false;
|
||||
}
|
||||
|
||||
const isProcessorSelected = processorType !== 'none';
|
||||
|
||||
const hasControlImage = Boolean(controlImage);
|
||||
|
||||
return isProcessorSelected && hasControlImage;
|
||||
};
|
||||
|
||||
const DEBOUNCE_MS = 300;
|
||||
|
||||
/**
|
||||
* Listener that automatically processes a ControlNet image when its processor parameters are changed.
|
||||
*
|
||||
* The network request is debounced.
|
||||
*/
|
||||
export const addControlNetAutoProcessListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
predicate,
|
||||
effect: async (action, { dispatch, cancelActiveListeners, delay }) => {
|
||||
const log = logger('session');
|
||||
const { id } = (action as AnyControlAdapterParamChangeAction).payload;
|
||||
|
||||
// Cancel any in-progress instances of this listener
|
||||
cancelActiveListeners();
|
||||
log.trace('ControlNet auto-process triggered');
|
||||
// Delay before starting actual work
|
||||
await delay(DEBOUNCE_MS);
|
||||
|
||||
dispatch(controlAdapterImageProcessed({ id }));
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,118 +0,0 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { controlAdapterImageProcessed } from 'features/controlAdapters/store/actions';
|
||||
import {
|
||||
controlAdapterImageChanged,
|
||||
controlAdapterProcessedImageChanged,
|
||||
pendingControlImagesCleared,
|
||||
selectControlAdapterById,
|
||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import { queueApi } from 'services/api/endpoints/queue';
|
||||
import type { BatchConfig, ImageDTO } from 'services/api/types';
|
||||
import { socketInvocationComplete } from 'services/events/actions';
|
||||
|
||||
export const addControlNetImageProcessedListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: controlAdapterImageProcessed,
|
||||
effect: async (action, { dispatch, getState, take }) => {
|
||||
const log = logger('session');
|
||||
const { id } = action.payload;
|
||||
const ca = selectControlAdapterById(getState().controlAdapters, id);
|
||||
|
||||
if (!ca?.controlImage || !isControlNetOrT2IAdapter(ca)) {
|
||||
log.error('Unable to process ControlNet image');
|
||||
return;
|
||||
}
|
||||
|
||||
if (ca.processorType === 'none' || ca.processorNode.type === 'none') {
|
||||
return;
|
||||
}
|
||||
|
||||
// ControlNet one-off procressing graph is just the processor node, no edges.
|
||||
// Also we need to grab the image.
|
||||
|
||||
const nodeId = ca.processorNode.id;
|
||||
const enqueueBatchArg: BatchConfig = {
|
||||
prepend: true,
|
||||
batch: {
|
||||
graph: {
|
||||
nodes: {
|
||||
[ca.processorNode.id]: {
|
||||
...ca.processorNode,
|
||||
is_intermediate: true,
|
||||
use_cache: false,
|
||||
image: { image_name: ca.controlImage },
|
||||
},
|
||||
},
|
||||
edges: [],
|
||||
},
|
||||
runs: 1,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const req = dispatch(
|
||||
queueApi.endpoints.enqueueBatch.initiate(enqueueBatchArg, {
|
||||
fixedCacheKey: 'enqueueBatch',
|
||||
})
|
||||
);
|
||||
const enqueueResult = await req.unwrap();
|
||||
req.reset();
|
||||
log.debug({ enqueueResult: parseify(enqueueResult) }, t('queue.graphQueued'));
|
||||
|
||||
const [invocationCompleteAction] = await take(
|
||||
(action): action is ReturnType<typeof socketInvocationComplete> =>
|
||||
socketInvocationComplete.match(action) &&
|
||||
action.payload.data.batch_id === enqueueResult.batch.batch_id &&
|
||||
action.payload.data.invocation_source_id === nodeId
|
||||
);
|
||||
|
||||
// We still have to check the output type
|
||||
if (invocationCompleteAction.payload.data.result.type === 'image_output') {
|
||||
const { image_name } = invocationCompleteAction.payload.data.result.image;
|
||||
|
||||
// Wait for the ImageDTO to be received
|
||||
const [{ payload }] = await take(
|
||||
(action) =>
|
||||
imagesApi.endpoints.getImageDTO.matchFulfilled(action) && action.payload.image_name === image_name
|
||||
);
|
||||
|
||||
const processedControlImage = payload as ImageDTO;
|
||||
|
||||
log.debug({ controlNetId: action.payload, processedControlImage }, 'ControlNet image processed');
|
||||
|
||||
// Update the processed image in the store
|
||||
dispatch(
|
||||
controlAdapterProcessedImageChanged({
|
||||
id,
|
||||
processedControlImage: processedControlImage.image_name,
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error({ enqueueBatchArg: parseify(enqueueBatchArg) }, t('queue.graphFailedToQueue'));
|
||||
|
||||
if (error instanceof Object) {
|
||||
if ('data' in error && 'status' in error) {
|
||||
if (error.status === 403) {
|
||||
dispatch(pendingControlImagesCleared());
|
||||
dispatch(controlAdapterImageChanged({ id, controlImage: null }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toast({
|
||||
id: 'GRAPH_QUEUE_FAILED',
|
||||
title: t('queue.graphFailedToQueue'),
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,144 +0,0 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { enqueueRequested } from 'app/store/actions';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { canvasBatchIdAdded, stagingAreaInitialized } from 'features/canvas/store/canvasSlice';
|
||||
import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
|
||||
import { getCanvasData } from 'features/canvas/util/getCanvasData';
|
||||
import { getCanvasGenerationMode } from 'features/canvas/util/getCanvasGenerationMode';
|
||||
import { canvasGraphBuilt } from 'features/nodes/store/actions';
|
||||
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
||||
import { buildCanvasGraph } from 'features/nodes/util/graph/canvas/buildCanvasGraph';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import { queueApi } from 'services/api/endpoints/queue';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
/**
|
||||
* This listener is responsible invoking the canvas. This involves a number of steps:
|
||||
*
|
||||
* 1. Generate image blobs from the canvas layers
|
||||
* 2. Determine the generation mode from the layers (txt2img, img2img, inpaint)
|
||||
* 3. Build the canvas graph
|
||||
* 4. Create the session with the graph
|
||||
* 5. Upload the init image if necessary
|
||||
* 6. Upload the mask image if necessary
|
||||
* 7. Update the init and mask images with the session ID
|
||||
* 8. Initialize the staging area if not yet initialized
|
||||
* 9. Dispatch the sessionReadyToInvoke action to invoke the session
|
||||
*/
|
||||
export const addEnqueueRequestedCanvasListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
predicate: (action): action is ReturnType<typeof enqueueRequested> =>
|
||||
enqueueRequested.match(action) && action.payload.tabName === 'canvas',
|
||||
effect: async (action, { getState, dispatch }) => {
|
||||
const log = logger('queue');
|
||||
const { prepend } = action.payload;
|
||||
const state = getState();
|
||||
|
||||
const { layerState, boundingBoxCoordinates, boundingBoxDimensions, isMaskEnabled, shouldPreserveMaskedArea } =
|
||||
state.canvas;
|
||||
|
||||
// Build canvas blobs
|
||||
const canvasBlobsAndImageData = await getCanvasData(
|
||||
layerState,
|
||||
boundingBoxCoordinates,
|
||||
boundingBoxDimensions,
|
||||
isMaskEnabled,
|
||||
shouldPreserveMaskedArea
|
||||
);
|
||||
|
||||
if (!canvasBlobsAndImageData) {
|
||||
log.error('Unable to create canvas data');
|
||||
return;
|
||||
}
|
||||
|
||||
const { baseBlob, baseImageData, maskBlob, maskImageData } = canvasBlobsAndImageData;
|
||||
|
||||
// Determine the generation mode
|
||||
const generationMode = getCanvasGenerationMode(baseImageData, maskImageData);
|
||||
|
||||
if (state.system.enableImageDebugging) {
|
||||
const baseDataURL = await blobToDataURL(baseBlob);
|
||||
const maskDataURL = await blobToDataURL(maskBlob);
|
||||
openBase64ImageInTab([
|
||||
{ base64: maskDataURL, caption: 'mask b64' },
|
||||
{ base64: baseDataURL, caption: 'image b64' },
|
||||
]);
|
||||
}
|
||||
|
||||
log.debug(`Generation mode: ${generationMode}`);
|
||||
|
||||
// Temp placeholders for the init and mask images
|
||||
let canvasInitImage: ImageDTO | undefined;
|
||||
let canvasMaskImage: ImageDTO | undefined;
|
||||
|
||||
// For img2img and inpaint/outpaint, we need to upload the init images
|
||||
if (['img2img', 'inpaint', 'outpaint'].includes(generationMode)) {
|
||||
// upload the image, saving the request id
|
||||
canvasInitImage = await dispatch(
|
||||
imagesApi.endpoints.uploadImage.initiate({
|
||||
file: new File([baseBlob], 'canvasInitImage.png', {
|
||||
type: 'image/png',
|
||||
}),
|
||||
image_category: 'general',
|
||||
is_intermediate: true,
|
||||
})
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
// For inpaint/outpaint, we also need to upload the mask layer
|
||||
if (['inpaint', 'outpaint'].includes(generationMode)) {
|
||||
// upload the image, saving the request id
|
||||
canvasMaskImage = await dispatch(
|
||||
imagesApi.endpoints.uploadImage.initiate({
|
||||
file: new File([maskBlob], 'canvasMaskImage.png', {
|
||||
type: 'image/png',
|
||||
}),
|
||||
image_category: 'mask',
|
||||
is_intermediate: true,
|
||||
})
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
const graph = await buildCanvasGraph(state, generationMode, canvasInitImage, canvasMaskImage);
|
||||
|
||||
log.debug({ graph: parseify(graph) }, `Canvas graph built`);
|
||||
|
||||
// currently this action is just listened to for logging
|
||||
dispatch(canvasGraphBuilt(graph));
|
||||
|
||||
const batchConfig = prepareLinearUIBatch(state, graph, prepend);
|
||||
|
||||
try {
|
||||
const req = dispatch(
|
||||
queueApi.endpoints.enqueueBatch.initiate(batchConfig, {
|
||||
fixedCacheKey: 'enqueueBatch',
|
||||
})
|
||||
);
|
||||
|
||||
const enqueueResult = await req.unwrap();
|
||||
req.reset();
|
||||
|
||||
const batchId = enqueueResult.batch.batch_id as string; // we know the is a string, backend provides it
|
||||
|
||||
// Prep the canvas staging area if it is not yet initialized
|
||||
if (!state.canvas.layerState.stagingArea.boundingBox) {
|
||||
dispatch(
|
||||
stagingAreaInitialized({
|
||||
boundingBox: {
|
||||
...state.canvas.boundingBoxCoordinates,
|
||||
...state.canvas.boundingBoxDimensions,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Associate the session with the canvas session ID
|
||||
dispatch(canvasBatchIdAdded(batchId));
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,10 +1,21 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { enqueueRequested } from 'app/store/actions';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||
import type { SerializableObject } from 'common/types';
|
||||
import type { Result } from 'common/util/result';
|
||||
import { isErr, withResult, withResultAsync } from 'common/util/result';
|
||||
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { sessionStagingAreaReset, sessionStartedStaging } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
|
||||
import { buildGenerationTabGraph } from 'features/nodes/util/graph/generation/buildGenerationTabGraph';
|
||||
import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/generation/buildGenerationTabSDXLGraph';
|
||||
import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph';
|
||||
import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGraph';
|
||||
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
|
||||
import { serializeError } from 'serialize-error';
|
||||
import { queueApi } from 'services/api/endpoints/queue';
|
||||
import type { Invocation } from 'services/api/types';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
const log = logger('generation');
|
||||
|
||||
export const addEnqueueRequestedLinear = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
@@ -12,33 +23,77 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
|
||||
enqueueRequested.match(action) && action.payload.tabName === 'generation',
|
||||
effect: async (action, { getState, dispatch }) => {
|
||||
const state = getState();
|
||||
const { shouldShowProgressInViewer } = state.ui;
|
||||
const model = state.generation.model;
|
||||
const model = state.canvasV2.params.model;
|
||||
const { prepend } = action.payload;
|
||||
|
||||
let graph;
|
||||
const manager = $canvasManager.get();
|
||||
assert(manager, 'No model found in state');
|
||||
|
||||
if (model?.base === 'sdxl') {
|
||||
graph = await buildGenerationTabSDXLGraph(state);
|
||||
} else {
|
||||
graph = await buildGenerationTabGraph(state);
|
||||
let didStartStaging = false;
|
||||
|
||||
if (!state.canvasV2.session.isStaging && state.canvasV2.session.mode === 'compose') {
|
||||
dispatch(sessionStartedStaging());
|
||||
didStartStaging = true;
|
||||
}
|
||||
|
||||
const batchConfig = prepareLinearUIBatch(state, graph, prepend);
|
||||
const abortStaging = () => {
|
||||
if (didStartStaging && getState().canvasV2.session.isStaging) {
|
||||
dispatch(sessionStagingAreaReset());
|
||||
}
|
||||
};
|
||||
|
||||
let buildGraphResult: Result<
|
||||
{ g: Graph; noise: Invocation<'noise'>; posCond: Invocation<'compel' | 'sdxl_compel_prompt'> },
|
||||
Error
|
||||
>;
|
||||
|
||||
assert(model, 'No model found in state');
|
||||
const base = model.base;
|
||||
|
||||
switch (base) {
|
||||
case 'sdxl':
|
||||
buildGraphResult = await withResultAsync(() => buildSDXLGraph(state, manager));
|
||||
break;
|
||||
case 'sd-1':
|
||||
case `sd-2`:
|
||||
buildGraphResult = await withResultAsync(() => buildSD1Graph(state, manager));
|
||||
break;
|
||||
default:
|
||||
assert(false, `No graph builders for base ${base}`);
|
||||
}
|
||||
|
||||
if (isErr(buildGraphResult)) {
|
||||
log.error({ error: serializeError(buildGraphResult.error) }, 'Failed to build graph');
|
||||
abortStaging();
|
||||
return;
|
||||
}
|
||||
|
||||
const { g, noise, posCond } = buildGraphResult.value;
|
||||
|
||||
const prepareBatchResult = withResult(() => prepareLinearUIBatch(state, g, prepend, noise, posCond));
|
||||
|
||||
if (isErr(prepareBatchResult)) {
|
||||
log.error({ error: serializeError(prepareBatchResult.error) }, 'Failed to prepare batch');
|
||||
abortStaging();
|
||||
return;
|
||||
}
|
||||
|
||||
const req = dispatch(
|
||||
queueApi.endpoints.enqueueBatch.initiate(batchConfig, {
|
||||
queueApi.endpoints.enqueueBatch.initiate(prepareBatchResult.value, {
|
||||
fixedCacheKey: 'enqueueBatch',
|
||||
})
|
||||
);
|
||||
try {
|
||||
await req.unwrap();
|
||||
if (shouldShowProgressInViewer) {
|
||||
dispatch(isImageViewerOpenChanged(true));
|
||||
}
|
||||
} finally {
|
||||
req.reset();
|
||||
req.reset();
|
||||
|
||||
const enqueueResult = await withResultAsync(() => req.unwrap());
|
||||
|
||||
if (isErr(enqueueResult)) {
|
||||
log.error({ error: serializeError(enqueueResult.error) }, 'Failed to enqueue batch');
|
||||
abortStaging();
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug({ batchConfig: prepareBatchResult.value } as SerializableObject, 'Enqueued batch');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -29,7 +29,8 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) =
|
||||
batch: {
|
||||
graph,
|
||||
workflow: builtWorkflow,
|
||||
runs: state.generation.iterations,
|
||||
runs: state.canvasV2.params.iterations,
|
||||
origin: 'workflows',
|
||||
},
|
||||
prepend: action.payload.prepend,
|
||||
};
|
||||
|
||||
@@ -14,9 +14,9 @@ export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening)
|
||||
const { shouldShowProgressInViewer } = state.ui;
|
||||
const { prepend } = action.payload;
|
||||
|
||||
const graph = await buildMultidiffusionUpscaleGraph(state);
|
||||
const { g, noise, posCond } = await buildMultidiffusionUpscaleGraph(state);
|
||||
|
||||
const batchConfig = prepareLinearUIBatch(state, graph, prepend);
|
||||
const batchConfig = prepareLinearUIBatch(state, g, prepend, noise, posCond);
|
||||
|
||||
const req = dispatch(
|
||||
queueApi.endpoints.enqueueBatch.initiate(batchConfig, {
|
||||
|
||||
@@ -27,7 +27,7 @@ export const galleryImageClicked = createAction<{
|
||||
export const addGalleryImageClickedListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: galleryImageClicked,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const { imageDTO, shiftKey, ctrlKey, metaKey, altKey } = action.payload;
|
||||
const state = getState();
|
||||
const queryArgs = selectListImagesQueryArgs(state);
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import type { SerializableObject } from 'common/types';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { parseSchema } from 'features/nodes/util/schema/parseSchema';
|
||||
import { size } from 'lodash-es';
|
||||
import { serializeError } from 'serialize-error';
|
||||
import { appInfoApi } from 'services/api/endpoints/appInfo';
|
||||
|
||||
const log = logger('system');
|
||||
|
||||
export const addGetOpenAPISchemaListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
matcher: appInfoApi.endpoints.getOpenAPISchema.matchFulfilled,
|
||||
effect: (action, { getState }) => {
|
||||
const log = logger('system');
|
||||
const schemaJSON = action.payload;
|
||||
|
||||
log.debug({ schemaJSON: parseify(schemaJSON) }, 'Received OpenAPI schema');
|
||||
log.debug({ schemaJSON: parseify(schemaJSON) } as SerializableObject, 'Received OpenAPI schema');
|
||||
const { nodesAllowlist, nodesDenylist } = getState().config;
|
||||
|
||||
const nodeTemplates = parseSchema(schemaJSON, nodesAllowlist, nodesDenylist);
|
||||
|
||||
log.debug({ nodeTemplates: parseify(nodeTemplates) }, `Built ${size(nodeTemplates)} node templates`);
|
||||
log.debug({ nodeTemplates } as SerializableObject, `Built ${size(nodeTemplates)} node templates`);
|
||||
|
||||
$templates.set(nodeTemplates);
|
||||
},
|
||||
@@ -30,8 +33,7 @@ export const addGetOpenAPISchemaListener = (startAppListening: AppStartListening
|
||||
// If action.meta.condition === true, the request was canceled/skipped because another request was in flight or
|
||||
// the value was already in the cache. We don't want to log these errors.
|
||||
if (!action.meta.condition) {
|
||||
const log = logger('system');
|
||||
log.error({ error: parseify(action.error) }, 'Problem retrieving OpenAPI Schema');
|
||||
log.error({ error: serializeError(action.error) }, 'Problem retrieving OpenAPI Schema');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,15 +2,13 @@ import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
|
||||
const log = logger('gallery');
|
||||
|
||||
export const addImageAddedToBoardFulfilledListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.addImageToBoard.matchFulfilled,
|
||||
effect: (action) => {
|
||||
const log = logger('images');
|
||||
const { board_id, imageDTO } = action.meta.arg.originalArgs;
|
||||
|
||||
// TODO: update listImages cache for this board
|
||||
|
||||
log.debug({ board_id, imageDTO }, 'Image added to board');
|
||||
},
|
||||
});
|
||||
@@ -18,9 +16,7 @@ export const addImageAddedToBoardFulfilledListener = (startAppListening: AppStar
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.addImageToBoard.matchRejected,
|
||||
effect: (action) => {
|
||||
const log = logger('images');
|
||||
const { board_id, imageDTO } = action.meta.arg.originalArgs;
|
||||
|
||||
log.debug({ board_id, imageDTO }, 'Problem adding image to board');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import type { AppDispatch, RootState } from 'app/store/store';
|
||||
import { resetCanvas } from 'features/canvas/store/canvasSlice';
|
||||
import {
|
||||
controlAdapterImageChanged,
|
||||
controlAdapterProcessedImageChanged,
|
||||
selectControlAdapterAll,
|
||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
|
||||
import {
|
||||
isControlAdapterLayer,
|
||||
isInitialImageLayer,
|
||||
isIPAdapterLayer,
|
||||
isRegionalGuidanceLayer,
|
||||
layerDeleted,
|
||||
} from 'features/controlLayers/store/controlLayersSlice';
|
||||
import { entityDeleted, ipaImageChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
|
||||
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
|
||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
@@ -26,6 +13,10 @@ import { forEach, intersectionBy } from 'lodash-es';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
const log = logger('gallery');
|
||||
|
||||
//TODO(psyche): handle image deletion (canvas sessions?)
|
||||
|
||||
// Some utils to delete images from different parts of the app
|
||||
const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||
state.nodes.present.nodes.forEach((node) => {
|
||||
@@ -47,52 +38,37 @@ const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: Im
|
||||
});
|
||||
};
|
||||
|
||||
const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||
forEach(selectControlAdapterAll(state.controlAdapters), (ca) => {
|
||||
if (
|
||||
ca.controlImage === imageDTO.image_name ||
|
||||
(isControlNetOrT2IAdapter(ca) && ca.processedControlImage === imageDTO.image_name)
|
||||
) {
|
||||
dispatch(
|
||||
controlAdapterImageChanged({
|
||||
id: ca.id,
|
||||
controlImage: null,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
controlAdapterProcessedImageChanged({
|
||||
id: ca.id,
|
||||
processedControlImage: null,
|
||||
})
|
||||
);
|
||||
// const deleteControlAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||
// state.canvasV2.controlAdapters.entities.forEach(({ id, imageObject, processedImageObject }) => {
|
||||
// if (
|
||||
// imageObject?.image.image_name === imageDTO.image_name ||
|
||||
// processedImageObject?.image.image_name === imageDTO.image_name
|
||||
// ) {
|
||||
// dispatch(caImageChanged({ id, imageDTO: null }));
|
||||
// dispatch(caProcessedImageChanged({ id, imageDTO: null }));
|
||||
// }
|
||||
// });
|
||||
// };
|
||||
|
||||
const deleteIPAdapterImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||
state.canvasV2.ipAdapters.entities.forEach(({ id, ipAdapter }) => {
|
||||
if (ipAdapter.image?.image_name === imageDTO.image_name) {
|
||||
dispatch(ipaImageChanged({ id, imageDTO: null }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||
state.controlLayers.present.layers.forEach((l) => {
|
||||
if (isRegionalGuidanceLayer(l)) {
|
||||
if (l.ipAdapters.some((ipa) => ipa.image?.name === imageDTO.image_name)) {
|
||||
dispatch(layerDeleted(l.id));
|
||||
const deleteLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||
state.canvasV2.rasterLayers.entities.forEach(({ id, objects }) => {
|
||||
let shouldDelete = false;
|
||||
for (const obj of objects) {
|
||||
if (obj.type === 'image' && obj.image.image_name === imageDTO.image_name) {
|
||||
shouldDelete = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isControlAdapterLayer(l)) {
|
||||
if (
|
||||
l.controlAdapter.image?.name === imageDTO.image_name ||
|
||||
l.controlAdapter.processedImage?.name === imageDTO.image_name
|
||||
) {
|
||||
dispatch(layerDeleted(l.id));
|
||||
}
|
||||
}
|
||||
if (isIPAdapterLayer(l)) {
|
||||
if (l.ipAdapter.image?.name === imageDTO.image_name) {
|
||||
dispatch(layerDeleted(l.id));
|
||||
}
|
||||
}
|
||||
if (isInitialImageLayer(l)) {
|
||||
if (l.image?.name === imageDTO.image_name) {
|
||||
dispatch(layerDeleted(l.id));
|
||||
}
|
||||
if (shouldDelete) {
|
||||
dispatch(entityDeleted({ entityIdentifier: { id, type: 'raster_layer' } }));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -145,14 +121,10 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening)
|
||||
}
|
||||
}
|
||||
|
||||
// We need to reset the features where the image is in use - none of these work if their image(s) don't exist
|
||||
if (imageUsage.isCanvasImage) {
|
||||
dispatch(resetCanvas());
|
||||
}
|
||||
|
||||
deleteControlAdapterImages(state, dispatch, imageDTO);
|
||||
deleteNodesImages(state, dispatch, imageDTO);
|
||||
deleteControlLayerImages(state, dispatch, imageDTO);
|
||||
// deleteControlAdapterImages(state, dispatch, imageDTO);
|
||||
deleteIPAdapterImages(state, dispatch, imageDTO);
|
||||
deleteLayerImages(state, dispatch, imageDTO);
|
||||
} catch {
|
||||
// no-op
|
||||
} finally {
|
||||
@@ -189,14 +161,11 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening)
|
||||
|
||||
// We need to reset the features where the image is in use - none of these work if their image(s) don't exist
|
||||
|
||||
if (imagesUsage.some((i) => i.isCanvasImage)) {
|
||||
dispatch(resetCanvas());
|
||||
}
|
||||
|
||||
imageDTOs.forEach((imageDTO) => {
|
||||
deleteControlAdapterImages(state, dispatch, imageDTO);
|
||||
deleteNodesImages(state, dispatch, imageDTO);
|
||||
deleteControlLayerImages(state, dispatch, imageDTO);
|
||||
// deleteControlAdapterImages(state, dispatch, imageDTO);
|
||||
deleteIPAdapterImages(state, dispatch, imageDTO);
|
||||
deleteLayerImages(state, dispatch, imageDTO);
|
||||
});
|
||||
} catch {
|
||||
// no-op
|
||||
@@ -220,7 +189,6 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening)
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.deleteImage.matchFulfilled,
|
||||
effect: (action) => {
|
||||
const log = logger('images');
|
||||
log.debug({ imageDTO: action.meta.arg.originalArgs }, 'Image deleted');
|
||||
},
|
||||
});
|
||||
@@ -228,7 +196,6 @@ export const addImageDeletionListeners = (startAppListening: AppStartListening)
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.deleteImage.matchRejected,
|
||||
effect: (action) => {
|
||||
const log = logger('images');
|
||||
log.debug({ imageDTO: action.meta.arg.originalArgs }, 'Unable to delete image');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,28 +1,18 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||
import {
|
||||
controlAdapterImageChanged,
|
||||
controlAdapterIsEnabledChanged,
|
||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||
import {
|
||||
caLayerImageChanged,
|
||||
iiLayerImageChanged,
|
||||
ipaLayerImageChanged,
|
||||
rgLayerIPAdapterImageChanged,
|
||||
} from 'features/controlLayers/store/controlLayersSlice';
|
||||
controlLayerAdded,
|
||||
ipaImageChanged,
|
||||
rasterLayerAdded,
|
||||
rgIPAdapterImageChanged,
|
||||
} from 'features/controlLayers/store/canvasV2Slice';
|
||||
import type { CanvasControlLayerState, CanvasRasterLayerState } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
||||
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
|
||||
import { isValidDrop } from 'features/dnd/util/isValidDrop';
|
||||
import {
|
||||
imageSelected,
|
||||
imageToCompareChanged,
|
||||
isImageViewerOpenChanged,
|
||||
selectionChanged,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { imageToCompareChanged, isImageViewerOpenChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
|
||||
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
|
||||
@@ -31,11 +21,12 @@ export const dndDropped = createAction<{
|
||||
activeData: TypesafeDraggableData;
|
||||
}>('dnd/dndDropped');
|
||||
|
||||
const log = logger('system');
|
||||
|
||||
export const addImageDroppedListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: dndDropped,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
const log = logger('dnd');
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const { activeData, overData } = action.payload;
|
||||
if (!isValidDrop(overData, activeData)) {
|
||||
return;
|
||||
@@ -46,81 +37,21 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
|
||||
} else if (activeData.payloadType === 'GALLERY_SELECTION') {
|
||||
log.debug({ activeData, overData }, `Images (${getState().gallery.selection.length}) dropped`);
|
||||
} else if (activeData.payloadType === 'NODE_FIELD') {
|
||||
log.debug({ activeData: parseify(activeData), overData: parseify(overData) }, 'Node field dropped');
|
||||
log.debug({ activeData, overData }, 'Node field dropped');
|
||||
} else {
|
||||
log.debug({ activeData, overData }, `Unknown payload dropped`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Image dropped on current image
|
||||
*/
|
||||
if (
|
||||
overData.actionType === 'SET_CURRENT_IMAGE' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
dispatch(imageSelected(activeData.payload.imageDTO));
|
||||
dispatch(isImageViewerOpenChanged(true));
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Image dropped on ControlNet
|
||||
*/
|
||||
if (
|
||||
overData.actionType === 'SET_CONTROL_ADAPTER_IMAGE' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
const { id } = overData.context;
|
||||
dispatch(
|
||||
controlAdapterImageChanged({
|
||||
id,
|
||||
controlImage: activeData.payload.imageDTO.image_name,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
controlAdapterIsEnabledChanged({
|
||||
id,
|
||||
isEnabled: true,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Image dropped on Control Adapter Layer
|
||||
*/
|
||||
if (
|
||||
overData.actionType === 'SET_CA_LAYER_IMAGE' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
const { layerId } = overData.context;
|
||||
dispatch(
|
||||
caLayerImageChanged({
|
||||
layerId,
|
||||
imageDTO: activeData.payload.imageDTO,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Image dropped on IP Adapter Layer
|
||||
*/
|
||||
if (
|
||||
overData.actionType === 'SET_IPA_LAYER_IMAGE' &&
|
||||
overData.actionType === 'SET_IPA_IMAGE' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
const { layerId } = overData.context;
|
||||
dispatch(
|
||||
ipaLayerImageChanged({
|
||||
layerId,
|
||||
imageDTO: activeData.payload.imageDTO,
|
||||
})
|
||||
);
|
||||
const { id } = overData.context;
|
||||
dispatch(ipaImageChanged({ id, imageDTO: activeData.payload.imageDTO }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -128,48 +59,48 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
|
||||
* Image dropped on RG Layer IP Adapter
|
||||
*/
|
||||
if (
|
||||
overData.actionType === 'SET_RG_LAYER_IP_ADAPTER_IMAGE' &&
|
||||
overData.actionType === 'SET_RG_IP_ADAPTER_IMAGE' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
const { layerId, ipAdapterId } = overData.context;
|
||||
dispatch(
|
||||
rgLayerIPAdapterImageChanged({
|
||||
layerId,
|
||||
ipAdapterId,
|
||||
imageDTO: activeData.payload.imageDTO,
|
||||
})
|
||||
);
|
||||
const { id, ipAdapterId } = overData.context;
|
||||
dispatch(rgIPAdapterImageChanged({ id, ipAdapterId, imageDTO: activeData.payload.imageDTO }));
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Image dropped on II Layer Image
|
||||
* Image dropped on Raster layer
|
||||
*/
|
||||
if (
|
||||
overData.actionType === 'SET_II_LAYER_IMAGE' &&
|
||||
overData.actionType === 'ADD_RASTER_LAYER_FROM_IMAGE' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
const { layerId } = overData.context;
|
||||
dispatch(
|
||||
iiLayerImageChanged({
|
||||
layerId,
|
||||
imageDTO: activeData.payload.imageDTO,
|
||||
})
|
||||
);
|
||||
const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
|
||||
const { x, y } = getState().canvasV2.bbox.rect;
|
||||
const overrides: Partial<CanvasRasterLayerState> = {
|
||||
objects: [imageObject],
|
||||
position: { x, y },
|
||||
};
|
||||
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Image dropped on Canvas
|
||||
* Image dropped on Raster layer
|
||||
*/
|
||||
if (
|
||||
overData.actionType === 'SET_CANVAS_INITIAL_IMAGE' &&
|
||||
overData.actionType === 'ADD_CONTROL_LAYER_FROM_IMAGE' &&
|
||||
activeData.payloadType === 'IMAGE_DTO' &&
|
||||
activeData.payload.imageDTO
|
||||
) {
|
||||
dispatch(setInitialCanvasImage(activeData.payload.imageDTO, selectOptimalDimension(getState())));
|
||||
const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
|
||||
const { x, y } = getState().canvasV2.bbox.rect;
|
||||
const overrides: Partial<CanvasControlLayerState> = {
|
||||
objects: [imageObject],
|
||||
position: { x, y },
|
||||
};
|
||||
dispatch(controlLayerAdded({ overrides, isSelected: true }));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
|
||||
const log = logger('gallery');
|
||||
|
||||
export const addImageRemovedFromBoardFulfilledListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.removeImageFromBoard.matchFulfilled,
|
||||
effect: (action) => {
|
||||
const log = logger('images');
|
||||
const imageDTO = action.meta.arg.originalArgs;
|
||||
|
||||
log.debug({ imageDTO }, 'Image removed from board');
|
||||
},
|
||||
});
|
||||
@@ -16,9 +16,7 @@ export const addImageRemovedFromBoardFulfilledListener = (startAppListening: App
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.removeImageFromBoard.matchRejected,
|
||||
effect: (action) => {
|
||||
const log = logger('images');
|
||||
const imageDTO = action.meta.arg.originalArgs;
|
||||
|
||||
log.debug({ imageDTO }, 'Problem removing image from board');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,16 +6,17 @@ import { imagesToDeleteSelected, isModalOpenChanged } from 'features/deleteImage
|
||||
export const addImageToDeleteSelectedListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: imagesToDeleteSelected,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const imageDTOs = action.payload;
|
||||
const state = getState();
|
||||
const { shouldConfirmOnDelete } = state.system;
|
||||
const imagesUsage = selectImageUsage(getState());
|
||||
|
||||
const isImageInUse =
|
||||
imagesUsage.some((i) => i.isCanvasImage) ||
|
||||
imagesUsage.some((i) => i.isControlImage) ||
|
||||
imagesUsage.some((i) => i.isNodesImage);
|
||||
imagesUsage.some((i) => i.isLayerImage) ||
|
||||
imagesUsage.some((i) => i.isControlAdapterImage) ||
|
||||
imagesUsage.some((i) => i.isIPAdapterImage) ||
|
||||
imagesUsage.some((i) => i.isLayerImage);
|
||||
|
||||
if (shouldConfirmOnDelete || isImageInUse) {
|
||||
dispatch(isModalOpenChanged(true));
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
|
||||
import {
|
||||
controlAdapterImageChanged,
|
||||
controlAdapterIsEnabledChanged,
|
||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||
import {
|
||||
caLayerImageChanged,
|
||||
iiLayerImageChanged,
|
||||
ipaLayerImageChanged,
|
||||
rgLayerIPAdapterImageChanged,
|
||||
} from 'features/controlLayers/store/controlLayersSlice';
|
||||
import { ipaImageChanged, rgIPAdapterImageChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { selectOptimalDimension } from 'features/parameters/store/generationSlice';
|
||||
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
@@ -21,11 +10,12 @@ import { omit } from 'lodash-es';
|
||||
import { boardsApi } from 'services/api/endpoints/boards';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
|
||||
const log = logger('gallery');
|
||||
|
||||
export const addImageUploadedFulfilledListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.uploadImage.matchFulfilled,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const log = logger('images');
|
||||
const imageDTO = action.payload;
|
||||
const state = getState();
|
||||
const { autoAddBoardId } = state.gallery;
|
||||
@@ -81,15 +71,6 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
|
||||
return;
|
||||
}
|
||||
|
||||
if (postUploadAction?.type === 'SET_CANVAS_INITIAL_IMAGE') {
|
||||
dispatch(setInitialCanvasImage(imageDTO, selectOptimalDimension(state)));
|
||||
toast({
|
||||
...DEFAULT_UPLOADED_TOAST,
|
||||
description: t('toast.setAsCanvasInitialImage'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (postUploadAction?.type === 'SET_UPSCALE_INITIAL_IMAGE') {
|
||||
dispatch(upscaleInitialImageChanged(imageDTO));
|
||||
toast({
|
||||
@@ -99,70 +80,31 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
|
||||
return;
|
||||
}
|
||||
|
||||
if (postUploadAction?.type === 'SET_CONTROL_ADAPTER_IMAGE') {
|
||||
// if (postUploadAction?.type === 'SET_CA_IMAGE') {
|
||||
// const { id } = postUploadAction;
|
||||
// dispatch(caImageChanged({ id, imageDTO }));
|
||||
// toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') });
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (postUploadAction?.type === 'SET_IPA_IMAGE') {
|
||||
const { id } = postUploadAction;
|
||||
dispatch(
|
||||
controlAdapterIsEnabledChanged({
|
||||
id,
|
||||
isEnabled: true,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
controlAdapterImageChanged({
|
||||
id,
|
||||
controlImage: imageDTO.image_name,
|
||||
})
|
||||
);
|
||||
toast({
|
||||
...DEFAULT_UPLOADED_TOAST,
|
||||
description: t('toast.setControlImage'),
|
||||
});
|
||||
dispatch(ipaImageChanged({ id, imageDTO }));
|
||||
toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') });
|
||||
return;
|
||||
}
|
||||
|
||||
if (postUploadAction?.type === 'SET_CA_LAYER_IMAGE') {
|
||||
const { layerId } = postUploadAction;
|
||||
dispatch(caLayerImageChanged({ layerId, imageDTO }));
|
||||
toast({
|
||||
...DEFAULT_UPLOADED_TOAST,
|
||||
description: t('toast.setControlImage'),
|
||||
});
|
||||
}
|
||||
|
||||
if (postUploadAction?.type === 'SET_IPA_LAYER_IMAGE') {
|
||||
const { layerId } = postUploadAction;
|
||||
dispatch(ipaLayerImageChanged({ layerId, imageDTO }));
|
||||
toast({
|
||||
...DEFAULT_UPLOADED_TOAST,
|
||||
description: t('toast.setControlImage'),
|
||||
});
|
||||
}
|
||||
|
||||
if (postUploadAction?.type === 'SET_RG_LAYER_IP_ADAPTER_IMAGE') {
|
||||
const { layerId, ipAdapterId } = postUploadAction;
|
||||
dispatch(rgLayerIPAdapterImageChanged({ layerId, ipAdapterId, imageDTO }));
|
||||
toast({
|
||||
...DEFAULT_UPLOADED_TOAST,
|
||||
description: t('toast.setControlImage'),
|
||||
});
|
||||
}
|
||||
|
||||
if (postUploadAction?.type === 'SET_II_LAYER_IMAGE') {
|
||||
const { layerId } = postUploadAction;
|
||||
dispatch(iiLayerImageChanged({ layerId, imageDTO }));
|
||||
toast({
|
||||
...DEFAULT_UPLOADED_TOAST,
|
||||
description: t('toast.setControlImage'),
|
||||
});
|
||||
if (postUploadAction?.type === 'SET_RG_IP_ADAPTER_IMAGE') {
|
||||
const { id, ipAdapterId } = postUploadAction;
|
||||
dispatch(rgIPAdapterImageChanged({ id, ipAdapterId, imageDTO }));
|
||||
toast({ ...DEFAULT_UPLOADED_TOAST, description: t('toast.setControlImage') });
|
||||
return;
|
||||
}
|
||||
|
||||
if (postUploadAction?.type === 'SET_NODES_IMAGE') {
|
||||
const { nodeId, fieldName } = postUploadAction;
|
||||
dispatch(fieldImageValueChanged({ nodeId, fieldName, value: imageDTO }));
|
||||
toast({
|
||||
...DEFAULT_UPLOADED_TOAST,
|
||||
description: `${t('toast.setNodeField')} ${fieldName}`,
|
||||
});
|
||||
toast({ ...DEFAULT_UPLOADED_TOAST, description: `${t('toast.setNodeField')} ${fieldName}` });
|
||||
return;
|
||||
}
|
||||
},
|
||||
@@ -171,7 +113,6 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.uploadImage.matchRejected,
|
||||
effect: (action) => {
|
||||
const log = logger('images');
|
||||
const sanitizedData = {
|
||||
arg: {
|
||||
...omit(action.meta.arg.originalArgs, ['file', 'postUploadAction']),
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { ImageDTO } from 'services/api/types';
|
||||
export const addImagesStarredListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.starImages.matchFulfilled,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const { updated_image_names: starredImages } = action.payload;
|
||||
|
||||
const state = getState();
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { ImageDTO } from 'services/api/types';
|
||||
export const addImagesUnstarredListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.unstarImages.matchFulfilled,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const { updated_image_names: unstarredImages } = action.payload;
|
||||
|
||||
const state = getState();
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import {
|
||||
controlAdapterIsEnabledChanged,
|
||||
selectControlAdapterAll,
|
||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||
import { loraRemoved } from 'features/lora/store/loraSlice';
|
||||
import { loraDeleted, modelChanged, vaeSelected } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { modelSelected } from 'features/parameters/store/actions';
|
||||
import { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice';
|
||||
import { zParameterModel } from 'features/parameters/types/parameterSchemas';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
import { forEach } from 'lodash-es';
|
||||
|
||||
const log = logger('models');
|
||||
|
||||
export const addModelSelectedListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: modelSelected,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const log = logger('models');
|
||||
|
||||
const state = getState();
|
||||
const result = zParameterModel.safeParse(action.payload);
|
||||
|
||||
@@ -29,34 +23,36 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
|
||||
const newModel = result.data;
|
||||
|
||||
const newBaseModel = newModel.base;
|
||||
const didBaseModelChange = state.generation.model?.base !== newBaseModel;
|
||||
const didBaseModelChange = state.canvasV2.params.model?.base !== newBaseModel;
|
||||
|
||||
if (didBaseModelChange) {
|
||||
// we may need to reset some incompatible submodels
|
||||
let modelsCleared = 0;
|
||||
|
||||
// handle incompatible loras
|
||||
forEach(state.lora.loras, (lora, id) => {
|
||||
state.canvasV2.loras.forEach((lora) => {
|
||||
if (lora.model.base !== newBaseModel) {
|
||||
dispatch(loraRemoved(id));
|
||||
dispatch(loraDeleted({ id: lora.id }));
|
||||
modelsCleared += 1;
|
||||
}
|
||||
});
|
||||
|
||||
// handle incompatible vae
|
||||
const { vae } = state.generation;
|
||||
const { vae } = state.canvasV2.params;
|
||||
if (vae && vae.base !== newBaseModel) {
|
||||
dispatch(vaeSelected(null));
|
||||
modelsCleared += 1;
|
||||
}
|
||||
|
||||
// handle incompatible controlnets
|
||||
selectControlAdapterAll(state.controlAdapters).forEach((ca) => {
|
||||
if (ca.model?.base !== newBaseModel) {
|
||||
dispatch(controlAdapterIsEnabledChanged({ id: ca.id, isEnabled: false }));
|
||||
modelsCleared += 1;
|
||||
}
|
||||
});
|
||||
// state.canvasV2.controlAdapters.entities.forEach((ca) => {
|
||||
// if (ca.model?.base !== newBaseModel) {
|
||||
// modelsCleared += 1;
|
||||
// if (ca.isEnabled) {
|
||||
// dispatch(entityIsEnabledToggled({ entityIdentifier: { id: ca.id, type: 'control_adapter' } }));
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
if (modelsCleared > 0) {
|
||||
toast({
|
||||
@@ -70,7 +66,7 @@ export const addModelSelectedListener = (startAppListening: AppStartListening) =
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(modelChanged(newModel, state.generation.model));
|
||||
dispatch(modelChanged({ model: newModel, previousModel: state.canvasV2.params.model }));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,36 +1,42 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import type { AppDispatch, RootState } from 'app/store/store';
|
||||
import type { JSONObject } from 'common/types';
|
||||
import type { SerializableObject } from 'common/types';
|
||||
import {
|
||||
controlAdapterModelCleared,
|
||||
selectControlAdapterAll,
|
||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||
import { loraRemoved } from 'features/lora/store/loraSlice';
|
||||
import { calculateNewSize } from 'features/parameters/components/ImageSize/calculateNewSize';
|
||||
import { modelChanged, vaeSelected } from 'features/parameters/store/generationSlice';
|
||||
bboxHeightChanged,
|
||||
bboxWidthChanged,
|
||||
controlLayerModelChanged,
|
||||
ipaModelChanged,
|
||||
loraDeleted,
|
||||
modelChanged,
|
||||
refinerModelChanged,
|
||||
rgIPAdapterModelChanged,
|
||||
vaeSelected,
|
||||
} from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
|
||||
import { postProcessingModelChanged, upscaleModelChanged } from 'features/parameters/store/upscaleSlice';
|
||||
import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas';
|
||||
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
||||
import { refinerModelChanged } from 'features/sdxl/store/sdxlSlice';
|
||||
import { forEach } from 'lodash-es';
|
||||
import type { Logger } from 'roarr';
|
||||
import { modelConfigsAdapterSelectors, modelsApi } from 'services/api/endpoints/models';
|
||||
import type { AnyModelConfig } from 'services/api/types';
|
||||
import {
|
||||
isControlNetOrT2IAdapterModelConfig,
|
||||
isIPAdapterModelConfig,
|
||||
isLoRAModelConfig,
|
||||
isNonRefinerMainModelConfig,
|
||||
isRefinerMainModelModelConfig,
|
||||
isSpandrelImageToImageModelConfig,
|
||||
isVAEModelConfig,
|
||||
} from 'services/api/types';
|
||||
|
||||
const log = logger('models');
|
||||
|
||||
export const addModelsLoadedListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
predicate: modelsApi.endpoints.getModelConfigs.matchFulfilled,
|
||||
effect: async (action, { getState, dispatch }) => {
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
// models loaded, we need to ensure the selected model is available and if not, select the first one
|
||||
const log = logger('models');
|
||||
log.info({ models: action.payload.entities }, `Models loaded (${action.payload.ids.length})`);
|
||||
|
||||
const state = getState();
|
||||
@@ -43,6 +49,7 @@ export const addModelsLoadedListener = (startAppListening: AppStartListening) =>
|
||||
handleLoRAModels(models, state, dispatch, log);
|
||||
handleControlAdapterModels(models, state, dispatch, log);
|
||||
handleSpandrelImageToImageModels(models, state, dispatch, log);
|
||||
handleIPAdapterModels(models, state, dispatch, log);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -51,15 +58,15 @@ type ModelHandler = (
|
||||
models: AnyModelConfig[],
|
||||
state: RootState,
|
||||
dispatch: AppDispatch,
|
||||
log: Logger<JSONObject>
|
||||
log: Logger<SerializableObject>
|
||||
) => undefined;
|
||||
|
||||
const handleMainModels: ModelHandler = (models, state, dispatch, log) => {
|
||||
const currentModel = state.generation.model;
|
||||
const currentModel = state.canvasV2.params.model;
|
||||
const mainModels = models.filter(isNonRefinerMainModelConfig);
|
||||
if (mainModels.length === 0) {
|
||||
// No models loaded at all
|
||||
dispatch(modelChanged(null));
|
||||
dispatch(modelChanged({ model: null }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -74,25 +81,19 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => {
|
||||
if (defaultModelInList) {
|
||||
const result = zParameterModel.safeParse(defaultModelInList);
|
||||
if (result.success) {
|
||||
dispatch(modelChanged(defaultModelInList, currentModel));
|
||||
dispatch(modelChanged({ model: defaultModelInList, previousModel: currentModel }));
|
||||
|
||||
const optimalDimension = getOptimalDimension(defaultModelInList);
|
||||
if (
|
||||
getIsSizeOptimal(
|
||||
state.controlLayers.present.size.width,
|
||||
state.controlLayers.present.size.height,
|
||||
optimalDimension
|
||||
)
|
||||
) {
|
||||
if (getIsSizeOptimal(state.canvasV2.bbox.rect.width, state.canvasV2.bbox.rect.height, optimalDimension)) {
|
||||
return;
|
||||
}
|
||||
const { width, height } = calculateNewSize(
|
||||
state.controlLayers.present.size.aspectRatio.value,
|
||||
state.canvasV2.bbox.aspectRatio.value,
|
||||
optimalDimension * optimalDimension
|
||||
);
|
||||
|
||||
dispatch(widthChanged({ width }));
|
||||
dispatch(heightChanged({ height }));
|
||||
dispatch(bboxWidthChanged({ width }));
|
||||
dispatch(bboxHeightChanged({ height }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -104,11 +105,11 @@ const handleMainModels: ModelHandler = (models, state, dispatch, log) => {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(modelChanged(result.data, currentModel));
|
||||
dispatch(modelChanged({ model: result.data, previousModel: currentModel }));
|
||||
};
|
||||
|
||||
const handleRefinerModels: ModelHandler = (models, state, dispatch, _log) => {
|
||||
const currentRefinerModel = state.sdxl.refinerModel;
|
||||
const currentRefinerModel = state.canvasV2.params.refinerModel;
|
||||
const refinerModels = models.filter(isRefinerMainModelModelConfig);
|
||||
if (models.length === 0) {
|
||||
// No models loaded at all
|
||||
@@ -127,7 +128,7 @@ const handleRefinerModels: ModelHandler = (models, state, dispatch, _log) => {
|
||||
};
|
||||
|
||||
const handleVAEModels: ModelHandler = (models, state, dispatch, log) => {
|
||||
const currentVae = state.generation.vae;
|
||||
const currentVae = state.canvasV2.params.vae;
|
||||
|
||||
if (currentVae === null) {
|
||||
// null is a valid VAE! it means "use the default with the main model"
|
||||
@@ -160,28 +161,45 @@ const handleVAEModels: ModelHandler = (models, state, dispatch, log) => {
|
||||
};
|
||||
|
||||
const handleLoRAModels: ModelHandler = (models, state, dispatch, _log) => {
|
||||
const loras = state.lora.loras;
|
||||
|
||||
forEach(loras, (lora, id) => {
|
||||
const isLoRAAvailable = models.some((m) => m.key === lora.model.key);
|
||||
|
||||
const loraModels = models.filter(isLoRAModelConfig);
|
||||
state.canvasV2.loras.forEach((lora) => {
|
||||
const isLoRAAvailable = loraModels.some((m) => m.key === lora.model.key);
|
||||
if (isLoRAAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(loraRemoved(id));
|
||||
dispatch(loraDeleted({ id: lora.id }));
|
||||
});
|
||||
};
|
||||
|
||||
const handleControlAdapterModels: ModelHandler = (models, state, dispatch, _log) => {
|
||||
selectControlAdapterAll(state.controlAdapters).forEach((ca) => {
|
||||
const isModelAvailable = models.some((m) => m.key === ca.model?.key);
|
||||
|
||||
const caModels = models.filter(isControlNetOrT2IAdapterModelConfig);
|
||||
state.canvasV2.controlLayers.entities.forEach((entity) => {
|
||||
const isModelAvailable = caModels.some((m) => m.key === entity.controlAdapter.model?.key);
|
||||
if (isModelAvailable) {
|
||||
return;
|
||||
}
|
||||
dispatch(controlLayerModelChanged({ id: entity.id, modelConfig: null }));
|
||||
});
|
||||
};
|
||||
|
||||
dispatch(controlAdapterModelCleared({ id: ca.id }));
|
||||
const handleIPAdapterModels: ModelHandler = (models, state, dispatch, _log) => {
|
||||
const ipaModels = models.filter(isIPAdapterModelConfig);
|
||||
state.canvasV2.ipAdapters.entities.forEach((entity) => {
|
||||
const isModelAvailable = ipaModels.some((m) => m.key === entity.ipAdapter.model?.key);
|
||||
if (isModelAvailable) {
|
||||
return;
|
||||
}
|
||||
dispatch(ipaModelChanged({ id: entity.id, modelConfig: null }));
|
||||
});
|
||||
|
||||
state.canvasV2.regions.entities.forEach(({ id, ipAdapters }) => {
|
||||
ipAdapters.forEach(({ id: ipAdapterId, model }) => {
|
||||
const isModelAvailable = ipaModels.some((m) => m.key === model?.key);
|
||||
if (isModelAvailable) {
|
||||
return;
|
||||
}
|
||||
dispatch(rgIPAdapterModelChanged({ id, ipAdapterId, modelConfig: null }));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isAnyOf } from '@reduxjs/toolkit';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||
import { positivePromptChanged } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import {
|
||||
combinatorialToggled,
|
||||
isErrorChanged,
|
||||
@@ -13,8 +13,9 @@ import {
|
||||
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
|
||||
import { getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
|
||||
import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
|
||||
import { stylePresetsApi } from 'services/api/endpoints/stylePresets';
|
||||
import { utilitiesApi } from 'services/api/endpoints/utilities';
|
||||
import { socketConnected } from 'services/events/actions';
|
||||
import { socketConnected } from 'services/events/setEventListeners';
|
||||
|
||||
const matcher = isAnyOf(
|
||||
positivePromptChanged,
|
||||
@@ -22,7 +23,8 @@ const matcher = isAnyOf(
|
||||
maxPromptsChanged,
|
||||
maxPromptsReset,
|
||||
socketConnected,
|
||||
activeStylePresetIdChanged
|
||||
activeStylePresetIdChanged,
|
||||
stylePresetsApi.endpoints.listStylePresets.matchFulfilled
|
||||
);
|
||||
|
||||
export const addDynamicPromptsListener = (startAppListening: AppStartListening) => {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { heightChanged, widthChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||
import { setDefaultSettings } from 'features/parameters/store/actions';
|
||||
import {
|
||||
bboxHeightChanged,
|
||||
bboxWidthChanged,
|
||||
setCfgRescaleMultiplier,
|
||||
setCfgScale,
|
||||
setScheduler,
|
||||
setSteps,
|
||||
vaePrecisionChanged,
|
||||
vaeSelected,
|
||||
} from 'features/parameters/store/generationSlice';
|
||||
} from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { setDefaultSettings } from 'features/parameters/store/actions';
|
||||
import {
|
||||
isParameterCFGRescaleMultiplier,
|
||||
isParameterCFGScale,
|
||||
@@ -30,7 +31,7 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
const state = getState();
|
||||
|
||||
const currentModel = state.generation.model;
|
||||
const currentModel = state.canvasV2.params.model;
|
||||
|
||||
if (!currentModel) {
|
||||
return;
|
||||
@@ -98,13 +99,13 @@ export const addSetDefaultSettingsListener = (startAppListening: AppStartListeni
|
||||
const setSizeOptions = { updateAspectRatio: true, clamp: true };
|
||||
if (width) {
|
||||
if (isParameterWidth(width)) {
|
||||
dispatch(widthChanged({ width, ...setSizeOptions }));
|
||||
dispatch(bboxWidthChanged({ width, ...setSizeOptions }));
|
||||
}
|
||||
}
|
||||
|
||||
if (height) {
|
||||
if (isParameterHeight(height)) {
|
||||
dispatch(heightChanged({ height, ...setSizeOptions }));
|
||||
dispatch(bboxHeightChanged({ height, ...setSizeOptions }));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import { atom } from 'nanostores';
|
||||
import { api } from 'services/api';
|
||||
import { modelsApi } from 'services/api/endpoints/models';
|
||||
import { queueApi, selectQueueStatus } from 'services/api/endpoints/queue';
|
||||
import { socketConnected } from 'services/events/actions';
|
||||
import { socketConnected } from 'services/events/setEventListeners';
|
||||
|
||||
const log = logger('socketio');
|
||||
const log = logger('events');
|
||||
|
||||
const $isFirstConnection = atom(true);
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { socketDisconnected } from 'services/events/actions';
|
||||
|
||||
const log = logger('socketio');
|
||||
|
||||
export const addSocketDisconnectedEventListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: socketDisconnected,
|
||||
effect: () => {
|
||||
log.debug('Disconnected');
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||
import { socketGeneratorProgress } from 'services/events/actions';
|
||||
|
||||
const log = logger('socketio');
|
||||
|
||||
export const addGeneratorProgressEventListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: socketGeneratorProgress,
|
||||
effect: (action) => {
|
||||
log.trace(parseify(action.payload), `Generator progress`);
|
||||
const { invocation_source_id, step, total_steps, progress_image } = action.payload.data;
|
||||
const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]);
|
||||
if (nes) {
|
||||
nes.status = zNodeStatus.enum.IN_PROGRESS;
|
||||
nes.progress = (step + 1) / total_steps;
|
||||
nes.progressImage = progress_image ?? null;
|
||||
upsertExecutionState(nes.nodeId, nes);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,122 +0,0 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
|
||||
import {
|
||||
boardIdSelected,
|
||||
galleryViewChanged,
|
||||
imageSelected,
|
||||
isImageViewerOpenChanged,
|
||||
offsetChanged,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||
import { CANVAS_OUTPUT } from 'features/nodes/util/graph/constants';
|
||||
import { boardsApi } from 'services/api/endpoints/boards';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import { getCategories, getListImagesUrl } from 'services/api/util';
|
||||
import { socketInvocationComplete } from 'services/events/actions';
|
||||
|
||||
// These nodes output an image, but do not actually *save* an image, so we don't want to handle the gallery logic on them
|
||||
const nodeTypeDenylist = ['load_image', 'image'];
|
||||
|
||||
const log = logger('socketio');
|
||||
|
||||
export const addInvocationCompleteEventListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: socketInvocationComplete,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
const { data } = action.payload;
|
||||
log.debug({ data: parseify(data) }, `Invocation complete (${data.invocation.type})`);
|
||||
|
||||
const { result, invocation_source_id } = data;
|
||||
// This complete event has an associated image output
|
||||
if (data.result.type === 'image_output' && !nodeTypeDenylist.includes(data.invocation.type)) {
|
||||
const { image_name } = data.result.image;
|
||||
const { canvas, gallery } = getState();
|
||||
|
||||
// This populates the `getImageDTO` cache
|
||||
const imageDTORequest = dispatch(
|
||||
imagesApi.endpoints.getImageDTO.initiate(image_name, {
|
||||
forceRefetch: true,
|
||||
})
|
||||
);
|
||||
|
||||
const imageDTO = await imageDTORequest.unwrap();
|
||||
imageDTORequest.unsubscribe();
|
||||
|
||||
// Add canvas images to the staging area
|
||||
if (canvas.batchIds.includes(data.batch_id) && data.invocation_source_id === CANVAS_OUTPUT) {
|
||||
dispatch(addImageToStagingArea(imageDTO));
|
||||
}
|
||||
|
||||
if (!imageDTO.is_intermediate) {
|
||||
// update the total images for the board
|
||||
dispatch(
|
||||
boardsApi.util.updateQueryData('getBoardImagesTotal', imageDTO.board_id ?? 'none', (draft) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
draft.total += 1;
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(
|
||||
imagesApi.util.invalidateTags([
|
||||
{ type: 'Board', id: imageDTO.board_id ?? 'none' },
|
||||
{
|
||||
type: 'ImageList',
|
||||
id: getListImagesUrl({
|
||||
board_id: imageDTO.board_id ?? 'none',
|
||||
categories: getCategories(imageDTO),
|
||||
}),
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
const { shouldAutoSwitch } = gallery;
|
||||
|
||||
// If auto-switch is enabled, select the new image
|
||||
if (shouldAutoSwitch) {
|
||||
// if auto-add is enabled, switch the gallery view and board if needed as the image comes in
|
||||
if (gallery.galleryView !== 'images') {
|
||||
dispatch(galleryViewChanged('images'));
|
||||
}
|
||||
|
||||
if (imageDTO.board_id && imageDTO.board_id !== gallery.selectedBoardId) {
|
||||
dispatch(
|
||||
boardIdSelected({
|
||||
boardId: imageDTO.board_id,
|
||||
selectedImageName: imageDTO.image_name,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
dispatch(offsetChanged({ offset: 0 }));
|
||||
|
||||
if (!imageDTO.board_id && gallery.selectedBoardId !== 'none') {
|
||||
dispatch(
|
||||
boardIdSelected({
|
||||
boardId: 'none',
|
||||
selectedImageName: imageDTO.image_name,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
dispatch(imageSelected(imageDTO));
|
||||
dispatch(isImageViewerOpenChanged(true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]);
|
||||
if (nes) {
|
||||
nes.status = zNodeStatus.enum.COMPLETED;
|
||||
if (nes.progress !== null) {
|
||||
nes.progress = 1;
|
||||
}
|
||||
nes.outputs.push(result);
|
||||
upsertExecutionState(nes.nodeId, nes);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||
import { socketInvocationError } from 'services/events/actions';
|
||||
|
||||
const log = logger('socketio');
|
||||
|
||||
export const addInvocationErrorEventListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: socketInvocationError,
|
||||
effect: (action) => {
|
||||
const { invocation_source_id, invocation, error_type, error_message, error_traceback } = action.payload.data;
|
||||
log.error(parseify(action.payload), `Invocation error (${invocation.type})`);
|
||||
const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]);
|
||||
if (nes) {
|
||||
nes.status = zNodeStatus.enum.FAILED;
|
||||
nes.progress = null;
|
||||
nes.progressImage = null;
|
||||
nes.error = {
|
||||
error_type,
|
||||
error_message,
|
||||
error_traceback,
|
||||
};
|
||||
upsertExecutionState(nes.nodeId, nes);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||
import { socketInvocationStarted } from 'services/events/actions';
|
||||
|
||||
const log = logger('socketio');
|
||||
|
||||
export const addInvocationStartedEventListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: socketInvocationStarted,
|
||||
effect: (action) => {
|
||||
log.debug(parseify(action.payload), `Invocation started (${action.payload.data.invocation.type})`);
|
||||
const { invocation_source_id } = action.payload.data;
|
||||
const nes = deepClone($nodeExecutionStates.get()[invocation_source_id]);
|
||||
if (nes) {
|
||||
nes.status = zNodeStatus.enum.IN_PROGRESS;
|
||||
upsertExecutionState(nes.nodeId, nes);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,196 +0,0 @@
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { api, LIST_TAG } from 'services/api';
|
||||
import { modelsApi } from 'services/api/endpoints/models';
|
||||
import {
|
||||
socketModelInstallCancelled,
|
||||
socketModelInstallComplete,
|
||||
socketModelInstallDownloadProgress,
|
||||
socketModelInstallDownloadsComplete,
|
||||
socketModelInstallDownloadStarted,
|
||||
socketModelInstallError,
|
||||
socketModelInstallStarted,
|
||||
} from 'services/events/actions';
|
||||
|
||||
/**
|
||||
* A model install has two main stages - downloading and installing. All these events are namespaced under `model_install_`
|
||||
* which is a bit misleading. For example, a `model_install_started` event is actually fired _after_ the model has fully
|
||||
* downloaded and is being "physically" installed.
|
||||
*
|
||||
* Note: the download events are only fired for remote model installs, not local.
|
||||
*
|
||||
* Here's the expected flow:
|
||||
* - API receives install request, model manager preps the install
|
||||
* - `model_install_download_started` fired when the download starts
|
||||
* - `model_install_download_progress` fired continually until the download is complete
|
||||
* - `model_install_download_complete` fired when the download is complete
|
||||
* - `model_install_started` fired when the "physical" installation starts
|
||||
* - `model_install_complete` fired when the installation is complete
|
||||
* - `model_install_cancelled` fired if the installation is cancelled
|
||||
* - `model_install_error` fired if the installation has an error
|
||||
*/
|
||||
|
||||
const selectModelInstalls = modelsApi.endpoints.listModelInstalls.select();
|
||||
|
||||
export const addModelInstallEventListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: socketModelInstallDownloadStarted,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
const { id } = action.payload.data;
|
||||
const { data } = selectModelInstalls(getState());
|
||||
|
||||
if (!data || !data.find((m) => m.id === id)) {
|
||||
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
|
||||
} else {
|
||||
dispatch(
|
||||
modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => {
|
||||
const modelImport = draft.find((m) => m.id === id);
|
||||
if (modelImport) {
|
||||
modelImport.status = 'downloading';
|
||||
}
|
||||
return draft;
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
actionCreator: socketModelInstallStarted,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
const { id } = action.payload.data;
|
||||
const { data } = selectModelInstalls(getState());
|
||||
|
||||
if (!data || !data.find((m) => m.id === id)) {
|
||||
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
|
||||
} else {
|
||||
dispatch(
|
||||
modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => {
|
||||
const modelImport = draft.find((m) => m.id === id);
|
||||
if (modelImport) {
|
||||
modelImport.status = 'running';
|
||||
}
|
||||
return draft;
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
actionCreator: socketModelInstallDownloadProgress,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
const { bytes, total_bytes, id } = action.payload.data;
|
||||
const { data } = selectModelInstalls(getState());
|
||||
|
||||
if (!data || !data.find((m) => m.id === id)) {
|
||||
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
|
||||
} else {
|
||||
dispatch(
|
||||
modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => {
|
||||
const modelImport = draft.find((m) => m.id === id);
|
||||
if (modelImport) {
|
||||
modelImport.bytes = bytes;
|
||||
modelImport.total_bytes = total_bytes;
|
||||
modelImport.status = 'downloading';
|
||||
}
|
||||
return draft;
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
actionCreator: socketModelInstallComplete,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const { id } = action.payload.data;
|
||||
|
||||
const { data } = selectModelInstalls(getState());
|
||||
|
||||
if (!data || !data.find((m) => m.id === id)) {
|
||||
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
|
||||
} else {
|
||||
dispatch(
|
||||
modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => {
|
||||
const modelImport = draft.find((m) => m.id === id);
|
||||
if (modelImport) {
|
||||
modelImport.status = 'completed';
|
||||
}
|
||||
return draft;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
dispatch(api.util.invalidateTags([{ type: 'ModelConfig', id: LIST_TAG }]));
|
||||
dispatch(api.util.invalidateTags([{ type: 'ModelScanFolderResults', id: LIST_TAG }]));
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
actionCreator: socketModelInstallError,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const { id, error, error_type } = action.payload.data;
|
||||
const { data } = selectModelInstalls(getState());
|
||||
|
||||
if (!data || !data.find((m) => m.id === id)) {
|
||||
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
|
||||
} else {
|
||||
dispatch(
|
||||
modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => {
|
||||
const modelImport = draft.find((m) => m.id === id);
|
||||
if (modelImport) {
|
||||
modelImport.status = 'error';
|
||||
modelImport.error_reason = error_type;
|
||||
modelImport.error = error;
|
||||
}
|
||||
return draft;
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
actionCreator: socketModelInstallCancelled,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const { id } = action.payload.data;
|
||||
const { data } = selectModelInstalls(getState());
|
||||
|
||||
if (!data || !data.find((m) => m.id === id)) {
|
||||
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
|
||||
} else {
|
||||
dispatch(
|
||||
modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => {
|
||||
const modelImport = draft.find((m) => m.id === id);
|
||||
if (modelImport) {
|
||||
modelImport.status = 'cancelled';
|
||||
}
|
||||
return draft;
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
actionCreator: socketModelInstallDownloadsComplete,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const { id } = action.payload.data;
|
||||
const { data } = selectModelInstalls(getState());
|
||||
|
||||
if (!data || !data.find((m) => m.id === id)) {
|
||||
dispatch(api.util.invalidateTags([{ type: 'ModelInstalls' }]));
|
||||
} else {
|
||||
dispatch(
|
||||
modelsApi.util.updateQueryData('listModelInstalls', undefined, (draft) => {
|
||||
const modelImport = draft.find((m) => m.id === id);
|
||||
if (modelImport) {
|
||||
modelImport.status = 'downloads_done';
|
||||
}
|
||||
return draft;
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { socketModelLoadComplete, socketModelLoadStarted } from 'services/events/actions';
|
||||
|
||||
const log = logger('socketio');
|
||||
|
||||
export const addModelLoadEventListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: socketModelLoadStarted,
|
||||
effect: (action) => {
|
||||
const { config, submodel_type } = action.payload.data;
|
||||
const { name, base, type } = config;
|
||||
|
||||
const extras: string[] = [base, type];
|
||||
|
||||
if (submodel_type) {
|
||||
extras.push(submodel_type);
|
||||
}
|
||||
|
||||
const message = `Model load started: ${name} (${extras.join(', ')})`;
|
||||
|
||||
log.debug(action.payload, message);
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
actionCreator: socketModelLoadComplete,
|
||||
effect: (action) => {
|
||||
const { config, submodel_type } = action.payload.data;
|
||||
const { name, base, type } = config;
|
||||
|
||||
const extras: string[] = [base, type];
|
||||
if (submodel_type) {
|
||||
extras.push(submodel_type);
|
||||
}
|
||||
|
||||
const message = `Model load complete: ${name} (${extras.join(', ')})`;
|
||||
|
||||
log.debug(action.payload, message);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,114 +0,0 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState';
|
||||
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||
import ErrorToastDescription, { getTitleFromErrorType } from 'features/toast/ErrorToastDescription';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { forEach } from 'lodash-es';
|
||||
import { queueApi, queueItemsAdapter } from 'services/api/endpoints/queue';
|
||||
import { socketQueueItemStatusChanged } from 'services/events/actions';
|
||||
|
||||
const log = logger('socketio');
|
||||
|
||||
export const addSocketQueueItemStatusChangedEventListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: socketQueueItemStatusChanged,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
// we've got new status for the queue item, batch and queue
|
||||
const {
|
||||
item_id,
|
||||
session_id,
|
||||
status,
|
||||
started_at,
|
||||
updated_at,
|
||||
completed_at,
|
||||
batch_status,
|
||||
queue_status,
|
||||
error_type,
|
||||
error_message,
|
||||
error_traceback,
|
||||
} = action.payload.data;
|
||||
|
||||
log.debug(action.payload, `Queue item ${item_id} status updated: ${status}`);
|
||||
|
||||
// Update this specific queue item in the list of queue items (this is the queue item DTO, without the session)
|
||||
dispatch(
|
||||
queueApi.util.updateQueryData('listQueueItems', undefined, (draft) => {
|
||||
queueItemsAdapter.updateOne(draft, {
|
||||
id: String(item_id),
|
||||
changes: {
|
||||
status,
|
||||
started_at,
|
||||
updated_at: updated_at ?? undefined,
|
||||
completed_at: completed_at ?? undefined,
|
||||
error_type,
|
||||
error_message,
|
||||
error_traceback,
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// Update the queue status (we do not get the processor status here)
|
||||
dispatch(
|
||||
queueApi.util.updateQueryData('getQueueStatus', undefined, (draft) => {
|
||||
if (!draft) {
|
||||
return;
|
||||
}
|
||||
Object.assign(draft.queue, queue_status);
|
||||
})
|
||||
);
|
||||
|
||||
// Update the batch status
|
||||
dispatch(
|
||||
queueApi.util.updateQueryData('getBatchStatus', { batch_id: batch_status.batch_id }, () => batch_status)
|
||||
);
|
||||
|
||||
// Invalidate caches for things we cannot update
|
||||
// TODO: technically, we could possibly update the current session queue item, but feels safer to just request it again
|
||||
dispatch(
|
||||
queueApi.util.invalidateTags([
|
||||
'CurrentSessionQueueItem',
|
||||
'NextSessionQueueItem',
|
||||
'InvocationCacheStatus',
|
||||
{ type: 'SessionQueueItem', id: item_id },
|
||||
])
|
||||
);
|
||||
|
||||
if (status === 'in_progress') {
|
||||
forEach($nodeExecutionStates.get(), (nes) => {
|
||||
if (!nes) {
|
||||
return;
|
||||
}
|
||||
const clone = deepClone(nes);
|
||||
clone.status = zNodeStatus.enum.PENDING;
|
||||
clone.error = null;
|
||||
clone.progress = null;
|
||||
clone.progressImage = null;
|
||||
clone.outputs = [];
|
||||
$nodeExecutionStates.setKey(clone.nodeId, clone);
|
||||
});
|
||||
} else if (status === 'failed' && error_type) {
|
||||
const isLocal = getState().config.isLocal ?? true;
|
||||
const sessionId = session_id;
|
||||
|
||||
toast({
|
||||
id: `INVOCATION_ERROR_${error_type}`,
|
||||
title: getTitleFromErrorType(error_type),
|
||||
status: 'error',
|
||||
duration: null,
|
||||
updateDescription: isLocal,
|
||||
description: (
|
||||
<ErrorToastDescription
|
||||
errorType={error_type}
|
||||
errorMessage={error_message}
|
||||
sessionId={sessionId}
|
||||
isLocal={isLocal}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { stagingAreaImageSaved } from 'features/canvas/store/actions';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
|
||||
export const addStagingAreaImageSavedListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: stagingAreaImageSaved,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
const { imageDTO } = action.payload;
|
||||
|
||||
try {
|
||||
const newImageDTO = await dispatch(
|
||||
imagesApi.endpoints.changeImageIsIntermediate.initiate({
|
||||
imageDTO,
|
||||
is_intermediate: false,
|
||||
})
|
||||
).unwrap();
|
||||
|
||||
// we may need to add it to the autoadd board
|
||||
const { autoAddBoardId } = getState().gallery;
|
||||
|
||||
if (autoAddBoardId && autoAddBoardId !== 'none') {
|
||||
await dispatch(
|
||||
imagesApi.endpoints.addImageToBoard.initiate({
|
||||
imageDTO: newImageDTO,
|
||||
board_id: autoAddBoardId,
|
||||
})
|
||||
);
|
||||
}
|
||||
toast({ id: 'IMAGE_SAVED', title: t('toast.imageSaved'), status: 'success' });
|
||||
} catch (error) {
|
||||
toast({
|
||||
id: 'IMAGE_SAVE_FAILED',
|
||||
title: t('toast.imageSavingFailed'),
|
||||
description: (error as Error)?.message,
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -8,11 +8,12 @@ import { getNeedsUpdate, updateNode } from 'features/nodes/util/node/nodeUpdate'
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
|
||||
const log = logger('workflows');
|
||||
|
||||
export const addUpdateAllNodesRequestedListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: updateAllNodesRequested,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const log = logger('nodes');
|
||||
const { nodes } = getState().nodes.present;
|
||||
const templates = $templates.get();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { $nodeExecutionStates } from 'features/nodes/hooks/useExecutionState';
|
||||
import { workflowLoaded, workflowLoadRequested } from 'features/nodes/store/actions';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { $needsFit } from 'features/nodes/store/reactFlowInstance';
|
||||
@@ -10,11 +10,14 @@ import { graphToWorkflow } from 'features/nodes/util/workflow/graphToWorkflow';
|
||||
import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { t } from 'i18next';
|
||||
import { serializeError } from 'serialize-error';
|
||||
import { checkBoardAccess, checkImageAccess, checkModelAccess } from 'services/api/hooks/accessChecks';
|
||||
import type { GraphAndWorkflowResponse, NonNullableGraph } from 'services/api/types';
|
||||
import { z } from 'zod';
|
||||
import { fromZodError } from 'zod-validation-error';
|
||||
|
||||
const log = logger('workflows');
|
||||
|
||||
const getWorkflow = async (data: GraphAndWorkflowResponse, templates: Templates) => {
|
||||
if (data.workflow) {
|
||||
// Prefer to load the workflow if it's available - it has more information
|
||||
@@ -34,7 +37,6 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
|
||||
startAppListening({
|
||||
actionCreator: workflowLoadRequested,
|
||||
effect: async (action, { dispatch }) => {
|
||||
const log = logger('nodes');
|
||||
const { data, asCopy } = action.payload;
|
||||
const nodeTemplates = $templates.get();
|
||||
|
||||
@@ -46,6 +48,7 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
|
||||
delete workflow.id;
|
||||
}
|
||||
|
||||
$nodeExecutionStates.set({});
|
||||
dispatch(workflowLoaded(workflow));
|
||||
if (!warnings.length) {
|
||||
toast({
|
||||
@@ -69,7 +72,7 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
|
||||
} catch (e) {
|
||||
if (e instanceof WorkflowVersionError) {
|
||||
// The workflow version was not recognized in the valid list of versions
|
||||
log.error({ error: parseify(e) }, e.message);
|
||||
log.error({ error: serializeError(e) }, e.message);
|
||||
toast({
|
||||
id: 'UNABLE_TO_VALIDATE_WORKFLOW',
|
||||
title: t('nodes.unableToValidateWorkflow'),
|
||||
@@ -78,7 +81,7 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
|
||||
});
|
||||
} else if (e instanceof WorkflowMigrationError) {
|
||||
// There was a problem migrating the workflow to the latest version
|
||||
log.error({ error: parseify(e) }, e.message);
|
||||
log.error({ error: serializeError(e) }, e.message);
|
||||
toast({
|
||||
id: 'UNABLE_TO_VALIDATE_WORKFLOW',
|
||||
title: t('nodes.unableToValidateWorkflow'),
|
||||
@@ -90,7 +93,7 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
|
||||
const { message } = fromZodError(e, {
|
||||
prefix: t('nodes.workflowValidation'),
|
||||
});
|
||||
log.error({ error: parseify(e) }, message);
|
||||
log.error({ error: serializeError(e) }, message);
|
||||
toast({
|
||||
id: 'UNABLE_TO_VALIDATE_WORKFLOW',
|
||||
title: t('nodes.unableToValidateWorkflow'),
|
||||
@@ -99,7 +102,7 @@ export const addWorkflowLoadRequestedListener = (startAppListening: AppStartList
|
||||
});
|
||||
} else {
|
||||
// Some other error occurred
|
||||
log.error({ error: parseify(e) }, t('nodes.unknownErrorValidatingWorkflow'));
|
||||
log.error({ error: serializeError(e) }, t('nodes.unknownErrorValidatingWorkflow'));
|
||||
toast({
|
||||
id: 'UNABLE_TO_VALIDATE_WORKFLOW',
|
||||
title: t('nodes.unableToValidateWorkflow'),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { createStore } from 'app/store/store';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import type { AppStore } from 'app/store/store';
|
||||
import { atom } from 'nanostores';
|
||||
|
||||
// Inject socket options and url into window for debugging
|
||||
@@ -22,7 +23,7 @@ class ReduxStoreNotInitialized extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export const $store = atom<Readonly<ReturnType<typeof createStore>> | undefined>();
|
||||
export const $store = atom<Readonly<AppStore | undefined>>();
|
||||
|
||||
export const getStore = () => {
|
||||
const store = $store.get();
|
||||
@@ -31,3 +32,11 @@ export const getStore = () => {
|
||||
}
|
||||
return store;
|
||||
};
|
||||
|
||||
export const useAppStore = () => {
|
||||
const store = useStore($store);
|
||||
if (!store) {
|
||||
throw new ReduxStoreNotInitialized();
|
||||
}
|
||||
return store;
|
||||
};
|
||||
|
||||
@@ -3,37 +3,26 @@ import { autoBatchEnhancer, combineReducers, configureStore } from '@reduxjs/too
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { idbKeyValDriver } from 'app/store/enhancers/reduxRemember/driver';
|
||||
import { errorHandler } from 'app/store/enhancers/reduxRemember/errors';
|
||||
import type { JSONObject } from 'common/types';
|
||||
import { canvasPersistConfig, canvasSlice } from 'features/canvas/store/canvasSlice';
|
||||
import type { SerializableObject } from 'common/types';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice';
|
||||
import {
|
||||
controlAdaptersPersistConfig,
|
||||
controlAdaptersSlice,
|
||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||
import {
|
||||
controlLayersPersistConfig,
|
||||
controlLayersSlice,
|
||||
controlLayersUndoableConfig,
|
||||
} from 'features/controlLayers/store/controlLayersSlice';
|
||||
import { canvasV2PersistConfig, canvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice';
|
||||
import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||
import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import { hrfPersistConfig, hrfSlice } from 'features/hrf/store/hrfSlice';
|
||||
import { loraPersistConfig, loraSlice } from 'features/lora/store/loraSlice';
|
||||
import { modelManagerV2PersistConfig, modelManagerV2Slice } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
||||
import { nodesPersistConfig, nodesSlice, nodesUndoableConfig } from 'features/nodes/store/nodesSlice';
|
||||
import { workflowSettingsPersistConfig, workflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
|
||||
import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workflowSlice';
|
||||
import { generationPersistConfig, generationSlice } from 'features/parameters/store/generationSlice';
|
||||
import { upscalePersistConfig, upscaleSlice } from 'features/parameters/store/upscaleSlice';
|
||||
import { queueSlice } from 'features/queue/store/queueSlice';
|
||||
import { sdxlPersistConfig, sdxlSlice } from 'features/sdxl/store/sdxlSlice';
|
||||
import { stylePresetPersistConfig, stylePresetSlice } from 'features/stylePresets/store/stylePresetSlice';
|
||||
import { configSlice } from 'features/system/store/configSlice';
|
||||
import { systemPersistConfig, systemSlice } from 'features/system/store/systemSlice';
|
||||
import { uiPersistConfig, uiSlice } from 'features/ui/store/uiSlice';
|
||||
import { diff } from 'jsondiffpatch';
|
||||
import { defaultsDeep, keys, omit, pick } from 'lodash-es';
|
||||
import { keys, mergeWith, omit, pick } from 'lodash-es';
|
||||
import dynamicMiddlewares from 'redux-dynamic-middlewares';
|
||||
import type { SerializeFunction, UnserializeFunction } from 'redux-remember';
|
||||
import { rememberEnhancer, rememberReducer } from 'redux-remember';
|
||||
@@ -48,27 +37,24 @@ import { actionsDenylist } from './middleware/devtools/actionsDenylist';
|
||||
import { stateSanitizer } from './middleware/devtools/stateSanitizer';
|
||||
import { listenerMiddleware } from './middleware/listenerMiddleware';
|
||||
|
||||
const log = logger('system');
|
||||
|
||||
const allReducers = {
|
||||
[canvasSlice.name]: canvasSlice.reducer,
|
||||
[api.reducerPath]: api.reducer,
|
||||
[gallerySlice.name]: gallerySlice.reducer,
|
||||
[generationSlice.name]: generationSlice.reducer,
|
||||
[nodesSlice.name]: undoable(nodesSlice.reducer, nodesUndoableConfig),
|
||||
[systemSlice.name]: systemSlice.reducer,
|
||||
[configSlice.name]: configSlice.reducer,
|
||||
[uiSlice.name]: uiSlice.reducer,
|
||||
[controlAdaptersSlice.name]: controlAdaptersSlice.reducer,
|
||||
[dynamicPromptsSlice.name]: dynamicPromptsSlice.reducer,
|
||||
[deleteImageModalSlice.name]: deleteImageModalSlice.reducer,
|
||||
[changeBoardModalSlice.name]: changeBoardModalSlice.reducer,
|
||||
[loraSlice.name]: loraSlice.reducer,
|
||||
[modelManagerV2Slice.name]: modelManagerV2Slice.reducer,
|
||||
[sdxlSlice.name]: sdxlSlice.reducer,
|
||||
[queueSlice.name]: queueSlice.reducer,
|
||||
[workflowSlice.name]: workflowSlice.reducer,
|
||||
[hrfSlice.name]: hrfSlice.reducer,
|
||||
[controlLayersSlice.name]: undoable(controlLayersSlice.reducer, controlLayersUndoableConfig),
|
||||
[canvasV2Slice.name]: canvasV2Slice.reducer,
|
||||
[workflowSettingsSlice.name]: workflowSettingsSlice.reducer,
|
||||
[api.reducerPath]: api.reducer,
|
||||
[upscaleSlice.name]: upscaleSlice.reducer,
|
||||
[stylePresetSlice.name]: stylePresetSlice.reducer,
|
||||
};
|
||||
@@ -100,27 +86,21 @@ export type PersistConfig<T = any> = {
|
||||
};
|
||||
|
||||
const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
|
||||
[canvasPersistConfig.name]: canvasPersistConfig,
|
||||
[galleryPersistConfig.name]: galleryPersistConfig,
|
||||
[generationPersistConfig.name]: generationPersistConfig,
|
||||
[nodesPersistConfig.name]: nodesPersistConfig,
|
||||
[systemPersistConfig.name]: systemPersistConfig,
|
||||
[workflowPersistConfig.name]: workflowPersistConfig,
|
||||
[uiPersistConfig.name]: uiPersistConfig,
|
||||
[controlAdaptersPersistConfig.name]: controlAdaptersPersistConfig,
|
||||
[dynamicPromptsPersistConfig.name]: dynamicPromptsPersistConfig,
|
||||
[sdxlPersistConfig.name]: sdxlPersistConfig,
|
||||
[loraPersistConfig.name]: loraPersistConfig,
|
||||
[modelManagerV2PersistConfig.name]: modelManagerV2PersistConfig,
|
||||
[hrfPersistConfig.name]: hrfPersistConfig,
|
||||
[controlLayersPersistConfig.name]: controlLayersPersistConfig,
|
||||
[canvasV2PersistConfig.name]: canvasV2PersistConfig,
|
||||
[workflowSettingsPersistConfig.name]: workflowSettingsPersistConfig,
|
||||
[upscalePersistConfig.name]: upscalePersistConfig,
|
||||
[stylePresetPersistConfig.name]: stylePresetPersistConfig,
|
||||
};
|
||||
|
||||
const unserialize: UnserializeFunction = (data, key) => {
|
||||
const log = logger('system');
|
||||
const persistConfig = persistConfigs[key as keyof typeof persistConfigs];
|
||||
if (!persistConfig) {
|
||||
throw new Error(`No persist config for slice "${key}"`);
|
||||
@@ -130,17 +110,21 @@ const unserialize: UnserializeFunction = (data, key) => {
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
// strip out old keys
|
||||
const stripped = pick(parsed, keys(initialState));
|
||||
const stripped = pick(deepClone(parsed), keys(initialState));
|
||||
// run (additive) migrations
|
||||
const migrated = migrate(stripped);
|
||||
// merge in initial state as default values, covering any missing keys
|
||||
const transformed = defaultsDeep(migrated, initialState);
|
||||
/*
|
||||
* Merge in initial state as default values, covering any missing keys. You might be tempted to use _.defaultsDeep,
|
||||
* but that merges arrays by index and partial objects by key. Using an identity function as the customizer results
|
||||
* in behaviour like defaultsDeep, but doesn't overwrite any values that are not undefined in the migrated state.
|
||||
*/
|
||||
const transformed = mergeWith(migrated, initialState, (objVal) => objVal);
|
||||
|
||||
log.debug(
|
||||
{
|
||||
persistedData: parsed,
|
||||
rehydratedData: transformed,
|
||||
diff: diff(parsed, transformed) as JSONObject, // this is always serializable
|
||||
diff: diff(parsed, transformed) as SerializableObject, // this is always serializable
|
||||
},
|
||||
`Rehydrated slice "${key}"`
|
||||
);
|
||||
@@ -202,7 +186,8 @@ export const createStore = (uniqueStoreKey?: string, persist = true) =>
|
||||
},
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<ReturnType<typeof createStore>['getState']>;
|
||||
export type AppStore = ReturnType<typeof createStore>;
|
||||
export type RootState = ReturnType<AppStore['getState']>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type AppThunkDispatch = ThunkDispatch<RootState, any, UnknownAction>;
|
||||
export type AppDispatch = ReturnType<typeof createStore>['dispatch'];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants';
|
||||
import type { FilterType } from 'features/controlLayers/store/types';
|
||||
import type { ParameterPrecision, ParameterScheduler } from 'features/parameters/types/parameterSchemas';
|
||||
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import type { O } from 'ts-toolbelt';
|
||||
|
||||
/**
|
||||
@@ -72,7 +72,7 @@ export type AppConfig = {
|
||||
maxUpscaleDimension?: number;
|
||||
allowPrivateBoards: boolean;
|
||||
allowPrivateStylePresets: boolean;
|
||||
disabledTabs: InvokeTabName[];
|
||||
disabledTabs: TabName[];
|
||||
disabledFeatures: AppFeature[];
|
||||
disabledSDFeatures: SDFeature[];
|
||||
nodesAllowlist: string[] | undefined;
|
||||
@@ -83,7 +83,7 @@ export type AppConfig = {
|
||||
sd: {
|
||||
defaultModel?: string;
|
||||
disabledControlNetModels: string[];
|
||||
disabledControlNetProcessors: (keyof typeof CONTROLNET_PROCESSORS)[];
|
||||
disabledControlNetProcessors: FilterType[];
|
||||
// Core parameters
|
||||
iterations: NumericalParameterConfig;
|
||||
width: NumericalParameterConfig; // initial value comes from model
|
||||
|
||||
@@ -33,28 +33,23 @@ type IAINoImageFallbackProps = FlexProps & {
|
||||
};
|
||||
|
||||
export const IAINoContentFallback = memo((props: IAINoImageFallbackProps) => {
|
||||
const { icon = PiImageBold, boxSize = 16, sx, ...rest } = props;
|
||||
|
||||
const styles = useMemo(
|
||||
() => ({
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 'base',
|
||||
flexDir: 'column',
|
||||
gap: 2,
|
||||
userSelect: 'none',
|
||||
opacity: 0.7,
|
||||
color: 'base.500',
|
||||
fontSize: 'md',
|
||||
...sx,
|
||||
}),
|
||||
[sx]
|
||||
);
|
||||
const { icon = PiImageBold, boxSize = 16, ...rest } = props;
|
||||
|
||||
return (
|
||||
<Flex sx={styles} {...rest}>
|
||||
<Flex
|
||||
w="full"
|
||||
h="full"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
borderRadius="base"
|
||||
flexDir="column"
|
||||
gap={2}
|
||||
userSelect="none"
|
||||
opacity={0.7}
|
||||
color="base.500"
|
||||
fontSize="md"
|
||||
{...rest}
|
||||
>
|
||||
{icon && <Icon as={icon} boxSize={boxSize} opacity={0.7} />}
|
||||
{props.label && <Text textAlign="center">{props.label}</Text>}
|
||||
</Flex>
|
||||
|
||||
158
invokeai/frontend/web/src/common/hooks/interactionScopes.ts
Normal file
158
invokeai/frontend/web/src/common/hooks/interactionScopes.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { objectKeys } from 'common/util/objectKeys';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import type { Atom } from 'nanostores';
|
||||
import { atom, computed } from 'nanostores';
|
||||
import type { RefObject } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
const log = logger('system');
|
||||
|
||||
const _INTERACTION_SCOPES = ['gallery', 'canvas', 'stagingArea', 'workflows', 'imageViewer'] as const;
|
||||
|
||||
type InteractionScope = (typeof _INTERACTION_SCOPES)[number];
|
||||
|
||||
export const $activeScopes = atom<Set<InteractionScope>>(new Set());
|
||||
|
||||
type InteractionScopeData = {
|
||||
targets: Set<HTMLElement>;
|
||||
$isActive: Atom<boolean>;
|
||||
};
|
||||
|
||||
export const INTERACTION_SCOPES: Record<InteractionScope, InteractionScopeData> = _INTERACTION_SCOPES.reduce(
|
||||
(acc, region) => {
|
||||
acc[region] = {
|
||||
targets: new Set(),
|
||||
$isActive: computed($activeScopes, (activeScopes) => activeScopes.has(region)),
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{} as Record<InteractionScope, InteractionScopeData>
|
||||
);
|
||||
|
||||
const formatScopes = (interactionScopes: Set<InteractionScope>) => {
|
||||
if (interactionScopes.size === 0) {
|
||||
return 'none';
|
||||
}
|
||||
return Array.from(interactionScopes).join(', ');
|
||||
};
|
||||
|
||||
export const addScope = (scope: InteractionScope) => {
|
||||
const currentScopes = $activeScopes.get();
|
||||
if (currentScopes.has(scope)) {
|
||||
return;
|
||||
}
|
||||
const newScopes = new Set(currentScopes);
|
||||
newScopes.add(scope);
|
||||
$activeScopes.set(newScopes);
|
||||
log.trace(`Added scope ${scope}: ${formatScopes($activeScopes.get())}`);
|
||||
};
|
||||
|
||||
export const removeScope = (scope: InteractionScope) => {
|
||||
const currentScopes = $activeScopes.get();
|
||||
if (!currentScopes.has(scope)) {
|
||||
return;
|
||||
}
|
||||
const newScopes = new Set(currentScopes);
|
||||
newScopes.delete(scope);
|
||||
$activeScopes.set(newScopes);
|
||||
log.trace(`Removed scope ${scope}: ${formatScopes($activeScopes.get())}`);
|
||||
};
|
||||
|
||||
export const setScopes = (scopes: InteractionScope[]) => {
|
||||
const newScopes = new Set(scopes);
|
||||
$activeScopes.set(newScopes);
|
||||
log.trace(`Set scopes: ${formatScopes($activeScopes.get())}`);
|
||||
};
|
||||
|
||||
export const useScopeOnFocus = (scope: InteractionScope, ref: RefObject<HTMLElement>) => {
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
INTERACTION_SCOPES[scope].targets.add(element);
|
||||
|
||||
return () => {
|
||||
INTERACTION_SCOPES[scope].targets.delete(element);
|
||||
};
|
||||
}, [ref, scope]);
|
||||
};
|
||||
|
||||
type UseScopeOnMountOptions = {
|
||||
mount?: boolean;
|
||||
unmount?: boolean;
|
||||
};
|
||||
|
||||
const defaultUseScopeOnMountOptions: UseScopeOnMountOptions = {
|
||||
mount: true,
|
||||
unmount: true,
|
||||
};
|
||||
|
||||
export const useScopeOnMount = (scope: InteractionScope, options?: UseScopeOnMountOptions) => {
|
||||
useEffect(() => {
|
||||
const { mount, unmount } = { ...defaultUseScopeOnMountOptions, ...options };
|
||||
|
||||
if (mount) {
|
||||
addScope(scope);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (unmount) {
|
||||
removeScope(scope);
|
||||
}
|
||||
};
|
||||
}, [options, scope]);
|
||||
};
|
||||
|
||||
export const useScopeImperativeApi = (scope: InteractionScope) => {
|
||||
const api = useMemo(() => {
|
||||
return {
|
||||
add: () => {
|
||||
addScope(scope);
|
||||
},
|
||||
remove: () => {
|
||||
removeScope(scope);
|
||||
},
|
||||
};
|
||||
}, [scope]);
|
||||
|
||||
return api;
|
||||
};
|
||||
|
||||
const handleFocusEvent = (_event: FocusEvent) => {
|
||||
const activeElement = document.activeElement;
|
||||
if (!(activeElement instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newActiveScopes = new Set<InteractionScope>();
|
||||
|
||||
for (const scope of objectKeys(INTERACTION_SCOPES)) {
|
||||
for (const element of INTERACTION_SCOPES[scope].targets) {
|
||||
if (element.contains(activeElement)) {
|
||||
newActiveScopes.add(scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const oldActiveScopes = $activeScopes.get();
|
||||
if (!isEqual(oldActiveScopes, newActiveScopes)) {
|
||||
$activeScopes.set(newActiveScopes);
|
||||
log.trace(`Scopes changed: ${formatScopes($activeScopes.get())}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const useScopeFocusWatcher = () => {
|
||||
useAssertSingleton('useScopeFocusWatcher');
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('focus', handleFocusEvent, true);
|
||||
return () => {
|
||||
window.removeEventListener('focus', handleFocusEvent, true);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { WritableAtom } from 'nanostores';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
export const useBoolean = (initialValue: boolean) => {
|
||||
@@ -19,3 +20,33 @@ export const useBoolean = (initialValue: boolean) => {
|
||||
|
||||
return api;
|
||||
};
|
||||
|
||||
export const buildUseBoolean = ($boolean: WritableAtom<boolean>) => {
|
||||
return () => {
|
||||
const setTrue = useCallback(() => {
|
||||
$boolean.set(true);
|
||||
}, []);
|
||||
const setFalse = useCallback(() => {
|
||||
$boolean.set(false);
|
||||
}, []);
|
||||
const set = useCallback((value: boolean) => {
|
||||
$boolean.set(value);
|
||||
}, []);
|
||||
const toggle = useCallback(() => {
|
||||
$boolean.set(!$boolean.get());
|
||||
}, []);
|
||||
|
||||
const api = useMemo(
|
||||
() => ({
|
||||
setTrue,
|
||||
setFalse,
|
||||
set,
|
||||
toggle,
|
||||
$boolean,
|
||||
}),
|
||||
[set, setFalse, setTrue, toggle]
|
||||
);
|
||||
|
||||
return api;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { Accept, FileRejection } from 'react-dropzone';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
@@ -14,13 +14,9 @@ const accept: Accept = {
|
||||
'image/jpeg': ['.jpg', '.jpeg', '.png'],
|
||||
};
|
||||
|
||||
const selectPostUploadAction = createMemoizedSelector(activeTabNameSelector, (activeTabName) => {
|
||||
const selectPostUploadAction = createMemoizedSelector(selectActiveTab, (activeTabName) => {
|
||||
let postUploadAction: PostUploadAction = { type: 'TOAST' };
|
||||
|
||||
if (activeTabName === 'canvas') {
|
||||
postUploadAction = { type: 'SET_CANVAS_INITIAL_IMAGE' };
|
||||
}
|
||||
|
||||
if (activeTabName === 'upscaling') {
|
||||
postUploadAction = { type: 'SET_UPSCALE_INITIAL_IMAGE' };
|
||||
}
|
||||
@@ -30,10 +26,9 @@ const selectPostUploadAction = createMemoizedSelector(activeTabNameSelector, (ac
|
||||
|
||||
export const useFullscreenDropzone = () => {
|
||||
const { t } = useTranslation();
|
||||
const postUploadAction = useAppSelector(selectPostUploadAction);
|
||||
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
|
||||
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
|
||||
|
||||
const postUploadAction = useAppSelector(selectPostUploadAction);
|
||||
const [uploadImage] = useUploadImageMutation();
|
||||
|
||||
const fileRejectionCallback = useCallback(
|
||||
@@ -51,7 +46,7 @@ export const useFullscreenDropzone = () => {
|
||||
);
|
||||
|
||||
const fileAcceptedCallback = useCallback(
|
||||
async (file: File) => {
|
||||
(file: File) => {
|
||||
uploadImage({
|
||||
file,
|
||||
image_category: 'user',
|
||||
@@ -101,7 +96,7 @@ export const useFullscreenDropzone = () => {
|
||||
|
||||
useEffect(() => {
|
||||
// This is a hack to allow pasting images into the uploader
|
||||
const handlePaste = async (e: ClipboardEvent) => {
|
||||
const handlePaste = (e: ClipboardEvent) => {
|
||||
if (!dropzone.inputRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { addScope, removeScope, setScopes } from 'common/hooks/interactionScopes';
|
||||
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
|
||||
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
||||
import { useQueueBack } from 'features/queue/hooks/useQueueBack';
|
||||
@@ -16,7 +17,7 @@ export const useGlobalHotkeys = () => {
|
||||
['ctrl+enter', 'meta+enter'],
|
||||
queueBack,
|
||||
{
|
||||
enabled: () => !isDisabledQueueBack && !isLoadingQueueBack,
|
||||
enabled: !isDisabledQueueBack && !isLoadingQueueBack,
|
||||
preventDefault: true,
|
||||
enableOnFormTags: ['input', 'textarea', 'select'],
|
||||
},
|
||||
@@ -29,7 +30,7 @@ export const useGlobalHotkeys = () => {
|
||||
['ctrl+shift+enter', 'meta+shift+enter'],
|
||||
queueFront,
|
||||
{
|
||||
enabled: () => !isDisabledQueueFront && !isLoadingQueueFront,
|
||||
enabled: !isDisabledQueueFront && !isLoadingQueueFront,
|
||||
preventDefault: true,
|
||||
enableOnFormTags: ['input', 'textarea', 'select'],
|
||||
},
|
||||
@@ -46,7 +47,7 @@ export const useGlobalHotkeys = () => {
|
||||
['shift+x'],
|
||||
cancelQueueItem,
|
||||
{
|
||||
enabled: () => !isDisabledCancelQueueItem && !isLoadingCancelQueueItem,
|
||||
enabled: !isDisabledCancelQueueItem && !isLoadingCancelQueueItem,
|
||||
preventDefault: true,
|
||||
},
|
||||
[cancelQueueItem, isDisabledCancelQueueItem, isLoadingCancelQueueItem]
|
||||
@@ -58,7 +59,7 @@ export const useGlobalHotkeys = () => {
|
||||
['ctrl+shift+x', 'meta+shift+x'],
|
||||
clearQueue,
|
||||
{
|
||||
enabled: () => !isDisabledClearQueue && !isLoadingClearQueue,
|
||||
enabled: !isDisabledClearQueue && !isLoadingClearQueue,
|
||||
preventDefault: true,
|
||||
},
|
||||
[clearQueue, isDisabledClearQueue, isLoadingClearQueue]
|
||||
@@ -68,6 +69,8 @@ export const useGlobalHotkeys = () => {
|
||||
'1',
|
||||
() => {
|
||||
dispatch(setActiveTab('generation'));
|
||||
addScope('canvas');
|
||||
removeScope('workflows');
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
@@ -75,7 +78,9 @@ export const useGlobalHotkeys = () => {
|
||||
useHotkeys(
|
||||
'2',
|
||||
() => {
|
||||
dispatch(setActiveTab('canvas'));
|
||||
dispatch(setActiveTab('upscaling'));
|
||||
removeScope('canvas');
|
||||
removeScope('workflows');
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
@@ -84,6 +89,8 @@ export const useGlobalHotkeys = () => {
|
||||
'3',
|
||||
() => {
|
||||
dispatch(setActiveTab('workflows'));
|
||||
removeScope('canvas');
|
||||
addScope('workflows');
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
@@ -93,6 +100,7 @@ export const useGlobalHotkeys = () => {
|
||||
() => {
|
||||
if (isModelManagerEnabled) {
|
||||
dispatch(setActiveTab('models'));
|
||||
setScopes([]);
|
||||
}
|
||||
},
|
||||
[dispatch, isModelManagerEnabled]
|
||||
@@ -102,6 +110,7 @@ export const useGlobalHotkeys = () => {
|
||||
isModelManagerEnabled ? '5' : '4',
|
||||
() => {
|
||||
dispatch(setActiveTab('queue'));
|
||||
setScopes([]);
|
||||
},
|
||||
[dispatch, isModelManagerEnabled]
|
||||
);
|
||||
|
||||
@@ -32,7 +32,7 @@ export const useGroupedModelCombobox = <T extends AnyModelConfig>(
|
||||
arg: UseGroupedModelComboboxArg<T>
|
||||
): UseGroupedModelComboboxReturn => {
|
||||
const { t } = useTranslation();
|
||||
const base_model = useAppSelector((s) => s.generation.model?.base ?? 'sdxl');
|
||||
const base_model = useAppSelector((s) => s.canvasV2.params.model?.base ?? 'sdxl');
|
||||
const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading, groupByType = false } = arg;
|
||||
const options = useMemo<GroupBase<ComboboxOption>[]>(() => {
|
||||
if (!modelConfigs) {
|
||||
|
||||
@@ -1,67 +1,46 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $isConnected } from 'app/hooks/useSocketIO';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
selectControlAdapterAll,
|
||||
selectControlAdaptersSlice,
|
||||
} from 'features/controlAdapters/store/controlAdaptersSlice';
|
||||
import { isControlNetOrT2IAdapter } from 'features/controlAdapters/store/types';
|
||||
import { selectControlLayersSlice } from 'features/controlLayers/store/controlLayersSlice';
|
||||
import type { Layer } from 'features/controlLayers/store/types';
|
||||
import { selectCanvasV2Slice } from 'features/controlLayers/store/canvasV2Slice';
|
||||
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
|
||||
import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||
import type { Templates } from 'features/nodes/store/types';
|
||||
import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { selectGenerationSlice } from 'features/parameters/store/generationSlice';
|
||||
import { selectUpscalelice } from 'features/parameters/store/upscaleSlice';
|
||||
import { selectConfigSlice } from 'features/system/store/configSlice';
|
||||
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import i18n from 'i18next';
|
||||
import { forEach, upperFirst } from 'lodash-es';
|
||||
import { useMemo } from 'react';
|
||||
import { getConnectedEdges } from 'reactflow';
|
||||
|
||||
const LAYER_TYPE_TO_TKEY: Record<Layer['type'], string> = {
|
||||
initial_image_layer: 'controlLayers.globalInitialImage',
|
||||
control_adapter_layer: 'controlLayers.globalControlAdapter',
|
||||
ip_adapter_layer: 'controlLayers.globalIPAdapter',
|
||||
regional_guidance_layer: 'controlLayers.regionalGuidance',
|
||||
};
|
||||
const LAYER_TYPE_TO_TKEY = {
|
||||
ip_adapter: 'controlLayers.ipAdapter',
|
||||
inpaint_mask: 'controlLayers.inpaintMask',
|
||||
regional_guidance: 'controlLayers.regionalGuidance',
|
||||
raster_layer: 'controlLayers.raster',
|
||||
control_layer: 'controlLayers.globalControlAdapter',
|
||||
} as const;
|
||||
|
||||
const createSelector = (templates: Templates) =>
|
||||
const createSelector = (templates: Templates, isConnected: boolean) =>
|
||||
createMemoizedSelector(
|
||||
[
|
||||
selectControlAdaptersSlice,
|
||||
selectGenerationSlice,
|
||||
selectSystemSlice,
|
||||
selectNodesSlice,
|
||||
selectWorkflowSettingsSlice,
|
||||
selectDynamicPromptsSlice,
|
||||
selectControlLayersSlice,
|
||||
activeTabNameSelector,
|
||||
selectCanvasV2Slice,
|
||||
selectUpscalelice,
|
||||
selectConfigSlice,
|
||||
selectActiveTab,
|
||||
],
|
||||
(
|
||||
controlAdapters,
|
||||
generation,
|
||||
system,
|
||||
nodes,
|
||||
workflowSettings,
|
||||
dynamicPrompts,
|
||||
controlLayers,
|
||||
activeTabName,
|
||||
upscale,
|
||||
config
|
||||
) => {
|
||||
const { model } = generation;
|
||||
const { size } = controlLayers.present;
|
||||
const { positivePrompt } = controlLayers.present;
|
||||
|
||||
const { isConnected } = system;
|
||||
(system, nodes, workflowSettings, dynamicPrompts, canvasV2, upscale, config, activeTabName) => {
|
||||
const { bbox } = canvasV2;
|
||||
const { model, positivePrompt } = canvasV2.params;
|
||||
|
||||
const reasons: { prefix?: string; content: string }[] = [];
|
||||
|
||||
@@ -114,6 +93,26 @@ const createSelector = (templates: Templates) =>
|
||||
});
|
||||
});
|
||||
}
|
||||
} else if (activeTabName === 'upscaling') {
|
||||
if (!upscale.upscaleInitialImage) {
|
||||
reasons.push({ content: i18n.t('upscaling.missingUpscaleInitialImage') });
|
||||
} else if (config.maxUpscaleDimension) {
|
||||
const { width, height } = upscale.upscaleInitialImage;
|
||||
const { scale } = upscale;
|
||||
|
||||
const maxPixels = config.maxUpscaleDimension ** 2;
|
||||
const upscaledPixels = width * scale * height * scale;
|
||||
|
||||
if (upscaledPixels > maxPixels) {
|
||||
reasons.push({ content: i18n.t('upscaling.exceedsMaxSize') });
|
||||
}
|
||||
}
|
||||
if (!upscale.upscaleModel) {
|
||||
reasons.push({ content: i18n.t('upscaling.missingUpscaleModel') });
|
||||
}
|
||||
if (!upscale.tileControlnetModel) {
|
||||
reasons.push({ content: i18n.t('upscaling.missingTileControlNetModel') });
|
||||
}
|
||||
} else {
|
||||
if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) {
|
||||
reasons.push({ content: i18n.t('parameters.invoke.noPrompts') });
|
||||
@@ -123,140 +122,115 @@ const createSelector = (templates: Templates) =>
|
||||
reasons.push({ content: i18n.t('parameters.invoke.noModelSelected') });
|
||||
}
|
||||
|
||||
if (activeTabName === 'generation') {
|
||||
// Handling for generation tab
|
||||
controlLayers.present.layers
|
||||
.filter((l) => l.isEnabled)
|
||||
.forEach((l, i) => {
|
||||
const layerLiteral = i18n.t('controlLayers.layers_one');
|
||||
const layerNumber = i + 1;
|
||||
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[l.type]);
|
||||
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
|
||||
const problems: string[] = [];
|
||||
if (l.type === 'control_adapter_layer') {
|
||||
// Must have model
|
||||
if (!l.controlAdapter.model) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected'));
|
||||
}
|
||||
// Model base must match
|
||||
if (l.controlAdapter.model?.base !== model?.base) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel'));
|
||||
}
|
||||
// Must have a control image OR, if it has a processor, it must have a processed image
|
||||
if (!l.controlAdapter.image) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoImageSelected'));
|
||||
} else if (l.controlAdapter.processorConfig && !l.controlAdapter.processedImage) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.controlAdapterImageNotProcessed'));
|
||||
}
|
||||
// T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL)
|
||||
if (l.controlAdapter.type === 't2i_adapter') {
|
||||
const multiple = model?.base === 'sdxl' ? 32 : 64;
|
||||
if (size.width % multiple !== 0 || size.height % multiple !== 0) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (l.type === 'ip_adapter_layer') {
|
||||
// Must have model
|
||||
if (!l.ipAdapter.model) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
|
||||
}
|
||||
// Model base must match
|
||||
if (l.ipAdapter.model?.base !== model?.base) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
|
||||
}
|
||||
// Must have an image
|
||||
if (!l.ipAdapter.image) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
|
||||
}
|
||||
}
|
||||
|
||||
if (l.type === 'initial_image_layer') {
|
||||
// Must have an image
|
||||
if (!l.image) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.initialImageNoImageSelected'));
|
||||
}
|
||||
}
|
||||
|
||||
if (l.type === 'regional_guidance_layer') {
|
||||
// Must have a region
|
||||
if (l.maskObjects.length === 0) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.rgNoRegion'));
|
||||
}
|
||||
// Must have at least 1 prompt or IP Adapter
|
||||
if (l.positivePrompt === null && l.negativePrompt === null && l.ipAdapters.length === 0) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters'));
|
||||
}
|
||||
l.ipAdapters.forEach((ipAdapter) => {
|
||||
// Must have model
|
||||
if (!ipAdapter.model) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
|
||||
}
|
||||
// Model base must match
|
||||
if (ipAdapter.model?.base !== model?.base) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
|
||||
}
|
||||
// Must have an image
|
||||
if (!ipAdapter.image) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (problems.length) {
|
||||
const content = upperFirst(problems.join(', '));
|
||||
reasons.push({ prefix, content });
|
||||
}
|
||||
});
|
||||
} else if (activeTabName === 'upscaling') {
|
||||
if (!upscale.upscaleInitialImage) {
|
||||
reasons.push({ content: i18n.t('upscaling.missingUpscaleInitialImage') });
|
||||
} else if (config.maxUpscaleDimension) {
|
||||
const { width, height } = upscale.upscaleInitialImage;
|
||||
const { scale } = upscale;
|
||||
|
||||
const maxPixels = config.maxUpscaleDimension ** 2;
|
||||
const upscaledPixels = width * scale * height * scale;
|
||||
|
||||
if (upscaledPixels > maxPixels) {
|
||||
reasons.push({ content: i18n.t('upscaling.exceedsMaxSize') });
|
||||
canvasV2.controlLayers.entities
|
||||
.filter((controlLayer) => controlLayer.isEnabled)
|
||||
.forEach((controlLayer, i) => {
|
||||
const layerLiteral = i18n.t('controlLayers.layers_one');
|
||||
const layerNumber = i + 1;
|
||||
const layerType = i18n.t(LAYER_TYPE_TO_TKEY['control_layer']);
|
||||
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
|
||||
const problems: string[] = [];
|
||||
// Must have model
|
||||
if (!controlLayer.controlAdapter.model) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.controlAdapterNoModelSelected'));
|
||||
}
|
||||
}
|
||||
if (!upscale.upscaleModel) {
|
||||
reasons.push({ content: i18n.t('upscaling.missingUpscaleModel') });
|
||||
}
|
||||
if (!upscale.tileControlnetModel) {
|
||||
reasons.push({ content: i18n.t('upscaling.missingTileControlNetModel') });
|
||||
}
|
||||
} else {
|
||||
// Handling for all other tabs
|
||||
selectControlAdapterAll(controlAdapters)
|
||||
.filter((ca) => ca.isEnabled)
|
||||
.forEach((ca, i) => {
|
||||
if (!ca.isEnabled) {
|
||||
return;
|
||||
// Model base must match
|
||||
if (controlLayer.controlAdapter.model?.base !== model?.base) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.controlAdapterIncompatibleBaseModel'));
|
||||
}
|
||||
// T2I Adapters require images have dimensions that are multiples of 64 (SD1.5) or 32 (SDXL)
|
||||
if (controlLayer.controlAdapter.type === 't2i_adapter') {
|
||||
const multiple = model?.base === 'sdxl' ? 32 : 64;
|
||||
if (bbox.rect.width % multiple !== 0 || bbox.rect.height % multiple !== 0) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.t2iAdapterIncompatibleDimensions', { multiple }));
|
||||
}
|
||||
}
|
||||
|
||||
if (!ca.model) {
|
||||
reasons.push({ content: i18n.t('parameters.invoke.noModelForControlAdapter', { number: i + 1 }) });
|
||||
} else if (ca.model.base !== model?.base) {
|
||||
// This should never happen, just a sanity check
|
||||
reasons.push({
|
||||
content: i18n.t('parameters.invoke.incompatibleBaseModelForControlAdapter', { number: i + 1 }),
|
||||
});
|
||||
if (problems.length) {
|
||||
const content = upperFirst(problems.join(', '));
|
||||
reasons.push({ prefix, content });
|
||||
}
|
||||
});
|
||||
|
||||
canvasV2.ipAdapters.entities
|
||||
.filter((entity) => entity.isEnabled)
|
||||
.forEach((entity, i) => {
|
||||
const layerLiteral = i18n.t('controlLayers.layers_one');
|
||||
const layerNumber = i + 1;
|
||||
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
|
||||
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
|
||||
const problems: string[] = [];
|
||||
|
||||
// Must have model
|
||||
if (!entity.ipAdapter.model) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
|
||||
}
|
||||
// Model base must match
|
||||
if (entity.ipAdapter.model?.base !== model?.base) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
|
||||
}
|
||||
// Must have an image
|
||||
if (!entity.ipAdapter.image) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
|
||||
}
|
||||
|
||||
if (problems.length) {
|
||||
const content = upperFirst(problems.join(', '));
|
||||
reasons.push({ prefix, content });
|
||||
}
|
||||
});
|
||||
|
||||
canvasV2.regions.entities
|
||||
.filter((entity) => entity.isEnabled)
|
||||
.forEach((entity, i) => {
|
||||
const layerLiteral = i18n.t('controlLayers.layers_one');
|
||||
const layerNumber = i + 1;
|
||||
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
|
||||
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
|
||||
const problems: string[] = [];
|
||||
// Must have a region
|
||||
if (entity.objects.length === 0) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.rgNoRegion'));
|
||||
}
|
||||
// Must have at least 1 prompt or IP Adapter
|
||||
if (entity.positivePrompt === null && entity.negativePrompt === null && entity.ipAdapters.length === 0) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.rgNoPromptsOrIPAdapters'));
|
||||
}
|
||||
entity.ipAdapters.forEach((ipAdapter) => {
|
||||
// Must have model
|
||||
if (!ipAdapter.model) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoModelSelected'));
|
||||
}
|
||||
|
||||
if (
|
||||
!ca.controlImage ||
|
||||
(isControlNetOrT2IAdapter(ca) && !ca.processedControlImage && ca.processorType !== 'none')
|
||||
) {
|
||||
reasons.push({
|
||||
content: i18n.t('parameters.invoke.noControlImageForControlAdapter', { number: i + 1 }),
|
||||
});
|
||||
// Model base must match
|
||||
if (ipAdapter.model?.base !== model?.base) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.ipAdapterIncompatibleBaseModel'));
|
||||
}
|
||||
// Must have an image
|
||||
if (!ipAdapter.image) {
|
||||
problems.push(i18n.t('parameters.invoke.layer.ipAdapterNoImageSelected'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (problems.length) {
|
||||
const content = upperFirst(problems.join(', '));
|
||||
reasons.push({ prefix, content });
|
||||
}
|
||||
});
|
||||
|
||||
canvasV2.rasterLayers.entities
|
||||
.filter((entity) => entity.isEnabled)
|
||||
.forEach((entity, i) => {
|
||||
const layerLiteral = i18n.t('controlLayers.layers_one');
|
||||
const layerNumber = i + 1;
|
||||
const layerType = i18n.t(LAYER_TYPE_TO_TKEY[entity.type]);
|
||||
const prefix = `${layerLiteral} #${layerNumber} (${layerType})`;
|
||||
const problems: string[] = [];
|
||||
|
||||
if (problems.length) {
|
||||
const content = upperFirst(problems.join(', '));
|
||||
reasons.push({ prefix, content });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { isReady: !reasons.length, reasons };
|
||||
@@ -265,7 +239,8 @@ const createSelector = (templates: Templates) =>
|
||||
|
||||
export const useIsReadyToEnqueue = () => {
|
||||
const templates = useStore($templates);
|
||||
const selector = useMemo(() => createSelector(templates), [templates]);
|
||||
const isConnected = useStore($isConnected);
|
||||
const selector = useMemo(() => createSelector(templates, isConnected), [templates, isConnected]);
|
||||
const value = useAppSelector(selector);
|
||||
return value;
|
||||
};
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
// https://stackoverflow.com/a/73731908
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
type UseSingleAndDoubleClickOptions = {
|
||||
onSingleClick: () => void;
|
||||
onDoubleClick: () => void;
|
||||
latency?: number;
|
||||
};
|
||||
|
||||
export function useSingleAndDoubleClick({
|
||||
onSingleClick,
|
||||
onDoubleClick,
|
||||
latency = 250,
|
||||
}: UseSingleAndDoubleClickOptions): () => void {
|
||||
const [click, setClick] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (click === 1) {
|
||||
onSingleClick();
|
||||
}
|
||||
setClick(0);
|
||||
}, latency);
|
||||
|
||||
if (click === 2) {
|
||||
onDoubleClick();
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [click, onDoubleClick, latency, onSingleClick]);
|
||||
|
||||
const onClick = useCallback(() => setClick((prev) => prev + 1), []);
|
||||
|
||||
return onClick;
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };
|
||||
|
||||
export interface JSONObject {
|
||||
[k: string]: JSONValue;
|
||||
}
|
||||
type SerializableValue = string | number | boolean | null | undefined | SerializableValue[] | SerializableObject;
|
||||
export type SerializableObject = {
|
||||
[k: string | number]: SerializableValue;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { SyncableMap } from './SyncableMap';
|
||||
|
||||
describe('SyncableMap', () => {
|
||||
it('should initialize with entries', () => {
|
||||
const initialEntries = [
|
||||
['key1', 'value1'],
|
||||
['key2', 'value2'],
|
||||
] as const;
|
||||
const map = new SyncableMap(initialEntries);
|
||||
expect(map.size).toBe(2);
|
||||
expect(map.get('key1')).toBe('value1');
|
||||
expect(map.get('key2')).toBe('value2');
|
||||
});
|
||||
|
||||
it('should notify subscribers when a key is set', () => {
|
||||
const map = new SyncableMap<string, string>();
|
||||
const subscriber = vi.fn();
|
||||
map.subscribe(subscriber);
|
||||
|
||||
map.set('key1', 'value1');
|
||||
|
||||
expect(subscriber).toHaveBeenCalledTimes(1);
|
||||
expect(map.get('key1')).toBe('value1');
|
||||
});
|
||||
|
||||
it('should notify subscribers when a key is deleted', () => {
|
||||
const map = new SyncableMap<string, string>([['key1', 'value1']]);
|
||||
const subscriber = vi.fn();
|
||||
map.subscribe(subscriber);
|
||||
|
||||
map.delete('key1');
|
||||
|
||||
expect(subscriber).toHaveBeenCalledTimes(1);
|
||||
expect(map.get('key1')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should notify subscribers when the map is cleared', () => {
|
||||
const map = new SyncableMap<string, string>([
|
||||
['key1', 'value1'],
|
||||
['key2', 'value2'],
|
||||
]);
|
||||
const subscriber = vi.fn();
|
||||
map.subscribe(subscriber);
|
||||
|
||||
map.clear();
|
||||
|
||||
expect(subscriber).toHaveBeenCalledTimes(1);
|
||||
expect(map.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should not notify unsubscribed callbacks', () => {
|
||||
const map = new SyncableMap<string, string>();
|
||||
const subscriber = vi.fn();
|
||||
const unsubscribe = map.subscribe(subscriber);
|
||||
|
||||
unsubscribe();
|
||||
|
||||
map.set('key1', 'value1');
|
||||
|
||||
expect(subscriber).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return a snapshot of the current state', () => {
|
||||
const map = new SyncableMap<string, string>([['key1', 'value1']]);
|
||||
|
||||
const snapshot = map.getSnapshot();
|
||||
|
||||
expect(snapshot.size).toBe(1);
|
||||
expect(snapshot.get('key1')).toBe('value1');
|
||||
});
|
||||
|
||||
it('should return the same snapshot if there were no changes', () => {
|
||||
const map = new SyncableMap<string, string>([['key1', 'value1']]);
|
||||
|
||||
const firstSnapshot = map.getSnapshot();
|
||||
const secondSnapshot = map.getSnapshot();
|
||||
|
||||
expect(firstSnapshot).toBe(secondSnapshot);
|
||||
});
|
||||
|
||||
it('should return a new snapshot if changes were made', () => {
|
||||
const map = new SyncableMap<string, string>([['key1', 'value1']]);
|
||||
|
||||
const firstSnapshot = map.getSnapshot();
|
||||
map.set('key2', 'value2');
|
||||
const secondSnapshot = map.getSnapshot();
|
||||
|
||||
expect(firstSnapshot).not.toBe(secondSnapshot);
|
||||
expect(secondSnapshot.size).toBe(2);
|
||||
});
|
||||
|
||||
it('should consider different snapshots unequal', () => {
|
||||
const map = new SyncableMap<string, string>([['key1', 'value1']]);
|
||||
|
||||
const firstSnapshot = map.getSnapshot();
|
||||
map.set('key2', 'value2');
|
||||
const secondSnapshot = map.getSnapshot();
|
||||
|
||||
expect(map['areSnapshotsEqual'](firstSnapshot, secondSnapshot)).toBe(false);
|
||||
});
|
||||
|
||||
it('should consider identical snapshots equal', () => {
|
||||
const map = new SyncableMap<string, string>([
|
||||
['key1', 'value1'],
|
||||
['key2', 'value2'],
|
||||
]);
|
||||
|
||||
const firstSnapshot = map.getSnapshot();
|
||||
const secondSnapshot = map.getSnapshot();
|
||||
|
||||
expect(map['areSnapshotsEqual'](firstSnapshot, secondSnapshot)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* A Map that allows for subscribing to changes and getting a snapshot of the current state.
|
||||
*
|
||||
* It can be used with the `useSyncExternalStore` hook to sync the state of the map with a React component.
|
||||
*
|
||||
* Reactivity is shallow, so changes to nested objects will not trigger a re-render.
|
||||
*/
|
||||
export class SyncableMap<K, V> extends Map<K, V> {
|
||||
private subscriptions = new Set<() => void>();
|
||||
private lastSnapshot: Map<K, V> | null = null;
|
||||
|
||||
constructor(entries?: readonly (readonly [K, V])[] | null) {
|
||||
super(entries);
|
||||
}
|
||||
|
||||
set = (key: K, value: V): this => {
|
||||
super.set(key, value);
|
||||
this.notifySubscribers();
|
||||
return this;
|
||||
};
|
||||
|
||||
delete = (key: K): boolean => {
|
||||
const result = super.delete(key);
|
||||
this.notifySubscribers();
|
||||
return result;
|
||||
};
|
||||
|
||||
clear = (): void => {
|
||||
super.clear();
|
||||
this.notifySubscribers();
|
||||
};
|
||||
|
||||
/**
|
||||
* Notify all subscribers that the map has changed.
|
||||
*/
|
||||
private notifySubscribers = () => {
|
||||
for (const callback of this.subscriptions) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribe to changes to the map.
|
||||
* @param callback A function to call when the map changes
|
||||
* @returns A function to unsubscribe from changes
|
||||
*/
|
||||
subscribe = (callback: () => void): (() => void) => {
|
||||
this.subscriptions.add(callback);
|
||||
return () => {
|
||||
this.subscriptions.delete(callback);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a snapshot of the current state of the map.
|
||||
* @returns A snapshot of the current state of the map
|
||||
*/
|
||||
getSnapshot = (): Map<K, V> => {
|
||||
const currentSnapshot = new Map(this);
|
||||
if (!this.lastSnapshot || !this.areSnapshotsEqual(this.lastSnapshot, currentSnapshot)) {
|
||||
this.lastSnapshot = currentSnapshot;
|
||||
}
|
||||
|
||||
return this.lastSnapshot;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compare two snapshots to determine if they are equal.
|
||||
* @param snapshotA The first snapshot to compare
|
||||
* @param snapshotB The second snapshot to compare
|
||||
* @returns Whether the two snapshots are equal
|
||||
*/
|
||||
private areSnapshotsEqual = (snapshotA: Map<K, V>, snapshotB: Map<K, V>): boolean => {
|
||||
if (snapshotA.size !== snapshotB.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const [key, value] of snapshotA) {
|
||||
if (!Object.is(value, snapshotB.get(key))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
export const getImageDataTransparency = (pixels: Uint8ClampedArray) => {
|
||||
let isFullyTransparent = true;
|
||||
let isPartiallyTransparent = false;
|
||||
const len = pixels.length;
|
||||
let i = 3;
|
||||
for (i; i < len; i += 4) {
|
||||
if (pixels[i] === 255) {
|
||||
isFullyTransparent = false;
|
||||
} else {
|
||||
isPartiallyTransparent = true;
|
||||
}
|
||||
if (!isFullyTransparent && isPartiallyTransparent) {
|
||||
return { isFullyTransparent, isPartiallyTransparent };
|
||||
}
|
||||
}
|
||||
return { isFullyTransparent, isPartiallyTransparent };
|
||||
};
|
||||
|
||||
export const areAnyPixelsBlack = (pixels: Uint8ClampedArray) => {
|
||||
const len = pixels.length;
|
||||
let i = 0;
|
||||
for (i; i < len; ) {
|
||||
if (pixels[i++] === 0 && pixels[i++] === 0 && pixels[i++] === 0 && pixels[i++] === 255) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -1,85 +1,170 @@
|
||||
import { moveBackward, moveForward, moveToBack, moveToFront } from 'common/util/arrayUtils';
|
||||
import { moveOneToEnd, moveOneToStart, moveToEnd, moveToStart } from 'common/util/arrayUtils';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('Array Manipulation Functions', () => {
|
||||
const originalArray = ['a', 'b', 'c', 'd'];
|
||||
describe('moveForwardOne', () => {
|
||||
it('should move an item forward by one position', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveForward(array, (item) => item === 'b');
|
||||
expect(result).toEqual(['a', 'c', 'b', 'd']);
|
||||
});
|
||||
|
||||
it('should do nothing if the item is at the end', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveForward(array, (item) => item === 'd');
|
||||
expect(result).toEqual(['a', 'b', 'c', 'd']);
|
||||
});
|
||||
describe('moveOneToEnd', () => {
|
||||
describe('with callback', () => {
|
||||
it('should move an item forward by one position', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveOneToEnd(array, (item) => item === 'b');
|
||||
expect(result).toEqual(['a', 'c', 'b', 'd']);
|
||||
});
|
||||
|
||||
it("should leave the array unchanged if the item isn't in the array", () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveForward(array, (item) => item === 'z');
|
||||
expect(result).toEqual(originalArray);
|
||||
it('should do nothing if the item is at the end', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveOneToEnd(array, (item) => item === 'd');
|
||||
expect(result).toEqual(['a', 'b', 'c', 'd']);
|
||||
});
|
||||
|
||||
it("should leave the array unchanged if the item isn't in the array", () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveOneToEnd(array, (item) => item === 'z');
|
||||
expect(result).toEqual(originalArray);
|
||||
});
|
||||
});
|
||||
describe('with item', () => {
|
||||
it('should move an item forward by one position', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveOneToEnd(array, (item) => item === 'b');
|
||||
expect(result).toEqual(['a', 'c', 'b', 'd']);
|
||||
});
|
||||
|
||||
it('should do nothing if the item is at the end', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveOneToEnd(array, (item) => item === 'd');
|
||||
expect(result).toEqual(['a', 'b', 'c', 'd']);
|
||||
});
|
||||
|
||||
it("should leave the array unchanged if the item isn't in the array", () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveOneToEnd(array, (item) => item === 'z');
|
||||
expect(result).toEqual(originalArray);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('moveToFront', () => {
|
||||
it('should move an item to the front', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveToFront(array, (item) => item === 'c');
|
||||
expect(result).toEqual(['c', 'a', 'b', 'd']);
|
||||
});
|
||||
describe('moveToStart', () => {
|
||||
describe('with callback', () => {
|
||||
it('should move an item to the front', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveToStart(array, (item) => item === 'c');
|
||||
expect(result).toEqual(['c', 'a', 'b', 'd']);
|
||||
});
|
||||
|
||||
it('should do nothing if the item is already at the front', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveToFront(array, (item) => item === 'a');
|
||||
expect(result).toEqual(['a', 'b', 'c', 'd']);
|
||||
});
|
||||
it('should do nothing if the item is already at the front', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveToStart(array, (item) => item === 'a');
|
||||
expect(result).toEqual(['a', 'b', 'c', 'd']);
|
||||
});
|
||||
|
||||
it("should leave the array unchanged if the item isn't in the array", () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveToFront(array, (item) => item === 'z');
|
||||
expect(result).toEqual(originalArray);
|
||||
it("should leave the array unchanged if the item isn't in the array", () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveToStart(array, (item) => item === 'z');
|
||||
expect(result).toEqual(originalArray);
|
||||
});
|
||||
});
|
||||
describe('with item', () => {
|
||||
it('should move an item to the front', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveToStart(array, 'c');
|
||||
expect(result).toEqual(['c', 'a', 'b', 'd']);
|
||||
});
|
||||
|
||||
it('should do nothing if the item is already at the front', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveToStart(array, 'a');
|
||||
expect(result).toEqual(['a', 'b', 'c', 'd']);
|
||||
});
|
||||
|
||||
it("should leave the array unchanged if the item isn't in the array", () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveToStart(array, 'z');
|
||||
expect(result).toEqual(originalArray);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('moveBackwardsOne', () => {
|
||||
it('should move an item backward by one position', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveBackward(array, (item) => item === 'c');
|
||||
expect(result).toEqual(['a', 'c', 'b', 'd']);
|
||||
});
|
||||
describe('moveOneToStart', () => {
|
||||
describe('with callback', () => {
|
||||
it('should move an item backward by one position', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveOneToStart(array, (item) => item === 'c');
|
||||
expect(result).toEqual(['a', 'c', 'b', 'd']);
|
||||
});
|
||||
|
||||
it('should do nothing if the item is at the beginning', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveBackward(array, (item) => item === 'a');
|
||||
expect(result).toEqual(['a', 'b', 'c', 'd']);
|
||||
});
|
||||
it('should do nothing if the item is at the beginning', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveOneToStart(array, (item) => item === 'a');
|
||||
expect(result).toEqual(['a', 'b', 'c', 'd']);
|
||||
});
|
||||
|
||||
it("should leave the array unchanged if the item isn't in the array", () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveBackward(array, (item) => item === 'z');
|
||||
expect(result).toEqual(originalArray);
|
||||
it("should leave the array unchanged if the item isn't in the array", () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveOneToStart(array, (item) => item === 'z');
|
||||
expect(result).toEqual(originalArray);
|
||||
});
|
||||
});
|
||||
describe('with item', () => {
|
||||
it('should move an item backward by one position', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveOneToStart(array, 'c');
|
||||
expect(result).toEqual(['a', 'c', 'b', 'd']);
|
||||
});
|
||||
|
||||
it('should do nothing if the item is at the beginning', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveOneToStart(array, 'a');
|
||||
expect(result).toEqual(['a', 'b', 'c', 'd']);
|
||||
});
|
||||
|
||||
it("should leave the array unchanged if the item isn't in the array", () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveOneToStart(array, 'z');
|
||||
expect(result).toEqual(originalArray);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('moveToBack', () => {
|
||||
it('should move an item to the back', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveToBack(array, (item) => item === 'b');
|
||||
expect(result).toEqual(['a', 'c', 'd', 'b']);
|
||||
});
|
||||
describe('moveToEnd', () => {
|
||||
describe('with callback', () => {
|
||||
it('should move an item to the back', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveToEnd(array, (item) => item === 'b');
|
||||
expect(result).toEqual(['a', 'c', 'd', 'b']);
|
||||
});
|
||||
|
||||
it('should do nothing if the item is already at the back', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveToBack(array, (item) => item === 'd');
|
||||
expect(result).toEqual(['a', 'b', 'c', 'd']);
|
||||
});
|
||||
it('should do nothing if the item is already at the back', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveToEnd(array, (item) => item === 'd');
|
||||
expect(result).toEqual(['a', 'b', 'c', 'd']);
|
||||
});
|
||||
|
||||
it("should leave the array unchanged if the item isn't in the array", () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveToBack(array, (item) => item === 'z');
|
||||
expect(result).toEqual(originalArray);
|
||||
it("should leave the array unchanged if the item isn't in the array", () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveToEnd(array, (item) => item === 'z');
|
||||
expect(result).toEqual(originalArray);
|
||||
});
|
||||
});
|
||||
describe('with item', () => {
|
||||
it('should move an item to the back', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveToEnd(array, 'b');
|
||||
expect(result).toEqual(['a', 'c', 'd', 'b']);
|
||||
});
|
||||
|
||||
it('should do nothing if the item is already at the back', () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveToEnd(array, 'd');
|
||||
expect(result).toEqual(['a', 'b', 'c', 'd']);
|
||||
});
|
||||
|
||||
it("should leave the array unchanged if the item isn't in the array", () => {
|
||||
const array = [...originalArray];
|
||||
const result = moveToEnd(array, 'z');
|
||||
expect(result).toEqual(originalArray);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,37 +1,45 @@
|
||||
export const moveForward = <T>(array: T[], callback: (item: T) => boolean): T[] => {
|
||||
const index = array.findIndex(callback);
|
||||
if (index >= 0 && index < array.length - 1) {
|
||||
//@ts-expect-error - These indicies are safe per the previous check
|
||||
[array[index], array[index + 1]] = [array[index + 1], array[index]];
|
||||
}
|
||||
return array;
|
||||
};
|
||||
|
||||
export const moveToFront = <T>(array: T[], callback: (item: T) => boolean): T[] => {
|
||||
const index = array.findIndex(callback);
|
||||
export function moveToStart<T>(array: T[], selectItemCallback: (item: T) => boolean): T[];
|
||||
export function moveToStart<T>(array: T[], item: T): T[];
|
||||
export function moveToStart<T>(array: T[], arg1: T | ((item: T) => boolean)): T[] {
|
||||
const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1);
|
||||
if (index > 0) {
|
||||
const [item] = array.splice(index, 1);
|
||||
//@ts-expect-error - These indicies are safe per the previous check
|
||||
array.unshift(item);
|
||||
}
|
||||
return array;
|
||||
};
|
||||
}
|
||||
|
||||
export const moveBackward = <T>(array: T[], callback: (item: T) => boolean): T[] => {
|
||||
const index = array.findIndex(callback);
|
||||
export function moveOneToStart<T>(array: T[], selectItemCallback: (item: T) => boolean): T[];
|
||||
export function moveOneToStart<T>(array: T[], item: T): T[];
|
||||
export function moveOneToStart<T>(array: T[], arg1: T | ((item: T) => boolean)): T[] {
|
||||
const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1);
|
||||
if (index > 0) {
|
||||
//@ts-expect-error - These indicies are safe per the previous check
|
||||
[array[index], array[index - 1]] = [array[index - 1], array[index]];
|
||||
}
|
||||
return array;
|
||||
};
|
||||
}
|
||||
|
||||
export const moveToBack = <T>(array: T[], callback: (item: T) => boolean): T[] => {
|
||||
const index = array.findIndex(callback);
|
||||
export function moveToEnd<T>(array: T[], selectItemCallback: (item: T) => boolean): T[];
|
||||
export function moveToEnd<T>(array: T[], item: T): T[];
|
||||
export function moveToEnd<T>(array: T[], arg1: T | ((item: T) => boolean)): T[] {
|
||||
const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1);
|
||||
if (index >= 0 && index < array.length - 1) {
|
||||
const [item] = array.splice(index, 1);
|
||||
//@ts-expect-error - These indicies are safe per the previous check
|
||||
array.push(item);
|
||||
}
|
||||
return array;
|
||||
};
|
||||
}
|
||||
|
||||
export function moveOneToEnd<T>(array: T[], selectItemCallback: (item: T) => boolean): T[];
|
||||
export function moveOneToEnd<T>(array: T[], item: T): T[];
|
||||
export function moveOneToEnd<T>(array: T[], arg1: T | ((item: T) => boolean)): T[] {
|
||||
const index = arg1 instanceof Function ? array.findIndex(arg1) : array.indexOf(arg1);
|
||||
if (index >= 0 && index < array.length - 1) {
|
||||
//@ts-expect-error - These indicies are safe per the previous check
|
||||
[array[index], array[index + 1]] = [array[index + 1], array[index]];
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { RgbaColor } from 'react-colorful';
|
||||
import type { RgbaColor, RgbColor } from 'react-colorful';
|
||||
|
||||
export function rgbaToHex(color: RgbaColor, alpha: boolean = false): string {
|
||||
const hex = ((1 << 24) + (color.r << 16) + (color.g << 8) + color.b).toString(16).slice(1);
|
||||
@@ -15,3 +15,13 @@ export function hexToRGBA(hex: string, alpha: number) {
|
||||
const b = parseInt(hex.substring(4, 6), 16);
|
||||
return { r, g, b, a: alpha };
|
||||
}
|
||||
|
||||
export const rgbaColorToString = (color: RgbaColor): string => {
|
||||
const { r, g, b, a } = color;
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
};
|
||||
|
||||
export const rgbColorToString = (color: RgbColor): string => {
|
||||
const { r, g, b } = color;
|
||||
return `rgba(${r}, ${g}, ${b})`;
|
||||
};
|
||||
|
||||
@@ -7,8 +7,8 @@ import { $authToken } from 'app/store/nanostores/authToken';
|
||||
* @returns A function that takes a URL and returns a Promise that resolves with a Blob
|
||||
*/
|
||||
|
||||
export const convertImageUrlToBlob = async (url: string) =>
|
||||
new Promise<Blob | null>((resolve) => {
|
||||
export const convertImageUrlToBlob = (url: string) =>
|
||||
new Promise<Blob | null>((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
@@ -17,17 +17,23 @@ export const convertImageUrlToBlob = async (url: string) =>
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) {
|
||||
reject(new Error('Failed to get canvas context'));
|
||||
return;
|
||||
}
|
||||
context.drawImage(img, 0, 0);
|
||||
resolve(
|
||||
new Promise<Blob | null>((resolve) => {
|
||||
canvas.toBlob(function (blob) {
|
||||
resolve(blob);
|
||||
}, 'image/png');
|
||||
})
|
||||
);
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
resolve(blob);
|
||||
} else {
|
||||
reject(new Error('Failed to convert image to blob'));
|
||||
}
|
||||
}, 'image/png');
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
reject(new Error('Image failed to load. The URL may be invalid or the object may not exist.'));
|
||||
};
|
||||
|
||||
img.crossOrigin = $authToken.get() ? 'use-credentials' : 'anonymous';
|
||||
img.src = url;
|
||||
});
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export const isInputElement = (el: HTMLElement) => {
|
||||
return (
|
||||
el.tagName.toLowerCase() === 'input' ||
|
||||
el.tagName.toLowerCase() === 'textarea' ||
|
||||
el.tagName.toLowerCase() === 'select'
|
||||
);
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
type Base64AndCaption = {
|
||||
base64: string;
|
||||
caption: string;
|
||||
};
|
||||
|
||||
const openBase64ImageInTab = (images: Base64AndCaption[]) => {
|
||||
const w = window.open('');
|
||||
if (!w) {
|
||||
return;
|
||||
}
|
||||
|
||||
images.forEach((i) => {
|
||||
const image = new Image();
|
||||
image.src = i.base64;
|
||||
|
||||
w.document.write(i.caption);
|
||||
w.document.write('</br>');
|
||||
w.document.write(image.outerHTML);
|
||||
w.document.write('</br></br>');
|
||||
});
|
||||
};
|
||||
|
||||
export default openBase64ImageInTab;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user