From f25e28a933aab2a669f89985ae429f81fa8dde5f Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Mon, 23 Sep 2024 09:58:57 +1000
Subject: [PATCH] feat(ui): split up StagingAreaToolbar
---
.../StagingArea/StagingAreaToolbar.tsx | 224 ++----------------
.../StagingAreaToolbarAcceptButton.tsx | 57 +++++
.../StagingAreaToolbarDiscardAllButton.tsx | 28 +++
...tagingAreaToolbarDiscardSelectedButton.tsx | 46 ++++
.../StagingAreaToolbarImageCountButton.tsx | 25 ++
.../StagingAreaToolbarNextButton.tsx | 50 ++++
.../StagingAreaToolbarPrevButton.tsx | 50 ++++
...AreaToolbarSaveSelectedToGalleryButton.tsx | 50 ++++
...gingAreaToolbarToggleShowResultsButton.tsx | 30 +++
.../store/canvasStagingAreaSlice.ts | 12 +
10 files changed, 367 insertions(+), 205 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx
create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarToggleShowResultsButton.tsx
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx
index f0844db414..7f22485d48 100644
--- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx
@@ -1,217 +1,31 @@
-import { Button, ButtonGroup, IconButton } from '@invoke-ai/ui-library';
-import { useStore } from '@nanostores/react';
-import { createSelector } from '@reduxjs/toolkit';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { INTERACTION_SCOPES, useScopeOnMount } from 'common/hooks/interactionScopes';
-import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
-import {
- selectCanvasStagingAreaSlice,
- stagingAreaImageAccepted,
- stagingAreaNextStagedImageSelected,
- stagingAreaPrevStagedImageSelected,
- stagingAreaReset,
- stagingAreaStagedImageDiscarded,
-} from 'features/controlLayers/store/canvasStagingAreaSlice';
-import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
-import { memo, useCallback, useMemo } from 'react';
-import { useHotkeys } from 'react-hotkeys-hook';
-import { useTranslation } from 'react-i18next';
-import {
- PiArrowLeftBold,
- PiArrowRightBold,
- PiCheckBold,
- PiEyeBold,
- PiEyeSlashBold,
- PiFloppyDiskBold,
- PiTrashSimpleBold,
- PiXBold,
-} from 'react-icons/pi';
-import { useAddImagesToBoardMutation, useChangeImageIsIntermediateMutation } from 'services/api/endpoints/images';
-
-const selectStagedImageIndex = createSelector(
- selectCanvasStagingAreaSlice,
- (stagingArea) => stagingArea.selectedStagedImageIndex
-);
-
-const selectSelectedImage = createSelector(
- [selectCanvasStagingAreaSlice, selectStagedImageIndex],
- (stagingArea, index) => stagingArea.stagedImages[index] ?? null
-);
-
-const selectImageCount = createSelector(selectCanvasStagingAreaSlice, (stagingArea) => stagingArea.stagedImages.length);
+import { ButtonGroup } from '@invoke-ai/ui-library';
+import { useScopeOnMount } from 'common/hooks/interactionScopes';
+import { StagingAreaToolbarAcceptButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton';
+import { StagingAreaToolbarDiscardAllButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton';
+import { StagingAreaToolbarDiscardSelectedButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton';
+import { StagingAreaToolbarImageCountButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton';
+import { StagingAreaToolbarNextButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton';
+import { StagingAreaToolbarPrevButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton';
+import { StagingAreaToolbarSaveSelectedToGalleryButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton';
+import { StagingAreaToolbarToggleShowResultsButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarToggleShowResultsButton';
+import { memo } from 'react';
export const StagingAreaToolbar = memo(() => {
- const dispatch = useAppDispatch();
- const canvasManager = useCanvasManager();
- const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
- const index = useAppSelector(selectStagedImageIndex);
- const selectedImage = useAppSelector(selectSelectedImage);
- const imageCount = useAppSelector(selectImageCount);
- const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
- const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
- const [addImageToBoard] = useAddImagesToBoardMutation();
- const [changeIsImageIntermediate] = useChangeImageIsIntermediateMutation();
useScopeOnMount('stagingArea');
- const { t } = useTranslation();
-
- const onPrev = useCallback(() => {
- dispatch(stagingAreaPrevStagedImageSelected());
- }, [dispatch]);
-
- const onNext = useCallback(() => {
- dispatch(stagingAreaNextStagedImageSelected());
- }, [dispatch]);
-
- const onAccept = useCallback(() => {
- if (!selectedImage) {
- return;
- }
- dispatch(stagingAreaImageAccepted({ index }));
- }, [dispatch, index, selectedImage]);
-
- const onDiscardOne = useCallback(() => {
- if (!selectedImage) {
- return;
- }
- if (imageCount === 1) {
- dispatch(stagingAreaReset());
- } else {
- dispatch(stagingAreaStagedImageDiscarded({ index }));
- }
- }, [selectedImage, imageCount, dispatch, index]);
-
- const onDiscardAll = useCallback(() => {
- dispatch(stagingAreaReset());
- }, [dispatch]);
-
- const onToggleShouldShowStagedImage = useCallback(() => {
- canvasManager.stagingArea.$shouldShowStagedImage.set(!shouldShowStagedImage);
- }, [canvasManager.stagingArea.$shouldShowStagedImage, shouldShowStagedImage]);
-
- const onSaveStagingImage = useCallback(async () => {
- if (!selectedImage) {
- return;
- }
- if (autoAddBoardId !== 'none') {
- await addImageToBoard({ imageDTOs: [selectedImage.imageDTO], board_id: autoAddBoardId }).unwrap();
- // The changeIsImageIntermediate request will use the board_id on this specific imageDTO object, so we need to
- // update it before making the request - else the optimistic board updates will get out of whack.
- changeIsImageIntermediate({
- imageDTO: { ...selectedImage.imageDTO, board_id: autoAddBoardId },
- is_intermediate: false,
- });
- } else {
- changeIsImageIntermediate({
- imageDTO: selectedImage.imageDTO,
- is_intermediate: false,
- });
- }
- }, [addImageToBoard, autoAddBoardId, changeIsImageIntermediate, selectedImage]);
-
- useHotkeys(
- ['left'],
- onPrev,
- {
- preventDefault: true,
- enabled: isCanvasActive && shouldShowStagedImage && imageCount > 1,
- },
- [isCanvasActive, shouldShowStagedImage, imageCount]
- );
-
- useHotkeys(
- ['right'],
- onNext,
- {
- preventDefault: true,
- enabled: isCanvasActive && shouldShowStagedImage && imageCount > 1,
- },
- [isCanvasActive, shouldShowStagedImage, imageCount]
- );
-
- useHotkeys(
- ['enter'],
- onAccept,
- {
- preventDefault: true,
- enabled: isCanvasActive && shouldShowStagedImage && imageCount > 1,
- },
- [isCanvasActive, shouldShowStagedImage, imageCount]
- );
-
- const counterText = useMemo(() => {
- if (imageCount > 0) {
- return `${(index ?? 0) + 1} of ${imageCount}`;
- } else {
- return `0 of 0`;
- }
- }, [imageCount, index]);
-
return (
<>
- }
- onClick={onPrev}
- colorScheme="invokeBlue"
- isDisabled={imageCount <= 1 || !shouldShowStagedImage}
- />
-
- }
- onClick={onNext}
- colorScheme="invokeBlue"
- isDisabled={imageCount <= 1 || !shouldShowStagedImage}
- />
+
+
+
- }
- onClick={onAccept}
- colorScheme="invokeBlue"
- isDisabled={!selectedImage}
- />
- : }
- onClick={onToggleShouldShowStagedImage}
- colorScheme="invokeBlue"
- />
- }
- onClick={onSaveStagingImage}
- colorScheme="invokeBlue"
- isDisabled={!selectedImage || !selectedImage.imageDTO.is_intermediate}
- />
- }
- onClick={onDiscardOne}
- colorScheme="invokeBlue"
- fontSize={16}
- isDisabled={!selectedImage}
- />
- }
- onClick={onDiscardAll}
- colorScheme="error"
- fontSize={16}
- />
+
+
+
+
+
>
);
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx
new file mode 100644
index 0000000000..9c6ddc4c0b
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton.tsx
@@ -0,0 +1,57 @@
+import { IconButton } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
+import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
+import {
+ selectImageCount,
+ selectSelectedImage,
+ selectStagedImageIndex,
+ stagingAreaImageAccepted,
+} from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { memo, useCallback } from 'react';
+import { useHotkeys } from 'react-hotkeys-hook';
+import { useTranslation } from 'react-i18next';
+import { PiCheckBold } from 'react-icons/pi';
+
+export const StagingAreaToolbarAcceptButton = memo(() => {
+ const dispatch = useAppDispatch();
+ const canvasManager = useCanvasManager();
+ const index = useAppSelector(selectStagedImageIndex);
+ const selectedImage = useAppSelector(selectSelectedImage);
+ const imageCount = useAppSelector(selectImageCount);
+ const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
+ const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
+
+ const { t } = useTranslation();
+
+ const acceptSelected = useCallback(() => {
+ if (!selectedImage) {
+ return;
+ }
+ dispatch(stagingAreaImageAccepted({ index }));
+ }, [dispatch, index, selectedImage]);
+
+ useHotkeys(
+ ['enter'],
+ acceptSelected,
+ {
+ preventDefault: true,
+ enabled: isCanvasActive && shouldShowStagedImage && imageCount > 1,
+ },
+ [isCanvasActive, shouldShowStagedImage, imageCount]
+ );
+
+ return (
+ }
+ onClick={acceptSelected}
+ colorScheme="invokeBlue"
+ isDisabled={!selectedImage}
+ />
+ );
+});
+
+StagingAreaToolbarAcceptButton.displayName = 'StagingAreaToolbarAcceptButton';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx
new file mode 100644
index 0000000000..0ebacb1d70
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton.tsx
@@ -0,0 +1,28 @@
+import { IconButton } from '@invoke-ai/ui-library';
+import { useAppDispatch } from 'app/store/storeHooks';
+import { stagingAreaReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiTrashSimpleBold } from 'react-icons/pi';
+
+export const StagingAreaToolbarDiscardAllButton = memo(() => {
+ const dispatch = useAppDispatch();
+ const { t } = useTranslation();
+
+ const discardAll = useCallback(() => {
+ dispatch(stagingAreaReset());
+ }, [dispatch]);
+
+ return (
+ }
+ onClick={discardAll}
+ colorScheme="error"
+ fontSize={16}
+ />
+ );
+});
+
+StagingAreaToolbarDiscardAllButton.displayName = 'StagingAreaToolbarDiscardAllButton';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
new file mode 100644
index 0000000000..4845e90403
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton.tsx
@@ -0,0 +1,46 @@
+import { IconButton } from '@invoke-ai/ui-library';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import {
+ selectImageCount,
+ selectSelectedImage,
+ selectStagedImageIndex,
+ stagingAreaReset,
+ stagingAreaStagedImageDiscarded,
+} from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiXBold } from 'react-icons/pi';
+
+export const StagingAreaToolbarDiscardSelectedButton = memo(() => {
+ const dispatch = useAppDispatch();
+ const index = useAppSelector(selectStagedImageIndex);
+ const selectedImage = useAppSelector(selectSelectedImage);
+ const imageCount = useAppSelector(selectImageCount);
+
+ const { t } = useTranslation();
+
+ const discardSelected = useCallback(() => {
+ if (!selectedImage) {
+ return;
+ }
+ if (imageCount === 1) {
+ dispatch(stagingAreaReset());
+ } else {
+ dispatch(stagingAreaStagedImageDiscarded({ index }));
+ }
+ }, [selectedImage, imageCount, dispatch, index]);
+
+ return (
+ }
+ onClick={discardSelected}
+ colorScheme="invokeBlue"
+ fontSize={16}
+ isDisabled={!selectedImage}
+ />
+ );
+});
+
+StagingAreaToolbarDiscardSelectedButton.displayName = 'StagingAreaToolbarDiscardSelectedButton';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton.tsx
new file mode 100644
index 0000000000..d408bf1c90
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton.tsx
@@ -0,0 +1,25 @@
+import { Button } from '@invoke-ai/ui-library';
+import { useAppSelector } from 'app/store/storeHooks';
+import { selectImageCount, selectStagedImageIndex } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { memo, useMemo } from 'react';
+
+export const StagingAreaToolbarImageCountButton = memo(() => {
+ const index = useAppSelector(selectStagedImageIndex);
+ const imageCount = useAppSelector(selectImageCount);
+
+ const counterText = useMemo(() => {
+ if (imageCount > 0) {
+ return `${(index ?? 0) + 1} of ${imageCount}`;
+ } else {
+ return `0 of 0`;
+ }
+ }, [imageCount, index]);
+
+ return (
+
+ );
+});
+
+StagingAreaToolbarImageCountButton.displayName = 'StagingAreaToolbarImageCountButton';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton.tsx
new file mode 100644
index 0000000000..609353709c
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton.tsx
@@ -0,0 +1,50 @@
+import { IconButton } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
+import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
+import {
+ selectImageCount,
+ stagingAreaNextStagedImageSelected,
+} from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { memo, useCallback } from 'react';
+import { useHotkeys } from 'react-hotkeys-hook';
+import { useTranslation } from 'react-i18next';
+import { PiArrowRightBold } from 'react-icons/pi';
+
+export const StagingAreaToolbarNextButton = memo(() => {
+ const dispatch = useAppDispatch();
+ const canvasManager = useCanvasManager();
+ const imageCount = useAppSelector(selectImageCount);
+ const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
+ const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
+
+ const { t } = useTranslation();
+
+ const selectNext = useCallback(() => {
+ dispatch(stagingAreaNextStagedImageSelected());
+ }, [dispatch]);
+
+ useHotkeys(
+ ['right'],
+ selectNext,
+ {
+ preventDefault: true,
+ enabled: isCanvasActive && shouldShowStagedImage && imageCount > 1,
+ },
+ [isCanvasActive, shouldShowStagedImage, imageCount]
+ );
+
+ return (
+ }
+ onClick={selectNext}
+ colorScheme="invokeBlue"
+ isDisabled={imageCount <= 1 || !shouldShowStagedImage}
+ />
+ );
+});
+
+StagingAreaToolbarNextButton.displayName = 'StagingAreaToolbarNextButton';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton.tsx
new file mode 100644
index 0000000000..23a4387a85
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton.tsx
@@ -0,0 +1,50 @@
+import { IconButton } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
+import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
+import {
+ selectImageCount,
+ stagingAreaPrevStagedImageSelected,
+} from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { memo, useCallback } from 'react';
+import { useHotkeys } from 'react-hotkeys-hook';
+import { useTranslation } from 'react-i18next';
+import { PiArrowLeftBold } from 'react-icons/pi';
+
+export const StagingAreaToolbarPrevButton = memo(() => {
+ const dispatch = useAppDispatch();
+ const canvasManager = useCanvasManager();
+ const imageCount = useAppSelector(selectImageCount);
+ const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
+ const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
+
+ const { t } = useTranslation();
+
+ const selectPrev = useCallback(() => {
+ dispatch(stagingAreaPrevStagedImageSelected());
+ }, [dispatch]);
+
+ useHotkeys(
+ ['left'],
+ selectPrev,
+ {
+ preventDefault: true,
+ enabled: isCanvasActive && shouldShowStagedImage && imageCount > 1,
+ },
+ [isCanvasActive, shouldShowStagedImage, imageCount]
+ );
+
+ return (
+ }
+ onClick={selectPrev}
+ colorScheme="invokeBlue"
+ isDisabled={imageCount <= 1 || !shouldShowStagedImage}
+ />
+ );
+});
+
+StagingAreaToolbarPrevButton.displayName = 'StagingAreaToolbarPrevButton';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx
new file mode 100644
index 0000000000..a8ae72c3fb
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton.tsx
@@ -0,0 +1,50 @@
+import { IconButton } from '@invoke-ai/ui-library';
+import { useAppSelector } from 'app/store/storeHooks';
+import { selectSelectedImage } from 'features/controlLayers/store/canvasStagingAreaSlice';
+import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiFloppyDiskBold } from 'react-icons/pi';
+import { useAddImagesToBoardMutation, useChangeImageIsIntermediateMutation } from 'services/api/endpoints/images';
+
+export const StagingAreaToolbarSaveSelectedToGalleryButton = memo(() => {
+ const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
+ const selectedImage = useAppSelector(selectSelectedImage);
+ const [addImageToBoard] = useAddImagesToBoardMutation();
+ const [changeIsImageIntermediate] = useChangeImageIsIntermediateMutation();
+
+ const { t } = useTranslation();
+
+ const saveSelectedImageToGallery = useCallback(async () => {
+ if (!selectedImage) {
+ return;
+ }
+ if (autoAddBoardId !== 'none') {
+ await addImageToBoard({ imageDTOs: [selectedImage.imageDTO], board_id: autoAddBoardId }).unwrap();
+ // The changeIsImageIntermediate request will use the board_id on this specific imageDTO object, so we need to
+ // update it before making the request - else the optimistic board updates will get out of whack.
+ changeIsImageIntermediate({
+ imageDTO: { ...selectedImage.imageDTO, board_id: autoAddBoardId },
+ is_intermediate: false,
+ });
+ } else {
+ changeIsImageIntermediate({
+ imageDTO: selectedImage.imageDTO,
+ is_intermediate: false,
+ });
+ }
+ }, [addImageToBoard, autoAddBoardId, changeIsImageIntermediate, selectedImage]);
+
+ return (
+ }
+ onClick={saveSelectedImageToGallery}
+ colorScheme="invokeBlue"
+ isDisabled={!selectedImage || !selectedImage.imageDTO.is_intermediate}
+ />
+ );
+});
+
+StagingAreaToolbarSaveSelectedToGalleryButton.displayName = 'StagingAreaToolbarSaveSelectedToGalleryButton';
diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarToggleShowResultsButton.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarToggleShowResultsButton.tsx
new file mode 100644
index 0000000000..1814d53470
--- /dev/null
+++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbarToggleShowResultsButton.tsx
@@ -0,0 +1,30 @@
+import { IconButton } from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
+import { memo, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { PiEyeBold, PiEyeSlashBold } from 'react-icons/pi';
+
+export const StagingAreaToolbarToggleShowResultsButton = memo(() => {
+ const canvasManager = useCanvasManager();
+ const shouldShowStagedImage = useStore(canvasManager.stagingArea.$shouldShowStagedImage);
+
+ const { t } = useTranslation();
+
+ const toggleShowResults = useCallback(() => {
+ canvasManager.stagingArea.$shouldShowStagedImage.set(!shouldShowStagedImage);
+ }, [canvasManager.stagingArea.$shouldShowStagedImage, shouldShowStagedImage]);
+
+ return (
+ : }
+ onClick={toggleShowResults}
+ colorScheme="invokeBlue"
+ />
+ );
+});
+
+StagingAreaToolbarToggleShowResultsButton.displayName = 'StagingAreaToolbarToggleShowResultsButton';
diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
index f11338c840..dab853c878 100644
--- a/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
+++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasStagingAreaSlice.ts
@@ -90,3 +90,15 @@ export const selectIsStaging = createSelector(
return data.in_progress > 0 || data.pending > 0;
}
);
+export const selectStagedImageIndex = createSelector(
+ selectCanvasStagingAreaSlice,
+ (stagingArea) => stagingArea.selectedStagedImageIndex
+);
+export const selectSelectedImage = createSelector(
+ [selectCanvasStagingAreaSlice, selectStagedImageIndex],
+ (stagingArea, index) => stagingArea.stagedImages[index] ?? null
+);
+export const selectImageCount = createSelector(
+ selectCanvasStagingAreaSlice,
+ (stagingArea) => stagingArea.stagedImages.length
+);