Merge branch 'pin-options-panel' of https://github.com/psychedelicious/stable-diffusion into psychedelicious-pin-options-panel

- from PR #1301
This commit is contained in:
Lincoln Stein 2022-10-31 09:37:13 -04:00
commit 89da42ad79
114 changed files with 2034 additions and 1232 deletions

View File

@ -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,20 +562,22 @@ class InvokeAIWebServer:
) )
generation_parameters["init_img"] = cropped_init_image generation_parameters["init_img"] = cropped_init_image
# grab an Image of the mask if generation_parameters["is_mask_empty"]:
mask_image = Image.open( generation_parameters["init_mask"] = None
io.BytesIO( else:
base64.decodebytes( # grab an Image of the mask
bytes(generation_parameters["init_mask"], "utf-8") mask_image = Image.open(
io.BytesIO(
base64.decodebytes(
bytes(generation_parameters["init_mask"], "utf-8")
)
) )
) )
) # crop the mask image
cropped_mask_image = copy_image_from_bounding_box(
# crop the mask image mask_image, **generation_parameters["bounding_box"]
cropped_mask_image = copy_image_from_bounding_box( )
mask_image, **generation_parameters["bounding_box"] generation_parameters["init_mask"] = cropped_mask_image
)
generation_parameters["init_mask"] = cropped_mask_image
totalSteps = self.calculate_real_steps( totalSteps = self.calculate_real_steps(
steps=generation_parameters["steps"], steps=generation_parameters["steps"],
@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

View 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

View 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

View 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

517
frontend/dist/assets/index.d3820055.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -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>

View File

@ -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;
}

View File

@ -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>
) : ( ) : (

View 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,
},
}
);

View File

@ -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);
}, },
}; };

View File

@ -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({

View File

@ -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>

View File

@ -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}

View File

@ -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);
} }
} }

View File

@ -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);

View File

@ -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;

View 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;

View File

@ -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) {

View File

@ -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, [
(system: SystemState) => { (state: RootState) => state.system,
(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>

View File

@ -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;

View File

@ -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 (

View File

@ -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 = () => {
dispatch(deleteImage(image)); if (isConnected && !isProcessing) {
toast({ dispatch(deleteImage(image));
title: 'Image Deleted', }
status: 'success',
duration: 2500,
isClosable: true,
});
onClose(); onClose();
}; };

View File

@ -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>

View File

@ -1,26 +1,26 @@
@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;
height: 100vh; height: 100vh;
@ -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);
} }

View File

@ -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>

View File

@ -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)
);
}

View File

@ -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;

View File

@ -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(

View File

@ -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,
}; };
} }
); );

View File

@ -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;

View File

@ -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>

View File

@ -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"
/> />
</> </>
); );

View File

@ -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}

View File

@ -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}
/> />
); );
} }

View File

@ -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}
/> />
); );
} }

View 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;

View File

@ -1,23 +1,27 @@
@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 {
@include Button( .invoke-btn {
$btn-color: var(--accent-color), flex-grow: 1;
$btn-color-hover: var(--accent-color-hover), svg {
$btn-width: 5rem width: 18px !important;
); height: 18px !important;
} }
@include Button(
.cancel-btn { $btn-color: var(--accent-color),
@include Button( $btn-color-hover: var(--accent-color-hover),
$btn-color: var(--destructive-color), // $btn-width: 5rem
$btn-color-hover: var(--destructive-color-hover), );
$btn-width: 3rem }
);
} .cancel-btn {
@include Button(
$btn-color: var(--destructive-color),
$btn-color-hover: var(--destructive-color-hover),
// $btn-width: 3rem
);
} }

View File

@ -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>
); );

View File

@ -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',
() => { () => {

View 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,
},
}
);

View File

@ -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;

View File

@ -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;

View File

@ -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}
/> />

View File

@ -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',

View File

@ -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);

View File

@ -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';

View File

@ -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;

View 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;
}
}
}

View 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;

View 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;

View File

@ -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;

View File

@ -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(
@ -45,14 +46,14 @@ export default function ImageToImagePanel() {
options: <UpscaleOptions />, options: <UpscaleOptions />,
}, },
other: { other: {
header: <OutputHeader /> , header: <OutputHeader />,
feature: Feature.OTHER, feature: Feature.OTHER,
options: <OutputOptions />, options: <OutputOptions />,
}, },
}; };
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>
); );
} }

View File

@ -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;
}
}
} }

View File

@ -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,15 +300,17 @@ const InpaintingCanvas = () => {
/> />
)} )}
</Layer> </Layer>
<Layer> {shouldShowMask && (
{shouldShowBoundingBox && shouldShowBoundingBoxFill && ( <Layer>
<InpaintingBoundingBoxPreviewOverlay /> {shouldShowBoundingBoxFill && shouldShowBoundingBox && (
)} <InpaintingBoundingBoxPreviewOverlay />
{shouldShowBoundingBox && <InpaintingBoundingBoxPreview />} )}
{shouldLockBoundingBox && ( {shouldShowBoundingBox && <InpaintingBoundingBoxPreview />}
<InpaintingCanvasBrushPreviewOutline /> {shouldLockBoundingBox && (
)} <InpaintingCanvasBrushPreviewOutline />
</Layer> )}
</Layer>
)}
</> </>
)} )}
</Stage> </Stage>

View File

@ -11,17 +11,19 @@ const InpaintingCanvasPlaceholder = () => {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
useLayoutEffect(() => { useLayoutEffect(() => {
if (!ref.current || !imageToInpaint) return; window.setTimeout(() => {
if (!ref.current || !imageToInpaint) return;
const width = ref.current.clientWidth; const width = ref.current.clientWidth;
const height = ref.current.clientHeight; const height = ref.current.clientHeight;
const scale = Math.min( const scale = Math.min(
1, 1,
Math.min(width / imageToInpaint.width, height / imageToInpaint.height) Math.min(width / imageToInpaint.width, height / imageToInpaint.height)
); );
dispatch(setStageScale(scale)); dispatch(setStageScale(scale));
}, 0);
}, [dispatch, imageToInpaint, needsCache]); }, [dispatch, imageToInpaint, needsCache]);
return ( return (

View File

@ -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}
/> />
} }

View File

@ -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>
); );
} }

View File

@ -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(

View File

@ -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,13 +87,13 @@ const KeyboardEventManager = () => {
} }
switch (e.key) { switch (e.key) {
case 'z': { case 'x': {
dispatch(toggleTool()); dispatch(toggleTool());
break; break;
} }
case ' ': { case ' ': {
if (!shouldShowMask) break; if (!shouldShowMask) break;
if (e.type === 'keydown') { if (e.type === 'keydown') {
dispatch(setIsDrawing(false)); dispatch(setIsDrawing(false));
} }

View File

@ -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,

View File

@ -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,
}; };
}, },
{ {

View File

@ -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;

View File

@ -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;

View 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;
}
}

View 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;

Some files were not shown because too many files have changed in this diff Show More