Merge branch 'pin-options-panel' of https://github.com/psychedelicious/stable-diffusion into psychedelicious-pin-options-panel
- from PR #1301
@ -336,7 +336,8 @@ class InvokeAIWebServer:
|
||||
|
||||
seed = (
|
||||
original_image["metadata"]["seed"]
|
||||
if "seed" in original_image["metadata"]
|
||||
if "metadata" in original_image
|
||||
and "seed" in original_image["metadata"]
|
||||
else "unknown_seed"
|
||||
)
|
||||
|
||||
@ -561,6 +562,9 @@ class InvokeAIWebServer:
|
||||
)
|
||||
generation_parameters["init_img"] = cropped_init_image
|
||||
|
||||
if generation_parameters["is_mask_empty"]:
|
||||
generation_parameters["init_mask"] = None
|
||||
else:
|
||||
# grab an Image of the mask
|
||||
mask_image = Image.open(
|
||||
io.BytesIO(
|
||||
@ -569,7 +573,6 @@ class InvokeAIWebServer:
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# crop the mask image
|
||||
cropped_mask_image = copy_image_from_bounding_box(
|
||||
mask_image, **generation_parameters["bounding_box"]
|
||||
@ -750,7 +753,7 @@ class InvokeAIWebServer:
|
||||
all_parameters["init_img"] = init_img_url
|
||||
|
||||
if "init_mask" in all_parameters:
|
||||
all_parameters["init_mask"] = "" #
|
||||
all_parameters["init_mask"] = "" # TODO: store the mask in metadata
|
||||
|
||||
metadata = self.parameters_to_generated_image_metadata(all_parameters)
|
||||
|
||||
|
After Width: | Height: | Size: 284 KiB |
BIN
docs/assets/preflight-checks/inputs/Lincoln-and-Parrot-512.png
Normal file
After Width: | Height: | Size: 252 KiB |
BIN
docs/assets/preflight-checks/inputs/curly.png
Normal file
After Width: | Height: | Size: 428 KiB |
BIN
docs/assets/preflight-checks/outputs/000001.1863159593.png
Normal file
After Width: | Height: | Size: 331 KiB |
BIN
docs/assets/preflight-checks/outputs/000002.1151955949.png
Normal file
After Width: | Height: | Size: 369 KiB |
BIN
docs/assets/preflight-checks/outputs/000003.2736230502.png
Normal file
After Width: | Height: | Size: 362 KiB |
BIN
docs/assets/preflight-checks/outputs/000004.42.png
Normal file
After Width: | Height: | Size: 329 KiB |
BIN
docs/assets/preflight-checks/outputs/000005.42.png
Normal file
After Width: | Height: | Size: 329 KiB |
BIN
docs/assets/preflight-checks/outputs/000006.478163327.png
Normal file
After Width: | Height: | Size: 377 KiB |
BIN
docs/assets/preflight-checks/outputs/000007.2407640369.png
Normal file
After Width: | Height: | Size: 328 KiB |
BIN
docs/assets/preflight-checks/outputs/000008.2772421987.png
Normal file
After Width: | Height: | Size: 380 KiB |
BIN
docs/assets/preflight-checks/outputs/000009.3532317557.png
Normal file
After Width: | Height: | Size: 372 KiB |
BIN
docs/assets/preflight-checks/outputs/000010.2028635318.png
Normal file
After Width: | Height: | Size: 401 KiB |
BIN
docs/assets/preflight-checks/outputs/000011.1111168647.png
Normal file
After Width: | Height: | Size: 441 KiB |
BIN
docs/assets/preflight-checks/outputs/000012.1476370516.png
Normal file
After Width: | Height: | Size: 451 KiB |
BIN
docs/assets/preflight-checks/outputs/000013.4281108706.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
docs/assets/preflight-checks/outputs/000014.2396987386.png
Normal file
After Width: | Height: | Size: 338 KiB |
BIN
docs/assets/preflight-checks/outputs/000015.1252923272.png
Normal file
After Width: | Height: | Size: 271 KiB |
BIN
docs/assets/preflight-checks/outputs/000016.2633891320.png
Normal file
After Width: | Height: | Size: 353 KiB |
BIN
docs/assets/preflight-checks/outputs/000017.1134411920.png
Normal file
After Width: | Height: | Size: 330 KiB |
BIN
docs/assets/preflight-checks/outputs/000018.47.png
Normal file
After Width: | Height: | Size: 439 KiB |
BIN
docs/assets/preflight-checks/outputs/000019.47.png
Normal file
After Width: | Height: | Size: 463 KiB |
BIN
docs/assets/preflight-checks/outputs/000020.47.png
Normal file
After Width: | Height: | Size: 444 KiB |
BIN
docs/assets/preflight-checks/outputs/000021.47.png
Normal file
After Width: | Height: | Size: 468 KiB |
BIN
docs/assets/preflight-checks/outputs/000022.47.png
Normal file
After Width: | Height: | Size: 466 KiB |
BIN
docs/assets/preflight-checks/outputs/000023.47.png
Normal file
After Width: | Height: | Size: 475 KiB |
BIN
docs/assets/preflight-checks/outputs/000024.1029061431.png
Normal file
After Width: | Height: | Size: 429 KiB |
BIN
docs/assets/preflight-checks/outputs/000025.1284519352.png
Normal file
After Width: | Height: | Size: 429 KiB |
BIN
docs/assets/preflight-checks/outputs/curly.942491079.gfpgan.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
After Width: | Height: | Size: 477 KiB |
BIN
docs/assets/preflight-checks/outputs/curly.942491079.outcrop.png
Normal file
After Width: | Height: | Size: 476 KiB |
After Width: | Height: | Size: 434 KiB |
116
docs/assets/preflight-checks/outputs/invoke_log.md
Normal file
@ -0,0 +1,116 @@
|
||||
## 000001.1863159593.png
|
||||
![](000001.1863159593.png)
|
||||
|
||||
banana sushi -s 50 -S 1863159593 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
## 000002.1151955949.png
|
||||
![](000002.1151955949.png)
|
||||
|
||||
banana sushi -s 50 -S 1151955949 -W 512 -H 512 -C 7.5 -A plms
|
||||
## 000003.2736230502.png
|
||||
![](000003.2736230502.png)
|
||||
|
||||
banana sushi -s 50 -S 2736230502 -W 512 -H 512 -C 7.5 -A ddim
|
||||
## 000004.42.png
|
||||
![](000004.42.png)
|
||||
|
||||
banana sushi -s 50 -S 42 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
## 000005.42.png
|
||||
![](000005.42.png)
|
||||
|
||||
banana sushi -s 50 -S 42 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
## 000006.478163327.png
|
||||
![](000006.478163327.png)
|
||||
|
||||
banana sushi -s 50 -S 478163327 -W 640 -H 448 -C 7.5 -A k_lms
|
||||
## 000007.2407640369.png
|
||||
![](000007.2407640369.png)
|
||||
|
||||
banana sushi -s 50 -S 42 -W 512 -H 512 -C 7.5 -A k_lms -V 2407640369:0.1
|
||||
## 000008.2772421987.png
|
||||
![](000008.2772421987.png)
|
||||
|
||||
banana sushi -s 50 -S 42 -W 512 -H 512 -C 7.5 -A k_lms -V 2772421987:0.1
|
||||
## 000009.3532317557.png
|
||||
![](000009.3532317557.png)
|
||||
|
||||
banana sushi -s 50 -S 42 -W 512 -H 512 -C 7.5 -A k_lms -V 3532317557:0.1
|
||||
## 000010.2028635318.png
|
||||
![](000010.2028635318.png)
|
||||
|
||||
banana sushi -s 50 -S 2028635318 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
## 000011.1111168647.png
|
||||
![](000011.1111168647.png)
|
||||
|
||||
pond with waterlillies -s 50 -S 1111168647 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
## 000012.1476370516.png
|
||||
![](000012.1476370516.png)
|
||||
|
||||
pond with waterlillies -s 50 -S 1476370516 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
## 000013.4281108706.png
|
||||
![](000013.4281108706.png)
|
||||
|
||||
banana sushi -s 50 -S 4281108706 -W 960 -H 960 -C 7.5 -A k_lms
|
||||
## 000014.2396987386.png
|
||||
![](000014.2396987386.png)
|
||||
|
||||
old sea captain with crow on shoulder -s 50 -S 2396987386 -W 512 -H 512 -C 7.5 -I docs/assets/preflight-checks/inputs/Lincoln-and-Parrot-512.png -A k_lms -f 0.75
|
||||
## 000015.1252923272.png
|
||||
![](000015.1252923272.png)
|
||||
|
||||
old sea captain with crow on shoulder -s 50 -S 1252923272 -W 512 -H 512 -C 7.5 -I docs/assets/preflight-checks/inputs/Lincoln-and-Parrot-512-transparent.png -A k_lms -f 0.75
|
||||
## 000016.2633891320.png
|
||||
![](000016.2633891320.png)
|
||||
|
||||
old sea captain with crow on shoulder -s 50 -S 2633891320 -W 512 -H 512 -C 7.5 -I docs/assets/preflight-checks/inputs/Lincoln-and-Parrot-512.png -A plms -f 0.75
|
||||
## 000017.1134411920.png
|
||||
![](000017.1134411920.png)
|
||||
|
||||
old sea captain with crow on shoulder -s 50 -S 1134411920 -W 512 -H 512 -C 7.5 -I docs/assets/preflight-checks/inputs/Lincoln-and-Parrot-512.png -A k_euler_a -f 0.75
|
||||
## 000018.47.png
|
||||
![](000018.47.png)
|
||||
|
||||
big red dog playing with cat -s 50 -S 47 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
## 000019.47.png
|
||||
![](000019.47.png)
|
||||
|
||||
big red++++ dog playing with cat -s 50 -S 47 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
## 000020.47.png
|
||||
![](000020.47.png)
|
||||
|
||||
big red dog playing with cat+++ -s 50 -S 47 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
## 000021.47.png
|
||||
![](000021.47.png)
|
||||
|
||||
big (red dog).swap(tiger) playing with cat -s 50 -S 47 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
## 000022.47.png
|
||||
![](000022.47.png)
|
||||
|
||||
dog:1,cat:2 -s 50 -S 47 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
## 000023.47.png
|
||||
![](000023.47.png)
|
||||
|
||||
dog:2,cat:1 -s 50 -S 47 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
## 000024.1029061431.png
|
||||
![](000024.1029061431.png)
|
||||
|
||||
medusa with cobras -s 50 -S 1029061431 -W 512 -H 512 -C 7.5 -I docs/assets/preflight-checks/inputs/curly.png -A k_lms -f 0.75 -tm hair
|
||||
## 000025.1284519352.png
|
||||
![](000025.1284519352.png)
|
||||
|
||||
bearded man -s 50 -S 1284519352 -W 512 -H 512 -C 7.5 -I docs/assets/preflight-checks/inputs/curly.png -A k_lms -f 0.75 -tm face
|
||||
## curly.942491079.gfpgan.png
|
||||
![](curly.942491079.gfpgan.png)
|
||||
|
||||
!fix ./docs/assets/preflight-checks/inputs/curly.png -s 50 -S 942491079 -W 512 -H 512 -C 7.5 -A k_lms -G 0.8 -ft gfpgan -U 2.0 0.75
|
||||
## curly.942491079.outcrop.png
|
||||
![](curly.942491079.outcrop.png)
|
||||
|
||||
!fix ./docs/assets/preflight-checks/inputs/curly.png -s 50 -S 942491079 -W 512 -H 512 -C 7.5 -A k_lms -c top 64
|
||||
## curly.942491079.outpaint.png
|
||||
![](curly.942491079.outpaint.png)
|
||||
|
||||
!fix ./docs/assets/preflight-checks/inputs/curly.png -s 50 -S 942491079 -W 512 -H 512 -C 7.5 -A k_lms -D top 64
|
||||
## curly.942491079.outcrop-01.png
|
||||
![](curly.942491079.outcrop-01.png)
|
||||
|
||||
!fix ./docs/assets/preflight-checks/inputs/curly.png -s 50 -S 942491079 -W 512 -H 512 -C 7.5 -A k_lms -c top 64
|
29
docs/assets/preflight-checks/outputs/invoke_log.txt
Normal file
@ -0,0 +1,29 @@
|
||||
outputs/preflight/000001.1863159593.png: banana sushi -s 50 -S 1863159593 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
outputs/preflight/000002.1151955949.png: banana sushi -s 50 -S 1151955949 -W 512 -H 512 -C 7.5 -A plms
|
||||
outputs/preflight/000003.2736230502.png: banana sushi -s 50 -S 2736230502 -W 512 -H 512 -C 7.5 -A ddim
|
||||
outputs/preflight/000004.42.png: banana sushi -s 50 -S 42 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
outputs/preflight/000005.42.png: banana sushi -s 50 -S 42 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
outputs/preflight/000006.478163327.png: banana sushi -s 50 -S 478163327 -W 640 -H 448 -C 7.5 -A k_lms
|
||||
outputs/preflight/000007.2407640369.png: banana sushi -s 50 -S 42 -W 512 -H 512 -C 7.5 -A k_lms -V 2407640369:0.1
|
||||
outputs/preflight/000008.2772421987.png: banana sushi -s 50 -S 42 -W 512 -H 512 -C 7.5 -A k_lms -V 2772421987:0.1
|
||||
outputs/preflight/000009.3532317557.png: banana sushi -s 50 -S 42 -W 512 -H 512 -C 7.5 -A k_lms -V 3532317557:0.1
|
||||
outputs/preflight/000010.2028635318.png: banana sushi -s 50 -S 2028635318 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
outputs/preflight/000011.1111168647.png: pond with waterlillies -s 50 -S 1111168647 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
outputs/preflight/000012.1476370516.png: pond with waterlillies -s 50 -S 1476370516 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
outputs/preflight/000013.4281108706.png: banana sushi -s 50 -S 4281108706 -W 960 -H 960 -C 7.5 -A k_lms
|
||||
outputs/preflight/000014.2396987386.png: old sea captain with crow on shoulder -s 50 -S 2396987386 -W 512 -H 512 -C 7.5 -I docs/assets/preflight-checks/inputs/Lincoln-and-Parrot-512.png -A k_lms -f 0.75
|
||||
outputs/preflight/000015.1252923272.png: old sea captain with crow on shoulder -s 50 -S 1252923272 -W 512 -H 512 -C 7.5 -I docs/assets/preflight-checks/inputs/Lincoln-and-Parrot-512-transparent.png -A k_lms -f 0.75
|
||||
outputs/preflight/000016.2633891320.png: old sea captain with crow on shoulder -s 50 -S 2633891320 -W 512 -H 512 -C 7.5 -I docs/assets/preflight-checks/inputs/Lincoln-and-Parrot-512.png -A plms -f 0.75
|
||||
outputs/preflight/000017.1134411920.png: old sea captain with crow on shoulder -s 50 -S 1134411920 -W 512 -H 512 -C 7.5 -I docs/assets/preflight-checks/inputs/Lincoln-and-Parrot-512.png -A k_euler_a -f 0.75
|
||||
outputs/preflight/000018.47.png: big red dog playing with cat -s 50 -S 47 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
outputs/preflight/000019.47.png: big red++++ dog playing with cat -s 50 -S 47 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
outputs/preflight/000020.47.png: big red dog playing with cat+++ -s 50 -S 47 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
outputs/preflight/000021.47.png: big (red dog).swap(tiger) playing with cat -s 50 -S 47 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
outputs/preflight/000022.47.png: dog:1,cat:2 -s 50 -S 47 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
outputs/preflight/000023.47.png: dog:2,cat:1 -s 50 -S 47 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
outputs/preflight/000024.1029061431.png: medusa with cobras -s 50 -S 1029061431 -W 512 -H 512 -C 7.5 -I docs/assets/preflight-checks/inputs/curly.png -A k_lms -f 0.75 -tm hair
|
||||
outputs/preflight/000025.1284519352.png: bearded man -s 50 -S 1284519352 -W 512 -H 512 -C 7.5 -I docs/assets/preflight-checks/inputs/curly.png -A k_lms -f 0.75 -tm face
|
||||
outputs/preflight/curly.942491079.gfpgan.png: !fix ./docs/assets/preflight-checks/inputs/curly.png -s 50 -S 942491079 -W 512 -H 512 -C 7.5 -A k_lms -G 0.8 -ft gfpgan -U 2.0 0.75
|
||||
outputs/preflight/curly.942491079.outcrop.png: !fix ./docs/assets/preflight-checks/inputs/curly.png -s 50 -S 942491079 -W 512 -H 512 -C 7.5 -A k_lms -c top 64
|
||||
outputs/preflight/curly.942491079.outpaint.png: !fix ./docs/assets/preflight-checks/inputs/curly.png -s 50 -S 942491079 -W 512 -H 512 -C 7.5 -A k_lms -D top 64
|
||||
outputs/preflight/curly.942491079.outcrop-01.png: !fix ./docs/assets/preflight-checks/inputs/curly.png -s 50 -S 942491079 -W 512 -H 512 -C 7.5 -A k_lms -c top 64
|
61
docs/assets/preflight-checks/preflight_prompts.txt
Normal file
@ -0,0 +1,61 @@
|
||||
# outputs/preflight/000001.1863159593.png
|
||||
banana sushi -s 50 -S 1863159593 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
# outputs/preflight/000002.1151955949.png
|
||||
banana sushi -s 50 -S 1151955949 -W 512 -H 512 -C 7.5 -A plms
|
||||
# outputs/preflight/000003.2736230502.png
|
||||
banana sushi -s 50 -S 2736230502 -W 512 -H 512 -C 7.5 -A ddim
|
||||
# outputs/preflight/000004.42.png
|
||||
banana sushi -s 50 -S 42 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
# outputs/preflight/000005.42.png
|
||||
banana sushi -s 50 -S 42 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
# outputs/preflight/000006.478163327.png
|
||||
banana sushi -s 50 -S 478163327 -W 640 -H 448 -C 7.5 -A k_lms
|
||||
# outputs/preflight/000007.2407640369.png
|
||||
banana sushi -s 50 -S 42 -W 512 -H 512 -C 7.5 -A k_lms -V 2407640369:0.1
|
||||
# outputs/preflight/000007.2772421987.png
|
||||
banana sushi -s 50 -S 42 -W 512 -H 512 -C 7.5 -A k_lms -V 2772421987:0.1
|
||||
# outputs/preflight/000007.3532317557.png
|
||||
banana sushi -s 50 -S 42 -W 512 -H 512 -C 7.5 -A k_lms -V 3532317557:0.1
|
||||
# outputs/preflight/000008.2028635318.png
|
||||
banana sushi -s 50 -S 2028635318 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
# outputs/preflight/000009.1111168647.png
|
||||
pond with waterlillies -s 50 -S 1111168647 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
# outputs/preflight/000010.1476370516.png
|
||||
pond with waterlillies -s 50 -S 1476370516 -W 512 -H 512 -C 7.5 -A k_lms --seamless
|
||||
# outputs/preflight/000011.4281108706.png
|
||||
banana sushi -s 50 -S 4281108706 -W 960 -H 960 -C 7.5 -A k_lms
|
||||
# outputs/preflight/000012.2396987386.png
|
||||
old sea captain with crow on shoulder -s 50 -S 2396987386 -W 512 -H 512 -C 7.5 -I docs/assets/preflight-checks/inputs/Lincoln-and-Parrot-512.png -A k_lms -f 0.75
|
||||
# outputs/preflight/000013.1252923272.png
|
||||
old sea captain with crow on shoulder -s 50 -S 1252923272 -W 512 -H 512 -C 7.5 -I docs/assets/preflight-checks/inputs/Lincoln-and-Parrot-512-transparent.png -A k_lms -f 0.75
|
||||
# outputs/preflight/000014.2633891320.png
|
||||
old sea captain with crow on shoulder -s 50 -S 2633891320 -W 512 -H 512 -C 7.5 -I docs/assets/preflight-checks/inputs/Lincoln-and-Parrot-512.png -A plms -f 0.75
|
||||
# outputs/preflight/000015.1134411920.png
|
||||
old sea captain with crow on shoulder -s 50 -S 1134411920 -W 512 -H 512 -C 7.5 -I docs/assets/preflight-checks/inputs/Lincoln-and-Parrot-512.png -A k_euler_a -f 0.75
|
||||
# outputs/preflight/000016.42.png
|
||||
big red dog playing with cat -s 50 -S 47 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
# outputs/preflight/000017.42.png
|
||||
big red++++ dog playing with cat -s 50 -S 47 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
# outputs/preflight/000018.42.png
|
||||
big red dog playing with cat+++ -s 50 -S 47 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
# outputs/preflight/000019.42.png
|
||||
big (red dog).swap(tiger) playing with cat -s 50 -S 47 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
# outputs/preflight/000020.42.png
|
||||
dog:1,cat:2 -s 50 -S 47 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
# outputs/preflight/000021.42.png
|
||||
dog:2,cat:1 -s 50 -S 47 -W 512 -H 512 -C 7.5 -A k_lms
|
||||
# outputs/preflight/000022.1029061431.png
|
||||
medusa with cobras -s 50 -S 1029061431 -W 512 -H 512 -C 7.5 -I docs/assets/preflight-checks/inputs/curly.png -A k_lms -f 0.75 -tm hair
|
||||
# outputs/preflight/000023.1284519352.png
|
||||
bearded man -s 50 -S 1284519352 -W 512 -H 512 -C 7.5 -I docs/assets/preflight-checks/inputs/curly.png -A k_lms -f 0.75 -tm face
|
||||
# outputs/preflight/000024.curly.hair.deselected.png
|
||||
!mask -I docs/assets/preflight-checks/inputs/curly.png -tm hair
|
||||
# outputs/preflight/curly.942491079.gfpgan.png
|
||||
!fix ./docs/assets/preflight-checks/inputs/curly.png -U2 -G0.8
|
||||
# outputs/preflight/curly.942491079.outcrop.png
|
||||
!fix ./docs/assets/preflight-checks/inputs/curly.png -c top 64
|
||||
# outputs/preflight/curly.942491079.outpaint.png
|
||||
!fix ./docs/assets/preflight-checks/inputs/curly.png -D top 64
|
||||
# outputs/preflight/curly.942491079.outcrop-01.png
|
||||
!switch inpainting-1.5
|
||||
!fix ./docs/assets/preflight-checks/inputs/curly.png -c top 64
|
1
frontend/dist/assets/index.352e4760.css
vendored
1
frontend/dist/assets/index.52c8231e.css
vendored
Normal file
517
frontend/dist/assets/index.64b87783.js
vendored
517
frontend/dist/assets/index.d3820055.js
vendored
Normal file
4
frontend/dist/index.html
vendored
@ -6,8 +6,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>InvokeAI - A Stable Diffusion Toolkit</title>
|
||||
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" />
|
||||
<script type="module" crossorigin src="./assets/index.64b87783.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.352e4760.css">
|
||||
<script type="module" crossorigin src="./assets/index.d3820055.js"></script>
|
||||
<link rel="stylesheet" href="./assets/index.52c8231e.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -9,13 +9,9 @@
|
||||
|
||||
.app-content {
|
||||
display: grid;
|
||||
row-gap: 0.5rem;
|
||||
row-gap: 1rem;
|
||||
padding: $app-padding;
|
||||
grid-auto-rows: max-content;
|
||||
grid-auto-rows: min-content auto;
|
||||
width: $app-width;
|
||||
height: $app-height;
|
||||
}
|
||||
|
||||
.app-console {
|
||||
z-index: 20;
|
||||
}
|
||||
|
@ -8,14 +8,82 @@ import { requestSystemConfig } from './socketio/actions';
|
||||
import { keepGUIAlive } from './utils';
|
||||
import InvokeTabs from '../features/tabs/InvokeTabs';
|
||||
import ImageUploader from '../common/components/ImageUploader';
|
||||
import { RootState, useAppSelector } from '../app/store';
|
||||
|
||||
import FloatingGalleryButton from '../features/tabs/FloatingGalleryButton';
|
||||
import FloatingOptionsPanelButtons from '../features/tabs/FloatingOptionsPanelButtons';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { GalleryState } from '../features/gallery/gallerySlice';
|
||||
import { OptionsState } from '../features/options/optionsSlice';
|
||||
import { activeTabNameSelector } from '../features/options/optionsSelectors';
|
||||
import { SystemState } from '../features/system/systemSlice';
|
||||
import _ from 'lodash';
|
||||
import { Model } from './invokeai';
|
||||
|
||||
keepGUIAlive();
|
||||
|
||||
const appSelector = createSelector(
|
||||
[
|
||||
(state: RootState) => state.gallery,
|
||||
(state: RootState) => state.options,
|
||||
(state: RootState) => state.system,
|
||||
activeTabNameSelector,
|
||||
],
|
||||
(
|
||||
gallery: GalleryState,
|
||||
options: OptionsState,
|
||||
system: SystemState,
|
||||
activeTabName
|
||||
) => {
|
||||
const { shouldShowGallery, shouldHoldGalleryOpen, shouldPinGallery } =
|
||||
gallery;
|
||||
const {
|
||||
shouldShowOptionsPanel,
|
||||
shouldHoldOptionsPanelOpen,
|
||||
shouldPinOptionsPanel,
|
||||
} = options;
|
||||
|
||||
const modelStatusText = _.reduce(
|
||||
system.model_list,
|
||||
(acc: string, cur: Model, key: string) => {
|
||||
if (cur.status === 'active') acc = key;
|
||||
return acc;
|
||||
},
|
||||
''
|
||||
);
|
||||
|
||||
const shouldShowGalleryButton = !(
|
||||
shouldShowGallery ||
|
||||
(shouldHoldGalleryOpen && !shouldPinGallery)
|
||||
);
|
||||
|
||||
const shouldShowOptionsPanelButton =
|
||||
!(
|
||||
shouldShowOptionsPanel ||
|
||||
(shouldHoldOptionsPanelOpen && !shouldPinOptionsPanel)
|
||||
) && ['txt2img', 'img2img', 'inpainting'].includes(activeTabName);
|
||||
|
||||
return {
|
||||
modelStatusText,
|
||||
shouldShowGalleryButton,
|
||||
shouldShowOptionsPanelButton,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const App = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [isReady, setIsReady] = useState<boolean>(false);
|
||||
|
||||
const { shouldShowGalleryButton, shouldShowOptionsPanelButton } =
|
||||
useAppSelector(appSelector);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(requestSystemConfig());
|
||||
setIsReady(true);
|
||||
@ -32,6 +100,8 @@ const App = () => {
|
||||
<div className="app-console">
|
||||
<Console />
|
||||
</div>
|
||||
{shouldShowGalleryButton && <FloatingGalleryButton />}
|
||||
{shouldShowOptionsPanelButton && <FloatingOptionsPanelButtons />}
|
||||
</ImageUploader>
|
||||
</div>
|
||||
) : (
|
||||
|
83
frontend/src/app/selectors/readinessSelector.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import _ from 'lodash';
|
||||
import { RootState } from '../../app/store';
|
||||
import { activeTabNameSelector } from '../../features/options/optionsSelectors';
|
||||
import { OptionsState } from '../../features/options/optionsSlice';
|
||||
|
||||
import { SystemState } from '../../features/system/systemSlice';
|
||||
import { InpaintingState } from '../../features/tabs/Inpainting/inpaintingSlice';
|
||||
import { validateSeedWeights } from '../../common/util/seedWeightPairs';
|
||||
|
||||
export const readinessSelector = createSelector(
|
||||
[
|
||||
(state: RootState) => state.options,
|
||||
(state: RootState) => state.system,
|
||||
(state: RootState) => state.inpainting,
|
||||
activeTabNameSelector,
|
||||
],
|
||||
(
|
||||
options: OptionsState,
|
||||
system: SystemState,
|
||||
inpainting: InpaintingState,
|
||||
activeTabName
|
||||
) => {
|
||||
const {
|
||||
prompt,
|
||||
shouldGenerateVariations,
|
||||
seedWeights,
|
||||
maskPath,
|
||||
initialImage,
|
||||
seed,
|
||||
} = options;
|
||||
|
||||
const { isProcessing, isConnected } = system;
|
||||
|
||||
const { imageToInpaint } = inpainting;
|
||||
|
||||
// Cannot generate without a prompt
|
||||
if (!prompt || Boolean(prompt.match(/^[\s\r\n]+$/))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (activeTabName === 'img2img' && !initialImage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (activeTabName === 'inpainting' && !imageToInpaint) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot generate with a mask without img2img
|
||||
if (maskPath && !initialImage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: job queue
|
||||
// Cannot generate if already processing an image
|
||||
if (isProcessing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot generate if not connected
|
||||
if (!isConnected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot generate variations without valid seed weights
|
||||
if (
|
||||
shouldGenerateVariations &&
|
||||
(!(validateSeedWeights(seedWeights) || seedWeights === '') || seed === -1)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// All good
|
||||
return true;
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
equalityCheck: _.isEqual,
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
@ -8,11 +8,13 @@ import {
|
||||
import {
|
||||
GalleryCategory,
|
||||
GalleryState,
|
||||
removeImage,
|
||||
} from '../../features/gallery/gallerySlice';
|
||||
import { OptionsState } from '../../features/options/optionsSlice';
|
||||
import {
|
||||
addLogEntry,
|
||||
errorOccurred,
|
||||
modelChangeRequested,
|
||||
setCurrentStatus,
|
||||
setIsCancelable,
|
||||
setIsProcessing,
|
||||
@ -163,6 +165,7 @@ const makeSocketIOEmitters = (
|
||||
},
|
||||
emitDeleteImage: (imageToDelete: InvokeAI.Image) => {
|
||||
const { url, uuid, category } = imageToDelete;
|
||||
dispatch(removeImage(imageToDelete));
|
||||
socketio.emit('deleteImage', url, uuid, category);
|
||||
},
|
||||
emitRequestImages: (category: GalleryCategory) => {
|
||||
@ -189,9 +192,7 @@ const makeSocketIOEmitters = (
|
||||
socketio.emit('requestSystemConfig');
|
||||
},
|
||||
emitRequestModelChange: (modelName: string) => {
|
||||
dispatch(setCurrentStatus('Changing Model'));
|
||||
dispatch(setIsProcessing(true));
|
||||
dispatch(setIsCancelable(false));
|
||||
dispatch(modelChangeRequested());
|
||||
socketio.emit('requestModelChange', modelName);
|
||||
},
|
||||
};
|
||||
|
@ -33,7 +33,11 @@ import {
|
||||
setMaskPath,
|
||||
} from '../../features/options/optionsSlice';
|
||||
import { requestImages, requestNewImages } from './actions';
|
||||
import { clearImageToInpaint, setImageToInpaint } from '../../features/tabs/Inpainting/inpaintingSlice';
|
||||
import {
|
||||
clearImageToInpaint,
|
||||
setImageToInpaint,
|
||||
} from '../../features/tabs/Inpainting/inpaintingSlice';
|
||||
import { tabMap } from '../../features/tabs/InvokeTabs';
|
||||
|
||||
/**
|
||||
* Returns an object containing listener callbacks for socketio events.
|
||||
@ -93,15 +97,34 @@ const makeSocketIOListeners = (
|
||||
*/
|
||||
onGenerationResult: (data: InvokeAI.ImageResultResponse) => {
|
||||
try {
|
||||
const { shouldLoopback, activeTab } = getState().options;
|
||||
const newImage = {
|
||||
uuid: uuidv4(),
|
||||
...data,
|
||||
category: 'result',
|
||||
};
|
||||
|
||||
dispatch(
|
||||
addImage({
|
||||
category: 'result',
|
||||
image: {
|
||||
uuid: uuidv4(),
|
||||
...data,
|
||||
},
|
||||
image: newImage,
|
||||
})
|
||||
);
|
||||
|
||||
if (shouldLoopback) {
|
||||
const activeTabName = tabMap[activeTab];
|
||||
switch (activeTabName) {
|
||||
case 'img2img': {
|
||||
dispatch(setInitialImage(newImage));
|
||||
break;
|
||||
}
|
||||
case 'inpainting': {
|
||||
dispatch(setImageToInpaint(newImage));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
@ -144,6 +167,7 @@ const makeSocketIOListeners = (
|
||||
image: {
|
||||
uuid: uuidv4(),
|
||||
...data,
|
||||
category: 'result',
|
||||
},
|
||||
})
|
||||
);
|
||||
@ -264,7 +288,7 @@ const makeSocketIOListeners = (
|
||||
* Callback to run when we receive a 'imageDeleted' event.
|
||||
*/
|
||||
onImageDeleted: (data: InvokeAI.ImageDeletedResponse) => {
|
||||
const { url, uuid, category } = data;
|
||||
const { url } = data;
|
||||
|
||||
// remove image from gallery
|
||||
dispatch(removeImage(data));
|
||||
@ -348,7 +372,7 @@ const makeSocketIOListeners = (
|
||||
dispatch(setModelList(model_list));
|
||||
dispatch(setCurrentStatus('Model Changed'));
|
||||
dispatch(setIsProcessing(false));
|
||||
dispatch(setIsCancelable(false));
|
||||
dispatch(setIsCancelable(true));
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
@ -361,7 +385,7 @@ const makeSocketIOListeners = (
|
||||
const { model_name, model_list } = data;
|
||||
dispatch(setModelList(model_list));
|
||||
dispatch(setIsProcessing(false));
|
||||
dispatch(setIsCancelable(false));
|
||||
dispatch(setIsCancelable(true));
|
||||
dispatch(errorOccurred());
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
|
@ -1,20 +1,19 @@
|
||||
import { Button, ButtonProps, Tooltip } from '@chakra-ui/react';
|
||||
|
||||
interface Props extends ButtonProps {
|
||||
export interface IAIButtonProps extends ButtonProps {
|
||||
label: string;
|
||||
tooltip?: string;
|
||||
styleClass?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable customized button component. Originally was more customized - now probably unecessary.
|
||||
*
|
||||
* TODO: Get rid of this.
|
||||
* Reusable customized button component.
|
||||
*/
|
||||
const IAIButton = (props: Props) => {
|
||||
const { label, tooltip = '', size = 'sm', ...rest } = props;
|
||||
const IAIButton = (props: IAIButtonProps) => {
|
||||
const { label, tooltip = '', styleClass, ...rest } = props;
|
||||
return (
|
||||
<Tooltip label={tooltip}>
|
||||
<Button size={size} {...rest}>
|
||||
<Button className={styleClass ? styleClass : ''} {...rest}>
|
||||
{label}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { FormControl, FormLabel, Select, SelectProps } from '@chakra-ui/react';
|
||||
import { MouseEvent } from 'react';
|
||||
|
||||
interface Props extends SelectProps {
|
||||
label: string;
|
||||
@ -21,7 +22,16 @@ const IAISelect = (props: Props) => {
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<FormControl isDisabled={isDisabled} className={`invokeai__select ${styleClass}`}>
|
||||
<FormControl
|
||||
isDisabled={isDisabled}
|
||||
className={`invokeai__select ${styleClass}`}
|
||||
onClick={(e: MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
e.nativeEvent.stopPropagation();
|
||||
e.nativeEvent.cancelBubble = true;
|
||||
}}
|
||||
>
|
||||
<FormLabel
|
||||
fontSize={fontSize}
|
||||
marginBottom={1}
|
||||
|
@ -39,10 +39,11 @@
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 0.5rem;
|
||||
color: var(--subtext-color-bright);
|
||||
color: var(--tab-list-text-inactive);
|
||||
background-color: var(--btn-grey);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--inpaint-bg-color);
|
||||
background-color: var(--btn-grey-hover);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,23 +1,11 @@
|
||||
import { useCallback, ReactNode, useState, useEffect } from 'react';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { tabMap } from '../../features/tabs/InvokeTabs';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||
import { Heading, Spinner, useToast } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { OptionsState } from '../../features/options/optionsSlice';
|
||||
import { uploadImage } from '../../app/socketio/actions';
|
||||
import { ImageUploadDestination, UploadImagePayload } from '../../app/invokeai';
|
||||
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
|
||||
|
||||
const appSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
const { activeTab } = options;
|
||||
return {
|
||||
activeTabName: tabMap[activeTab],
|
||||
};
|
||||
}
|
||||
);
|
||||
import { activeTabNameSelector } from '../../features/options/optionsSelectors';
|
||||
|
||||
type ImageUploaderProps = {
|
||||
children: ReactNode;
|
||||
@ -26,7 +14,7 @@ type ImageUploaderProps = {
|
||||
const ImageUploader = (props: ImageUploaderProps) => {
|
||||
const { children } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const { activeTabName } = useAppSelector(appSelector);
|
||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||
const toast = useToast({});
|
||||
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
|
||||
|
||||
|
@ -1,114 +0,0 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import _ from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import { OptionsState } from '../../features/options/optionsSlice';
|
||||
|
||||
import { SystemState } from '../../features/system/systemSlice';
|
||||
import { InpaintingState } from '../../features/tabs/Inpainting/inpaintingSlice';
|
||||
import { tabMap } from '../../features/tabs/InvokeTabs';
|
||||
import { validateSeedWeights } from '../util/seedWeightPairs';
|
||||
|
||||
export const useCheckParametersSelector = createSelector(
|
||||
[
|
||||
(state: RootState) => state.options,
|
||||
(state: RootState) => state.system,
|
||||
(state: RootState) => state.inpainting,
|
||||
],
|
||||
(options: OptionsState, system: SystemState, inpainting: InpaintingState) => {
|
||||
return {
|
||||
// options
|
||||
prompt: options.prompt,
|
||||
shouldGenerateVariations: options.shouldGenerateVariations,
|
||||
seedWeights: options.seedWeights,
|
||||
maskPath: options.maskPath,
|
||||
initialImage: options.initialImage,
|
||||
seed: options.seed,
|
||||
activeTabName: tabMap[options.activeTab],
|
||||
// system
|
||||
isProcessing: system.isProcessing,
|
||||
isConnected: system.isConnected,
|
||||
// inpainting
|
||||
hasInpaintingImage: Boolean(inpainting.imageToInpaint),
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
/**
|
||||
* Checks relevant pieces of state to confirm generation will not deterministically fail.
|
||||
* This is used to prevent the 'Generate' button from being clicked.
|
||||
*/
|
||||
const useCheckParameters = (): boolean => {
|
||||
const {
|
||||
prompt,
|
||||
shouldGenerateVariations,
|
||||
seedWeights,
|
||||
maskPath,
|
||||
initialImage,
|
||||
seed,
|
||||
activeTabName,
|
||||
isProcessing,
|
||||
isConnected,
|
||||
hasInpaintingImage,
|
||||
} = useAppSelector(useCheckParametersSelector);
|
||||
|
||||
return useMemo(() => {
|
||||
// Cannot generate without a prompt
|
||||
if (!prompt || Boolean(prompt.match(/^[\s\r\n]+$/))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (activeTabName === 'img2img' && !initialImage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (activeTabName === 'inpainting' && !hasInpaintingImage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot generate with a mask without img2img
|
||||
if (maskPath && !initialImage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: job queue
|
||||
// Cannot generate if already processing an image
|
||||
if (isProcessing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot generate if not connected
|
||||
if (!isConnected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot generate variations without valid seed weights
|
||||
if (
|
||||
shouldGenerateVariations &&
|
||||
(!(validateSeedWeights(seedWeights) || seedWeights === '') || seed === -1)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// All good
|
||||
return true;
|
||||
}, [
|
||||
prompt,
|
||||
maskPath,
|
||||
isProcessing,
|
||||
initialImage,
|
||||
isConnected,
|
||||
shouldGenerateVariations,
|
||||
seedWeights,
|
||||
seed,
|
||||
activeTabName,
|
||||
hasInpaintingImage,
|
||||
]);
|
||||
};
|
||||
|
||||
export default useCheckParameters;
|
25
frontend/src/common/hooks/useClickOutsideWatcher.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { RefObject, useEffect } from 'react';
|
||||
|
||||
const useClickOutsideWatcher = (
|
||||
ref: RefObject<HTMLElement>,
|
||||
callback: () => void,
|
||||
req = true
|
||||
) => {
|
||||
useEffect(() => {
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
if (req) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
return () => {
|
||||
if (req) {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
};
|
||||
}, [ref, req, callback]);
|
||||
};
|
||||
|
||||
export default useClickOutsideWatcher;
|
@ -100,47 +100,41 @@ export const frontendToBackendParameters = (
|
||||
if (generationMode === 'inpainting' && maskImageElement) {
|
||||
const {
|
||||
lines,
|
||||
boundingBoxCoordinate: { x, y },
|
||||
boundingBoxDimensions: { width, height },
|
||||
shouldShowBoundingBox,
|
||||
boundingBoxCoordinate,
|
||||
boundingBoxDimensions,
|
||||
inpaintReplace,
|
||||
shouldUseInpaintReplace,
|
||||
} = inpaintingState;
|
||||
|
||||
let bx = x,
|
||||
by = y,
|
||||
bwidth = width,
|
||||
bheight = height;
|
||||
|
||||
if (!shouldShowBoundingBox) {
|
||||
bx = 0;
|
||||
by = 0;
|
||||
bwidth = maskImageElement.width;
|
||||
bheight = maskImageElement.height;
|
||||
}
|
||||
|
||||
const boundingBox = {
|
||||
x: bx,
|
||||
y: by,
|
||||
width: bwidth,
|
||||
height: bheight,
|
||||
...boundingBoxCoordinate,
|
||||
...boundingBoxDimensions,
|
||||
};
|
||||
|
||||
if (shouldUseInpaintReplace) {
|
||||
generationParameters.inpaint_replace = inpaintReplace;
|
||||
}
|
||||
|
||||
generationParameters.init_img = imageToProcessUrl;
|
||||
generationParameters.strength = img2imgStrength;
|
||||
generationParameters.fit = false;
|
||||
|
||||
const maskDataURL = generateMask(maskImageElement, lines, boundingBox);
|
||||
const { maskDataURL, isMaskEmpty } = generateMask(
|
||||
maskImageElement,
|
||||
lines,
|
||||
boundingBox
|
||||
);
|
||||
|
||||
generationParameters.is_mask_empty = isMaskEmpty;
|
||||
|
||||
generationParameters.init_mask = maskDataURL.split(
|
||||
'data:image/png;base64,'
|
||||
)[1];
|
||||
|
||||
if (shouldUseInpaintReplace) {
|
||||
generationParameters.inpaint_replace = inpaintReplace;
|
||||
}
|
||||
|
||||
generationParameters.bounding_box = boundingBox;
|
||||
|
||||
// TODO: The server metadata generation needs to be changed to fix this.
|
||||
generationParameters.progress_images = false;
|
||||
}
|
||||
|
||||
if (shouldGenerateVariations) {
|
||||
|
@ -6,6 +6,7 @@ import * as InvokeAI from '../../app/invokeai';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import {
|
||||
OptionsState,
|
||||
setActiveTab,
|
||||
setAllParameters,
|
||||
setInitialImage,
|
||||
@ -17,13 +18,7 @@ import { SystemState } from '../system/systemSlice';
|
||||
import IAIButton from '../../common/components/IAIButton';
|
||||
import { runESRGAN, runFacetool } from '../../app/socketio/actions';
|
||||
import IAIIconButton from '../../common/components/IAIIconButton';
|
||||
import {
|
||||
MdDelete,
|
||||
MdFace,
|
||||
MdHd,
|
||||
MdImage,
|
||||
MdInfo,
|
||||
} from 'react-icons/md';
|
||||
import { MdDelete, MdFace, MdHd, MdImage, MdInfo } from 'react-icons/md';
|
||||
import InvokePopover from './InvokePopover';
|
||||
import UpscaleOptions from '../options/AdvancedOptions/Upscale/UpscaleOptions';
|
||||
import FaceRestoreOptions from '../options/AdvancedOptions/FaceRestore/FaceRestoreOptions';
|
||||
@ -32,15 +27,49 @@ import { useToast } from '@chakra-ui/react';
|
||||
import { FaCopy, FaPaintBrush, FaSeedling } from 'react-icons/fa';
|
||||
import { setImageToInpaint } from '../tabs/Inpainting/inpaintingSlice';
|
||||
import { hoverableImageSelector } from './gallerySliceSelectors';
|
||||
import { GalleryState } from './gallerySlice';
|
||||
import { activeTabNameSelector } from '../options/optionsSelectors';
|
||||
|
||||
const intermediateImageSelector = createSelector(
|
||||
(state: RootState) => state.gallery,
|
||||
(gallery: GalleryState) => gallery.intermediateImage,
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: (a, b) =>
|
||||
(a === undefined && b === undefined) || a.uuid === b.uuid,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const systemSelector = createSelector(
|
||||
[
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
(state: RootState) => state.options,
|
||||
intermediateImageSelector,
|
||||
activeTabNameSelector,
|
||||
],
|
||||
(
|
||||
system: SystemState,
|
||||
options: OptionsState,
|
||||
intermediateImage,
|
||||
activeTabName
|
||||
) => {
|
||||
const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } =
|
||||
system;
|
||||
|
||||
const { upscalingLevel, facetoolStrength, shouldShowImageDetails } =
|
||||
options;
|
||||
|
||||
return {
|
||||
isProcessing: system.isProcessing,
|
||||
isConnected: system.isConnected,
|
||||
isGFPGANAvailable: system.isGFPGANAvailable,
|
||||
isESRGANAvailable: system.isESRGANAvailable,
|
||||
isProcessing,
|
||||
isConnected,
|
||||
isGFPGANAvailable,
|
||||
isESRGANAvailable,
|
||||
upscalingLevel,
|
||||
facetoolStrength,
|
||||
intermediateImage,
|
||||
shouldShowImageDetails,
|
||||
activeTabName,
|
||||
};
|
||||
},
|
||||
{
|
||||
@ -60,29 +89,20 @@ type CurrentImageButtonsProps = {
|
||||
*/
|
||||
const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { activeTabName } = useAppSelector(hoverableImageSelector);
|
||||
|
||||
const shouldShowImageDetails = useAppSelector(
|
||||
(state: RootState) => state.options.shouldShowImageDetails
|
||||
);
|
||||
const {
|
||||
isProcessing,
|
||||
isConnected,
|
||||
isGFPGANAvailable,
|
||||
isESRGANAvailable,
|
||||
upscalingLevel,
|
||||
facetoolStrength,
|
||||
intermediateImage,
|
||||
shouldShowImageDetails,
|
||||
activeTabName,
|
||||
} = useAppSelector(systemSelector);
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const intermediateImage = useAppSelector(
|
||||
(state: RootState) => state.gallery.intermediateImage
|
||||
);
|
||||
|
||||
const upscalingLevel = useAppSelector(
|
||||
(state: RootState) => state.options.upscalingLevel
|
||||
);
|
||||
|
||||
const facetoolStrength = useAppSelector(
|
||||
(state: RootState) => state.options.facetoolStrength
|
||||
);
|
||||
|
||||
const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } =
|
||||
useAppSelector(systemSelector);
|
||||
|
||||
const handleClickUseAsInitialImage = () => {
|
||||
dispatch(setInitialImage(image));
|
||||
dispatch(setActiveTab(1));
|
||||
@ -360,7 +380,9 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||
icon={<MdDelete />}
|
||||
tooltip="Delete Image"
|
||||
aria-label="Delete Image"
|
||||
isDisabled={Boolean(intermediateImage)}
|
||||
isDisabled={
|
||||
Boolean(intermediateImage) || !isConnected || isProcessing
|
||||
}
|
||||
/>
|
||||
</DeleteImageModal>
|
||||
</div>
|
||||
|
@ -2,22 +2,26 @@ import { RootState, useAppSelector } from '../../app/store';
|
||||
import CurrentImageButtons from './CurrentImageButtons';
|
||||
import { MdPhoto } from 'react-icons/md';
|
||||
import CurrentImagePreview from './CurrentImagePreview';
|
||||
import { tabMap } from '../tabs/InvokeTabs';
|
||||
import { GalleryState } from './gallerySlice';
|
||||
import { OptionsState } from '../options/optionsSlice';
|
||||
import _ from 'lodash';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { activeTabNameSelector } from '../options/optionsSelectors';
|
||||
|
||||
export const currentImageDisplaySelector = createSelector(
|
||||
[(state: RootState) => state.gallery, (state: RootState) => state.options],
|
||||
(gallery: GalleryState, options: OptionsState) => {
|
||||
[
|
||||
(state: RootState) => state.gallery,
|
||||
(state: RootState) => state.options,
|
||||
activeTabNameSelector,
|
||||
],
|
||||
(gallery: GalleryState, options: OptionsState, activeTabName) => {
|
||||
const { currentImage, intermediateImage } = gallery;
|
||||
const { activeTab, shouldShowImageDetails } = options;
|
||||
const { shouldShowImageDetails } = options;
|
||||
|
||||
return {
|
||||
currentImage,
|
||||
intermediateImage,
|
||||
activeTabName: tabMap[activeTab],
|
||||
activeTabName,
|
||||
shouldShowImageDetails,
|
||||
};
|
||||
},
|
||||
@ -32,11 +36,9 @@ export const currentImageDisplaySelector = createSelector(
|
||||
* Displays the current image if there is one, plus associated actions.
|
||||
*/
|
||||
const CurrentImageDisplay = () => {
|
||||
const {
|
||||
currentImage,
|
||||
intermediateImage,
|
||||
activeTabName,
|
||||
} = useAppSelector(currentImageDisplaySelector);
|
||||
const { currentImage, intermediateImage, activeTabName } = useAppSelector(
|
||||
currentImageDisplaySelector
|
||||
);
|
||||
|
||||
const imageToDisplay = intermediateImage || currentImage;
|
||||
|
||||
|
@ -62,11 +62,11 @@ export default function CurrentImagePreview(props: CurrentImagePreviewProps) {
|
||||
};
|
||||
|
||||
const handleClickPrevButton = () => {
|
||||
dispatch(selectPrevImage(currentCategory));
|
||||
dispatch(selectPrevImage());
|
||||
};
|
||||
|
||||
const handleClickNextButton = () => {
|
||||
dispatch(selectNextImage(currentCategory));
|
||||
dispatch(selectNextImage());
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -12,7 +12,6 @@ import {
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Flex,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import {
|
||||
@ -30,6 +29,13 @@ import { setShouldConfirmOnDelete, SystemState } from '../system/systemSlice';
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
const { shouldConfirmOnDelete, isConnected, isProcessing } = system;
|
||||
return { shouldConfirmOnDelete, isConnected, isProcessing };
|
||||
}
|
||||
);
|
||||
interface DeleteImageModalProps {
|
||||
/**
|
||||
* Component which, on click, should delete the image/open the modal.
|
||||
@ -41,11 +47,6 @@ interface DeleteImageModalProps {
|
||||
image: InvokeAI.Image;
|
||||
}
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => system.shouldConfirmOnDelete
|
||||
);
|
||||
|
||||
/**
|
||||
* Needs a child, which will act as the button to delete an image.
|
||||
* If system.shouldConfirmOnDelete is true, a confirmation modal is displayed.
|
||||
@ -56,9 +57,9 @@ const DeleteImageModal = forwardRef(
|
||||
({ image, children }: DeleteImageModalProps, ref) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const dispatch = useAppDispatch();
|
||||
const shouldConfirmOnDelete = useAppSelector(systemSelector);
|
||||
const { shouldConfirmOnDelete, isConnected, isProcessing } =
|
||||
useAppSelector(systemSelector);
|
||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||
const toast = useToast();
|
||||
|
||||
const handleClickDelete = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
@ -66,13 +67,9 @@ const DeleteImageModal = forwardRef(
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (isConnected && !isProcessing) {
|
||||
dispatch(deleteImage(image));
|
||||
toast({
|
||||
title: 'Image Deleted',
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
@ -7,11 +7,7 @@ import {
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import {
|
||||
setCurrentImage,
|
||||
setShouldHoldGalleryOpen,
|
||||
setShouldShowGallery,
|
||||
} from './gallerySlice';
|
||||
import { setCurrentImage } from './gallerySlice';
|
||||
import { FaCheck, FaTrashAlt } from 'react-icons/fa';
|
||||
import DeleteImageModal from './DeleteImageModal';
|
||||
import { memo, useState } from 'react';
|
||||
@ -25,7 +21,6 @@ import {
|
||||
} from '../options/optionsSlice';
|
||||
import * as InvokeAI from '../../app/invokeai';
|
||||
import * as ContextMenu from '@radix-ui/react-context-menu';
|
||||
import { tabMap } from '../tabs/InvokeTabs';
|
||||
import { setImageToInpaint } from '../tabs/Inpainting/inpaintingSlice';
|
||||
import { hoverableImageSelector } from './gallerySliceSelectors';
|
||||
|
||||
@ -44,8 +39,12 @@ const memoEqualityCheck = (
|
||||
*/
|
||||
const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { activeTabName, galleryImageObjectFit, galleryImageMinimumWidth } =
|
||||
useAppSelector(hoverableImageSelector);
|
||||
const {
|
||||
activeTabName,
|
||||
galleryImageObjectFit,
|
||||
galleryImageMinimumWidth,
|
||||
mayDeleteImage,
|
||||
} = useAppSelector(hoverableImageSelector);
|
||||
const { image, isSelected } = props;
|
||||
const { url, uuid, metadata } = image;
|
||||
|
||||
@ -118,7 +117,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
if (metadata?.image?.init_image_path) {
|
||||
const response = await fetch(metadata.image.init_image_path);
|
||||
if (response.ok) {
|
||||
dispatch(setActiveTab(tabMap.indexOf('img2img')));
|
||||
dispatch(setActiveTab('img2img'));
|
||||
dispatch(setAllImageToImageParameters(metadata));
|
||||
toast({
|
||||
title: 'Initial Image Set',
|
||||
@ -142,10 +141,10 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
|
||||
return (
|
||||
<ContextMenu.Root
|
||||
onOpenChange={(open: boolean) => {
|
||||
dispatch(setShouldHoldGalleryOpen(open));
|
||||
dispatch(setShouldShowGallery(true));
|
||||
}}
|
||||
// onOpenChange={(open: boolean) => {
|
||||
// dispatch(setShouldHoldGalleryOpen(open));
|
||||
// dispatch(setShouldShowGallery(true));
|
||||
// }}
|
||||
>
|
||||
<ContextMenu.Trigger>
|
||||
<Box
|
||||
@ -182,6 +181,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
size="xs"
|
||||
variant={'imageHoverIconButton'}
|
||||
fontSize={14}
|
||||
isDisabled={!mayDeleteImage}
|
||||
/>
|
||||
</DeleteImageModal>
|
||||
</Tooltip>
|
||||
|
@ -1,25 +1,25 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.image-gallery-area-enter {
|
||||
.image-gallery-wrapper-enter {
|
||||
transform: translateX(150%);
|
||||
}
|
||||
|
||||
.image-gallery-area-enter-active {
|
||||
.image-gallery-wrapper-enter-active {
|
||||
transform: translateX(0);
|
||||
transition: all 120ms ease-out;
|
||||
}
|
||||
|
||||
.image-gallery-area-exit {
|
||||
.image-gallery-wrapper-exit {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.image-gallery-area-exit-active {
|
||||
.image-gallery-wrapper-exit-active {
|
||||
transform: translateX(150%);
|
||||
transition: all 120ms ease-out;
|
||||
}
|
||||
|
||||
.image-gallery-area {
|
||||
z-index: 10;
|
||||
.image-gallery-wrapper {
|
||||
z-index: 100;
|
||||
|
||||
&[data-pinned='false'] {
|
||||
position: fixed;
|
||||
@ -29,6 +29,7 @@
|
||||
|
||||
.image-gallery-popup {
|
||||
border-radius: 0;
|
||||
box-shadow: 0 0 1rem var(--text-color-a3);
|
||||
.image-gallery-container {
|
||||
max-height: calc($app-height + 5rem);
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { NumberSize, Resizable, Size } from 're-resizable';
|
||||
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { MdClear, MdPhotoLibrary } from 'react-icons/md';
|
||||
import { BsPinAngleFill } from 'react-icons/bs';
|
||||
import { BsPin, BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
|
||||
import { requestImages } from '../../app/socketio/actions';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import IAIIconButton from '../../common/components/IAIIconButton';
|
||||
@ -33,6 +33,7 @@ import { BiReset } from 'react-icons/bi';
|
||||
import IAICheckbox from '../../common/components/IAICheckbox';
|
||||
import { setNeedsCache } from '../tabs/Inpainting/inpaintingSlice';
|
||||
import _ from 'lodash';
|
||||
import useClickOutsideWatcher from '../../common/hooks/useClickOutsideWatcher';
|
||||
|
||||
const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 320;
|
||||
|
||||
@ -96,8 +97,8 @@ export default function ImageGallery() {
|
||||
const timeoutIdRef = useRef<number | null>(null);
|
||||
|
||||
const handleSetShouldPinGallery = () => {
|
||||
dispatch(setNeedsCache(true));
|
||||
dispatch(setShouldPinGallery(!shouldPinGallery));
|
||||
dispatch(setNeedsCache(true));
|
||||
};
|
||||
|
||||
const handleToggleGallery = () => {
|
||||
@ -106,18 +107,19 @@ export default function ImageGallery() {
|
||||
|
||||
const handleOpenGallery = () => {
|
||||
dispatch(setShouldShowGallery(true));
|
||||
dispatch(setNeedsCache(true));
|
||||
shouldPinGallery && dispatch(setNeedsCache(true));
|
||||
};
|
||||
|
||||
const handleCloseGallery = () => {
|
||||
// if (shouldPinGallery) return;
|
||||
dispatch(setShouldShowGallery(false));
|
||||
dispatch(
|
||||
setGalleryScrollPosition(
|
||||
galleryContainerRef.current ? galleryContainerRef.current.scrollTop : 0
|
||||
)
|
||||
);
|
||||
dispatch(setShouldShowGallery(false));
|
||||
dispatch(setShouldHoldGalleryOpen(false));
|
||||
dispatch(setNeedsCache(true));
|
||||
// dispatch(setNeedsCache(true));
|
||||
};
|
||||
|
||||
const handleClickLoadMore = () => {
|
||||
@ -145,24 +147,16 @@ export default function ImageGallery() {
|
||||
[shouldShowGallery]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'left',
|
||||
() => {
|
||||
dispatch(selectPrevImage(currentCategory));
|
||||
},
|
||||
[currentCategory]
|
||||
);
|
||||
useHotkeys('left', () => {
|
||||
dispatch(selectPrevImage());
|
||||
});
|
||||
|
||||
useHotkeys('right', () => {
|
||||
dispatch(selectNextImage());
|
||||
});
|
||||
|
||||
useHotkeys(
|
||||
'right',
|
||||
() => {
|
||||
dispatch(selectNextImage(currentCategory));
|
||||
},
|
||||
[currentCategory]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'shift+p',
|
||||
'shift+g',
|
||||
() => {
|
||||
handleSetShouldPinGallery();
|
||||
},
|
||||
@ -251,16 +245,22 @@ export default function ImageGallery() {
|
||||
galleryContainerRef.current.scrollTop = galleryScrollPosition;
|
||||
}, [galleryScrollPosition, shouldShowGallery]);
|
||||
|
||||
useEffect(() => {
|
||||
setShouldShowButtons(galleryWidth >= 280);
|
||||
}, [galleryWidth]);
|
||||
|
||||
useClickOutsideWatcher(galleryRef, handleCloseGallery, !shouldPinGallery);
|
||||
|
||||
return (
|
||||
<CSSTransition
|
||||
nodeRef={galleryRef}
|
||||
in={shouldShowGallery || (shouldHoldGalleryOpen && !shouldPinGallery)}
|
||||
unmountOnExit
|
||||
timeout={200}
|
||||
classNames="image-gallery-area"
|
||||
classNames="image-gallery-wrapper"
|
||||
>
|
||||
<div
|
||||
className="image-gallery-area"
|
||||
className="image-gallery-wrapper"
|
||||
data-pinned={shouldPinGallery}
|
||||
ref={galleryRef}
|
||||
onMouseLeave={!shouldPinGallery ? setCloseGalleryTimer : undefined}
|
||||
@ -270,7 +270,6 @@ export default function ImageGallery() {
|
||||
<Resizable
|
||||
minWidth={galleryMinWidth}
|
||||
maxWidth={galleryMaxWidth}
|
||||
// maxHeight={'100%'}
|
||||
className={'image-gallery-popup'}
|
||||
handleStyles={{ left: { width: '15px' } }}
|
||||
enable={{
|
||||
@ -316,9 +315,9 @@ export default function ImageGallery() {
|
||||
Number(galleryMaxWidth)
|
||||
);
|
||||
|
||||
if (newWidth >= 320 && !shouldShowButtons) {
|
||||
if (newWidth >= 280 && !shouldShowButtons) {
|
||||
setShouldShowButtons(true);
|
||||
} else if (newWidth < 320 && shouldShowButtons) {
|
||||
} else if (newWidth < 280 && shouldShowButtons) {
|
||||
setShouldShowButtons(false);
|
||||
}
|
||||
|
||||
@ -374,8 +373,8 @@ export default function ImageGallery() {
|
||||
</div>
|
||||
<div>
|
||||
<IAIPopover
|
||||
isLazy
|
||||
trigger="hover"
|
||||
hasArrow={activeTabName === 'inpainting' ? false : true}
|
||||
placement={'left'}
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
@ -442,20 +441,11 @@ export default function ImageGallery() {
|
||||
|
||||
<IAIIconButton
|
||||
size={'sm'}
|
||||
className={'image-gallery-icon-btn'}
|
||||
aria-label={'Pin Gallery'}
|
||||
tooltip={'Pin Gallery (Shift+P)'}
|
||||
tooltip={'Pin Gallery (Shift+G)'}
|
||||
onClick={handleSetShouldPinGallery}
|
||||
icon={<BsPinAngleFill />}
|
||||
data-selected={shouldPinGallery}
|
||||
/>
|
||||
|
||||
<IAIIconButton
|
||||
size={'sm'}
|
||||
aria-label={'Close Gallery'}
|
||||
tooltip={'Close Gallery (G)'}
|
||||
onClick={handleCloseGallery}
|
||||
className="image-gallery-icon-btn"
|
||||
icon={<MdClear />}
|
||||
icon={shouldPinGallery ? <BsPinAngleFill /> : <BsPinAngle />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,20 +0,0 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.show-hide-gallery-button {
|
||||
position: absolute !important;
|
||||
top: 50%;
|
||||
right: -1rem;
|
||||
transform: translate(0, -50%);
|
||||
z-index: 10;
|
||||
|
||||
border-radius: 0.5rem 0 0 0.5rem !important;
|
||||
padding: 0 0.5rem;
|
||||
|
||||
@include Button(
|
||||
$btn-width: 1rem,
|
||||
$btn-height: 12rem,
|
||||
$icon-size: 20px,
|
||||
$btn-color: var(--btn-grey),
|
||||
$btn-color-hover: var(--btn-grey-hover)
|
||||
);
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { MdPhotoLibrary } from 'react-icons/md';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import IAIIconButton from '../../common/components/IAIIconButton';
|
||||
import { setShouldShowGallery } from '../gallery/gallerySlice';
|
||||
import { selectNextImage, selectPrevImage } from './gallerySlice';
|
||||
|
||||
const ShowHideGalleryButton = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { shouldPinGallery, shouldShowGallery } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
|
||||
const handleShowGalleryToggle = () => {
|
||||
dispatch(setShouldShowGallery(!shouldShowGallery));
|
||||
};
|
||||
|
||||
// useHotkeys(
|
||||
// 'g',
|
||||
// () => {
|
||||
// handleShowGalleryToggle();
|
||||
// },
|
||||
// [shouldShowGallery]
|
||||
// );
|
||||
|
||||
// useHotkeys(
|
||||
// 'left',
|
||||
// () => {
|
||||
// dispatch(selectPrevImage());
|
||||
// },
|
||||
// []
|
||||
// );
|
||||
|
||||
// useHotkeys(
|
||||
// 'right',
|
||||
// () => {
|
||||
// dispatch(selectNextImage());
|
||||
// },
|
||||
// []
|
||||
// );
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
tooltip="Show Gallery (G)"
|
||||
tooltipPlacement="top"
|
||||
aria-label="Show Gallery"
|
||||
onClick={handleShowGalleryToggle}
|
||||
styleClass="show-hide-gallery-button"
|
||||
onMouseOver={!shouldPinGallery ? handleShowGalleryToggle : undefined}
|
||||
>
|
||||
<MdPhotoLibrary />
|
||||
</IAIIconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShowHideGalleryButton;
|
@ -143,9 +143,7 @@ export const gallerySlice = createSlice({
|
||||
if (state.shouldAutoSwitchToNewImages) {
|
||||
state.currentImageUuid = uuid;
|
||||
state.currentImage = newImage;
|
||||
if (category === 'result') {
|
||||
state.currentCategory = 'result';
|
||||
}
|
||||
state.currentCategory = category;
|
||||
}
|
||||
state.intermediateImage = undefined;
|
||||
tempCategory.latest_mtime = mtime;
|
||||
@ -156,10 +154,11 @@ export const gallerySlice = createSlice({
|
||||
clearIntermediateImage: (state) => {
|
||||
state.intermediateImage = undefined;
|
||||
},
|
||||
selectNextImage: (state, action: PayloadAction<GalleryCategory>) => {
|
||||
const category = action.payload;
|
||||
selectNextImage: (state) => {
|
||||
const { currentImage } = state;
|
||||
const tempImages = state.categories[category].images;
|
||||
if (!currentImage) return;
|
||||
const tempImages =
|
||||
state.categories[currentImage.category as GalleryCategory].images;
|
||||
|
||||
if (currentImage) {
|
||||
const currentImageIndex = tempImages.findIndex(
|
||||
@ -172,10 +171,11 @@ export const gallerySlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
selectPrevImage: (state, action: PayloadAction<GalleryCategory>) => {
|
||||
const category = action.payload;
|
||||
selectPrevImage: (state) => {
|
||||
const { currentImage } = state;
|
||||
const tempImages = state.categories[category].images;
|
||||
if (!currentImage) return;
|
||||
const tempImages =
|
||||
state.categories[currentImage.category as GalleryCategory].images;
|
||||
|
||||
if (currentImage) {
|
||||
const currentImageIndex = tempImages.findIndex(
|
||||
|
@ -1,12 +1,17 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from '../../app/store';
|
||||
import { activeTabNameSelector } from '../options/optionsSelectors';
|
||||
import { OptionsState } from '../options/optionsSlice';
|
||||
import { tabMap } from '../tabs/InvokeTabs';
|
||||
import { SystemState } from '../system/systemSlice';
|
||||
import { GalleryState } from './gallerySlice';
|
||||
|
||||
export const imageGallerySelector = createSelector(
|
||||
[(state: RootState) => state.gallery, (state: RootState) => state.options],
|
||||
(gallery: GalleryState, options: OptionsState) => {
|
||||
[
|
||||
(state: RootState) => state.gallery,
|
||||
(state: RootState) => state.options,
|
||||
activeTabNameSelector,
|
||||
],
|
||||
(gallery: GalleryState, options: OptionsState, activeTabName) => {
|
||||
const {
|
||||
categories,
|
||||
currentCategory,
|
||||
@ -21,8 +26,6 @@ export const imageGallerySelector = createSelector(
|
||||
galleryWidth,
|
||||
} = gallery;
|
||||
|
||||
const { activeTab } = options;
|
||||
|
||||
return {
|
||||
currentImageUuid,
|
||||
shouldPinGallery,
|
||||
@ -31,7 +34,7 @@ export const imageGallerySelector = createSelector(
|
||||
galleryImageMinimumWidth,
|
||||
galleryImageObjectFit,
|
||||
galleryGridTemplateColumns: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, auto))`,
|
||||
activeTabName: tabMap[activeTab],
|
||||
activeTabName,
|
||||
shouldHoldGalleryOpen,
|
||||
shouldAutoSwitchToNewImages,
|
||||
images: categories[currentCategory].images,
|
||||
@ -44,12 +47,23 @@ export const imageGallerySelector = createSelector(
|
||||
);
|
||||
|
||||
export const hoverableImageSelector = createSelector(
|
||||
[(state: RootState) => state.options, (state: RootState) => state.gallery],
|
||||
(options: OptionsState, gallery: GalleryState) => {
|
||||
[
|
||||
(state: RootState) => state.options,
|
||||
(state: RootState) => state.gallery,
|
||||
(state: RootState) => state.system,
|
||||
activeTabNameSelector,
|
||||
],
|
||||
(
|
||||
options: OptionsState,
|
||||
gallery: GalleryState,
|
||||
system: SystemState,
|
||||
activeTabName
|
||||
) => {
|
||||
return {
|
||||
mayDeleteImage: system.isConnected && !system.isProcessing,
|
||||
galleryImageObjectFit: gallery.galleryImageObjectFit,
|
||||
galleryImageMinimumWidth: gallery.galleryImageMinimumWidth,
|
||||
activeTabName: tabMap[options.activeTab],
|
||||
activeTabName,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@ -12,6 +12,16 @@
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.4rem 0.4rem 0 0;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
width: 0.5rem !important;
|
||||
height: 1.2rem !important;
|
||||
background: none !important;
|
||||
&:hover {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-weight: bold;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import _ from 'lodash';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { BiReset } from 'react-icons/bi';
|
||||
|
||||
import { BiHide, BiReset, BiShow } from 'react-icons/bi';
|
||||
|
||||
import {
|
||||
RootState,
|
||||
@ -14,7 +14,6 @@ import IAIIconButton from '../../../../common/components/IAIIconButton';
|
||||
|
||||
import IAINumberInput from '../../../../common/components/IAINumberInput';
|
||||
import IAISlider from '../../../../common/components/IAISlider';
|
||||
import IAISwitch from '../../../../common/components/IAISwitch';
|
||||
import { roundDownToMultiple } from '../../../../common/util/roundDownToMultiple';
|
||||
import {
|
||||
InpaintingState,
|
||||
@ -64,16 +63,23 @@ const BoundingBoxSettings = () => {
|
||||
} = useAppSelector(boundingBoxDimensionsSelector);
|
||||
|
||||
const handleChangeBoundingBoxWidth = (v: number) => {
|
||||
dispatch(setBoundingBoxDimensions({ ...boundingBoxDimensions, width: Math.floor(v) }));
|
||||
dispatch(
|
||||
setBoundingBoxDimensions({
|
||||
...boundingBoxDimensions,
|
||||
width: Math.floor(v),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleChangeBoundingBoxHeight = (v: number) => {
|
||||
dispatch(setBoundingBoxDimensions({ ...boundingBoxDimensions, height: Math.floor(v) }));
|
||||
dispatch(
|
||||
setBoundingBoxDimensions({
|
||||
...boundingBoxDimensions,
|
||||
height: Math.floor(v),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleShowBoundingBox = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldShowBoundingBox(e.target.checked));
|
||||
|
||||
const handleChangeShouldShowBoundingBoxFill = () => {
|
||||
dispatch(setShouldShowBoundingBoxFill(!shouldShowBoundingBoxFill));
|
||||
};
|
||||
@ -100,14 +106,21 @@ const BoundingBoxSettings = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleShowBoundingBox = () =>
|
||||
dispatch(setShouldShowBoundingBox(!shouldShowBoundingBox));
|
||||
|
||||
return (
|
||||
<div className="inpainting-bounding-box-settings">
|
||||
<div className="inpainting-bounding-box-header">
|
||||
<p>Inpaint Box</p>
|
||||
<IAISwitch
|
||||
isChecked={shouldShowBoundingBox}
|
||||
width={'auto'}
|
||||
onChange={handleShowBoundingBox}
|
||||
<IAIIconButton
|
||||
aria-label="Toggle Bounding Box Visibility"
|
||||
icon={
|
||||
shouldShowBoundingBox ? <BiShow size={22} /> : <BiHide size={22} />
|
||||
}
|
||||
onClick={handleShowBoundingBox}
|
||||
background={'none'}
|
||||
padding={0}
|
||||
/>
|
||||
</div>
|
||||
<div className="inpainting-bounding-box-settings-items">
|
||||
@ -119,7 +132,6 @@ const BoundingBoxSettings = () => {
|
||||
step={64}
|
||||
value={boundingBoxDimensions.width}
|
||||
onChange={handleChangeBoundingBoxWidth}
|
||||
isDisabled={!shouldShowBoundingBox}
|
||||
width={'5rem'}
|
||||
/>
|
||||
<IAINumberInput
|
||||
@ -128,7 +140,6 @@ const BoundingBoxSettings = () => {
|
||||
min={64}
|
||||
max={roundDownToMultiple(canvasDimensions.width, 64)}
|
||||
step={64}
|
||||
isDisabled={!shouldShowBoundingBox}
|
||||
width={'5rem'}
|
||||
/>
|
||||
<IAIIconButton
|
||||
@ -138,10 +149,7 @@ const BoundingBoxSettings = () => {
|
||||
onClick={handleResetWidth}
|
||||
icon={<BiReset />}
|
||||
styleClass="inpainting-bounding-box-reset-icon-btn"
|
||||
isDisabled={
|
||||
!shouldShowBoundingBox ||
|
||||
canvasDimensions.width === boundingBoxDimensions.width
|
||||
}
|
||||
isDisabled={canvasDimensions.width === boundingBoxDimensions.width}
|
||||
/>
|
||||
</div>
|
||||
<div className="inpainting-bounding-box-dimensions-slider-numberinput">
|
||||
@ -152,7 +160,6 @@ const BoundingBoxSettings = () => {
|
||||
step={64}
|
||||
value={boundingBoxDimensions.height}
|
||||
onChange={handleChangeBoundingBoxHeight}
|
||||
isDisabled={!shouldShowBoundingBox}
|
||||
width={'5rem'}
|
||||
/>
|
||||
<IAINumberInput
|
||||
@ -162,7 +169,6 @@ const BoundingBoxSettings = () => {
|
||||
max={roundDownToMultiple(canvasDimensions.height, 64)}
|
||||
step={64}
|
||||
padding="0"
|
||||
isDisabled={!shouldShowBoundingBox}
|
||||
width={'5rem'}
|
||||
/>
|
||||
<IAIIconButton
|
||||
@ -173,7 +179,6 @@ const BoundingBoxSettings = () => {
|
||||
icon={<BiReset />}
|
||||
styleClass="inpainting-bounding-box-reset-icon-btn"
|
||||
isDisabled={
|
||||
!shouldShowBoundingBox ||
|
||||
canvasDimensions.height === boundingBoxDimensions.height
|
||||
}
|
||||
/>
|
||||
@ -184,14 +189,12 @@ const BoundingBoxSettings = () => {
|
||||
isChecked={shouldShowBoundingBoxFill}
|
||||
onChange={handleChangeShouldShowBoundingBoxFill}
|
||||
styleClass="inpainting-bounding-box-darken"
|
||||
isDisabled={!shouldShowBoundingBox}
|
||||
/>
|
||||
<IAICheckbox
|
||||
label="Lock Bounding Box"
|
||||
isChecked={shouldLockBoundingBox}
|
||||
onChange={handleChangeShouldLockBoundingBox}
|
||||
styleClass="inpainting-bounding-box-darken"
|
||||
isDisabled={!shouldShowBoundingBox}
|
||||
/>
|
||||
</Flex>
|
||||
</div>
|
||||
|
@ -89,6 +89,7 @@ export default function InpaintingSettings() {
|
||||
onClick={handleClearBrushHistory}
|
||||
tooltip="Clears brush stroke history"
|
||||
disabled={futureLines.length > 0 || pastLines.length > 0 ? false : true}
|
||||
styleClass="inpainting-options-btn"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -2,14 +2,13 @@ import React, { ChangeEvent } from 'react';
|
||||
import { HEIGHTS } from '../../../app/constants';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAISelect from '../../../common/components/IAISelect';
|
||||
import { tabMap } from '../../tabs/InvokeTabs';
|
||||
import { activeTabNameSelector } from '../optionsSelectors';
|
||||
import { setHeight } from '../optionsSlice';
|
||||
import { fontSize } from './MainOptions';
|
||||
|
||||
export default function MainHeight() {
|
||||
const { activeTab, height } = useAppSelector(
|
||||
(state: RootState) => state.options
|
||||
);
|
||||
const { height } = useAppSelector((state: RootState) => state.options);
|
||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChangeHeight = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||
@ -17,7 +16,7 @@ export default function MainHeight() {
|
||||
|
||||
return (
|
||||
<IAISelect
|
||||
isDisabled={tabMap[activeTab] === 'inpainting'}
|
||||
isDisabled={activeTabName === 'inpainting'}
|
||||
label="Height"
|
||||
value={height}
|
||||
flexGrow={1}
|
||||
|
@ -6,6 +6,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { SystemState } from '../../system/systemSlice';
|
||||
import _ from 'lodash';
|
||||
import { IAIButtonProps } from '../../../common/components/IAIButton';
|
||||
|
||||
const cancelButtonSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
@ -23,7 +24,8 @@ const cancelButtonSelector = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
export default function CancelButton() {
|
||||
export default function CancelButton(props: Omit<IAIButtonProps, 'label'>) {
|
||||
const { ...rest } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const { isProcessing, isConnected, isCancelable } =
|
||||
useAppSelector(cancelButtonSelector);
|
||||
@ -47,6 +49,7 @@ export default function CancelButton() {
|
||||
isDisabled={!isConnected || !isProcessing || !isCancelable}
|
||||
onClick={handleClickCancel}
|
||||
styleClass="cancel-btn"
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1,22 +1,51 @@
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { FaPlay } from 'react-icons/fa';
|
||||
import { readinessSelector } from '../../../app/selectors/readinessSelector';
|
||||
import { generateImage } from '../../../app/socketio/actions';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAIButton from '../../../common/components/IAIButton';
|
||||
import useCheckParameters from '../../../common/hooks/useCheckParameters';
|
||||
import { tabMap } from '../../tabs/InvokeTabs';
|
||||
import { useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAIButton, {
|
||||
IAIButtonProps,
|
||||
} from '../../../common/components/IAIButton';
|
||||
import IAIIconButton from '../../../common/components/IAIIconButton';
|
||||
import { activeTabNameSelector } from '../optionsSelectors';
|
||||
|
||||
export default function InvokeButton() {
|
||||
interface InvokeButton extends Omit<IAIButtonProps, 'label'> {
|
||||
iconButton?: boolean;
|
||||
}
|
||||
|
||||
export default function InvokeButton(props: InvokeButton) {
|
||||
const { iconButton = false, ...rest } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const isReady = useCheckParameters();
|
||||
|
||||
const activeTab = useAppSelector(
|
||||
(state: RootState) => state.options.activeTab
|
||||
);
|
||||
const isReady = useAppSelector(readinessSelector);
|
||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||
|
||||
const handleClickGenerate = () => {
|
||||
dispatch(generateImage(tabMap[activeTab]));
|
||||
dispatch(generateImage(activeTabName));
|
||||
};
|
||||
|
||||
return (
|
||||
useHotkeys(
|
||||
'ctrl+enter, cmd+enter',
|
||||
() => {
|
||||
if (isReady) {
|
||||
dispatch(generateImage(activeTabName));
|
||||
}
|
||||
},
|
||||
[isReady, activeTabName]
|
||||
);
|
||||
|
||||
return iconButton ? (
|
||||
<IAIIconButton
|
||||
aria-label="Invoke"
|
||||
type="submit"
|
||||
icon={<FaPlay />}
|
||||
isDisabled={!isReady}
|
||||
onClick={handleClickGenerate}
|
||||
className="invoke-btn invoke"
|
||||
tooltip="Invoke"
|
||||
tooltipPlacement="bottom"
|
||||
{...rest}
|
||||
/>
|
||||
) : (
|
||||
<IAIButton
|
||||
label="Invoke"
|
||||
aria-label="Invoke"
|
||||
@ -24,6 +53,7 @@ export default function InvokeButton() {
|
||||
isDisabled={!isReady}
|
||||
onClick={handleClickGenerate}
|
||||
className="invoke-btn"
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
29
frontend/src/features/options/ProcessButtons/Loopback.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { FaRecycle } from 'react-icons/fa';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAIIconButton from '../../../common/components/IAIIconButton';
|
||||
import { OptionsState, setShouldLoopback } from '../optionsSlice';
|
||||
|
||||
const loopbackSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => options.shouldLoopback
|
||||
);
|
||||
|
||||
const LoopbackButton = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const shouldLoopback = useAppSelector(loopbackSelector);
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
aria-label="Loopback"
|
||||
tooltip="Loopback"
|
||||
data-selected={shouldLoopback}
|
||||
icon={<FaRecycle />}
|
||||
onClick={() => {
|
||||
dispatch(setShouldLoopback(!shouldLoopback));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoopbackButton;
|
@ -1,23 +1,27 @@
|
||||
@use '../../../styles/Mixins/' as *;
|
||||
|
||||
.process-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
display: flex;
|
||||
column-gap: 0.5rem;
|
||||
}
|
||||
|
||||
.invoke-btn {
|
||||
.invoke-btn {
|
||||
flex-grow: 1;
|
||||
svg {
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
}
|
||||
@include Button(
|
||||
$btn-color: var(--accent-color),
|
||||
$btn-color-hover: var(--accent-color-hover),
|
||||
$btn-width: 5rem
|
||||
// $btn-width: 5rem
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
.cancel-btn {
|
||||
@include Button(
|
||||
$btn-color: var(--destructive-color),
|
||||
$btn-color-hover: var(--destructive-color-hover),
|
||||
$btn-width: 3rem
|
||||
// $btn-width: 3rem
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import InvokeButton from './InvokeButton';
|
||||
import CancelButton from './CancelButton';
|
||||
import LoopbackButton from './Loopback';
|
||||
|
||||
/**
|
||||
* Buttons to start and cancel image generation.
|
||||
@ -8,6 +9,7 @@ const ProcessButtons = () => {
|
||||
return (
|
||||
<div className="process-buttons">
|
||||
<InvokeButton />
|
||||
<LoopbackButton />
|
||||
<CancelButton />
|
||||
</div>
|
||||
);
|
||||
|
@ -6,16 +6,16 @@ import { generateImage } from '../../../app/socketio/actions';
|
||||
import { OptionsState, setPrompt } from '../optionsSlice';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import _ from 'lodash';
|
||||
import useCheckParameters from '../../../common/hooks/useCheckParameters';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { tabMap } from '../../tabs/InvokeTabs';
|
||||
import { activeTabNameSelector } from '../optionsSelectors';
|
||||
import { readinessSelector } from '../../../app/selectors/readinessSelector';
|
||||
|
||||
const promptInputSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
[(state: RootState) => state.options, activeTabNameSelector],
|
||||
(options: OptionsState, activeTabName) => {
|
||||
return {
|
||||
prompt: options.prompt,
|
||||
activeTabName: tabMap[options.activeTab],
|
||||
activeTabName,
|
||||
};
|
||||
},
|
||||
{
|
||||
@ -29,25 +29,16 @@ const promptInputSelector = createSelector(
|
||||
* Prompt input text area.
|
||||
*/
|
||||
const PromptInput = () => {
|
||||
const promptRef = useRef<HTMLTextAreaElement>(null);
|
||||
const { prompt, activeTabName } = useAppSelector(promptInputSelector);
|
||||
const dispatch = useAppDispatch();
|
||||
const isReady = useCheckParameters();
|
||||
const { prompt, activeTabName } = useAppSelector(promptInputSelector);
|
||||
const isReady = useAppSelector(readinessSelector);
|
||||
|
||||
const promptRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const handleChangePrompt = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
dispatch(setPrompt(e.target.value));
|
||||
};
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+enter, cmd+enter',
|
||||
() => {
|
||||
if (isReady) {
|
||||
dispatch(generateImage(activeTabName));
|
||||
}
|
||||
},
|
||||
[isReady, activeTabName]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'alt+a',
|
||||
() => {
|
||||
|
15
frontend/src/features/options/optionsSelectors.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import _ from 'lodash';
|
||||
import { RootState } from '../../app/store';
|
||||
import { tabMap } from '../tabs/InvokeTabs';
|
||||
import { OptionsState } from './optionsSlice';
|
||||
|
||||
export const activeTabNameSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => tabMap[options.activeTab],
|
||||
{
|
||||
memoizeOptions: {
|
||||
equalityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
@ -42,6 +42,11 @@ export interface OptionsState {
|
||||
activeTab: number;
|
||||
shouldShowImageDetails: boolean;
|
||||
showDualDisplay: boolean;
|
||||
shouldShowOptionsPanel: boolean;
|
||||
shouldPinOptionsPanel: boolean;
|
||||
optionsPanelScrollPosition: number;
|
||||
shouldHoldOptionsPanelOpen: boolean;
|
||||
shouldLoopback: boolean;
|
||||
}
|
||||
|
||||
const initialOptionsState: OptionsState = {
|
||||
@ -75,6 +80,11 @@ const initialOptionsState: OptionsState = {
|
||||
activeTab: 0,
|
||||
shouldShowImageDetails: false,
|
||||
showDualDisplay: true,
|
||||
shouldShowOptionsPanel: true,
|
||||
shouldPinOptionsPanel: true,
|
||||
optionsPanelScrollPosition: 0,
|
||||
shouldHoldOptionsPanelOpen: false,
|
||||
shouldLoopback: true,
|
||||
};
|
||||
|
||||
const initialState: OptionsState = initialOptionsState;
|
||||
@ -324,6 +334,21 @@ export const optionsSlice = createSlice({
|
||||
clearInitialImage: (state) => {
|
||||
state.initialImage = undefined;
|
||||
},
|
||||
setShouldPinOptionsPanel: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldPinOptionsPanel = action.payload;
|
||||
},
|
||||
setShouldShowOptionsPanel: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldShowOptionsPanel = action.payload;
|
||||
},
|
||||
setOptionsPanelScrollPosition: (state, action: PayloadAction<number>) => {
|
||||
state.optionsPanelScrollPosition = action.payload;
|
||||
},
|
||||
setShouldHoldOptionsPanelOpen: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldHoldOptionsPanelOpen = action.payload;
|
||||
},
|
||||
setShouldLoopback: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldLoopback = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -366,6 +391,11 @@ export const {
|
||||
setShowDualDisplay,
|
||||
setInitialImage,
|
||||
clearInitialImage,
|
||||
setShouldShowOptionsPanel,
|
||||
setShouldPinOptionsPanel,
|
||||
setOptionsPanelScrollPosition,
|
||||
setShouldHoldOptionsPanelOpen,
|
||||
setShouldLoopback,
|
||||
} = optionsSlice.actions;
|
||||
|
||||
export default optionsSlice.reducer;
|
||||
|
@ -1,3 +1,10 @@
|
||||
.console-resizable {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.console {
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
@ -41,12 +48,13 @@
|
||||
position: fixed !important;
|
||||
left: 0.5rem;
|
||||
bottom: 0.5rem;
|
||||
z-index: 21;
|
||||
|
||||
&:hover {
|
||||
background: var(--console-icon-button-bg-color-hover) !important;
|
||||
}
|
||||
|
||||
&.error-seen {
|
||||
&[data-error-seen='true'] {
|
||||
background: var(--status-bad-color) !important;
|
||||
&:hover {
|
||||
background: var(--status-bad-color) !important;
|
||||
@ -59,12 +67,13 @@
|
||||
position: fixed !important;
|
||||
left: 0.5rem;
|
||||
bottom: 3rem;
|
||||
z-index: 21;
|
||||
|
||||
&:hover {
|
||||
background: var(--console-icon-button-bg-color-hover) !important;
|
||||
}
|
||||
|
||||
&.autoscroll-enabled {
|
||||
&[data-autoscroll-enabled='true'] {
|
||||
background: var(--accent-color) !important;
|
||||
&:hover {
|
||||
background: var(--accent-color-hover) !important;
|
||||
|
@ -2,7 +2,7 @@ import { IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import { errorSeen, setShouldShowLogViewer, SystemState } from './systemSlice';
|
||||
import { useLayoutEffect, useRef, useState } from 'react';
|
||||
import { UIEvent, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { FaAngleDoubleDown, FaCode, FaMinus } from 'react-icons/fa';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
@ -75,6 +75,17 @@ const Console = () => {
|
||||
[shouldShowLogViewer]
|
||||
);
|
||||
|
||||
const handleOnScroll = () => {
|
||||
if (!viewerRef.current) return;
|
||||
if (
|
||||
shouldAutoscroll &&
|
||||
viewerRef.current.scrollTop <
|
||||
viewerRef.current.scrollHeight - viewerRef.current.clientHeight
|
||||
) {
|
||||
setShouldAutoscroll(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{shouldShowLogViewer && (
|
||||
@ -83,10 +94,16 @@ const Console = () => {
|
||||
width: '100%',
|
||||
height: 200,
|
||||
}}
|
||||
style={{ display: 'flex', position: 'fixed', left: 0, bottom: 0 }}
|
||||
style={{
|
||||
display: 'flex',
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
zIndex: 20,
|
||||
}}
|
||||
maxHeight={'90vh'}
|
||||
>
|
||||
<div className="console" ref={viewerRef}>
|
||||
<div className="console" ref={viewerRef} onScroll={handleOnScroll}>
|
||||
{log.map((entry, i) => {
|
||||
const { timestamp, message, level } = entry;
|
||||
return (
|
||||
@ -100,11 +117,13 @@ const Console = () => {
|
||||
</Resizable>
|
||||
)}
|
||||
{shouldShowLogViewer && (
|
||||
<Tooltip hasArrow label={shouldAutoscroll ? 'Autoscroll On' : 'Autoscroll Off'}>
|
||||
<Tooltip
|
||||
hasArrow
|
||||
label={shouldAutoscroll ? 'Autoscroll On' : 'Autoscroll Off'}
|
||||
>
|
||||
<IconButton
|
||||
className={`console-autoscroll-icon-button ${
|
||||
shouldAutoscroll && 'autoscroll-enabled'
|
||||
}`}
|
||||
className={'console-autoscroll-icon-button'}
|
||||
data-autoscroll-enabled={shouldAutoscroll}
|
||||
size="sm"
|
||||
aria-label="Toggle autoscroll"
|
||||
variant={'solid'}
|
||||
@ -113,16 +132,17 @@ const Console = () => {
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip hasArrow label={shouldShowLogViewer ? 'Hide Console' : 'Show Console'}>
|
||||
<Tooltip
|
||||
hasArrow
|
||||
label={shouldShowLogViewer ? 'Hide Console' : 'Show Console'}
|
||||
>
|
||||
<IconButton
|
||||
className={`console-toggle-icon-button ${
|
||||
(hasError || !wasErrorSeen) && 'error-seen'
|
||||
}`}
|
||||
className={'console-toggle-icon-button'}
|
||||
data-error-seen={hasError || !wasErrorSeen}
|
||||
size="sm"
|
||||
position={'fixed'}
|
||||
variant={'solid'}
|
||||
aria-label="Toggle Log Viewer"
|
||||
// colorScheme={hasError || !wasErrorSeen ? 'red' : 'gray'}
|
||||
icon={shouldShowLogViewer ? <FaMinus /> : <FaCode />}
|
||||
onClick={handleClickLogViewerToggle}
|
||||
/>
|
||||
|
@ -39,6 +39,16 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
||||
desc: 'Focus the prompt input area',
|
||||
hotkey: 'Alt+A',
|
||||
},
|
||||
{
|
||||
title: 'Toggle Options',
|
||||
desc: 'Open and close the options panel',
|
||||
hotkey: 'O',
|
||||
},
|
||||
{
|
||||
title: 'Pin Options',
|
||||
desc: 'Pin the options panel',
|
||||
hotkey: 'Shift+O',
|
||||
},
|
||||
{
|
||||
title: 'Toggle Gallery',
|
||||
desc: 'Open and close the gallery drawer',
|
||||
@ -101,7 +111,7 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
||||
{
|
||||
title: 'Toggle Gallery Pin',
|
||||
desc: 'Pins and unpins the gallery to the UI',
|
||||
hotkey: 'Shift+P',
|
||||
hotkey: 'Shift+G',
|
||||
},
|
||||
{
|
||||
title: 'Increase Gallery Image Size',
|
||||
@ -134,7 +144,7 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
||||
{
|
||||
title: 'Quick Toggle Brush/Eraser',
|
||||
desc: 'Quick toggle between brush and eraser',
|
||||
hotkey: 'Z',
|
||||
hotkey: 'X',
|
||||
},
|
||||
{
|
||||
title: 'Decrease Brush Size',
|
||||
|
@ -3,6 +3,7 @@
|
||||
.progress-bar {
|
||||
background-color: var(--root-bg-color);
|
||||
height: $progress-bar-thickness !important;
|
||||
z-index: 99;
|
||||
|
||||
div {
|
||||
background-color: var(--progress-bar-color);
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { IconButton, Link, Tooltip, useColorMode } from '@chakra-ui/react';
|
||||
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import { FaSun, FaMoon, FaGithub, FaDiscord } from 'react-icons/fa';
|
||||
import { MdHelp, MdKeyboard, MdSettings } from 'react-icons/md';
|
||||
|
||||
import InvokeAILogo from '../../assets/images/logo.png';
|
||||
|
||||
import HotkeysModal from './HotkeysModal/HotkeysModal';
|
||||
|
||||
import SettingsModal from './SettingsModal/SettingsModal';
|
||||
|
@ -172,6 +172,12 @@ export const systemSlice = createSlice({
|
||||
setIsCancelable: (state, action: PayloadAction<boolean>) => {
|
||||
state.isCancelable = action.payload;
|
||||
},
|
||||
modelChangeRequested: (state) => {
|
||||
state.currentStatus = 'Loading Model';
|
||||
state.isCancelable = false;
|
||||
state.isProcessing = true;
|
||||
state.currentStatusHasSteps = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -193,6 +199,7 @@ export const {
|
||||
errorSeen,
|
||||
setModelList,
|
||||
setIsCancelable,
|
||||
modelChangeRequested,
|
||||
} = systemSlice.actions;
|
||||
|
||||
export default systemSlice.reducer;
|
||||
|
52
frontend/src/features/tabs/FloatingButton.scss
Normal file
@ -0,0 +1,52 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.floating-show-hide-button {
|
||||
position: absolute !important;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
z-index: 20;
|
||||
padding: 0;
|
||||
|
||||
&.left {
|
||||
left: 0;
|
||||
border-radius: 0 0.5rem 0.5rem 0 !important;
|
||||
}
|
||||
|
||||
&.right {
|
||||
right: 0;
|
||||
border-radius: 0.5rem 0 0 0.5rem !important;
|
||||
}
|
||||
|
||||
@include Button(
|
||||
$btn-width: 1rem,
|
||||
$btn-height: 12rem,
|
||||
$icon-size: 20px,
|
||||
$btn-color: var(--btn-grey),
|
||||
$btn-color-hover: var(--btn-grey-hover)
|
||||
);
|
||||
}
|
||||
|
||||
.show-hide-button-options {
|
||||
position: absolute !important;
|
||||
transform: translate(0, -50%);
|
||||
z-index: 20;
|
||||
min-width: 2rem !important;
|
||||
|
||||
top: 50%;
|
||||
left: calc(42px + 2rem);
|
||||
|
||||
border-radius: 0 0.5rem 0.5rem 0 !important;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 0.5rem;
|
||||
|
||||
button {
|
||||
border-radius: 0 0.3rem 0.3rem 0;
|
||||
background-color: var(--btn-grey);
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
}
|
||||
}
|
||||
}
|
26
frontend/src/features/tabs/FloatingGalleryButton.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { MdPhotoLibrary } from 'react-icons/md';
|
||||
import { useAppDispatch } from '../../app/store';
|
||||
import IAIIconButton from '../../common/components/IAIIconButton';
|
||||
import { setShouldShowGallery } from '../gallery/gallerySlice';
|
||||
|
||||
const FloatingGalleryButton = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleShowGallery = () => {
|
||||
dispatch(setShouldShowGallery(true));
|
||||
};
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
tooltip="Show Gallery (G)"
|
||||
tooltipPlacement="top"
|
||||
aria-label="Show Gallery"
|
||||
styleClass="floating-show-hide-button right"
|
||||
onMouseOver={handleShowGallery}
|
||||
>
|
||||
<MdPhotoLibrary />
|
||||
</IAIIconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default FloatingGalleryButton;
|
56
frontend/src/features/tabs/FloatingOptionsPanelButtons.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { IoMdOptions } from 'react-icons/io';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import IAIIconButton from '../../common/components/IAIIconButton';
|
||||
import {
|
||||
OptionsState,
|
||||
setShouldShowOptionsPanel,
|
||||
} from '../options/optionsSlice';
|
||||
import CancelButton from '../options/ProcessButtons/CancelButton';
|
||||
import InvokeButton from '../options/ProcessButtons/InvokeButton';
|
||||
import _ from 'lodash';
|
||||
import LoopbackButton from '../options/ProcessButtons/Loopback';
|
||||
|
||||
const canInvokeSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
|
||||
(options: OptionsState) => {
|
||||
const { shouldPinOptionsPanel, shouldShowOptionsPanel } = options;
|
||||
return {
|
||||
shouldShowProcessButtons:
|
||||
!shouldPinOptionsPanel || !shouldShowOptionsPanel,
|
||||
};
|
||||
},
|
||||
{ memoizeOptions: { resultEqualityCheck: _.isEqual } }
|
||||
);
|
||||
|
||||
const FloatingOptionsPanelButtons = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { shouldShowProcessButtons } = useAppSelector(canInvokeSelector);
|
||||
|
||||
const handleShowOptionsPanel = () => {
|
||||
dispatch(setShouldShowOptionsPanel(true));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="show-hide-button-options">
|
||||
<IAIIconButton
|
||||
tooltip="Show Options Panel (O)"
|
||||
tooltipPlacement="top"
|
||||
aria-label="Show Options Panel"
|
||||
onClick={handleShowOptionsPanel}
|
||||
>
|
||||
<IoMdOptions />
|
||||
</IAIIconButton>
|
||||
{shouldShowProcessButtons && (
|
||||
<>
|
||||
<InvokeButton iconButton />
|
||||
<LoopbackButton />
|
||||
<CancelButton />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FloatingOptionsPanelButtons;
|
@ -8,15 +8,6 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.image-to-image-panel {
|
||||
display: grid;
|
||||
row-gap: 1rem;
|
||||
grid-auto-rows: max-content;
|
||||
height: $app-content-height;
|
||||
overflow-y: scroll;
|
||||
@include HideScrollbar;
|
||||
}
|
||||
|
||||
.image-to-image-strength-main-option {
|
||||
display: grid;
|
||||
grid-template-columns: none !important;
|
||||
|
@ -17,6 +17,7 @@ import MainOptions from '../../options/MainOptions/MainOptions';
|
||||
import OptionsAccordion from '../../options/OptionsAccordion';
|
||||
import ProcessButtons from '../../options/ProcessButtons/ProcessButtons';
|
||||
import PromptInput from '../../options/PromptInput/PromptInput';
|
||||
import InvokeOptionsPanel from '../InvokeOptionsPanel';
|
||||
|
||||
export default function ImageToImagePanel() {
|
||||
const showAdvancedOptions = useAppSelector(
|
||||
@ -45,14 +46,14 @@ export default function ImageToImagePanel() {
|
||||
options: <UpscaleOptions />,
|
||||
},
|
||||
other: {
|
||||
header: <OutputHeader /> ,
|
||||
header: <OutputHeader />,
|
||||
feature: Feature.OTHER,
|
||||
options: <OutputOptions />,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-to-image-panel">
|
||||
<InvokeOptionsPanel>
|
||||
<PromptInput />
|
||||
<ProcessButtons />
|
||||
<MainOptions />
|
||||
@ -65,6 +66,6 @@ export default function ImageToImagePanel() {
|
||||
{showAdvancedOptions ? (
|
||||
<OptionsAccordion accordionInfo={imageToImageAccordions} />
|
||||
) : null}
|
||||
</div>
|
||||
</InvokeOptionsPanel>
|
||||
);
|
||||
}
|
||||
|
@ -83,11 +83,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Overrides
|
||||
.inpainting-workarea-overrides {
|
||||
.image-gallery-area {
|
||||
.chakra-popover__popper {
|
||||
inset: 0 auto auto -75px !important;
|
||||
}
|
||||
}
|
||||
.inpainting-options-btn {
|
||||
min-height: 2rem;
|
||||
}
|
||||
|
@ -53,10 +53,11 @@ const InpaintingCanvas = () => {
|
||||
maskColor,
|
||||
imageToInpaint,
|
||||
stageScale,
|
||||
shouldShowBoundingBox,
|
||||
shouldShowBoundingBoxFill,
|
||||
isDrawing,
|
||||
shouldLockBoundingBox,
|
||||
shouldShowBoundingBox,
|
||||
boundingBoxDimensions,
|
||||
} = useAppSelector(inpaintingCanvasSelector);
|
||||
|
||||
const toast = useToast();
|
||||
@ -95,7 +96,7 @@ const InpaintingCanvas = () => {
|
||||
};
|
||||
image.src = imageToInpaint.url;
|
||||
} else {
|
||||
setCanvasBgImage(null)
|
||||
setCanvasBgImage(null);
|
||||
}
|
||||
}, [imageToInpaint, dispatch, stageScale, toast]);
|
||||
|
||||
@ -243,7 +244,7 @@ const InpaintingCanvas = () => {
|
||||
)}
|
||||
{!shouldLockBoundingBox && (
|
||||
<div style={{ pointerEvents: 'none' }}>
|
||||
Transforming Bounding Box (M)
|
||||
{`Transforming Bounding Box ${boundingBoxDimensions.width}x${boundingBoxDimensions.height} (M)`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -299,8 +300,9 @@ const InpaintingCanvas = () => {
|
||||
/>
|
||||
)}
|
||||
</Layer>
|
||||
{shouldShowMask && (
|
||||
<Layer>
|
||||
{shouldShowBoundingBox && shouldShowBoundingBoxFill && (
|
||||
{shouldShowBoundingBoxFill && shouldShowBoundingBox && (
|
||||
<InpaintingBoundingBoxPreviewOverlay />
|
||||
)}
|
||||
{shouldShowBoundingBox && <InpaintingBoundingBoxPreview />}
|
||||
@ -308,6 +310,7 @@ const InpaintingCanvas = () => {
|
||||
<InpaintingCanvasBrushPreviewOutline />
|
||||
)}
|
||||
</Layer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Stage>
|
||||
|
@ -11,6 +11,7 @@ const InpaintingCanvasPlaceholder = () => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
window.setTimeout(() => {
|
||||
if (!ref.current || !imageToInpaint) return;
|
||||
|
||||
const width = ref.current.clientWidth;
|
||||
@ -22,6 +23,7 @@ const InpaintingCanvasPlaceholder = () => {
|
||||
);
|
||||
|
||||
dispatch(setStageScale(scale));
|
||||
}, 0);
|
||||
}, [dispatch, imageToInpaint, needsCache]);
|
||||
|
||||
return (
|
||||
|
@ -51,7 +51,6 @@ const InpaintingControls = () => {
|
||||
isMaskEmpty,
|
||||
activeTabName,
|
||||
showDualDisplay,
|
||||
shouldShowBoundingBox
|
||||
} = useAppSelector(inpaintingControlsSelector);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
@ -161,9 +160,9 @@ const InpaintingControls = () => {
|
||||
dispatch(toggleShouldLockBoundingBox());
|
||||
},
|
||||
{
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask && shouldShowBoundingBox,
|
||||
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||
},
|
||||
[activeTabName, shouldShowMask, shouldShowBoundingBox]
|
||||
[activeTabName, shouldShowMask]
|
||||
);
|
||||
|
||||
// Undo
|
||||
@ -349,7 +348,6 @@ const InpaintingControls = () => {
|
||||
tooltip="Mask Options"
|
||||
icon={<FaMask />}
|
||||
cursor={'pointer'}
|
||||
isDisabled={isMaskEmpty}
|
||||
data-selected={maskOptionsOpen}
|
||||
/>
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import MainOptions from '../../options/MainOptions/MainOptions';
|
||||
import OptionsAccordion from '../../options/OptionsAccordion';
|
||||
import ProcessButtons from '../../options/ProcessButtons/ProcessButtons';
|
||||
import PromptInput from '../../options/PromptInput/PromptInput';
|
||||
import InvokeOptionsPanel from '../InvokeOptionsPanel';
|
||||
|
||||
export default function InpaintingPanel() {
|
||||
const showAdvancedOptions = useAppSelector(
|
||||
@ -45,7 +46,7 @@ export default function InpaintingPanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="image-to-image-panel">
|
||||
<InvokeOptionsPanel>
|
||||
<PromptInput />
|
||||
<ProcessButtons />
|
||||
<MainOptions />
|
||||
@ -58,6 +59,6 @@ export default function InpaintingPanel() {
|
||||
{showAdvancedOptions ? (
|
||||
<OptionsAccordion accordionInfo={imageToImageAccordions} />
|
||||
) : null}
|
||||
</div>
|
||||
</InvokeOptionsPanel>
|
||||
);
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import { roundToMultiple } from '../../../../common/util/roundDownToMultiple';
|
||||
import { stageRef } from '../InpaintingCanvas';
|
||||
import {
|
||||
InpaintingState,
|
||||
setBoundingBoxCoordinate,
|
||||
@ -107,6 +108,15 @@ const InpaintingBoundingBoxPreview = () => {
|
||||
transformerRef.current.getLayer()?.batchDraw();
|
||||
}, [shouldLockBoundingBox]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
const container = stageRef.current?.container();
|
||||
if (!container) return;
|
||||
container.style.cursor = 'unset';
|
||||
},
|
||||
[shouldLockBoundingBox]
|
||||
);
|
||||
|
||||
const scaledStep = 64 * stageScale;
|
||||
|
||||
const handleOnDragMove = useCallback(
|
||||
|
@ -6,8 +6,8 @@ import {
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import { activeTabNameSelector } from '../../../options/optionsSelectors';
|
||||
import { OptionsState } from '../../../options/optionsSlice';
|
||||
import { tabMap } from '../../InvokeTabs';
|
||||
import {
|
||||
InpaintingState,
|
||||
setIsDrawing,
|
||||
@ -16,12 +16,16 @@ import {
|
||||
} from '../inpaintingSlice';
|
||||
|
||||
const keyboardEventManagerSelector = createSelector(
|
||||
[(state: RootState) => state.options, (state: RootState) => state.inpainting],
|
||||
(options: OptionsState, inpainting: InpaintingState) => {
|
||||
[
|
||||
(state: RootState) => state.options,
|
||||
(state: RootState) => state.inpainting,
|
||||
activeTabNameSelector,
|
||||
],
|
||||
(options: OptionsState, inpainting: InpaintingState, activeTabName) => {
|
||||
const { shouldShowMask, cursorPosition, shouldLockBoundingBox } =
|
||||
inpainting;
|
||||
return {
|
||||
activeTabName: tabMap[options.activeTab],
|
||||
activeTabName,
|
||||
shouldShowMask,
|
||||
isCursorOnCanvas: Boolean(cursorPosition),
|
||||
shouldLockBoundingBox,
|
||||
@ -49,7 +53,7 @@ const KeyboardEventManager = () => {
|
||||
useEffect(() => {
|
||||
const listener = (e: KeyboardEvent) => {
|
||||
if (
|
||||
!['z', ' '].includes(e.key) ||
|
||||
!['x', ' '].includes(e.key) ||
|
||||
activeTabName !== 'inpainting' ||
|
||||
!shouldShowMask
|
||||
) {
|
||||
@ -83,7 +87,7 @@ const KeyboardEventManager = () => {
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case 'z': {
|
||||
case 'x': {
|
||||
dispatch(toggleTool());
|
||||
break;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { Vector2d } from 'konva/lib/types';
|
||||
import { IRect, Vector2d } from 'konva/lib/types';
|
||||
import { RgbaColor } from 'react-colorful';
|
||||
import * as InvokeAI from '../../../app/invokeai';
|
||||
import _ from 'lodash';
|
||||
@ -61,11 +61,11 @@ const initialInpaintingState: InpaintingState = {
|
||||
brushSize: 50,
|
||||
maskColor: { r: 255, g: 90, b: 90, a: 0.5 },
|
||||
canvasDimensions: { width: 0, height: 0 },
|
||||
boundingBoxDimensions: { width: 64, height: 64 },
|
||||
boundingBoxDimensions: { width: 512, height: 512 },
|
||||
boundingBoxCoordinate: { x: 0, y: 0 },
|
||||
boundingBoxPreviewFill: { r: 0, g: 0, b: 0, a: 0.7 },
|
||||
shouldShowBoundingBox: false,
|
||||
shouldShowBoundingBoxFill: false,
|
||||
shouldShowBoundingBox: true,
|
||||
shouldShowBoundingBoxFill: true,
|
||||
cursorPosition: null,
|
||||
lines: [],
|
||||
pastLines: [],
|
||||
@ -164,36 +164,32 @@ export const inpaintingSlice = createSlice({
|
||||
},
|
||||
setImageToInpaint: (state, action: PayloadAction<InvokeAI.Image>) => {
|
||||
const { width: imageWidth, height: imageHeight } = action.payload;
|
||||
const { width: boundingBoxWidth, height: boundingBoxHeight } =
|
||||
state.boundingBoxDimensions;
|
||||
const { width, height } = state.boundingBoxDimensions;
|
||||
const { x, y } = state.boundingBoxCoordinate;
|
||||
|
||||
const newBoundingBoxWidth = roundDownToMultiple(
|
||||
_.clamp(boundingBoxWidth, 64, imageWidth),
|
||||
64
|
||||
);
|
||||
const newCoordinates: Vector2d = { x, y };
|
||||
const newDimensions: Dimensions = { width, height };
|
||||
|
||||
const newBoundingBoxHeight = roundDownToMultiple(
|
||||
_.clamp(boundingBoxHeight, 64, imageHeight),
|
||||
64
|
||||
);
|
||||
if (width + x > imageWidth) {
|
||||
// Bounding box at least needs to be translated
|
||||
if (width > imageWidth) {
|
||||
// Bounding box also needs to be resized
|
||||
newDimensions.width = roundDownToMultiple(imageWidth, 64);
|
||||
}
|
||||
newCoordinates.x = imageWidth - newDimensions.width;
|
||||
}
|
||||
|
||||
const newBoundingBoxX = roundDownToMultiple(
|
||||
_.clamp(x, 0, imageWidth - newBoundingBoxWidth),
|
||||
64
|
||||
);
|
||||
if (height + y > imageHeight) {
|
||||
// Bounding box at least needs to be translated
|
||||
if (height > imageHeight) {
|
||||
// Bounding box also needs to be resized
|
||||
newDimensions.height = roundDownToMultiple(imageHeight, 64);
|
||||
}
|
||||
newCoordinates.y = imageHeight - newDimensions.height;
|
||||
}
|
||||
|
||||
const newBoundingBoxY = roundDownToMultiple(
|
||||
_.clamp(y, 0, imageHeight - newBoundingBoxHeight),
|
||||
64
|
||||
);
|
||||
|
||||
state.boundingBoxDimensions = {
|
||||
width: newBoundingBoxWidth,
|
||||
height: newBoundingBoxHeight,
|
||||
};
|
||||
|
||||
state.boundingBoxCoordinate = { x: newBoundingBoxX, y: newBoundingBoxY };
|
||||
state.boundingBoxDimensions = newDimensions;
|
||||
state.boundingBoxCoordinate = newCoordinates;
|
||||
|
||||
state.canvasDimensions = {
|
||||
width: imageWidth,
|
||||
@ -304,9 +300,6 @@ export const inpaintingSlice = createSlice({
|
||||
setIsDrawing: (state, action: PayloadAction<boolean>) => {
|
||||
state.isDrawing = action.payload;
|
||||
},
|
||||
setShouldShowBoundingBox: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldShowBoundingBox = action.payload;
|
||||
},
|
||||
setClearBrushHistory: (state) => {
|
||||
state.pastLines = [];
|
||||
state.futureLines = [];
|
||||
@ -323,6 +316,9 @@ export const inpaintingSlice = createSlice({
|
||||
toggleShouldLockBoundingBox: (state) => {
|
||||
state.shouldLockBoundingBox = !state.shouldLockBoundingBox;
|
||||
},
|
||||
setShouldShowBoundingBox: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldShowBoundingBox = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -349,10 +345,10 @@ export const {
|
||||
setNeedsCache,
|
||||
setStageScale,
|
||||
toggleTool,
|
||||
setShouldShowBoundingBox,
|
||||
setShouldShowBoundingBoxFill,
|
||||
setIsDrawing,
|
||||
setShouldShowBrush,
|
||||
setShouldShowBoundingBox,
|
||||
setClearBrushHistory,
|
||||
setShouldUseInpaintReplace,
|
||||
setInpaintReplace,
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import _ from 'lodash';
|
||||
import { RootState } from '../../../app/store';
|
||||
import { activeTabNameSelector } from '../../options/optionsSelectors';
|
||||
import { OptionsState } from '../../options/optionsSlice';
|
||||
import { tabMap } from '../InvokeTabs';
|
||||
import { InpaintingState } from './inpaintingSlice';
|
||||
import { rgbaColorToRgbString } from './util/colorToString';
|
||||
|
||||
@ -18,8 +18,12 @@ export const inpaintingCanvasLinesSelector = createSelector(
|
||||
);
|
||||
|
||||
export const inpaintingControlsSelector = createSelector(
|
||||
[(state: RootState) => state.inpainting, (state: RootState) => state.options],
|
||||
(inpainting: InpaintingState, options: OptionsState) => {
|
||||
[
|
||||
(state: RootState) => state.inpainting,
|
||||
(state: RootState) => state.options,
|
||||
activeTabNameSelector,
|
||||
],
|
||||
(inpainting: InpaintingState, options: OptionsState, activeTabName) => {
|
||||
const {
|
||||
tool,
|
||||
brushSize,
|
||||
@ -31,10 +35,9 @@ export const inpaintingControlsSelector = createSelector(
|
||||
pastLines,
|
||||
futureLines,
|
||||
shouldShowBoundingBoxFill,
|
||||
shouldShowBoundingBox,
|
||||
} = inpainting;
|
||||
|
||||
const { activeTab, showDualDisplay } = options;
|
||||
const { showDualDisplay } = options;
|
||||
|
||||
return {
|
||||
tool,
|
||||
@ -46,10 +49,9 @@ export const inpaintingControlsSelector = createSelector(
|
||||
canUndo: pastLines.length > 0,
|
||||
canRedo: futureLines.length > 0,
|
||||
isMaskEmpty: lines.length === 0,
|
||||
activeTabName: tabMap[activeTab],
|
||||
activeTabName,
|
||||
showDualDisplay,
|
||||
shouldShowBoundingBoxFill,
|
||||
shouldShowBoundingBox,
|
||||
};
|
||||
},
|
||||
{
|
||||
@ -71,10 +73,11 @@ export const inpaintingCanvasSelector = createSelector(
|
||||
shouldShowCheckboardTransparency,
|
||||
imageToInpaint,
|
||||
stageScale,
|
||||
shouldShowBoundingBox,
|
||||
shouldShowBoundingBoxFill,
|
||||
isDrawing,
|
||||
shouldLockBoundingBox,
|
||||
shouldShowBoundingBox,
|
||||
boundingBoxDimensions,
|
||||
} = inpainting;
|
||||
return {
|
||||
tool,
|
||||
@ -85,10 +88,11 @@ export const inpaintingCanvasSelector = createSelector(
|
||||
maskColor,
|
||||
imageToInpaint,
|
||||
stageScale,
|
||||
shouldShowBoundingBox,
|
||||
shouldShowBoundingBoxFill,
|
||||
isDrawing,
|
||||
shouldLockBoundingBox,
|
||||
shouldShowBoundingBox,
|
||||
boundingBoxDimensions,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
@ -1,49 +0,0 @@
|
||||
import Konva from 'konva';
|
||||
import { MaskLine } from '../inpaintingSlice';
|
||||
|
||||
/**
|
||||
* Converts canvas into pixel buffer and checks if it is empty (all pixels full alpha).
|
||||
*
|
||||
* I DON' THINK THIS WORKS ACTUALLY
|
||||
*/
|
||||
const checkIsMaskEmpty = (image: HTMLImageElement, lines: MaskLine[]) => {
|
||||
const offscreenContainer = document.createElement('div');
|
||||
|
||||
const { width, height } = image;
|
||||
|
||||
const stage = new Konva.Stage({
|
||||
container: offscreenContainer,
|
||||
width: width,
|
||||
height: height,
|
||||
});
|
||||
|
||||
const layer = new Konva.Layer();
|
||||
|
||||
stage.add(layer);
|
||||
|
||||
lines.forEach((line) =>
|
||||
layer.add(
|
||||
new Konva.Line({
|
||||
points: line.points,
|
||||
stroke: 'rgb(255,255,255)',
|
||||
strokeWidth: line.strokeWidth * 2,
|
||||
tension: 0,
|
||||
lineCap: 'round',
|
||||
lineJoin: 'round',
|
||||
shadowForStrokeEnabled: false,
|
||||
globalCompositeOperation:
|
||||
line.tool === 'brush' ? 'source-over' : 'destination-out',
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
offscreenContainer.remove();
|
||||
|
||||
const pixelBuffer = new Uint32Array(
|
||||
layer.getContext().getImageData(0, 0, width, height).data.buffer
|
||||
);
|
||||
|
||||
return !pixelBuffer.some((color) => color !== 0);
|
||||
};
|
||||
|
||||
export default checkIsMaskEmpty;
|
@ -3,20 +3,15 @@ import { IRect } from 'konva/lib/types';
|
||||
import { MaskLine } from '../inpaintingSlice';
|
||||
|
||||
/**
|
||||
* Generating a mask image from InpaintingCanvas.tsx is not as simple
|
||||
* as calling toDataURL() on the canvas, because the mask may be represented
|
||||
* by colored lines or transparency, or the user may have inverted the mask
|
||||
* display.
|
||||
*
|
||||
* So we need to regenerate the mask image by creating an offscreen canvas,
|
||||
* drawing the mask and compositing everything correctly to output a valid
|
||||
* mask image.
|
||||
* Re-draws the mask canvas onto a new Konva stage.
|
||||
*/
|
||||
const generateMask = (
|
||||
export const generateMaskCanvas = (
|
||||
image: HTMLImageElement,
|
||||
lines: MaskLine[],
|
||||
boundingBox: IRect
|
||||
) => {
|
||||
lines: MaskLine[]
|
||||
): {
|
||||
stage: Konva.Stage;
|
||||
layer: Konva.Layer;
|
||||
} => {
|
||||
const { width, height } = image;
|
||||
|
||||
const offscreenContainer = document.createElement('div');
|
||||
@ -35,7 +30,7 @@ const generateMask = (
|
||||
layer.add(
|
||||
new Konva.Line({
|
||||
points: line.points,
|
||||
stroke: 'rgb(255,255,255)',
|
||||
stroke: 'rgb(0,0,0)',
|
||||
strokeWidth: line.strokeWidth * 2,
|
||||
tension: 0,
|
||||
lineCap: 'round',
|
||||
@ -47,13 +42,68 @@ const generateMask = (
|
||||
)
|
||||
);
|
||||
|
||||
layer.draw();
|
||||
|
||||
offscreenContainer.remove();
|
||||
|
||||
return { stage, layer };
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the bounding box region has only fully transparent pixels.
|
||||
*/
|
||||
export const checkIsRegionEmpty = (
|
||||
stage: Konva.Stage,
|
||||
boundingBox: IRect
|
||||
): boolean => {
|
||||
const imageData = stage
|
||||
.toCanvas()
|
||||
.getContext('2d')
|
||||
?.getImageData(
|
||||
boundingBox.x,
|
||||
boundingBox.y,
|
||||
boundingBox.width,
|
||||
boundingBox.height
|
||||
);
|
||||
|
||||
if (!imageData) {
|
||||
throw new Error('Unable to get image data from generated canvas');
|
||||
}
|
||||
|
||||
const pixelBuffer = new Uint32Array(imageData.data.buffer);
|
||||
|
||||
return !pixelBuffer.some((color) => color !== 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generating a mask image from InpaintingCanvas.tsx is not as simple
|
||||
* as calling toDataURL() on the canvas, because the mask may be represented
|
||||
* by colored lines or transparency, or the user may have inverted the mask
|
||||
* display.
|
||||
*
|
||||
* So we need to regenerate the mask image by creating an offscreen canvas,
|
||||
* drawing the mask and compositing everything correctly to output a valid
|
||||
* mask image.
|
||||
*/
|
||||
const generateMask = (
|
||||
image: HTMLImageElement,
|
||||
lines: MaskLine[],
|
||||
boundingBox: IRect
|
||||
): { maskDataURL: string; isMaskEmpty: boolean } => {
|
||||
// create an offscreen canvas and add the mask to it
|
||||
const { stage, layer } = generateMaskCanvas(image, lines);
|
||||
|
||||
// check if the mask layer is empty
|
||||
const isMaskEmpty = checkIsRegionEmpty(stage, boundingBox);
|
||||
|
||||
// composite the image onto the mask layer
|
||||
layer.add(
|
||||
new Konva.Image({ image: image, globalCompositeOperation: 'source-out' })
|
||||
);
|
||||
|
||||
offscreenContainer.remove();
|
||||
const maskDataURL = stage.toDataURL();
|
||||
|
||||
return stage.toDataURL();
|
||||
return { maskDataURL, isMaskEmpty };
|
||||
};
|
||||
|
||||
export default generateMask;
|
||||
|
90
frontend/src/features/tabs/InvokeOptionsPanel.scss
Normal file
@ -0,0 +1,90 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.options-panel-wrapper-enter {
|
||||
transform: translateX(-150%);
|
||||
}
|
||||
|
||||
.options-panel-wrapper-enter-active {
|
||||
transform: translateX(0);
|
||||
transition: all 120ms ease-out;
|
||||
}
|
||||
|
||||
.options-panel-wrapper-exit {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.options-panel-wrapper-exit-active {
|
||||
transform: translateX(-150%);
|
||||
transition: all 120ms ease-out;
|
||||
}
|
||||
|
||||
.options-panel-wrapper {
|
||||
background-color: var(--background-color);
|
||||
height: $app-content-height;
|
||||
width: $options-bar-max-width;
|
||||
max-width: $options-bar-max-width;
|
||||
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
overflow-y: scroll;
|
||||
@include HideScrollbar;
|
||||
|
||||
.options-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 1rem;
|
||||
height: 100%;
|
||||
@include HideScrollbar;
|
||||
background-color: var(--background-color) !important;
|
||||
}
|
||||
|
||||
&[data-pinned='false'] {
|
||||
z-index: 20;
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
filter: var(--floating-panel-drop-shadow);
|
||||
width: calc($options-bar-max-width + 2rem);
|
||||
max-width: calc($options-bar-max-width + 2rem);
|
||||
height: 100%;
|
||||
|
||||
.options-panel-margin {
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.options-panel-pin-button {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 20;
|
||||
|
||||
&[data-selected='true'] {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
svg {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invoke-ai-logo-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 0.7rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-top: $progress-bar-thickness;
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
182
frontend/src/features/tabs/InvokeOptionsPanel.tsx
Normal file
@ -0,0 +1,182 @@
|
||||
import { Tooltip } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import _ from 'lodash';
|
||||
import { MouseEvent, ReactNode, useCallback, useRef } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import useClickOutsideWatcher from '../../common/hooks/useClickOutsideWatcher';
|
||||
import {
|
||||
OptionsState,
|
||||
setOptionsPanelScrollPosition,
|
||||
setShouldHoldOptionsPanelOpen,
|
||||
setShouldPinOptionsPanel,
|
||||
setShouldShowOptionsPanel,
|
||||
} from '../options/optionsSlice';
|
||||
import { setNeedsCache } from './Inpainting/inpaintingSlice';
|
||||
import InvokeAILogo from '../../assets/images/logo.png';
|
||||
|
||||
type Props = { children: ReactNode };
|
||||
|
||||
const optionsPanelSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
const {
|
||||
shouldShowOptionsPanel,
|
||||
shouldHoldOptionsPanelOpen,
|
||||
shouldPinOptionsPanel,
|
||||
optionsPanelScrollPosition,
|
||||
} = options;
|
||||
|
||||
return {
|
||||
shouldShowOptionsPanel,
|
||||
shouldHoldOptionsPanelOpen,
|
||||
shouldPinOptionsPanel,
|
||||
optionsPanelScrollPosition,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: _.isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const InvokeOptionsPanel = (props: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const {
|
||||
shouldShowOptionsPanel,
|
||||
shouldHoldOptionsPanelOpen,
|
||||
shouldPinOptionsPanel,
|
||||
} = useAppSelector(optionsPanelSelector);
|
||||
|
||||
const optionsPanelRef = useRef<HTMLDivElement>(null);
|
||||
const optionsPanelContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const timeoutIdRef = useRef<number | null>(null);
|
||||
|
||||
const { children } = props;
|
||||
|
||||
// Hotkeys
|
||||
useHotkeys(
|
||||
'o',
|
||||
() => {
|
||||
dispatch(setShouldShowOptionsPanel(!shouldShowOptionsPanel));
|
||||
},
|
||||
[shouldShowOptionsPanel]
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'shift+o',
|
||||
() => {
|
||||
handleClickPinOptionsPanel();
|
||||
},
|
||||
[shouldPinOptionsPanel]
|
||||
);
|
||||
//
|
||||
|
||||
const handleCloseOptionsPanel = useCallback(() => {
|
||||
if (shouldPinOptionsPanel) return;
|
||||
dispatch(
|
||||
setOptionsPanelScrollPosition(
|
||||
optionsPanelContainerRef.current
|
||||
? optionsPanelContainerRef.current.scrollTop
|
||||
: 0
|
||||
)
|
||||
);
|
||||
dispatch(setShouldShowOptionsPanel(false));
|
||||
dispatch(setShouldHoldOptionsPanelOpen(false));
|
||||
// dispatch(setNeedsCache(true));
|
||||
}, [dispatch, shouldPinOptionsPanel]);
|
||||
|
||||
useClickOutsideWatcher(
|
||||
optionsPanelRef,
|
||||
handleCloseOptionsPanel,
|
||||
!shouldPinOptionsPanel
|
||||
);
|
||||
|
||||
const setCloseOptionsPanelTimer = () => {
|
||||
timeoutIdRef.current = window.setTimeout(
|
||||
() => handleCloseOptionsPanel(),
|
||||
500
|
||||
);
|
||||
};
|
||||
|
||||
const cancelCloseOptionsPanelTimer = () => {
|
||||
timeoutIdRef.current && window.clearTimeout(timeoutIdRef.current);
|
||||
};
|
||||
|
||||
const handleClickPinOptionsPanel = () => {
|
||||
dispatch(setShouldPinOptionsPanel(!shouldPinOptionsPanel));
|
||||
dispatch(setNeedsCache(true));
|
||||
};
|
||||
|
||||
// // set gallery scroll position
|
||||
// useEffect(() => {
|
||||
// if (!optionsPanelContainerRef.current) return;
|
||||
// optionsPanelContainerRef.current.scrollTop = optionsPanelScrollPosition;
|
||||
// }, [optionsPanelScrollPosition, shouldShowOptionsPanel]);
|
||||
|
||||
return (
|
||||
<CSSTransition
|
||||
nodeRef={optionsPanelRef}
|
||||
in={
|
||||
shouldShowOptionsPanel ||
|
||||
(shouldHoldOptionsPanelOpen && !shouldPinOptionsPanel)
|
||||
}
|
||||
unmountOnExit
|
||||
timeout={200}
|
||||
classNames="options-panel-wrapper"
|
||||
>
|
||||
<div
|
||||
className="options-panel-wrapper"
|
||||
data-pinned={shouldPinOptionsPanel}
|
||||
tabIndex={1}
|
||||
ref={optionsPanelRef}
|
||||
onMouseEnter={
|
||||
!shouldPinOptionsPanel ? cancelCloseOptionsPanelTimer : undefined
|
||||
}
|
||||
onMouseOver={
|
||||
!shouldPinOptionsPanel ? cancelCloseOptionsPanelTimer : undefined
|
||||
}
|
||||
>
|
||||
<div className="options-panel-margin">
|
||||
<div
|
||||
className="options-panel"
|
||||
ref={optionsPanelContainerRef}
|
||||
onMouseLeave={(e: MouseEvent<HTMLDivElement>) => {
|
||||
if (e.target !== optionsPanelContainerRef.current) {
|
||||
cancelCloseOptionsPanelTimer();
|
||||
} else {
|
||||
!shouldPinOptionsPanel && setCloseOptionsPanelTimer();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tooltip label="Pin Options Panel">
|
||||
<div
|
||||
className="options-panel-pin-button"
|
||||
data-selected={shouldPinOptionsPanel}
|
||||
onClick={handleClickPinOptionsPanel}
|
||||
>
|
||||
{shouldPinOptionsPanel ? <BsPinAngleFill /> : <BsPinAngle />}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{!shouldPinOptionsPanel && (
|
||||
<div className="invoke-ai-logo-wrapper">
|
||||
<img src={InvokeAILogo} alt="invoke-ai-logo" />
|
||||
<h1>
|
||||
invoke <strong>ai</strong>
|
||||
</h1>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
);
|
||||
};
|
||||
|
||||
export default InvokeOptionsPanel;
|