mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-15 19:35:08 -05:00
feat(ui): split up StagingAreaToolbar
This commit is contained in:
committed by
Kent Keirsey
parent
752fb88210
commit
f25e28a933
@@ -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 (
|
||||
<>
|
||||
<ButtonGroup borderRadius="base" shadow="dark-lg">
|
||||
<IconButton
|
||||
tooltip={`${t('unifiedCanvas.previous')} (Left)`}
|
||||
aria-label={`${t('unifiedCanvas.previous')} (Left)`}
|
||||
icon={<PiArrowLeftBold />}
|
||||
onClick={onPrev}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={imageCount <= 1 || !shouldShowStagedImage}
|
||||
/>
|
||||
<Button colorScheme="base" pointerEvents="none" minW={28}>
|
||||
{counterText}
|
||||
</Button>
|
||||
<IconButton
|
||||
tooltip={`${t('unifiedCanvas.next')} (Right)`}
|
||||
aria-label={`${t('unifiedCanvas.next')} (Right)`}
|
||||
icon={<PiArrowRightBold />}
|
||||
onClick={onNext}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={imageCount <= 1 || !shouldShowStagedImage}
|
||||
/>
|
||||
<StagingAreaToolbarPrevButton />
|
||||
<StagingAreaToolbarImageCountButton />
|
||||
<StagingAreaToolbarNextButton />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup borderRadius="base" shadow="dark-lg">
|
||||
<IconButton
|
||||
tooltip={`${t('unifiedCanvas.accept')} (Enter)`}
|
||||
aria-label={`${t('unifiedCanvas.accept')} (Enter)`}
|
||||
icon={<PiCheckBold />}
|
||||
onClick={onAccept}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={!selectedImage}
|
||||
/>
|
||||
<IconButton
|
||||
tooltip={shouldShowStagedImage ? t('unifiedCanvas.showResultsOn') : t('unifiedCanvas.showResultsOff')}
|
||||
aria-label={shouldShowStagedImage ? t('unifiedCanvas.showResultsOn') : t('unifiedCanvas.showResultsOff')}
|
||||
data-alert={!shouldShowStagedImage}
|
||||
icon={shouldShowStagedImage ? <PiEyeBold /> : <PiEyeSlashBold />}
|
||||
onClick={onToggleShouldShowStagedImage}
|
||||
colorScheme="invokeBlue"
|
||||
/>
|
||||
<IconButton
|
||||
tooltip={`${t('unifiedCanvas.saveToGallery')} (Shift+S)`}
|
||||
aria-label={t('unifiedCanvas.saveToGallery')}
|
||||
icon={<PiFloppyDiskBold />}
|
||||
onClick={onSaveStagingImage}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={!selectedImage || !selectedImage.imageDTO.is_intermediate}
|
||||
/>
|
||||
<IconButton
|
||||
tooltip={`${t('unifiedCanvas.discardCurrent')}`}
|
||||
aria-label={t('unifiedCanvas.discardCurrent')}
|
||||
icon={<PiXBold />}
|
||||
onClick={onDiscardOne}
|
||||
colorScheme="invokeBlue"
|
||||
fontSize={16}
|
||||
isDisabled={!selectedImage}
|
||||
/>
|
||||
<IconButton
|
||||
tooltip={`${t('unifiedCanvas.discardAll')} (Esc)`}
|
||||
aria-label={t('unifiedCanvas.discardAll')}
|
||||
icon={<PiTrashSimpleBold />}
|
||||
onClick={onDiscardAll}
|
||||
colorScheme="error"
|
||||
fontSize={16}
|
||||
/>
|
||||
<StagingAreaToolbarAcceptButton />
|
||||
<StagingAreaToolbarToggleShowResultsButton />
|
||||
<StagingAreaToolbarSaveSelectedToGalleryButton />
|
||||
<StagingAreaToolbarDiscardSelectedButton />
|
||||
<StagingAreaToolbarDiscardAllButton />
|
||||
</ButtonGroup>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<IconButton
|
||||
tooltip={`${t('unifiedCanvas.accept')} (Enter)`}
|
||||
aria-label={`${t('unifiedCanvas.accept')} (Enter)`}
|
||||
icon={<PiCheckBold />}
|
||||
onClick={acceptSelected}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={!selectedImage}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
StagingAreaToolbarAcceptButton.displayName = 'StagingAreaToolbarAcceptButton';
|
||||
@@ -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 (
|
||||
<IconButton
|
||||
tooltip={`${t('unifiedCanvas.discardAll')} (Esc)`}
|
||||
aria-label={t('unifiedCanvas.discardAll')}
|
||||
icon={<PiTrashSimpleBold />}
|
||||
onClick={discardAll}
|
||||
colorScheme="error"
|
||||
fontSize={16}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
StagingAreaToolbarDiscardAllButton.displayName = 'StagingAreaToolbarDiscardAllButton';
|
||||
@@ -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 (
|
||||
<IconButton
|
||||
tooltip={`${t('unifiedCanvas.discardCurrent')}`}
|
||||
aria-label={t('unifiedCanvas.discardCurrent')}
|
||||
icon={<PiXBold />}
|
||||
onClick={discardSelected}
|
||||
colorScheme="invokeBlue"
|
||||
fontSize={16}
|
||||
isDisabled={!selectedImage}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
StagingAreaToolbarDiscardSelectedButton.displayName = 'StagingAreaToolbarDiscardSelectedButton';
|
||||
@@ -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 (
|
||||
<Button colorScheme="base" pointerEvents="none" minW={28}>
|
||||
{counterText}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
StagingAreaToolbarImageCountButton.displayName = 'StagingAreaToolbarImageCountButton';
|
||||
@@ -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 (
|
||||
<IconButton
|
||||
tooltip={`${t('unifiedCanvas.next')} (Right)`}
|
||||
aria-label={`${t('unifiedCanvas.next')} (Right)`}
|
||||
icon={<PiArrowRightBold />}
|
||||
onClick={selectNext}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={imageCount <= 1 || !shouldShowStagedImage}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
StagingAreaToolbarNextButton.displayName = 'StagingAreaToolbarNextButton';
|
||||
@@ -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 (
|
||||
<IconButton
|
||||
tooltip={`${t('unifiedCanvas.previous')} (Left)`}
|
||||
aria-label={`${t('unifiedCanvas.previous')} (Left)`}
|
||||
icon={<PiArrowLeftBold />}
|
||||
onClick={selectPrev}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={imageCount <= 1 || !shouldShowStagedImage}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
StagingAreaToolbarPrevButton.displayName = 'StagingAreaToolbarPrevButton';
|
||||
@@ -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 (
|
||||
<IconButton
|
||||
tooltip={t('unifiedCanvas.saveToGallery')}
|
||||
aria-label={t('unifiedCanvas.saveToGallery')}
|
||||
icon={<PiFloppyDiskBold />}
|
||||
onClick={saveSelectedImageToGallery}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={!selectedImage || !selectedImage.imageDTO.is_intermediate}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
StagingAreaToolbarSaveSelectedToGalleryButton.displayName = 'StagingAreaToolbarSaveSelectedToGalleryButton';
|
||||
@@ -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 (
|
||||
<IconButton
|
||||
tooltip={shouldShowStagedImage ? t('unifiedCanvas.showResultsOn') : t('unifiedCanvas.showResultsOff')}
|
||||
aria-label={shouldShowStagedImage ? t('unifiedCanvas.showResultsOn') : t('unifiedCanvas.showResultsOff')}
|
||||
data-alert={!shouldShowStagedImage}
|
||||
icon={shouldShowStagedImage ? <PiEyeBold /> : <PiEyeSlashBold />}
|
||||
onClick={toggleShowResults}
|
||||
colorScheme="invokeBlue"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
StagingAreaToolbarToggleShowResultsButton.displayName = 'StagingAreaToolbarToggleShowResultsButton';
|
||||
@@ -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
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user