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 = (
|
seed = (
|
||||||
original_image["metadata"]["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"
|
else "unknown_seed"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -561,6 +562,9 @@ class InvokeAIWebServer:
|
|||||||
)
|
)
|
||||||
generation_parameters["init_img"] = cropped_init_image
|
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
|
# grab an Image of the mask
|
||||||
mask_image = Image.open(
|
mask_image = Image.open(
|
||||||
io.BytesIO(
|
io.BytesIO(
|
||||||
@ -569,7 +573,6 @@ class InvokeAIWebServer:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# crop the mask image
|
# crop the mask image
|
||||||
cropped_mask_image = copy_image_from_bounding_box(
|
cropped_mask_image = copy_image_from_bounding_box(
|
||||||
mask_image, **generation_parameters["bounding_box"]
|
mask_image, **generation_parameters["bounding_box"]
|
||||||
@ -750,7 +753,7 @@ class InvokeAIWebServer:
|
|||||||
all_parameters["init_img"] = init_img_url
|
all_parameters["init_img"] = init_img_url
|
||||||
|
|
||||||
if "init_mask" in all_parameters:
|
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)
|
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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>InvokeAI - A Stable Diffusion Toolkit</title>
|
<title>InvokeAI - A Stable Diffusion Toolkit</title>
|
||||||
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" />
|
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" />
|
||||||
<script type="module" crossorigin src="./assets/index.64b87783.js"></script>
|
<script type="module" crossorigin src="./assets/index.d3820055.js"></script>
|
||||||
<link rel="stylesheet" href="./assets/index.352e4760.css">
|
<link rel="stylesheet" href="./assets/index.52c8231e.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -9,13 +9,9 @@
|
|||||||
|
|
||||||
.app-content {
|
.app-content {
|
||||||
display: grid;
|
display: grid;
|
||||||
row-gap: 0.5rem;
|
row-gap: 1rem;
|
||||||
padding: $app-padding;
|
padding: $app-padding;
|
||||||
grid-auto-rows: max-content;
|
grid-auto-rows: min-content auto;
|
||||||
width: $app-width;
|
width: $app-width;
|
||||||
height: $app-height;
|
height: $app-height;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-console {
|
|
||||||
z-index: 20;
|
|
||||||
}
|
|
||||||
|
@ -8,14 +8,82 @@ import { requestSystemConfig } from './socketio/actions';
|
|||||||
import { keepGUIAlive } from './utils';
|
import { keepGUIAlive } from './utils';
|
||||||
import InvokeTabs from '../features/tabs/InvokeTabs';
|
import InvokeTabs from '../features/tabs/InvokeTabs';
|
||||||
import ImageUploader from '../common/components/ImageUploader';
|
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();
|
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 App = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const [isReady, setIsReady] = useState<boolean>(false);
|
const [isReady, setIsReady] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const { shouldShowGalleryButton, shouldShowOptionsPanelButton } =
|
||||||
|
useAppSelector(appSelector);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(requestSystemConfig());
|
dispatch(requestSystemConfig());
|
||||||
setIsReady(true);
|
setIsReady(true);
|
||||||
@ -32,6 +100,8 @@ const App = () => {
|
|||||||
<div className="app-console">
|
<div className="app-console">
|
||||||
<Console />
|
<Console />
|
||||||
</div>
|
</div>
|
||||||
|
{shouldShowGalleryButton && <FloatingGalleryButton />}
|
||||||
|
{shouldShowOptionsPanelButton && <FloatingOptionsPanelButtons />}
|
||||||
</ImageUploader>
|
</ImageUploader>
|
||||||
</div>
|
</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 {
|
import {
|
||||||
GalleryCategory,
|
GalleryCategory,
|
||||||
GalleryState,
|
GalleryState,
|
||||||
|
removeImage,
|
||||||
} from '../../features/gallery/gallerySlice';
|
} from '../../features/gallery/gallerySlice';
|
||||||
import { OptionsState } from '../../features/options/optionsSlice';
|
import { OptionsState } from '../../features/options/optionsSlice';
|
||||||
import {
|
import {
|
||||||
addLogEntry,
|
addLogEntry,
|
||||||
errorOccurred,
|
errorOccurred,
|
||||||
|
modelChangeRequested,
|
||||||
setCurrentStatus,
|
setCurrentStatus,
|
||||||
setIsCancelable,
|
setIsCancelable,
|
||||||
setIsProcessing,
|
setIsProcessing,
|
||||||
@ -163,6 +165,7 @@ const makeSocketIOEmitters = (
|
|||||||
},
|
},
|
||||||
emitDeleteImage: (imageToDelete: InvokeAI.Image) => {
|
emitDeleteImage: (imageToDelete: InvokeAI.Image) => {
|
||||||
const { url, uuid, category } = imageToDelete;
|
const { url, uuid, category } = imageToDelete;
|
||||||
|
dispatch(removeImage(imageToDelete));
|
||||||
socketio.emit('deleteImage', url, uuid, category);
|
socketio.emit('deleteImage', url, uuid, category);
|
||||||
},
|
},
|
||||||
emitRequestImages: (category: GalleryCategory) => {
|
emitRequestImages: (category: GalleryCategory) => {
|
||||||
@ -189,9 +192,7 @@ const makeSocketIOEmitters = (
|
|||||||
socketio.emit('requestSystemConfig');
|
socketio.emit('requestSystemConfig');
|
||||||
},
|
},
|
||||||
emitRequestModelChange: (modelName: string) => {
|
emitRequestModelChange: (modelName: string) => {
|
||||||
dispatch(setCurrentStatus('Changing Model'));
|
dispatch(modelChangeRequested());
|
||||||
dispatch(setIsProcessing(true));
|
|
||||||
dispatch(setIsCancelable(false));
|
|
||||||
socketio.emit('requestModelChange', modelName);
|
socketio.emit('requestModelChange', modelName);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -33,7 +33,11 @@ import {
|
|||||||
setMaskPath,
|
setMaskPath,
|
||||||
} from '../../features/options/optionsSlice';
|
} from '../../features/options/optionsSlice';
|
||||||
import { requestImages, requestNewImages } from './actions';
|
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.
|
* Returns an object containing listener callbacks for socketio events.
|
||||||
@ -93,15 +97,34 @@ const makeSocketIOListeners = (
|
|||||||
*/
|
*/
|
||||||
onGenerationResult: (data: InvokeAI.ImageResultResponse) => {
|
onGenerationResult: (data: InvokeAI.ImageResultResponse) => {
|
||||||
try {
|
try {
|
||||||
|
const { shouldLoopback, activeTab } = getState().options;
|
||||||
|
const newImage = {
|
||||||
|
uuid: uuidv4(),
|
||||||
|
...data,
|
||||||
|
category: 'result',
|
||||||
|
};
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
addImage({
|
addImage({
|
||||||
category: 'result',
|
category: 'result',
|
||||||
image: {
|
image: newImage,
|
||||||
uuid: uuidv4(),
|
|
||||||
...data,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (shouldLoopback) {
|
||||||
|
const activeTabName = tabMap[activeTab];
|
||||||
|
switch (activeTabName) {
|
||||||
|
case 'img2img': {
|
||||||
|
dispatch(setInitialImage(newImage));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'inpainting': {
|
||||||
|
dispatch(setImageToInpaint(newImage));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
addLogEntry({
|
addLogEntry({
|
||||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||||
@ -144,6 +167,7 @@ const makeSocketIOListeners = (
|
|||||||
image: {
|
image: {
|
||||||
uuid: uuidv4(),
|
uuid: uuidv4(),
|
||||||
...data,
|
...data,
|
||||||
|
category: 'result',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -264,7 +288,7 @@ const makeSocketIOListeners = (
|
|||||||
* Callback to run when we receive a 'imageDeleted' event.
|
* Callback to run when we receive a 'imageDeleted' event.
|
||||||
*/
|
*/
|
||||||
onImageDeleted: (data: InvokeAI.ImageDeletedResponse) => {
|
onImageDeleted: (data: InvokeAI.ImageDeletedResponse) => {
|
||||||
const { url, uuid, category } = data;
|
const { url } = data;
|
||||||
|
|
||||||
// remove image from gallery
|
// remove image from gallery
|
||||||
dispatch(removeImage(data));
|
dispatch(removeImage(data));
|
||||||
@ -348,7 +372,7 @@ const makeSocketIOListeners = (
|
|||||||
dispatch(setModelList(model_list));
|
dispatch(setModelList(model_list));
|
||||||
dispatch(setCurrentStatus('Model Changed'));
|
dispatch(setCurrentStatus('Model Changed'));
|
||||||
dispatch(setIsProcessing(false));
|
dispatch(setIsProcessing(false));
|
||||||
dispatch(setIsCancelable(false));
|
dispatch(setIsCancelable(true));
|
||||||
dispatch(
|
dispatch(
|
||||||
addLogEntry({
|
addLogEntry({
|
||||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||||
@ -361,7 +385,7 @@ const makeSocketIOListeners = (
|
|||||||
const { model_name, model_list } = data;
|
const { model_name, model_list } = data;
|
||||||
dispatch(setModelList(model_list));
|
dispatch(setModelList(model_list));
|
||||||
dispatch(setIsProcessing(false));
|
dispatch(setIsProcessing(false));
|
||||||
dispatch(setIsCancelable(false));
|
dispatch(setIsCancelable(true));
|
||||||
dispatch(errorOccurred());
|
dispatch(errorOccurred());
|
||||||
dispatch(
|
dispatch(
|
||||||
addLogEntry({
|
addLogEntry({
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
import { Button, ButtonProps, Tooltip } from '@chakra-ui/react';
|
import { Button, ButtonProps, Tooltip } from '@chakra-ui/react';
|
||||||
|
|
||||||
interface Props extends ButtonProps {
|
export interface IAIButtonProps extends ButtonProps {
|
||||||
label: string;
|
label: string;
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
|
styleClass?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reusable customized button component. Originally was more customized - now probably unecessary.
|
* Reusable customized button component.
|
||||||
*
|
|
||||||
* TODO: Get rid of this.
|
|
||||||
*/
|
*/
|
||||||
const IAIButton = (props: Props) => {
|
const IAIButton = (props: IAIButtonProps) => {
|
||||||
const { label, tooltip = '', size = 'sm', ...rest } = props;
|
const { label, tooltip = '', styleClass, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<Tooltip label={tooltip}>
|
<Tooltip label={tooltip}>
|
||||||
<Button size={size} {...rest}>
|
<Button className={styleClass ? styleClass : ''} {...rest}>
|
||||||
{label}
|
{label}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { FormControl, FormLabel, Select, SelectProps } from '@chakra-ui/react';
|
import { FormControl, FormLabel, Select, SelectProps } from '@chakra-ui/react';
|
||||||
|
import { MouseEvent } from 'react';
|
||||||
|
|
||||||
interface Props extends SelectProps {
|
interface Props extends SelectProps {
|
||||||
label: string;
|
label: string;
|
||||||
@ -21,7 +22,16 @@ const IAISelect = (props: Props) => {
|
|||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
return (
|
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
|
<FormLabel
|
||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
marginBottom={1}
|
marginBottom={1}
|
||||||
|
@ -39,10 +39,11 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
color: var(--subtext-color-bright);
|
color: var(--tab-list-text-inactive);
|
||||||
|
background-color: var(--btn-grey);
|
||||||
|
|
||||||
&:hover {
|
&: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 { useCallback, ReactNode, useState, useEffect } from 'react';
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
import { tabMap } from '../../features/tabs/InvokeTabs';
|
|
||||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||||
import { Heading, Spinner, useToast } from '@chakra-ui/react';
|
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 { uploadImage } from '../../app/socketio/actions';
|
||||||
import { ImageUploadDestination, UploadImagePayload } from '../../app/invokeai';
|
import { ImageUploadDestination, UploadImagePayload } from '../../app/invokeai';
|
||||||
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
|
import { ImageUploaderTriggerContext } from '../../app/contexts/ImageUploaderTriggerContext';
|
||||||
|
import { activeTabNameSelector } from '../../features/options/optionsSelectors';
|
||||||
const appSelector = createSelector(
|
|
||||||
(state: RootState) => state.options,
|
|
||||||
(options: OptionsState) => {
|
|
||||||
const { activeTab } = options;
|
|
||||||
return {
|
|
||||||
activeTabName: tabMap[activeTab],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
type ImageUploaderProps = {
|
type ImageUploaderProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -26,7 +14,7 @@ type ImageUploaderProps = {
|
|||||||
const ImageUploader = (props: ImageUploaderProps) => {
|
const ImageUploader = (props: ImageUploaderProps) => {
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { activeTabName } = useAppSelector(appSelector);
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
const toast = useToast({});
|
const toast = useToast({});
|
||||||
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
|
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) {
|
if (generationMode === 'inpainting' && maskImageElement) {
|
||||||
const {
|
const {
|
||||||
lines,
|
lines,
|
||||||
boundingBoxCoordinate: { x, y },
|
boundingBoxCoordinate,
|
||||||
boundingBoxDimensions: { width, height },
|
boundingBoxDimensions,
|
||||||
shouldShowBoundingBox,
|
|
||||||
inpaintReplace,
|
inpaintReplace,
|
||||||
shouldUseInpaintReplace,
|
shouldUseInpaintReplace,
|
||||||
} = inpaintingState;
|
} = inpaintingState;
|
||||||
|
|
||||||
let bx = x,
|
|
||||||
by = y,
|
|
||||||
bwidth = width,
|
|
||||||
bheight = height;
|
|
||||||
|
|
||||||
if (!shouldShowBoundingBox) {
|
|
||||||
bx = 0;
|
|
||||||
by = 0;
|
|
||||||
bwidth = maskImageElement.width;
|
|
||||||
bheight = maskImageElement.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
const boundingBox = {
|
const boundingBox = {
|
||||||
x: bx,
|
...boundingBoxCoordinate,
|
||||||
y: by,
|
...boundingBoxDimensions,
|
||||||
width: bwidth,
|
|
||||||
height: bheight,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (shouldUseInpaintReplace) {
|
|
||||||
generationParameters.inpaint_replace = inpaintReplace;
|
|
||||||
}
|
|
||||||
|
|
||||||
generationParameters.init_img = imageToProcessUrl;
|
generationParameters.init_img = imageToProcessUrl;
|
||||||
generationParameters.strength = img2imgStrength;
|
generationParameters.strength = img2imgStrength;
|
||||||
generationParameters.fit = false;
|
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(
|
generationParameters.init_mask = maskDataURL.split(
|
||||||
'data:image/png;base64,'
|
'data:image/png;base64,'
|
||||||
)[1];
|
)[1];
|
||||||
|
|
||||||
|
if (shouldUseInpaintReplace) {
|
||||||
|
generationParameters.inpaint_replace = inpaintReplace;
|
||||||
|
}
|
||||||
|
|
||||||
generationParameters.bounding_box = boundingBox;
|
generationParameters.bounding_box = boundingBox;
|
||||||
|
|
||||||
|
// TODO: The server metadata generation needs to be changed to fix this.
|
||||||
|
generationParameters.progress_images = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldGenerateVariations) {
|
if (shouldGenerateVariations) {
|
||||||
|
@ -6,6 +6,7 @@ import * as InvokeAI from '../../app/invokeai';
|
|||||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from '../../app/store';
|
||||||
import {
|
import {
|
||||||
|
OptionsState,
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
setAllParameters,
|
setAllParameters,
|
||||||
setInitialImage,
|
setInitialImage,
|
||||||
@ -17,13 +18,7 @@ import { SystemState } from '../system/systemSlice';
|
|||||||
import IAIButton from '../../common/components/IAIButton';
|
import IAIButton from '../../common/components/IAIButton';
|
||||||
import { runESRGAN, runFacetool } from '../../app/socketio/actions';
|
import { runESRGAN, runFacetool } from '../../app/socketio/actions';
|
||||||
import IAIIconButton from '../../common/components/IAIIconButton';
|
import IAIIconButton from '../../common/components/IAIIconButton';
|
||||||
import {
|
import { MdDelete, MdFace, MdHd, MdImage, MdInfo } from 'react-icons/md';
|
||||||
MdDelete,
|
|
||||||
MdFace,
|
|
||||||
MdHd,
|
|
||||||
MdImage,
|
|
||||||
MdInfo,
|
|
||||||
} from 'react-icons/md';
|
|
||||||
import InvokePopover from './InvokePopover';
|
import InvokePopover from './InvokePopover';
|
||||||
import UpscaleOptions from '../options/AdvancedOptions/Upscale/UpscaleOptions';
|
import UpscaleOptions from '../options/AdvancedOptions/Upscale/UpscaleOptions';
|
||||||
import FaceRestoreOptions from '../options/AdvancedOptions/FaceRestore/FaceRestoreOptions';
|
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 { FaCopy, FaPaintBrush, FaSeedling } from 'react-icons/fa';
|
||||||
import { setImageToInpaint } from '../tabs/Inpainting/inpaintingSlice';
|
import { setImageToInpaint } from '../tabs/Inpainting/inpaintingSlice';
|
||||||
import { hoverableImageSelector } from './gallerySliceSelectors';
|
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(
|
const systemSelector = createSelector(
|
||||||
|
[
|
||||||
(state: RootState) => state.system,
|
(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 {
|
return {
|
||||||
isProcessing: system.isProcessing,
|
isProcessing,
|
||||||
isConnected: system.isConnected,
|
isConnected,
|
||||||
isGFPGANAvailable: system.isGFPGANAvailable,
|
isGFPGANAvailable,
|
||||||
isESRGANAvailable: system.isESRGANAvailable,
|
isESRGANAvailable,
|
||||||
|
upscalingLevel,
|
||||||
|
facetoolStrength,
|
||||||
|
intermediateImage,
|
||||||
|
shouldShowImageDetails,
|
||||||
|
activeTabName,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -60,29 +89,20 @@ type CurrentImageButtonsProps = {
|
|||||||
*/
|
*/
|
||||||
const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { activeTabName } = useAppSelector(hoverableImageSelector);
|
const {
|
||||||
|
isProcessing,
|
||||||
const shouldShowImageDetails = useAppSelector(
|
isConnected,
|
||||||
(state: RootState) => state.options.shouldShowImageDetails
|
isGFPGANAvailable,
|
||||||
);
|
isESRGANAvailable,
|
||||||
|
upscalingLevel,
|
||||||
|
facetoolStrength,
|
||||||
|
intermediateImage,
|
||||||
|
shouldShowImageDetails,
|
||||||
|
activeTabName,
|
||||||
|
} = useAppSelector(systemSelector);
|
||||||
|
|
||||||
const toast = useToast();
|
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 = () => {
|
const handleClickUseAsInitialImage = () => {
|
||||||
dispatch(setInitialImage(image));
|
dispatch(setInitialImage(image));
|
||||||
dispatch(setActiveTab(1));
|
dispatch(setActiveTab(1));
|
||||||
@ -360,7 +380,9 @@ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
|
|||||||
icon={<MdDelete />}
|
icon={<MdDelete />}
|
||||||
tooltip="Delete Image"
|
tooltip="Delete Image"
|
||||||
aria-label="Delete Image"
|
aria-label="Delete Image"
|
||||||
isDisabled={Boolean(intermediateImage)}
|
isDisabled={
|
||||||
|
Boolean(intermediateImage) || !isConnected || isProcessing
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</DeleteImageModal>
|
</DeleteImageModal>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,22 +2,26 @@ import { RootState, useAppSelector } from '../../app/store';
|
|||||||
import CurrentImageButtons from './CurrentImageButtons';
|
import CurrentImageButtons from './CurrentImageButtons';
|
||||||
import { MdPhoto } from 'react-icons/md';
|
import { MdPhoto } from 'react-icons/md';
|
||||||
import CurrentImagePreview from './CurrentImagePreview';
|
import CurrentImagePreview from './CurrentImagePreview';
|
||||||
import { tabMap } from '../tabs/InvokeTabs';
|
|
||||||
import { GalleryState } from './gallerySlice';
|
import { GalleryState } from './gallerySlice';
|
||||||
import { OptionsState } from '../options/optionsSlice';
|
import { OptionsState } from '../options/optionsSlice';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { activeTabNameSelector } from '../options/optionsSelectors';
|
||||||
|
|
||||||
export const currentImageDisplaySelector = createSelector(
|
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 { currentImage, intermediateImage } = gallery;
|
||||||
const { activeTab, shouldShowImageDetails } = options;
|
const { shouldShowImageDetails } = options;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentImage,
|
currentImage,
|
||||||
intermediateImage,
|
intermediateImage,
|
||||||
activeTabName: tabMap[activeTab],
|
activeTabName,
|
||||||
shouldShowImageDetails,
|
shouldShowImageDetails,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -32,11 +36,9 @@ export const currentImageDisplaySelector = createSelector(
|
|||||||
* Displays the current image if there is one, plus associated actions.
|
* Displays the current image if there is one, plus associated actions.
|
||||||
*/
|
*/
|
||||||
const CurrentImageDisplay = () => {
|
const CurrentImageDisplay = () => {
|
||||||
const {
|
const { currentImage, intermediateImage, activeTabName } = useAppSelector(
|
||||||
currentImage,
|
currentImageDisplaySelector
|
||||||
intermediateImage,
|
);
|
||||||
activeTabName,
|
|
||||||
} = useAppSelector(currentImageDisplaySelector);
|
|
||||||
|
|
||||||
const imageToDisplay = intermediateImage || currentImage;
|
const imageToDisplay = intermediateImage || currentImage;
|
||||||
|
|
||||||
|
@ -62,11 +62,11 @@ export default function CurrentImagePreview(props: CurrentImagePreviewProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClickPrevButton = () => {
|
const handleClickPrevButton = () => {
|
||||||
dispatch(selectPrevImage(currentCategory));
|
dispatch(selectPrevImage());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickNextButton = () => {
|
const handleClickNextButton = () => {
|
||||||
dispatch(selectNextImage(currentCategory));
|
dispatch(selectNextImage());
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -12,7 +12,6 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
Flex,
|
Flex,
|
||||||
useToast,
|
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import {
|
import {
|
||||||
@ -30,6 +29,13 @@ import { setShouldConfirmOnDelete, SystemState } from '../system/systemSlice';
|
|||||||
import * as InvokeAI from '../../app/invokeai';
|
import * as InvokeAI from '../../app/invokeai';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
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 {
|
interface DeleteImageModalProps {
|
||||||
/**
|
/**
|
||||||
* Component which, on click, should delete the image/open the modal.
|
* Component which, on click, should delete the image/open the modal.
|
||||||
@ -41,11 +47,6 @@ interface DeleteImageModalProps {
|
|||||||
image: InvokeAI.Image;
|
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.
|
* Needs a child, which will act as the button to delete an image.
|
||||||
* If system.shouldConfirmOnDelete is true, a confirmation modal is displayed.
|
* If system.shouldConfirmOnDelete is true, a confirmation modal is displayed.
|
||||||
@ -56,9 +57,9 @@ const DeleteImageModal = forwardRef(
|
|||||||
({ image, children }: DeleteImageModalProps, ref) => {
|
({ image, children }: DeleteImageModalProps, ref) => {
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const shouldConfirmOnDelete = useAppSelector(systemSelector);
|
const { shouldConfirmOnDelete, isConnected, isProcessing } =
|
||||||
|
useAppSelector(systemSelector);
|
||||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const handleClickDelete = (e: SyntheticEvent) => {
|
const handleClickDelete = (e: SyntheticEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -66,13 +67,9 @@ const DeleteImageModal = forwardRef(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
|
if (isConnected && !isProcessing) {
|
||||||
dispatch(deleteImage(image));
|
dispatch(deleteImage(image));
|
||||||
toast({
|
}
|
||||||
title: 'Image Deleted',
|
|
||||||
status: 'success',
|
|
||||||
duration: 2500,
|
|
||||||
isClosable: true,
|
|
||||||
});
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -7,11 +7,7 @@ import {
|
|||||||
useToast,
|
useToast,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
import {
|
import { setCurrentImage } from './gallerySlice';
|
||||||
setCurrentImage,
|
|
||||||
setShouldHoldGalleryOpen,
|
|
||||||
setShouldShowGallery,
|
|
||||||
} from './gallerySlice';
|
|
||||||
import { FaCheck, FaTrashAlt } from 'react-icons/fa';
|
import { FaCheck, FaTrashAlt } from 'react-icons/fa';
|
||||||
import DeleteImageModal from './DeleteImageModal';
|
import DeleteImageModal from './DeleteImageModal';
|
||||||
import { memo, useState } from 'react';
|
import { memo, useState } from 'react';
|
||||||
@ -25,7 +21,6 @@ import {
|
|||||||
} from '../options/optionsSlice';
|
} from '../options/optionsSlice';
|
||||||
import * as InvokeAI from '../../app/invokeai';
|
import * as InvokeAI from '../../app/invokeai';
|
||||||
import * as ContextMenu from '@radix-ui/react-context-menu';
|
import * as ContextMenu from '@radix-ui/react-context-menu';
|
||||||
import { tabMap } from '../tabs/InvokeTabs';
|
|
||||||
import { setImageToInpaint } from '../tabs/Inpainting/inpaintingSlice';
|
import { setImageToInpaint } from '../tabs/Inpainting/inpaintingSlice';
|
||||||
import { hoverableImageSelector } from './gallerySliceSelectors';
|
import { hoverableImageSelector } from './gallerySliceSelectors';
|
||||||
|
|
||||||
@ -44,8 +39,12 @@ const memoEqualityCheck = (
|
|||||||
*/
|
*/
|
||||||
const HoverableImage = memo((props: HoverableImageProps) => {
|
const HoverableImage = memo((props: HoverableImageProps) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { activeTabName, galleryImageObjectFit, galleryImageMinimumWidth } =
|
const {
|
||||||
useAppSelector(hoverableImageSelector);
|
activeTabName,
|
||||||
|
galleryImageObjectFit,
|
||||||
|
galleryImageMinimumWidth,
|
||||||
|
mayDeleteImage,
|
||||||
|
} = useAppSelector(hoverableImageSelector);
|
||||||
const { image, isSelected } = props;
|
const { image, isSelected } = props;
|
||||||
const { url, uuid, metadata } = image;
|
const { url, uuid, metadata } = image;
|
||||||
|
|
||||||
@ -118,7 +117,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
if (metadata?.image?.init_image_path) {
|
if (metadata?.image?.init_image_path) {
|
||||||
const response = await fetch(metadata.image.init_image_path);
|
const response = await fetch(metadata.image.init_image_path);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
dispatch(setActiveTab(tabMap.indexOf('img2img')));
|
dispatch(setActiveTab('img2img'));
|
||||||
dispatch(setAllImageToImageParameters(metadata));
|
dispatch(setAllImageToImageParameters(metadata));
|
||||||
toast({
|
toast({
|
||||||
title: 'Initial Image Set',
|
title: 'Initial Image Set',
|
||||||
@ -142,10 +141,10 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu.Root
|
<ContextMenu.Root
|
||||||
onOpenChange={(open: boolean) => {
|
// onOpenChange={(open: boolean) => {
|
||||||
dispatch(setShouldHoldGalleryOpen(open));
|
// dispatch(setShouldHoldGalleryOpen(open));
|
||||||
dispatch(setShouldShowGallery(true));
|
// dispatch(setShouldShowGallery(true));
|
||||||
}}
|
// }}
|
||||||
>
|
>
|
||||||
<ContextMenu.Trigger>
|
<ContextMenu.Trigger>
|
||||||
<Box
|
<Box
|
||||||
@ -182,6 +181,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
size="xs"
|
size="xs"
|
||||||
variant={'imageHoverIconButton'}
|
variant={'imageHoverIconButton'}
|
||||||
fontSize={14}
|
fontSize={14}
|
||||||
|
isDisabled={!mayDeleteImage}
|
||||||
/>
|
/>
|
||||||
</DeleteImageModal>
|
</DeleteImageModal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -1,25 +1,25 @@
|
|||||||
@use '../../styles/Mixins/' as *;
|
@use '../../styles/Mixins/' as *;
|
||||||
|
|
||||||
.image-gallery-area-enter {
|
.image-gallery-wrapper-enter {
|
||||||
transform: translateX(150%);
|
transform: translateX(150%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-gallery-area-enter-active {
|
.image-gallery-wrapper-enter-active {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
transition: all 120ms ease-out;
|
transition: all 120ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-gallery-area-exit {
|
.image-gallery-wrapper-exit {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-gallery-area-exit-active {
|
.image-gallery-wrapper-exit-active {
|
||||||
transform: translateX(150%);
|
transform: translateX(150%);
|
||||||
transition: all 120ms ease-out;
|
transition: all 120ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-gallery-area {
|
.image-gallery-wrapper {
|
||||||
z-index: 10;
|
z-index: 100;
|
||||||
|
|
||||||
&[data-pinned='false'] {
|
&[data-pinned='false'] {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -29,6 +29,7 @@
|
|||||||
|
|
||||||
.image-gallery-popup {
|
.image-gallery-popup {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
box-shadow: 0 0 1rem var(--text-color-a3);
|
||||||
.image-gallery-container {
|
.image-gallery-container {
|
||||||
max-height: calc($app-height + 5rem);
|
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 { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { MdClear, MdPhotoLibrary } from 'react-icons/md';
|
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 { requestImages } from '../../app/socketio/actions';
|
||||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
import IAIIconButton from '../../common/components/IAIIconButton';
|
import IAIIconButton from '../../common/components/IAIIconButton';
|
||||||
@ -33,6 +33,7 @@ import { BiReset } from 'react-icons/bi';
|
|||||||
import IAICheckbox from '../../common/components/IAICheckbox';
|
import IAICheckbox from '../../common/components/IAICheckbox';
|
||||||
import { setNeedsCache } from '../tabs/Inpainting/inpaintingSlice';
|
import { setNeedsCache } from '../tabs/Inpainting/inpaintingSlice';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import useClickOutsideWatcher from '../../common/hooks/useClickOutsideWatcher';
|
||||||
|
|
||||||
const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 320;
|
const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 320;
|
||||||
|
|
||||||
@ -96,8 +97,8 @@ export default function ImageGallery() {
|
|||||||
const timeoutIdRef = useRef<number | null>(null);
|
const timeoutIdRef = useRef<number | null>(null);
|
||||||
|
|
||||||
const handleSetShouldPinGallery = () => {
|
const handleSetShouldPinGallery = () => {
|
||||||
dispatch(setNeedsCache(true));
|
|
||||||
dispatch(setShouldPinGallery(!shouldPinGallery));
|
dispatch(setShouldPinGallery(!shouldPinGallery));
|
||||||
|
dispatch(setNeedsCache(true));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleGallery = () => {
|
const handleToggleGallery = () => {
|
||||||
@ -106,18 +107,19 @@ export default function ImageGallery() {
|
|||||||
|
|
||||||
const handleOpenGallery = () => {
|
const handleOpenGallery = () => {
|
||||||
dispatch(setShouldShowGallery(true));
|
dispatch(setShouldShowGallery(true));
|
||||||
dispatch(setNeedsCache(true));
|
shouldPinGallery && dispatch(setNeedsCache(true));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseGallery = () => {
|
const handleCloseGallery = () => {
|
||||||
|
// if (shouldPinGallery) return;
|
||||||
|
dispatch(setShouldShowGallery(false));
|
||||||
dispatch(
|
dispatch(
|
||||||
setGalleryScrollPosition(
|
setGalleryScrollPosition(
|
||||||
galleryContainerRef.current ? galleryContainerRef.current.scrollTop : 0
|
galleryContainerRef.current ? galleryContainerRef.current.scrollTop : 0
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
dispatch(setShouldShowGallery(false));
|
|
||||||
dispatch(setShouldHoldGalleryOpen(false));
|
dispatch(setShouldHoldGalleryOpen(false));
|
||||||
dispatch(setNeedsCache(true));
|
// dispatch(setNeedsCache(true));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickLoadMore = () => {
|
const handleClickLoadMore = () => {
|
||||||
@ -145,24 +147,16 @@ export default function ImageGallery() {
|
|||||||
[shouldShowGallery]
|
[shouldShowGallery]
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys('left', () => {
|
||||||
'left',
|
dispatch(selectPrevImage());
|
||||||
() => {
|
});
|
||||||
dispatch(selectPrevImage(currentCategory));
|
|
||||||
},
|
useHotkeys('right', () => {
|
||||||
[currentCategory]
|
dispatch(selectNextImage());
|
||||||
);
|
});
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'right',
|
'shift+g',
|
||||||
() => {
|
|
||||||
dispatch(selectNextImage(currentCategory));
|
|
||||||
},
|
|
||||||
[currentCategory]
|
|
||||||
);
|
|
||||||
|
|
||||||
useHotkeys(
|
|
||||||
'shift+p',
|
|
||||||
() => {
|
() => {
|
||||||
handleSetShouldPinGallery();
|
handleSetShouldPinGallery();
|
||||||
},
|
},
|
||||||
@ -251,16 +245,22 @@ export default function ImageGallery() {
|
|||||||
galleryContainerRef.current.scrollTop = galleryScrollPosition;
|
galleryContainerRef.current.scrollTop = galleryScrollPosition;
|
||||||
}, [galleryScrollPosition, shouldShowGallery]);
|
}, [galleryScrollPosition, shouldShowGallery]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShouldShowButtons(galleryWidth >= 280);
|
||||||
|
}, [galleryWidth]);
|
||||||
|
|
||||||
|
useClickOutsideWatcher(galleryRef, handleCloseGallery, !shouldPinGallery);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CSSTransition
|
<CSSTransition
|
||||||
nodeRef={galleryRef}
|
nodeRef={galleryRef}
|
||||||
in={shouldShowGallery || (shouldHoldGalleryOpen && !shouldPinGallery)}
|
in={shouldShowGallery || (shouldHoldGalleryOpen && !shouldPinGallery)}
|
||||||
unmountOnExit
|
unmountOnExit
|
||||||
timeout={200}
|
timeout={200}
|
||||||
classNames="image-gallery-area"
|
classNames="image-gallery-wrapper"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="image-gallery-area"
|
className="image-gallery-wrapper"
|
||||||
data-pinned={shouldPinGallery}
|
data-pinned={shouldPinGallery}
|
||||||
ref={galleryRef}
|
ref={galleryRef}
|
||||||
onMouseLeave={!shouldPinGallery ? setCloseGalleryTimer : undefined}
|
onMouseLeave={!shouldPinGallery ? setCloseGalleryTimer : undefined}
|
||||||
@ -270,7 +270,6 @@ export default function ImageGallery() {
|
|||||||
<Resizable
|
<Resizable
|
||||||
minWidth={galleryMinWidth}
|
minWidth={galleryMinWidth}
|
||||||
maxWidth={galleryMaxWidth}
|
maxWidth={galleryMaxWidth}
|
||||||
// maxHeight={'100%'}
|
|
||||||
className={'image-gallery-popup'}
|
className={'image-gallery-popup'}
|
||||||
handleStyles={{ left: { width: '15px' } }}
|
handleStyles={{ left: { width: '15px' } }}
|
||||||
enable={{
|
enable={{
|
||||||
@ -316,9 +315,9 @@ export default function ImageGallery() {
|
|||||||
Number(galleryMaxWidth)
|
Number(galleryMaxWidth)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newWidth >= 320 && !shouldShowButtons) {
|
if (newWidth >= 280 && !shouldShowButtons) {
|
||||||
setShouldShowButtons(true);
|
setShouldShowButtons(true);
|
||||||
} else if (newWidth < 320 && shouldShowButtons) {
|
} else if (newWidth < 280 && shouldShowButtons) {
|
||||||
setShouldShowButtons(false);
|
setShouldShowButtons(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -374,8 +373,8 @@ export default function ImageGallery() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<IAIPopover
|
<IAIPopover
|
||||||
|
isLazy
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
hasArrow={activeTabName === 'inpainting' ? false : true}
|
|
||||||
placement={'left'}
|
placement={'left'}
|
||||||
triggerComponent={
|
triggerComponent={
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
@ -442,20 +441,11 @@ export default function ImageGallery() {
|
|||||||
|
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
|
className={'image-gallery-icon-btn'}
|
||||||
aria-label={'Pin Gallery'}
|
aria-label={'Pin Gallery'}
|
||||||
tooltip={'Pin Gallery (Shift+P)'}
|
tooltip={'Pin Gallery (Shift+G)'}
|
||||||
onClick={handleSetShouldPinGallery}
|
onClick={handleSetShouldPinGallery}
|
||||||
icon={<BsPinAngleFill />}
|
icon={shouldPinGallery ? <BsPinAngleFill /> : <BsPinAngle />}
|
||||||
data-selected={shouldPinGallery}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IAIIconButton
|
|
||||||
size={'sm'}
|
|
||||||
aria-label={'Close Gallery'}
|
|
||||||
tooltip={'Close Gallery (G)'}
|
|
||||||
onClick={handleCloseGallery}
|
|
||||||
className="image-gallery-icon-btn"
|
|
||||||
icon={<MdClear />}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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) {
|
if (state.shouldAutoSwitchToNewImages) {
|
||||||
state.currentImageUuid = uuid;
|
state.currentImageUuid = uuid;
|
||||||
state.currentImage = newImage;
|
state.currentImage = newImage;
|
||||||
if (category === 'result') {
|
state.currentCategory = category;
|
||||||
state.currentCategory = 'result';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
state.intermediateImage = undefined;
|
state.intermediateImage = undefined;
|
||||||
tempCategory.latest_mtime = mtime;
|
tempCategory.latest_mtime = mtime;
|
||||||
@ -156,10 +154,11 @@ export const gallerySlice = createSlice({
|
|||||||
clearIntermediateImage: (state) => {
|
clearIntermediateImage: (state) => {
|
||||||
state.intermediateImage = undefined;
|
state.intermediateImage = undefined;
|
||||||
},
|
},
|
||||||
selectNextImage: (state, action: PayloadAction<GalleryCategory>) => {
|
selectNextImage: (state) => {
|
||||||
const category = action.payload;
|
|
||||||
const { currentImage } = state;
|
const { currentImage } = state;
|
||||||
const tempImages = state.categories[category].images;
|
if (!currentImage) return;
|
||||||
|
const tempImages =
|
||||||
|
state.categories[currentImage.category as GalleryCategory].images;
|
||||||
|
|
||||||
if (currentImage) {
|
if (currentImage) {
|
||||||
const currentImageIndex = tempImages.findIndex(
|
const currentImageIndex = tempImages.findIndex(
|
||||||
@ -172,10 +171,11 @@ export const gallerySlice = createSlice({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
selectPrevImage: (state, action: PayloadAction<GalleryCategory>) => {
|
selectPrevImage: (state) => {
|
||||||
const category = action.payload;
|
|
||||||
const { currentImage } = state;
|
const { currentImage } = state;
|
||||||
const tempImages = state.categories[category].images;
|
if (!currentImage) return;
|
||||||
|
const tempImages =
|
||||||
|
state.categories[currentImage.category as GalleryCategory].images;
|
||||||
|
|
||||||
if (currentImage) {
|
if (currentImage) {
|
||||||
const currentImageIndex = tempImages.findIndex(
|
const currentImageIndex = tempImages.findIndex(
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from '../../app/store';
|
||||||
|
import { activeTabNameSelector } from '../options/optionsSelectors';
|
||||||
import { OptionsState } from '../options/optionsSlice';
|
import { OptionsState } from '../options/optionsSlice';
|
||||||
import { tabMap } from '../tabs/InvokeTabs';
|
import { SystemState } from '../system/systemSlice';
|
||||||
import { GalleryState } from './gallerySlice';
|
import { GalleryState } from './gallerySlice';
|
||||||
|
|
||||||
export const imageGallerySelector = createSelector(
|
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 {
|
const {
|
||||||
categories,
|
categories,
|
||||||
currentCategory,
|
currentCategory,
|
||||||
@ -21,8 +26,6 @@ export const imageGallerySelector = createSelector(
|
|||||||
galleryWidth,
|
galleryWidth,
|
||||||
} = gallery;
|
} = gallery;
|
||||||
|
|
||||||
const { activeTab } = options;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentImageUuid,
|
currentImageUuid,
|
||||||
shouldPinGallery,
|
shouldPinGallery,
|
||||||
@ -31,7 +34,7 @@ export const imageGallerySelector = createSelector(
|
|||||||
galleryImageMinimumWidth,
|
galleryImageMinimumWidth,
|
||||||
galleryImageObjectFit,
|
galleryImageObjectFit,
|
||||||
galleryGridTemplateColumns: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, auto))`,
|
galleryGridTemplateColumns: `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, auto))`,
|
||||||
activeTabName: tabMap[activeTab],
|
activeTabName,
|
||||||
shouldHoldGalleryOpen,
|
shouldHoldGalleryOpen,
|
||||||
shouldAutoSwitchToNewImages,
|
shouldAutoSwitchToNewImages,
|
||||||
images: categories[currentCategory].images,
|
images: categories[currentCategory].images,
|
||||||
@ -44,12 +47,23 @@ export const imageGallerySelector = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const hoverableImageSelector = 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 {
|
return {
|
||||||
|
mayDeleteImage: system.isConnected && !system.isProcessing,
|
||||||
galleryImageObjectFit: gallery.galleryImageObjectFit,
|
galleryImageObjectFit: gallery.galleryImageObjectFit,
|
||||||
galleryImageMinimumWidth: gallery.galleryImageMinimumWidth,
|
galleryImageMinimumWidth: gallery.galleryImageMinimumWidth,
|
||||||
activeTabName: tabMap[options.activeTab],
|
activeTabName,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -12,6 +12,16 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border-radius: 0.4rem 0.4rem 0 0;
|
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 {
|
p {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { ChangeEvent } from 'react';
|
|
||||||
import { BiReset } from 'react-icons/bi';
|
import { BiHide, BiReset, BiShow } from 'react-icons/bi';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
RootState,
|
RootState,
|
||||||
@ -14,7 +14,6 @@ import IAIIconButton from '../../../../common/components/IAIIconButton';
|
|||||||
|
|
||||||
import IAINumberInput from '../../../../common/components/IAINumberInput';
|
import IAINumberInput from '../../../../common/components/IAINumberInput';
|
||||||
import IAISlider from '../../../../common/components/IAISlider';
|
import IAISlider from '../../../../common/components/IAISlider';
|
||||||
import IAISwitch from '../../../../common/components/IAISwitch';
|
|
||||||
import { roundDownToMultiple } from '../../../../common/util/roundDownToMultiple';
|
import { roundDownToMultiple } from '../../../../common/util/roundDownToMultiple';
|
||||||
import {
|
import {
|
||||||
InpaintingState,
|
InpaintingState,
|
||||||
@ -64,16 +63,23 @@ const BoundingBoxSettings = () => {
|
|||||||
} = useAppSelector(boundingBoxDimensionsSelector);
|
} = useAppSelector(boundingBoxDimensionsSelector);
|
||||||
|
|
||||||
const handleChangeBoundingBoxWidth = (v: number) => {
|
const handleChangeBoundingBoxWidth = (v: number) => {
|
||||||
dispatch(setBoundingBoxDimensions({ ...boundingBoxDimensions, width: Math.floor(v) }));
|
dispatch(
|
||||||
|
setBoundingBoxDimensions({
|
||||||
|
...boundingBoxDimensions,
|
||||||
|
width: Math.floor(v),
|
||||||
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeBoundingBoxHeight = (v: number) => {
|
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 = () => {
|
const handleChangeShouldShowBoundingBoxFill = () => {
|
||||||
dispatch(setShouldShowBoundingBoxFill(!shouldShowBoundingBoxFill));
|
dispatch(setShouldShowBoundingBoxFill(!shouldShowBoundingBoxFill));
|
||||||
};
|
};
|
||||||
@ -100,14 +106,21 @@ const BoundingBoxSettings = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleShowBoundingBox = () =>
|
||||||
|
dispatch(setShouldShowBoundingBox(!shouldShowBoundingBox));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inpainting-bounding-box-settings">
|
<div className="inpainting-bounding-box-settings">
|
||||||
<div className="inpainting-bounding-box-header">
|
<div className="inpainting-bounding-box-header">
|
||||||
<p>Inpaint Box</p>
|
<p>Inpaint Box</p>
|
||||||
<IAISwitch
|
<IAIIconButton
|
||||||
isChecked={shouldShowBoundingBox}
|
aria-label="Toggle Bounding Box Visibility"
|
||||||
width={'auto'}
|
icon={
|
||||||
onChange={handleShowBoundingBox}
|
shouldShowBoundingBox ? <BiShow size={22} /> : <BiHide size={22} />
|
||||||
|
}
|
||||||
|
onClick={handleShowBoundingBox}
|
||||||
|
background={'none'}
|
||||||
|
padding={0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="inpainting-bounding-box-settings-items">
|
<div className="inpainting-bounding-box-settings-items">
|
||||||
@ -119,7 +132,6 @@ const BoundingBoxSettings = () => {
|
|||||||
step={64}
|
step={64}
|
||||||
value={boundingBoxDimensions.width}
|
value={boundingBoxDimensions.width}
|
||||||
onChange={handleChangeBoundingBoxWidth}
|
onChange={handleChangeBoundingBoxWidth}
|
||||||
isDisabled={!shouldShowBoundingBox}
|
|
||||||
width={'5rem'}
|
width={'5rem'}
|
||||||
/>
|
/>
|
||||||
<IAINumberInput
|
<IAINumberInput
|
||||||
@ -128,7 +140,6 @@ const BoundingBoxSettings = () => {
|
|||||||
min={64}
|
min={64}
|
||||||
max={roundDownToMultiple(canvasDimensions.width, 64)}
|
max={roundDownToMultiple(canvasDimensions.width, 64)}
|
||||||
step={64}
|
step={64}
|
||||||
isDisabled={!shouldShowBoundingBox}
|
|
||||||
width={'5rem'}
|
width={'5rem'}
|
||||||
/>
|
/>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
@ -138,10 +149,7 @@ const BoundingBoxSettings = () => {
|
|||||||
onClick={handleResetWidth}
|
onClick={handleResetWidth}
|
||||||
icon={<BiReset />}
|
icon={<BiReset />}
|
||||||
styleClass="inpainting-bounding-box-reset-icon-btn"
|
styleClass="inpainting-bounding-box-reset-icon-btn"
|
||||||
isDisabled={
|
isDisabled={canvasDimensions.width === boundingBoxDimensions.width}
|
||||||
!shouldShowBoundingBox ||
|
|
||||||
canvasDimensions.width === boundingBoxDimensions.width
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="inpainting-bounding-box-dimensions-slider-numberinput">
|
<div className="inpainting-bounding-box-dimensions-slider-numberinput">
|
||||||
@ -152,7 +160,6 @@ const BoundingBoxSettings = () => {
|
|||||||
step={64}
|
step={64}
|
||||||
value={boundingBoxDimensions.height}
|
value={boundingBoxDimensions.height}
|
||||||
onChange={handleChangeBoundingBoxHeight}
|
onChange={handleChangeBoundingBoxHeight}
|
||||||
isDisabled={!shouldShowBoundingBox}
|
|
||||||
width={'5rem'}
|
width={'5rem'}
|
||||||
/>
|
/>
|
||||||
<IAINumberInput
|
<IAINumberInput
|
||||||
@ -162,7 +169,6 @@ const BoundingBoxSettings = () => {
|
|||||||
max={roundDownToMultiple(canvasDimensions.height, 64)}
|
max={roundDownToMultiple(canvasDimensions.height, 64)}
|
||||||
step={64}
|
step={64}
|
||||||
padding="0"
|
padding="0"
|
||||||
isDisabled={!shouldShowBoundingBox}
|
|
||||||
width={'5rem'}
|
width={'5rem'}
|
||||||
/>
|
/>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
@ -173,7 +179,6 @@ const BoundingBoxSettings = () => {
|
|||||||
icon={<BiReset />}
|
icon={<BiReset />}
|
||||||
styleClass="inpainting-bounding-box-reset-icon-btn"
|
styleClass="inpainting-bounding-box-reset-icon-btn"
|
||||||
isDisabled={
|
isDisabled={
|
||||||
!shouldShowBoundingBox ||
|
|
||||||
canvasDimensions.height === boundingBoxDimensions.height
|
canvasDimensions.height === boundingBoxDimensions.height
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -184,14 +189,12 @@ const BoundingBoxSettings = () => {
|
|||||||
isChecked={shouldShowBoundingBoxFill}
|
isChecked={shouldShowBoundingBoxFill}
|
||||||
onChange={handleChangeShouldShowBoundingBoxFill}
|
onChange={handleChangeShouldShowBoundingBoxFill}
|
||||||
styleClass="inpainting-bounding-box-darken"
|
styleClass="inpainting-bounding-box-darken"
|
||||||
isDisabled={!shouldShowBoundingBox}
|
|
||||||
/>
|
/>
|
||||||
<IAICheckbox
|
<IAICheckbox
|
||||||
label="Lock Bounding Box"
|
label="Lock Bounding Box"
|
||||||
isChecked={shouldLockBoundingBox}
|
isChecked={shouldLockBoundingBox}
|
||||||
onChange={handleChangeShouldLockBoundingBox}
|
onChange={handleChangeShouldLockBoundingBox}
|
||||||
styleClass="inpainting-bounding-box-darken"
|
styleClass="inpainting-bounding-box-darken"
|
||||||
isDisabled={!shouldShowBoundingBox}
|
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</div>
|
||||||
|
@ -89,6 +89,7 @@ export default function InpaintingSettings() {
|
|||||||
onClick={handleClearBrushHistory}
|
onClick={handleClearBrushHistory}
|
||||||
tooltip="Clears brush stroke history"
|
tooltip="Clears brush stroke history"
|
||||||
disabled={futureLines.length > 0 || pastLines.length > 0 ? false : true}
|
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 { HEIGHTS } from '../../../app/constants';
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||||
import IAISelect from '../../../common/components/IAISelect';
|
import IAISelect from '../../../common/components/IAISelect';
|
||||||
import { tabMap } from '../../tabs/InvokeTabs';
|
import { activeTabNameSelector } from '../optionsSelectors';
|
||||||
import { setHeight } from '../optionsSlice';
|
import { setHeight } from '../optionsSlice';
|
||||||
import { fontSize } from './MainOptions';
|
import { fontSize } from './MainOptions';
|
||||||
|
|
||||||
export default function MainHeight() {
|
export default function MainHeight() {
|
||||||
const { activeTab, height } = useAppSelector(
|
const { height } = useAppSelector((state: RootState) => state.options);
|
||||||
(state: RootState) => state.options
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
);
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const handleChangeHeight = (e: ChangeEvent<HTMLSelectElement>) =>
|
const handleChangeHeight = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||||
@ -17,7 +16,7 @@ export default function MainHeight() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IAISelect
|
<IAISelect
|
||||||
isDisabled={tabMap[activeTab] === 'inpainting'}
|
isDisabled={activeTabName === 'inpainting'}
|
||||||
label="Height"
|
label="Height"
|
||||||
value={height}
|
value={height}
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
|
@ -6,6 +6,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { SystemState } from '../../system/systemSlice';
|
import { SystemState } from '../../system/systemSlice';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { IAIButtonProps } from '../../../common/components/IAIButton';
|
||||||
|
|
||||||
const cancelButtonSelector = createSelector(
|
const cancelButtonSelector = createSelector(
|
||||||
(state: RootState) => state.system,
|
(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 dispatch = useAppDispatch();
|
||||||
const { isProcessing, isConnected, isCancelable } =
|
const { isProcessing, isConnected, isCancelable } =
|
||||||
useAppSelector(cancelButtonSelector);
|
useAppSelector(cancelButtonSelector);
|
||||||
@ -47,6 +49,7 @@ export default function CancelButton() {
|
|||||||
isDisabled={!isConnected || !isProcessing || !isCancelable}
|
isDisabled={!isConnected || !isProcessing || !isCancelable}
|
||||||
onClick={handleClickCancel}
|
onClick={handleClickCancel}
|
||||||
styleClass="cancel-btn"
|
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 { generateImage } from '../../../app/socketio/actions';
|
||||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
import { useAppDispatch, useAppSelector } from '../../../app/store';
|
||||||
import IAIButton from '../../../common/components/IAIButton';
|
import IAIButton, {
|
||||||
import useCheckParameters from '../../../common/hooks/useCheckParameters';
|
IAIButtonProps,
|
||||||
import { tabMap } from '../../tabs/InvokeTabs';
|
} 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 dispatch = useAppDispatch();
|
||||||
const isReady = useCheckParameters();
|
const isReady = useAppSelector(readinessSelector);
|
||||||
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
const activeTab = useAppSelector(
|
|
||||||
(state: RootState) => state.options.activeTab
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClickGenerate = () => {
|
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
|
<IAIButton
|
||||||
label="Invoke"
|
label="Invoke"
|
||||||
aria-label="Invoke"
|
aria-label="Invoke"
|
||||||
@ -24,6 +53,7 @@ export default function InvokeButton() {
|
|||||||
isDisabled={!isReady}
|
isDisabled={!isReady}
|
||||||
onClick={handleClickGenerate}
|
onClick={handleClickGenerate}
|
||||||
className="invoke-btn"
|
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,15 +1,20 @@
|
|||||||
@use '../../../styles/Mixins/' as *;
|
@use '../../../styles/Mixins/' as *;
|
||||||
|
|
||||||
.process-buttons {
|
.process-buttons {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: auto max-content;
|
|
||||||
column-gap: 0.5rem;
|
column-gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.invoke-btn {
|
.invoke-btn {
|
||||||
|
flex-grow: 1;
|
||||||
|
svg {
|
||||||
|
width: 18px !important;
|
||||||
|
height: 18px !important;
|
||||||
|
}
|
||||||
@include Button(
|
@include Button(
|
||||||
$btn-color: var(--accent-color),
|
$btn-color: var(--accent-color),
|
||||||
$btn-color-hover: var(--accent-color-hover),
|
$btn-color-hover: var(--accent-color-hover),
|
||||||
$btn-width: 5rem
|
// $btn-width: 5rem
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,7 +22,6 @@
|
|||||||
@include Button(
|
@include Button(
|
||||||
$btn-color: var(--destructive-color),
|
$btn-color: var(--destructive-color),
|
||||||
$btn-color-hover: var(--destructive-color-hover),
|
$btn-color-hover: var(--destructive-color-hover),
|
||||||
$btn-width: 3rem
|
// $btn-width: 3rem
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import InvokeButton from './InvokeButton';
|
import InvokeButton from './InvokeButton';
|
||||||
import CancelButton from './CancelButton';
|
import CancelButton from './CancelButton';
|
||||||
|
import LoopbackButton from './Loopback';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Buttons to start and cancel image generation.
|
* Buttons to start and cancel image generation.
|
||||||
@ -8,6 +9,7 @@ const ProcessButtons = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="process-buttons">
|
<div className="process-buttons">
|
||||||
<InvokeButton />
|
<InvokeButton />
|
||||||
|
<LoopbackButton />
|
||||||
<CancelButton />
|
<CancelButton />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -6,16 +6,16 @@ import { generateImage } from '../../../app/socketio/actions';
|
|||||||
import { OptionsState, setPrompt } from '../optionsSlice';
|
import { OptionsState, setPrompt } from '../optionsSlice';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import useCheckParameters from '../../../common/hooks/useCheckParameters';
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { tabMap } from '../../tabs/InvokeTabs';
|
import { activeTabNameSelector } from '../optionsSelectors';
|
||||||
|
import { readinessSelector } from '../../../app/selectors/readinessSelector';
|
||||||
|
|
||||||
const promptInputSelector = createSelector(
|
const promptInputSelector = createSelector(
|
||||||
(state: RootState) => state.options,
|
[(state: RootState) => state.options, activeTabNameSelector],
|
||||||
(options: OptionsState) => {
|
(options: OptionsState, activeTabName) => {
|
||||||
return {
|
return {
|
||||||
prompt: options.prompt,
|
prompt: options.prompt,
|
||||||
activeTabName: tabMap[options.activeTab],
|
activeTabName,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -29,25 +29,16 @@ const promptInputSelector = createSelector(
|
|||||||
* Prompt input text area.
|
* Prompt input text area.
|
||||||
*/
|
*/
|
||||||
const PromptInput = () => {
|
const PromptInput = () => {
|
||||||
const promptRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
const { prompt, activeTabName } = useAppSelector(promptInputSelector);
|
|
||||||
const dispatch = useAppDispatch();
|
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>) => {
|
const handleChangePrompt = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
dispatch(setPrompt(e.target.value));
|
dispatch(setPrompt(e.target.value));
|
||||||
};
|
};
|
||||||
|
|
||||||
useHotkeys(
|
|
||||||
'ctrl+enter, cmd+enter',
|
|
||||||
() => {
|
|
||||||
if (isReady) {
|
|
||||||
dispatch(generateImage(activeTabName));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[isReady, activeTabName]
|
|
||||||
);
|
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'alt+a',
|
'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;
|
activeTab: number;
|
||||||
shouldShowImageDetails: boolean;
|
shouldShowImageDetails: boolean;
|
||||||
showDualDisplay: boolean;
|
showDualDisplay: boolean;
|
||||||
|
shouldShowOptionsPanel: boolean;
|
||||||
|
shouldPinOptionsPanel: boolean;
|
||||||
|
optionsPanelScrollPosition: number;
|
||||||
|
shouldHoldOptionsPanelOpen: boolean;
|
||||||
|
shouldLoopback: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialOptionsState: OptionsState = {
|
const initialOptionsState: OptionsState = {
|
||||||
@ -75,6 +80,11 @@ const initialOptionsState: OptionsState = {
|
|||||||
activeTab: 0,
|
activeTab: 0,
|
||||||
shouldShowImageDetails: false,
|
shouldShowImageDetails: false,
|
||||||
showDualDisplay: true,
|
showDualDisplay: true,
|
||||||
|
shouldShowOptionsPanel: true,
|
||||||
|
shouldPinOptionsPanel: true,
|
||||||
|
optionsPanelScrollPosition: 0,
|
||||||
|
shouldHoldOptionsPanelOpen: false,
|
||||||
|
shouldLoopback: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState: OptionsState = initialOptionsState;
|
const initialState: OptionsState = initialOptionsState;
|
||||||
@ -324,6 +334,21 @@ export const optionsSlice = createSlice({
|
|||||||
clearInitialImage: (state) => {
|
clearInitialImage: (state) => {
|
||||||
state.initialImage = undefined;
|
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,
|
setShowDualDisplay,
|
||||||
setInitialImage,
|
setInitialImage,
|
||||||
clearInitialImage,
|
clearInitialImage,
|
||||||
|
setShouldShowOptionsPanel,
|
||||||
|
setShouldPinOptionsPanel,
|
||||||
|
setOptionsPanelScrollPosition,
|
||||||
|
setShouldHoldOptionsPanelOpen,
|
||||||
|
setShouldLoopback,
|
||||||
} = optionsSlice.actions;
|
} = optionsSlice.actions;
|
||||||
|
|
||||||
export default optionsSlice.reducer;
|
export default optionsSlice.reducer;
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
|
.console-resizable {
|
||||||
|
display: flex;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.console {
|
.console {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -41,12 +48,13 @@
|
|||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
left: 0.5rem;
|
left: 0.5rem;
|
||||||
bottom: 0.5rem;
|
bottom: 0.5rem;
|
||||||
|
z-index: 21;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--console-icon-button-bg-color-hover) !important;
|
background: var(--console-icon-button-bg-color-hover) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.error-seen {
|
&[data-error-seen='true'] {
|
||||||
background: var(--status-bad-color) !important;
|
background: var(--status-bad-color) !important;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--status-bad-color) !important;
|
background: var(--status-bad-color) !important;
|
||||||
@ -59,12 +67,13 @@
|
|||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
left: 0.5rem;
|
left: 0.5rem;
|
||||||
bottom: 3rem;
|
bottom: 3rem;
|
||||||
|
z-index: 21;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--console-icon-button-bg-color-hover) !important;
|
background: var(--console-icon-button-bg-color-hover) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.autoscroll-enabled {
|
&[data-autoscroll-enabled='true'] {
|
||||||
background: var(--accent-color) !important;
|
background: var(--accent-color) !important;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--accent-color-hover) !important;
|
background: var(--accent-color-hover) !important;
|
||||||
|
@ -2,7 +2,7 @@ import { IconButton, Tooltip } from '@chakra-ui/react';
|
|||||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||||
import { RootState } from '../../app/store';
|
import { RootState } from '../../app/store';
|
||||||
import { errorSeen, setShouldShowLogViewer, SystemState } from './systemSlice';
|
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 { FaAngleDoubleDown, FaCode, FaMinus } from 'react-icons/fa';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
@ -75,6 +75,17 @@ const Console = () => {
|
|||||||
[shouldShowLogViewer]
|
[shouldShowLogViewer]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleOnScroll = () => {
|
||||||
|
if (!viewerRef.current) return;
|
||||||
|
if (
|
||||||
|
shouldAutoscroll &&
|
||||||
|
viewerRef.current.scrollTop <
|
||||||
|
viewerRef.current.scrollHeight - viewerRef.current.clientHeight
|
||||||
|
) {
|
||||||
|
setShouldAutoscroll(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{shouldShowLogViewer && (
|
{shouldShowLogViewer && (
|
||||||
@ -83,10 +94,16 @@ const Console = () => {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
height: 200,
|
height: 200,
|
||||||
}}
|
}}
|
||||||
style={{ display: 'flex', position: 'fixed', left: 0, bottom: 0 }}
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
position: 'fixed',
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
zIndex: 20,
|
||||||
|
}}
|
||||||
maxHeight={'90vh'}
|
maxHeight={'90vh'}
|
||||||
>
|
>
|
||||||
<div className="console" ref={viewerRef}>
|
<div className="console" ref={viewerRef} onScroll={handleOnScroll}>
|
||||||
{log.map((entry, i) => {
|
{log.map((entry, i) => {
|
||||||
const { timestamp, message, level } = entry;
|
const { timestamp, message, level } = entry;
|
||||||
return (
|
return (
|
||||||
@ -100,11 +117,13 @@ const Console = () => {
|
|||||||
</Resizable>
|
</Resizable>
|
||||||
)}
|
)}
|
||||||
{shouldShowLogViewer && (
|
{shouldShowLogViewer && (
|
||||||
<Tooltip hasArrow label={shouldAutoscroll ? 'Autoscroll On' : 'Autoscroll Off'}>
|
<Tooltip
|
||||||
|
hasArrow
|
||||||
|
label={shouldAutoscroll ? 'Autoscroll On' : 'Autoscroll Off'}
|
||||||
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
className={`console-autoscroll-icon-button ${
|
className={'console-autoscroll-icon-button'}
|
||||||
shouldAutoscroll && 'autoscroll-enabled'
|
data-autoscroll-enabled={shouldAutoscroll}
|
||||||
}`}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
aria-label="Toggle autoscroll"
|
aria-label="Toggle autoscroll"
|
||||||
variant={'solid'}
|
variant={'solid'}
|
||||||
@ -113,16 +132,17 @@ const Console = () => {
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Tooltip hasArrow label={shouldShowLogViewer ? 'Hide Console' : 'Show Console'}>
|
<Tooltip
|
||||||
|
hasArrow
|
||||||
|
label={shouldShowLogViewer ? 'Hide Console' : 'Show Console'}
|
||||||
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
className={`console-toggle-icon-button ${
|
className={'console-toggle-icon-button'}
|
||||||
(hasError || !wasErrorSeen) && 'error-seen'
|
data-error-seen={hasError || !wasErrorSeen}
|
||||||
}`}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
position={'fixed'}
|
position={'fixed'}
|
||||||
variant={'solid'}
|
variant={'solid'}
|
||||||
aria-label="Toggle Log Viewer"
|
aria-label="Toggle Log Viewer"
|
||||||
// colorScheme={hasError || !wasErrorSeen ? 'red' : 'gray'}
|
|
||||||
icon={shouldShowLogViewer ? <FaMinus /> : <FaCode />}
|
icon={shouldShowLogViewer ? <FaMinus /> : <FaCode />}
|
||||||
onClick={handleClickLogViewerToggle}
|
onClick={handleClickLogViewerToggle}
|
||||||
/>
|
/>
|
||||||
|
@ -39,6 +39,16 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
|||||||
desc: 'Focus the prompt input area',
|
desc: 'Focus the prompt input area',
|
||||||
hotkey: 'Alt+A',
|
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',
|
title: 'Toggle Gallery',
|
||||||
desc: 'Open and close the gallery drawer',
|
desc: 'Open and close the gallery drawer',
|
||||||
@ -101,7 +111,7 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
|||||||
{
|
{
|
||||||
title: 'Toggle Gallery Pin',
|
title: 'Toggle Gallery Pin',
|
||||||
desc: 'Pins and unpins the gallery to the UI',
|
desc: 'Pins and unpins the gallery to the UI',
|
||||||
hotkey: 'Shift+P',
|
hotkey: 'Shift+G',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Increase Gallery Image Size',
|
title: 'Increase Gallery Image Size',
|
||||||
@ -134,7 +144,7 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
|
|||||||
{
|
{
|
||||||
title: 'Quick Toggle Brush/Eraser',
|
title: 'Quick Toggle Brush/Eraser',
|
||||||
desc: 'Quick toggle between brush and eraser',
|
desc: 'Quick toggle between brush and eraser',
|
||||||
hotkey: 'Z',
|
hotkey: 'X',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Decrease Brush Size',
|
title: 'Decrease Brush Size',
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
.progress-bar {
|
.progress-bar {
|
||||||
background-color: var(--root-bg-color);
|
background-color: var(--root-bg-color);
|
||||||
height: $progress-bar-thickness !important;
|
height: $progress-bar-thickness !important;
|
||||||
|
z-index: 99;
|
||||||
|
|
||||||
div {
|
div {
|
||||||
background-color: var(--progress-bar-color);
|
background-color: var(--progress-bar-color);
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { IconButton, Link, Tooltip, useColorMode } from '@chakra-ui/react';
|
import { IconButton, Link, Tooltip, useColorMode } from '@chakra-ui/react';
|
||||||
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
import { FaSun, FaMoon, FaGithub, FaDiscord } from 'react-icons/fa';
|
import { FaSun, FaMoon, FaGithub, FaDiscord } from 'react-icons/fa';
|
||||||
import { MdHelp, MdKeyboard, MdSettings } from 'react-icons/md';
|
import { MdHelp, MdKeyboard, MdSettings } from 'react-icons/md';
|
||||||
|
|
||||||
import InvokeAILogo from '../../assets/images/logo.png';
|
import InvokeAILogo from '../../assets/images/logo.png';
|
||||||
|
|
||||||
import HotkeysModal from './HotkeysModal/HotkeysModal';
|
import HotkeysModal from './HotkeysModal/HotkeysModal';
|
||||||
|
|
||||||
import SettingsModal from './SettingsModal/SettingsModal';
|
import SettingsModal from './SettingsModal/SettingsModal';
|
||||||
|
@ -172,6 +172,12 @@ export const systemSlice = createSlice({
|
|||||||
setIsCancelable: (state, action: PayloadAction<boolean>) => {
|
setIsCancelable: (state, action: PayloadAction<boolean>) => {
|
||||||
state.isCancelable = action.payload;
|
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,
|
errorSeen,
|
||||||
setModelList,
|
setModelList,
|
||||||
setIsCancelable,
|
setIsCancelable,
|
||||||
|
modelChangeRequested,
|
||||||
} = systemSlice.actions;
|
} = systemSlice.actions;
|
||||||
|
|
||||||
export default systemSlice.reducer;
|
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%;
|
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 {
|
.image-to-image-strength-main-option {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: none !important;
|
grid-template-columns: none !important;
|
||||||
|
@ -17,6 +17,7 @@ import MainOptions from '../../options/MainOptions/MainOptions';
|
|||||||
import OptionsAccordion from '../../options/OptionsAccordion';
|
import OptionsAccordion from '../../options/OptionsAccordion';
|
||||||
import ProcessButtons from '../../options/ProcessButtons/ProcessButtons';
|
import ProcessButtons from '../../options/ProcessButtons/ProcessButtons';
|
||||||
import PromptInput from '../../options/PromptInput/PromptInput';
|
import PromptInput from '../../options/PromptInput/PromptInput';
|
||||||
|
import InvokeOptionsPanel from '../InvokeOptionsPanel';
|
||||||
|
|
||||||
export default function ImageToImagePanel() {
|
export default function ImageToImagePanel() {
|
||||||
const showAdvancedOptions = useAppSelector(
|
const showAdvancedOptions = useAppSelector(
|
||||||
@ -52,7 +53,7 @@ export default function ImageToImagePanel() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="image-to-image-panel">
|
<InvokeOptionsPanel>
|
||||||
<PromptInput />
|
<PromptInput />
|
||||||
<ProcessButtons />
|
<ProcessButtons />
|
||||||
<MainOptions />
|
<MainOptions />
|
||||||
@ -65,6 +66,6 @@ export default function ImageToImagePanel() {
|
|||||||
{showAdvancedOptions ? (
|
{showAdvancedOptions ? (
|
||||||
<OptionsAccordion accordionInfo={imageToImageAccordions} />
|
<OptionsAccordion accordionInfo={imageToImageAccordions} />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</InvokeOptionsPanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -83,11 +83,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overrides
|
.inpainting-options-btn {
|
||||||
.inpainting-workarea-overrides {
|
min-height: 2rem;
|
||||||
.image-gallery-area {
|
|
||||||
.chakra-popover__popper {
|
|
||||||
inset: 0 auto auto -75px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -53,10 +53,11 @@ const InpaintingCanvas = () => {
|
|||||||
maskColor,
|
maskColor,
|
||||||
imageToInpaint,
|
imageToInpaint,
|
||||||
stageScale,
|
stageScale,
|
||||||
|
shouldShowBoundingBox,
|
||||||
shouldShowBoundingBoxFill,
|
shouldShowBoundingBoxFill,
|
||||||
isDrawing,
|
isDrawing,
|
||||||
shouldLockBoundingBox,
|
shouldLockBoundingBox,
|
||||||
shouldShowBoundingBox,
|
boundingBoxDimensions,
|
||||||
} = useAppSelector(inpaintingCanvasSelector);
|
} = useAppSelector(inpaintingCanvasSelector);
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@ -95,7 +96,7 @@ const InpaintingCanvas = () => {
|
|||||||
};
|
};
|
||||||
image.src = imageToInpaint.url;
|
image.src = imageToInpaint.url;
|
||||||
} else {
|
} else {
|
||||||
setCanvasBgImage(null)
|
setCanvasBgImage(null);
|
||||||
}
|
}
|
||||||
}, [imageToInpaint, dispatch, stageScale, toast]);
|
}, [imageToInpaint, dispatch, stageScale, toast]);
|
||||||
|
|
||||||
@ -243,7 +244,7 @@ const InpaintingCanvas = () => {
|
|||||||
)}
|
)}
|
||||||
{!shouldLockBoundingBox && (
|
{!shouldLockBoundingBox && (
|
||||||
<div style={{ pointerEvents: 'none' }}>
|
<div style={{ pointerEvents: 'none' }}>
|
||||||
Transforming Bounding Box (M)
|
{`Transforming Bounding Box ${boundingBoxDimensions.width}x${boundingBoxDimensions.height} (M)`}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -299,8 +300,9 @@ const InpaintingCanvas = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Layer>
|
</Layer>
|
||||||
|
{shouldShowMask && (
|
||||||
<Layer>
|
<Layer>
|
||||||
{shouldShowBoundingBox && shouldShowBoundingBoxFill && (
|
{shouldShowBoundingBoxFill && shouldShowBoundingBox && (
|
||||||
<InpaintingBoundingBoxPreviewOverlay />
|
<InpaintingBoundingBoxPreviewOverlay />
|
||||||
)}
|
)}
|
||||||
{shouldShowBoundingBox && <InpaintingBoundingBoxPreview />}
|
{shouldShowBoundingBox && <InpaintingBoundingBoxPreview />}
|
||||||
@ -308,6 +310,7 @@ const InpaintingCanvas = () => {
|
|||||||
<InpaintingCanvasBrushPreviewOutline />
|
<InpaintingCanvasBrushPreviewOutline />
|
||||||
)}
|
)}
|
||||||
</Layer>
|
</Layer>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stage>
|
</Stage>
|
||||||
|
@ -11,6 +11,7 @@ const InpaintingCanvasPlaceholder = () => {
|
|||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
window.setTimeout(() => {
|
||||||
if (!ref.current || !imageToInpaint) return;
|
if (!ref.current || !imageToInpaint) return;
|
||||||
|
|
||||||
const width = ref.current.clientWidth;
|
const width = ref.current.clientWidth;
|
||||||
@ -22,6 +23,7 @@ const InpaintingCanvasPlaceholder = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
dispatch(setStageScale(scale));
|
dispatch(setStageScale(scale));
|
||||||
|
}, 0);
|
||||||
}, [dispatch, imageToInpaint, needsCache]);
|
}, [dispatch, imageToInpaint, needsCache]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -51,7 +51,6 @@ const InpaintingControls = () => {
|
|||||||
isMaskEmpty,
|
isMaskEmpty,
|
||||||
activeTabName,
|
activeTabName,
|
||||||
showDualDisplay,
|
showDualDisplay,
|
||||||
shouldShowBoundingBox
|
|
||||||
} = useAppSelector(inpaintingControlsSelector);
|
} = useAppSelector(inpaintingControlsSelector);
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -161,9 +160,9 @@ const InpaintingControls = () => {
|
|||||||
dispatch(toggleShouldLockBoundingBox());
|
dispatch(toggleShouldLockBoundingBox());
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: activeTabName === 'inpainting' && shouldShowMask && shouldShowBoundingBox,
|
enabled: activeTabName === 'inpainting' && shouldShowMask,
|
||||||
},
|
},
|
||||||
[activeTabName, shouldShowMask, shouldShowBoundingBox]
|
[activeTabName, shouldShowMask]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Undo
|
// Undo
|
||||||
@ -349,7 +348,6 @@ const InpaintingControls = () => {
|
|||||||
tooltip="Mask Options"
|
tooltip="Mask Options"
|
||||||
icon={<FaMask />}
|
icon={<FaMask />}
|
||||||
cursor={'pointer'}
|
cursor={'pointer'}
|
||||||
isDisabled={isMaskEmpty}
|
|
||||||
data-selected={maskOptionsOpen}
|
data-selected={maskOptionsOpen}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import MainOptions from '../../options/MainOptions/MainOptions';
|
|||||||
import OptionsAccordion from '../../options/OptionsAccordion';
|
import OptionsAccordion from '../../options/OptionsAccordion';
|
||||||
import ProcessButtons from '../../options/ProcessButtons/ProcessButtons';
|
import ProcessButtons from '../../options/ProcessButtons/ProcessButtons';
|
||||||
import PromptInput from '../../options/PromptInput/PromptInput';
|
import PromptInput from '../../options/PromptInput/PromptInput';
|
||||||
|
import InvokeOptionsPanel from '../InvokeOptionsPanel';
|
||||||
|
|
||||||
export default function InpaintingPanel() {
|
export default function InpaintingPanel() {
|
||||||
const showAdvancedOptions = useAppSelector(
|
const showAdvancedOptions = useAppSelector(
|
||||||
@ -45,7 +46,7 @@ export default function InpaintingPanel() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="image-to-image-panel">
|
<InvokeOptionsPanel>
|
||||||
<PromptInput />
|
<PromptInput />
|
||||||
<ProcessButtons />
|
<ProcessButtons />
|
||||||
<MainOptions />
|
<MainOptions />
|
||||||
@ -58,6 +59,6 @@ export default function InpaintingPanel() {
|
|||||||
{showAdvancedOptions ? (
|
{showAdvancedOptions ? (
|
||||||
<OptionsAccordion accordionInfo={imageToImageAccordions} />
|
<OptionsAccordion accordionInfo={imageToImageAccordions} />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</InvokeOptionsPanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
useAppSelector,
|
useAppSelector,
|
||||||
} from '../../../../app/store';
|
} from '../../../../app/store';
|
||||||
import { roundToMultiple } from '../../../../common/util/roundDownToMultiple';
|
import { roundToMultiple } from '../../../../common/util/roundDownToMultiple';
|
||||||
|
import { stageRef } from '../InpaintingCanvas';
|
||||||
import {
|
import {
|
||||||
InpaintingState,
|
InpaintingState,
|
||||||
setBoundingBoxCoordinate,
|
setBoundingBoxCoordinate,
|
||||||
@ -107,6 +108,15 @@ const InpaintingBoundingBoxPreview = () => {
|
|||||||
transformerRef.current.getLayer()?.batchDraw();
|
transformerRef.current.getLayer()?.batchDraw();
|
||||||
}, [shouldLockBoundingBox]);
|
}, [shouldLockBoundingBox]);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
const container = stageRef.current?.container();
|
||||||
|
if (!container) return;
|
||||||
|
container.style.cursor = 'unset';
|
||||||
|
},
|
||||||
|
[shouldLockBoundingBox]
|
||||||
|
);
|
||||||
|
|
||||||
const scaledStep = 64 * stageScale;
|
const scaledStep = 64 * stageScale;
|
||||||
|
|
||||||
const handleOnDragMove = useCallback(
|
const handleOnDragMove = useCallback(
|
||||||
|
@ -6,8 +6,8 @@ import {
|
|||||||
useAppDispatch,
|
useAppDispatch,
|
||||||
useAppSelector,
|
useAppSelector,
|
||||||
} from '../../../../app/store';
|
} from '../../../../app/store';
|
||||||
|
import { activeTabNameSelector } from '../../../options/optionsSelectors';
|
||||||
import { OptionsState } from '../../../options/optionsSlice';
|
import { OptionsState } from '../../../options/optionsSlice';
|
||||||
import { tabMap } from '../../InvokeTabs';
|
|
||||||
import {
|
import {
|
||||||
InpaintingState,
|
InpaintingState,
|
||||||
setIsDrawing,
|
setIsDrawing,
|
||||||
@ -16,12 +16,16 @@ import {
|
|||||||
} from '../inpaintingSlice';
|
} from '../inpaintingSlice';
|
||||||
|
|
||||||
const keyboardEventManagerSelector = createSelector(
|
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 } =
|
const { shouldShowMask, cursorPosition, shouldLockBoundingBox } =
|
||||||
inpainting;
|
inpainting;
|
||||||
return {
|
return {
|
||||||
activeTabName: tabMap[options.activeTab],
|
activeTabName,
|
||||||
shouldShowMask,
|
shouldShowMask,
|
||||||
isCursorOnCanvas: Boolean(cursorPosition),
|
isCursorOnCanvas: Boolean(cursorPosition),
|
||||||
shouldLockBoundingBox,
|
shouldLockBoundingBox,
|
||||||
@ -49,7 +53,7 @@ const KeyboardEventManager = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listener = (e: KeyboardEvent) => {
|
const listener = (e: KeyboardEvent) => {
|
||||||
if (
|
if (
|
||||||
!['z', ' '].includes(e.key) ||
|
!['x', ' '].includes(e.key) ||
|
||||||
activeTabName !== 'inpainting' ||
|
activeTabName !== 'inpainting' ||
|
||||||
!shouldShowMask
|
!shouldShowMask
|
||||||
) {
|
) {
|
||||||
@ -83,7 +87,7 @@ const KeyboardEventManager = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case 'z': {
|
case 'x': {
|
||||||
dispatch(toggleTool());
|
dispatch(toggleTool());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import type { PayloadAction } 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 { RgbaColor } from 'react-colorful';
|
||||||
import * as InvokeAI from '../../../app/invokeai';
|
import * as InvokeAI from '../../../app/invokeai';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
@ -61,11 +61,11 @@ const initialInpaintingState: InpaintingState = {
|
|||||||
brushSize: 50,
|
brushSize: 50,
|
||||||
maskColor: { r: 255, g: 90, b: 90, a: 0.5 },
|
maskColor: { r: 255, g: 90, b: 90, a: 0.5 },
|
||||||
canvasDimensions: { width: 0, height: 0 },
|
canvasDimensions: { width: 0, height: 0 },
|
||||||
boundingBoxDimensions: { width: 64, height: 64 },
|
boundingBoxDimensions: { width: 512, height: 512 },
|
||||||
boundingBoxCoordinate: { x: 0, y: 0 },
|
boundingBoxCoordinate: { x: 0, y: 0 },
|
||||||
boundingBoxPreviewFill: { r: 0, g: 0, b: 0, a: 0.7 },
|
boundingBoxPreviewFill: { r: 0, g: 0, b: 0, a: 0.7 },
|
||||||
shouldShowBoundingBox: false,
|
shouldShowBoundingBox: true,
|
||||||
shouldShowBoundingBoxFill: false,
|
shouldShowBoundingBoxFill: true,
|
||||||
cursorPosition: null,
|
cursorPosition: null,
|
||||||
lines: [],
|
lines: [],
|
||||||
pastLines: [],
|
pastLines: [],
|
||||||
@ -164,36 +164,32 @@ export const inpaintingSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setImageToInpaint: (state, action: PayloadAction<InvokeAI.Image>) => {
|
setImageToInpaint: (state, action: PayloadAction<InvokeAI.Image>) => {
|
||||||
const { width: imageWidth, height: imageHeight } = action.payload;
|
const { width: imageWidth, height: imageHeight } = action.payload;
|
||||||
const { width: boundingBoxWidth, height: boundingBoxHeight } =
|
const { width, height } = state.boundingBoxDimensions;
|
||||||
state.boundingBoxDimensions;
|
|
||||||
const { x, y } = state.boundingBoxCoordinate;
|
const { x, y } = state.boundingBoxCoordinate;
|
||||||
|
|
||||||
const newBoundingBoxWidth = roundDownToMultiple(
|
const newCoordinates: Vector2d = { x, y };
|
||||||
_.clamp(boundingBoxWidth, 64, imageWidth),
|
const newDimensions: Dimensions = { width, height };
|
||||||
64
|
|
||||||
);
|
|
||||||
|
|
||||||
const newBoundingBoxHeight = roundDownToMultiple(
|
if (width + x > imageWidth) {
|
||||||
_.clamp(boundingBoxHeight, 64, imageHeight),
|
// Bounding box at least needs to be translated
|
||||||
64
|
if (width > imageWidth) {
|
||||||
);
|
// Bounding box also needs to be resized
|
||||||
|
newDimensions.width = roundDownToMultiple(imageWidth, 64);
|
||||||
|
}
|
||||||
|
newCoordinates.x = imageWidth - newDimensions.width;
|
||||||
|
}
|
||||||
|
|
||||||
const newBoundingBoxX = roundDownToMultiple(
|
if (height + y > imageHeight) {
|
||||||
_.clamp(x, 0, imageWidth - newBoundingBoxWidth),
|
// Bounding box at least needs to be translated
|
||||||
64
|
if (height > imageHeight) {
|
||||||
);
|
// Bounding box also needs to be resized
|
||||||
|
newDimensions.height = roundDownToMultiple(imageHeight, 64);
|
||||||
|
}
|
||||||
|
newCoordinates.y = imageHeight - newDimensions.height;
|
||||||
|
}
|
||||||
|
|
||||||
const newBoundingBoxY = roundDownToMultiple(
|
state.boundingBoxDimensions = newDimensions;
|
||||||
_.clamp(y, 0, imageHeight - newBoundingBoxHeight),
|
state.boundingBoxCoordinate = newCoordinates;
|
||||||
64
|
|
||||||
);
|
|
||||||
|
|
||||||
state.boundingBoxDimensions = {
|
|
||||||
width: newBoundingBoxWidth,
|
|
||||||
height: newBoundingBoxHeight,
|
|
||||||
};
|
|
||||||
|
|
||||||
state.boundingBoxCoordinate = { x: newBoundingBoxX, y: newBoundingBoxY };
|
|
||||||
|
|
||||||
state.canvasDimensions = {
|
state.canvasDimensions = {
|
||||||
width: imageWidth,
|
width: imageWidth,
|
||||||
@ -304,9 +300,6 @@ export const inpaintingSlice = createSlice({
|
|||||||
setIsDrawing: (state, action: PayloadAction<boolean>) => {
|
setIsDrawing: (state, action: PayloadAction<boolean>) => {
|
||||||
state.isDrawing = action.payload;
|
state.isDrawing = action.payload;
|
||||||
},
|
},
|
||||||
setShouldShowBoundingBox: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.shouldShowBoundingBox = action.payload;
|
|
||||||
},
|
|
||||||
setClearBrushHistory: (state) => {
|
setClearBrushHistory: (state) => {
|
||||||
state.pastLines = [];
|
state.pastLines = [];
|
||||||
state.futureLines = [];
|
state.futureLines = [];
|
||||||
@ -323,6 +316,9 @@ export const inpaintingSlice = createSlice({
|
|||||||
toggleShouldLockBoundingBox: (state) => {
|
toggleShouldLockBoundingBox: (state) => {
|
||||||
state.shouldLockBoundingBox = !state.shouldLockBoundingBox;
|
state.shouldLockBoundingBox = !state.shouldLockBoundingBox;
|
||||||
},
|
},
|
||||||
|
setShouldShowBoundingBox: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.shouldShowBoundingBox = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -349,10 +345,10 @@ export const {
|
|||||||
setNeedsCache,
|
setNeedsCache,
|
||||||
setStageScale,
|
setStageScale,
|
||||||
toggleTool,
|
toggleTool,
|
||||||
|
setShouldShowBoundingBox,
|
||||||
setShouldShowBoundingBoxFill,
|
setShouldShowBoundingBoxFill,
|
||||||
setIsDrawing,
|
setIsDrawing,
|
||||||
setShouldShowBrush,
|
setShouldShowBrush,
|
||||||
setShouldShowBoundingBox,
|
|
||||||
setClearBrushHistory,
|
setClearBrushHistory,
|
||||||
setShouldUseInpaintReplace,
|
setShouldUseInpaintReplace,
|
||||||
setInpaintReplace,
|
setInpaintReplace,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { RootState } from '../../../app/store';
|
import { RootState } from '../../../app/store';
|
||||||
|
import { activeTabNameSelector } from '../../options/optionsSelectors';
|
||||||
import { OptionsState } from '../../options/optionsSlice';
|
import { OptionsState } from '../../options/optionsSlice';
|
||||||
import { tabMap } from '../InvokeTabs';
|
|
||||||
import { InpaintingState } from './inpaintingSlice';
|
import { InpaintingState } from './inpaintingSlice';
|
||||||
import { rgbaColorToRgbString } from './util/colorToString';
|
import { rgbaColorToRgbString } from './util/colorToString';
|
||||||
|
|
||||||
@ -18,8 +18,12 @@ export const inpaintingCanvasLinesSelector = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const inpaintingControlsSelector = 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 {
|
const {
|
||||||
tool,
|
tool,
|
||||||
brushSize,
|
brushSize,
|
||||||
@ -31,10 +35,9 @@ export const inpaintingControlsSelector = createSelector(
|
|||||||
pastLines,
|
pastLines,
|
||||||
futureLines,
|
futureLines,
|
||||||
shouldShowBoundingBoxFill,
|
shouldShowBoundingBoxFill,
|
||||||
shouldShowBoundingBox,
|
|
||||||
} = inpainting;
|
} = inpainting;
|
||||||
|
|
||||||
const { activeTab, showDualDisplay } = options;
|
const { showDualDisplay } = options;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tool,
|
tool,
|
||||||
@ -46,10 +49,9 @@ export const inpaintingControlsSelector = createSelector(
|
|||||||
canUndo: pastLines.length > 0,
|
canUndo: pastLines.length > 0,
|
||||||
canRedo: futureLines.length > 0,
|
canRedo: futureLines.length > 0,
|
||||||
isMaskEmpty: lines.length === 0,
|
isMaskEmpty: lines.length === 0,
|
||||||
activeTabName: tabMap[activeTab],
|
activeTabName,
|
||||||
showDualDisplay,
|
showDualDisplay,
|
||||||
shouldShowBoundingBoxFill,
|
shouldShowBoundingBoxFill,
|
||||||
shouldShowBoundingBox,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -71,10 +73,11 @@ export const inpaintingCanvasSelector = createSelector(
|
|||||||
shouldShowCheckboardTransparency,
|
shouldShowCheckboardTransparency,
|
||||||
imageToInpaint,
|
imageToInpaint,
|
||||||
stageScale,
|
stageScale,
|
||||||
|
shouldShowBoundingBox,
|
||||||
shouldShowBoundingBoxFill,
|
shouldShowBoundingBoxFill,
|
||||||
isDrawing,
|
isDrawing,
|
||||||
shouldLockBoundingBox,
|
shouldLockBoundingBox,
|
||||||
shouldShowBoundingBox,
|
boundingBoxDimensions,
|
||||||
} = inpainting;
|
} = inpainting;
|
||||||
return {
|
return {
|
||||||
tool,
|
tool,
|
||||||
@ -85,10 +88,11 @@ export const inpaintingCanvasSelector = createSelector(
|
|||||||
maskColor,
|
maskColor,
|
||||||
imageToInpaint,
|
imageToInpaint,
|
||||||
stageScale,
|
stageScale,
|
||||||
|
shouldShowBoundingBox,
|
||||||
shouldShowBoundingBoxFill,
|
shouldShowBoundingBoxFill,
|
||||||
isDrawing,
|
isDrawing,
|
||||||
shouldLockBoundingBox,
|
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';
|
import { MaskLine } from '../inpaintingSlice';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generating a mask image from InpaintingCanvas.tsx is not as simple
|
* Re-draws the mask canvas onto a new Konva stage.
|
||||||
* 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 = (
|
export const generateMaskCanvas = (
|
||||||
image: HTMLImageElement,
|
image: HTMLImageElement,
|
||||||
lines: MaskLine[],
|
lines: MaskLine[]
|
||||||
boundingBox: IRect
|
): {
|
||||||
) => {
|
stage: Konva.Stage;
|
||||||
|
layer: Konva.Layer;
|
||||||
|
} => {
|
||||||
const { width, height } = image;
|
const { width, height } = image;
|
||||||
|
|
||||||
const offscreenContainer = document.createElement('div');
|
const offscreenContainer = document.createElement('div');
|
||||||
@ -35,7 +30,7 @@ const generateMask = (
|
|||||||
layer.add(
|
layer.add(
|
||||||
new Konva.Line({
|
new Konva.Line({
|
||||||
points: line.points,
|
points: line.points,
|
||||||
stroke: 'rgb(255,255,255)',
|
stroke: 'rgb(0,0,0)',
|
||||||
strokeWidth: line.strokeWidth * 2,
|
strokeWidth: line.strokeWidth * 2,
|
||||||
tension: 0,
|
tension: 0,
|
||||||
lineCap: 'round',
|
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(
|
layer.add(
|
||||||
new Konva.Image({ image: image, globalCompositeOperation: 'source-out' })
|
new Konva.Image({ image: image, globalCompositeOperation: 'source-out' })
|
||||||
);
|
);
|
||||||
|
|
||||||
offscreenContainer.remove();
|
const maskDataURL = stage.toDataURL();
|
||||||
|
|
||||||
return stage.toDataURL();
|
return { maskDataURL, isMaskEmpty };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default generateMask;
|
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;
|