mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04: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 { ButtonGroup } from '@invoke-ai/ui-library';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useScopeOnMount } from 'common/hooks/interactionScopes';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { StagingAreaToolbarAcceptButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarAcceptButton';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { StagingAreaToolbarDiscardAllButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardAllButton';
|
||||||
import { INTERACTION_SCOPES, useScopeOnMount } from 'common/hooks/interactionScopes';
|
import { StagingAreaToolbarDiscardSelectedButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarDiscardSelectedButton';
|
||||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
import { StagingAreaToolbarImageCountButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarImageCountButton';
|
||||||
import {
|
import { StagingAreaToolbarNextButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarNextButton';
|
||||||
selectCanvasStagingAreaSlice,
|
import { StagingAreaToolbarPrevButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarPrevButton';
|
||||||
stagingAreaImageAccepted,
|
import { StagingAreaToolbarSaveSelectedToGalleryButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarSaveSelectedToGalleryButton';
|
||||||
stagingAreaNextStagedImageSelected,
|
import { StagingAreaToolbarToggleShowResultsButton } from 'features/controlLayers/components/StagingArea/StagingAreaToolbarToggleShowResultsButton';
|
||||||
stagingAreaPrevStagedImageSelected,
|
import { memo } from 'react';
|
||||||
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);
|
|
||||||
|
|
||||||
export const StagingAreaToolbar = memo(() => {
|
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');
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<ButtonGroup borderRadius="base" shadow="dark-lg">
|
<ButtonGroup borderRadius="base" shadow="dark-lg">
|
||||||
<IconButton
|
<StagingAreaToolbarPrevButton />
|
||||||
tooltip={`${t('unifiedCanvas.previous')} (Left)`}
|
<StagingAreaToolbarImageCountButton />
|
||||||
aria-label={`${t('unifiedCanvas.previous')} (Left)`}
|
<StagingAreaToolbarNextButton />
|
||||||
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}
|
|
||||||
/>
|
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<ButtonGroup borderRadius="base" shadow="dark-lg">
|
<ButtonGroup borderRadius="base" shadow="dark-lg">
|
||||||
<IconButton
|
<StagingAreaToolbarAcceptButton />
|
||||||
tooltip={`${t('unifiedCanvas.accept')} (Enter)`}
|
<StagingAreaToolbarToggleShowResultsButton />
|
||||||
aria-label={`${t('unifiedCanvas.accept')} (Enter)`}
|
<StagingAreaToolbarSaveSelectedToGalleryButton />
|
||||||
icon={<PiCheckBold />}
|
<StagingAreaToolbarDiscardSelectedButton />
|
||||||
onClick={onAccept}
|
<StagingAreaToolbarDiscardAllButton />
|
||||||
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}
|
|
||||||
/>
|
|
||||||
</ButtonGroup>
|
</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;
|
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