feat(ui): refactor image deletion

Add `DeleteImageContext`:
- provide a single function to delete an image
- opens the modal or immediately deletes, if confirm is off
This commit is contained in:
psychedelicious 2023-06-06 00:40:40 +10:00
parent fa338ddb6a
commit 3d249c4fa3
9 changed files with 191 additions and 216 deletions

View File

@ -21,6 +21,7 @@ import { ReactNode, memo, useCallback, useEffect, useState } from 'react';
import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants'; import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants';
import GlobalHotkeys from './GlobalHotkeys'; import GlobalHotkeys from './GlobalHotkeys';
import Toaster from './Toaster'; import Toaster from './Toaster';
import DeleteModal from 'features/gallery/components/DeleteModal';
const DEFAULT_CONFIG = {}; const DEFAULT_CONFIG = {};
@ -133,6 +134,7 @@ const App = ({
<FloatingGalleryButton /> <FloatingGalleryButton />
</Portal> </Portal>
</Grid> </Grid>
<DeleteModal />
<Toaster /> <Toaster />
<GlobalHotkeys /> <GlobalHotkeys />
</> </>

View File

@ -17,6 +17,10 @@ import '../../i18n';
import { socketMiddleware } from 'services/events/middleware'; import { socketMiddleware } from 'services/events/middleware';
import { Middleware } from '@reduxjs/toolkit'; import { Middleware } from '@reduxjs/toolkit';
import ImageDndContext from './ImageDnd/ImageDndContext'; import ImageDndContext from './ImageDnd/ImageDndContext';
import {
DeleteImageContext,
DeleteImageContextProvider,
} from 'app/contexts/DeleteImageContext';
const App = lazy(() => import('./App')); const App = lazy(() => import('./App'));
const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider')); const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider'));
@ -71,11 +75,13 @@ const InvokeAIUI = ({
<React.Suspense fallback={<Loading />}> <React.Suspense fallback={<Loading />}>
<ThemeLocaleProvider> <ThemeLocaleProvider>
<ImageDndContext> <ImageDndContext>
<App <DeleteImageContextProvider>
config={config} <App
headerComponent={headerComponent} config={config}
setIsReady={setIsReady} headerComponent={headerComponent}
/> setIsReady={setIsReady}
/>
</DeleteImageContextProvider>
</ImageDndContext> </ImageDndContext>
</ThemeLocaleProvider> </ThemeLocaleProvider>
</React.Suspense> </React.Suspense>

View File

@ -0,0 +1,107 @@
import { useDisclosure } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { requestedImageDeletion } from 'features/gallery/store/actions';
import { systemSelector } from 'features/system/store/systemSelectors';
import { PropsWithChildren, createContext, useCallback, useState } from 'react';
import { ImageDTO } from 'services/api';
type DeleteImageContextValue = {
/**
* Whether the delete image dialog is open.
*/
isOpen: boolean;
/**
* Closes the delete image dialog.
*/
onClose: () => void;
/**
* Immediately deletes an image.
*
* You probably don't want to use this - use `onDelete` instead.
*/
onImmediatelyDelete: () => void;
/**
* Opens the delete image dialog and handles all deletion-related checks.
*/
onDelete: (image?: ImageDTO) => void;
};
export const DeleteImageContext = createContext<DeleteImageContextValue>({
isOpen: false,
onClose: () => undefined,
onImmediatelyDelete: () => undefined,
onDelete: () => undefined,
});
const selector = createSelector(
[systemSelector],
(system) => {
const { isProcessing, isConnected, shouldConfirmOnDelete } = system;
return {
canDeleteImage: isConnected && !isProcessing,
shouldConfirmOnDelete,
isProcessing,
isConnected,
};
},
defaultSelectorOptions
);
type Props = PropsWithChildren;
export const DeleteImageContextProvider = (props: Props) => {
const { canDeleteImage, shouldConfirmOnDelete } = useAppSelector(selector);
const [imageToDelete, setImageToDelete] = useState<ImageDTO>();
const dispatch = useAppDispatch();
const { isOpen, onOpen, onClose } = useDisclosure();
const closeAndClearImageToDelete = useCallback(() => {
setImageToDelete(undefined);
onClose();
}, [onClose]);
const onImmediatelyDelete = useCallback(() => {
if (canDeleteImage && imageToDelete) {
dispatch(requestedImageDeletion(imageToDelete));
}
closeAndClearImageToDelete();
}, [canDeleteImage, imageToDelete, closeAndClearImageToDelete, dispatch]);
const handleDelete = useCallback(
(image: ImageDTO) => {
if (shouldConfirmOnDelete) {
onOpen();
} else {
dispatch(requestedImageDeletion(image));
}
},
[shouldConfirmOnDelete, onOpen, dispatch]
);
const onDelete = useCallback(
(image?: ImageDTO) => {
if (!image) {
return;
}
setImageToDelete(image);
handleDelete(image);
},
[handleDelete]
);
return (
<DeleteImageContext.Provider
value={{
isOpen,
onClose: closeAndClearImageToDelete,
onDelete,
onImmediatelyDelete,
}}
>
{props.children}
</DeleteImageContext.Provider>
);
};

View File

@ -1,13 +1,7 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { import { ButtonGroup, Flex, FlexProps, Link } from '@chakra-ui/react';
ButtonGroup,
Flex,
FlexProps,
Link,
useDisclosure,
} from '@chakra-ui/react';
// import { runESRGAN, runFacetool } from 'app/socketio/actions'; // import { runESRGAN, runFacetool } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton'; import IAIButton from 'common/components/IAIButton';
@ -45,21 +39,18 @@ import {
FaShareAlt, FaShareAlt,
} from 'react-icons/fa'; } from 'react-icons/fa';
import { gallerySelector } from '../store/gallerySelectors'; import { gallerySelector } from '../store/gallerySelectors';
import { useCallback } from 'react'; import { useCallback, useContext } from 'react';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
import { initialImageSelected } from 'features/parameters/store/actions'; import { initialImageSelected } from 'features/parameters/store/actions';
import { import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions';
requestedImageDeletion,
sentImageToCanvas,
sentImageToImg2Img,
} from '../store/actions';
import FaceRestoreSettings from 'features/parameters/components/Parameters/FaceRestore/FaceRestoreSettings'; import FaceRestoreSettings from 'features/parameters/components/Parameters/FaceRestore/FaceRestoreSettings';
import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/UpscaleSettings'; import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/UpscaleSettings';
import DeleteImageButton from './ImageActionButtons/DeleteImageButton';
import { useAppToaster } from 'app/components/Toaster'; import { useAppToaster } from 'app/components/Toaster';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { DeleteImageContext } from 'app/contexts/DeleteImageContext';
import { DeleteImageButton } from './DeleteModal';
const currentImageButtonsSelector = createSelector( const currentImageButtonsSelector = createSelector(
[ [
@ -122,10 +113,6 @@ const currentImageButtonsSelector = createSelector(
type CurrentImageButtonsProps = FlexProps; type CurrentImageButtonsProps = FlexProps;
/**
* Row of buttons for common actions:
* Use as init image, use all params, use seed, upscale, fix faces, details, delete.
*/
const CurrentImageButtons = (props: CurrentImageButtonsProps) => { const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { const {
@ -137,13 +124,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
facetoolStrength, facetoolStrength,
shouldDisableToolbarButtons, shouldDisableToolbarButtons,
shouldShowImageDetails, shouldShowImageDetails,
// currentImage,
isLightboxOpen, isLightboxOpen,
activeTabName, activeTabName,
shouldHidePreview, shouldHidePreview,
image, image,
canDeleteImage,
shouldConfirmOnDelete,
shouldShowProgressInViewer, shouldShowProgressInViewer,
} = useAppSelector(currentImageButtonsSelector); } = useAppSelector(currentImageButtonsSelector);
@ -152,18 +136,14 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled; const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled;
const isFaceRestoreEnabled = useFeatureStatus('faceRestore').isFeatureEnabled; const isFaceRestoreEnabled = useFeatureStatus('faceRestore').isFeatureEnabled;
const {
isOpen: isDeleteDialogOpen,
onOpen: onDeleteDialogOpen,
onClose: onDeleteDialogClose,
} = useDisclosure();
const toaster = useAppToaster(); const toaster = useAppToaster();
const { t } = useTranslation(); const { t } = useTranslation();
const { recallBothPrompts, recallSeed, recallAllParameters } = const { recallBothPrompts, recallSeed, recallAllParameters } =
useRecallParameters(); useRecallParameters();
const { onDelete } = useContext(DeleteImageContext);
// const handleCopyImage = useCallback(async () => { // const handleCopyImage = useCallback(async () => {
// if (!image?.url) { // if (!image?.url) {
// return; // return;
@ -262,6 +242,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
// selectedImage && dispatch(runESRGAN(selectedImage)); // selectedImage && dispatch(runESRGAN(selectedImage));
}, []); }, []);
const handleDelete = useCallback(() => {
onDelete(image);
}, [image, onDelete]);
useHotkeys( useHotkeys(
'Shift+U', 'Shift+U',
() => { () => {
@ -363,31 +347,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
[image, shouldShowImageDetails, toaster] [image, shouldShowImageDetails, toaster]
); );
const handleDelete = useCallback(() => {
if (canDeleteImage && image) {
dispatch(requestedImageDeletion(image));
}
}, [image, canDeleteImage, dispatch]);
const handleInitiateDelete = useCallback(() => {
if (shouldConfirmOnDelete) {
onDeleteDialogOpen();
} else {
handleDelete();
}
}, [shouldConfirmOnDelete, onDeleteDialogOpen, handleDelete]);
const handleClickProgressImagesToggle = useCallback(() => { const handleClickProgressImagesToggle = useCallback(() => {
dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer)); dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer));
}, [dispatch, shouldShowProgressInViewer]); }, [dispatch, shouldShowProgressInViewer]);
useHotkeys('delete', handleInitiateDelete, [
image,
shouldConfirmOnDelete,
isConnected,
isProcessing,
]);
const handleLightBox = useCallback(() => { const handleLightBox = useCallback(() => {
dispatch(setIsLightboxOpen(!isLightboxOpen)); dispatch(setIsLightboxOpen(!isLightboxOpen));
}, [dispatch, isLightboxOpen]); }, [dispatch, isLightboxOpen]);
@ -596,7 +559,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
</ButtonGroup> </ButtonGroup>
<ButtonGroup isAttached={true}> <ButtonGroup isAttached={true}>
<DeleteImageButton image={image} /> <DeleteImageButton onClick={handleDelete} />
</ButtonGroup> </ButtonGroup>
</Flex> </Flex>
</> </>

View File

@ -15,6 +15,7 @@ import { imageSelected } from '../store/gallerySlice';
import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImage from 'common/components/IAIDndImage';
import { ImageDTO } from 'services/api'; import { ImageDTO } from 'services/api';
import { IAIImageFallback } from 'common/components/IAIImageFallback'; import { IAIImageFallback } from 'common/components/IAIImageFallback';
import { useGetIsImageInUse } from 'common/hooks/useGetIsImageInUse';
export const imagesSelector = createSelector( export const imagesSelector = createSelector(
[uiSelector, gallerySelector, systemSelector], [uiSelector, gallerySelector, systemSelector],
@ -54,6 +55,8 @@ const CurrentImagePreview = () => {
const toaster = useAppToaster(); const toaster = useAppToaster();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isImageInUse = useGetIsImageInUse(image?.image_name);
console.log(isImageInUse);
const handleError = useCallback(() => { const handleError = useCallback(() => {
dispatch(imageSelected()); dispatch(imageSelected());
if (shouldFetchImages) { if (shouldFetchImages) {

View File

@ -9,16 +9,19 @@ import {
Text, Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { DeleteImageContext } from 'app/contexts/DeleteImageContext';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton'; import IAIButton from 'common/components/IAIButton';
import IAIIconButton from 'common/components/IAIIconButton';
import IAISwitch from 'common/components/IAISwitch'; import IAISwitch from 'common/components/IAISwitch';
import { configSelector } from 'features/system/store/configSelectors'; import { configSelector } from 'features/system/store/configSelectors';
import { systemSelector } from 'features/system/store/systemSelectors'; import { systemSelector } from 'features/system/store/systemSelectors';
import { setShouldConfirmOnDelete } from 'features/system/store/systemSlice'; import { setShouldConfirmOnDelete } from 'features/system/store/systemSlice';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { ChangeEvent, memo, useCallback, useRef } from 'react'; import { ChangeEvent, memo, useCallback, useContext, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaTrash } from 'react-icons/fa';
const selector = createSelector( const selector = createSelector(
[systemSelector, configSelector], [systemSelector, configSelector],
@ -34,22 +37,12 @@ const selector = createSelector(
} }
); );
interface DeleteImageModalProps { const DeleteImageModal = () => {
isOpen: boolean;
onClose: () => void;
handleDelete: () => void;
}
const DeleteImageModal = ({
isOpen,
onClose,
handleDelete,
}: DeleteImageModalProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const { shouldConfirmOnDelete, canRestoreDeletedImagesFromBin } = const { shouldConfirmOnDelete, canRestoreDeletedImagesFromBin } =
useAppSelector(selector); useAppSelector(selector);
const cancelRef = useRef<HTMLButtonElement>(null);
const handleChangeShouldConfirmOnDelete = useCallback( const handleChangeShouldConfirmOnDelete = useCallback(
(e: ChangeEvent<HTMLInputElement>) => (e: ChangeEvent<HTMLInputElement>) =>
@ -57,10 +50,10 @@ const DeleteImageModal = ({
[dispatch] [dispatch]
); );
const handleClickDelete = useCallback(() => { const { isOpen, onClose, onImmediatelyDelete } =
handleDelete(); useContext(DeleteImageContext);
onClose();
}, [handleDelete, onClose]); const cancelRef = useRef<HTMLButtonElement>(null);
return ( return (
<AlertDialog <AlertDialog
@ -96,7 +89,7 @@ const DeleteImageModal = ({
<IAIButton ref={cancelRef} onClick={onClose}> <IAIButton ref={cancelRef} onClick={onClose}>
Cancel Cancel
</IAIButton> </IAIButton>
<IAIButton colorScheme="error" onClick={handleClickDelete} ml={3}> <IAIButton colorScheme="error" onClick={onImmediatelyDelete} ml={3}>
Delete Delete
</IAIButton> </IAIButton>
</AlertDialogFooter> </AlertDialogFooter>
@ -107,3 +100,33 @@ const DeleteImageModal = ({
}; };
export default memo(DeleteImageModal); export default memo(DeleteImageModal);
const deleteImageButtonsSelector = createSelector(
[systemSelector],
(system) => {
const { isProcessing, isConnected } = system;
return isConnected && !isProcessing;
}
);
type DeleteImageButtonProps = {
onClick: () => void;
};
export const DeleteImageButton = (props: DeleteImageButtonProps) => {
const { onClick } = props;
const { t } = useTranslation();
const canDeleteImage = useAppSelector(deleteImageButtonsSelector);
return (
<IAIIconButton
onClick={onClick}
icon={<FaTrash />}
tooltip={`${t('gallery.deleteImage')} (Del)`}
aria-label={`${t('gallery.deleteImage')} (Del)`}
isDisabled={!canDeleteImage}
colorScheme="error"
/>
);
};

View File

@ -1,17 +1,8 @@
import { import { Box, Flex, Icon, Image, MenuItem, MenuList } from '@chakra-ui/react';
Box,
Flex,
Icon,
Image,
MenuItem,
MenuList,
useDisclosure,
} from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { imageSelected } from 'features/gallery/store/gallerySlice'; import { imageSelected } from 'features/gallery/store/gallerySlice';
import { DragEvent, MouseEvent, memo, useCallback, useState } from 'react'; import { memo, useCallback, useContext, useState } from 'react';
import { FaCheck, FaExpand, FaImage, FaShare, FaTrash } from 'react-icons/fa'; import { FaCheck, FaExpand, FaImage, FaShare, FaTrash } from 'react-icons/fa';
import DeleteImageModal from './DeleteImageModal';
import { ContextMenu } from 'chakra-ui-contextmenu'; import { ContextMenu } from 'chakra-ui-contextmenu';
import { import {
resizeAndScaleCanvas, resizeAndScaleCanvas,
@ -31,14 +22,11 @@ import { isEqual } from 'lodash-es';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
import { initialImageSelected } from 'features/parameters/store/actions'; import { initialImageSelected } from 'features/parameters/store/actions';
import { import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions';
requestedImageDeletion,
sentImageToCanvas,
sentImageToImg2Img,
} from '../store/actions';
import { useAppToaster } from 'app/components/Toaster'; import { useAppToaster } from 'app/components/Toaster';
import { ImageDTO } from 'services/api'; import { ImageDTO } from 'services/api';
import { useDraggable } from '@dnd-kit/core'; import { useDraggable } from '@dnd-kit/core';
import { DeleteImageContext } from 'app/contexts/DeleteImageContext';
export const selector = createSelector( export const selector = createSelector(
[gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector], [gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector],
@ -92,27 +80,22 @@ const HoverableImage = memo((props: HoverableImageProps) => {
galleryImageMinimumWidth, galleryImageMinimumWidth,
canDeleteImage, canDeleteImage,
shouldUseSingleGalleryColumn, shouldUseSingleGalleryColumn,
shouldConfirmOnDelete,
} = useAppSelector(selector); } = useAppSelector(selector);
const {
isOpen: isDeleteDialogOpen,
onOpen: onDeleteDialogOpen,
onClose: onDeleteDialogClose,
} = useDisclosure();
const { image, isSelected } = props; const { image, isSelected } = props;
const { image_url, thumbnail_url, image_name } = image; const { image_url, thumbnail_url, image_name } = image;
const [isHovered, setIsHovered] = useState<boolean>(false); const [isHovered, setIsHovered] = useState<boolean>(false);
const toaster = useAppToaster(); const toaster = useAppToaster();
const { t } = useTranslation(); const { t } = useTranslation();
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled; const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled; const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
const { onDelete } = useContext(DeleteImageContext);
const handleDelete = useCallback(() => {
onDelete(image);
}, [image, onDelete]);
const { recallBothPrompts, recallSeed, recallAllParameters } = const { recallBothPrompts, recallSeed, recallAllParameters } =
useRecallParameters(); useRecallParameters();
@ -126,26 +109,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const handleMouseOver = () => setIsHovered(true); const handleMouseOver = () => setIsHovered(true);
const handleMouseOut = () => setIsHovered(false); const handleMouseOut = () => setIsHovered(false);
// Immediately deletes an image
const handleDelete = useCallback(() => {
if (canDeleteImage && image) {
dispatch(requestedImageDeletion(image));
}
}, [dispatch, image, canDeleteImage]);
// Opens the alert dialog to check if user is sure they want to delete
const handleInitiateDelete = useCallback(
(e: MouseEvent) => {
e.stopPropagation();
if (shouldConfirmOnDelete) {
onDeleteDialogOpen();
} else {
handleDelete();
}
},
[handleDelete, onDeleteDialogOpen, shouldConfirmOnDelete]
);
const handleSelectImage = useCallback(() => { const handleSelectImage = useCallback(() => {
dispatch(imageSelected(image)); dispatch(imageSelected(image));
}, [image, dispatch]); }, [image, dispatch]);
@ -281,7 +244,11 @@ const HoverableImage = memo((props: HoverableImageProps) => {
{t('parameters.sendToUnifiedCanvas')} {t('parameters.sendToUnifiedCanvas')}
</MenuItem> </MenuItem>
)} )}
<MenuItem icon={<FaTrash />} onClickCapture={onDeleteDialogOpen}> <MenuItem
sx={{ color: 'error.300' }}
icon={<FaTrash />}
onClickCapture={handleDelete}
>
{t('gallery.deleteImage')} {t('gallery.deleteImage')}
</MenuItem> </MenuItem>
</MenuList> </MenuList>
@ -357,7 +324,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
}} }}
> >
<IAIIconButton <IAIIconButton
onClickCapture={handleInitiateDelete} onClickCapture={handleDelete}
aria-label={t('gallery.deleteImage')} aria-label={t('gallery.deleteImage')}
icon={<FaTrash />} icon={<FaTrash />}
size="xs" size="xs"
@ -369,11 +336,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
</Box> </Box>
)} )}
</ContextMenu> </ContextMenu>
<DeleteImageModal
isOpen={isDeleteDialogOpen}
onClose={onDeleteDialogClose}
handleDelete={handleDelete}
/>
</Box> </Box>
); );
}, memoEqualityCheck); }, memoEqualityCheck);

View File

@ -1,92 +0,0 @@
import { createSelector } from '@reduxjs/toolkit';
import { useDisclosure } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { systemSelector } from 'features/system/store/systemSelectors';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { FaTrash } from 'react-icons/fa';
import { memo, useCallback } from 'react';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import DeleteImageModal from '../DeleteImageModal';
import { requestedImageDeletion } from 'features/gallery/store/actions';
import { ImageDTO } from 'services/api';
const selector = createSelector(
[systemSelector],
(system) => {
const { isProcessing, isConnected, shouldConfirmOnDelete } = system;
return {
canDeleteImage: isConnected && !isProcessing,
shouldConfirmOnDelete,
isProcessing,
isConnected,
};
},
defaultSelectorOptions
);
type DeleteImageButtonProps = {
image: ImageDTO | undefined;
};
const DeleteImageButton = (props: DeleteImageButtonProps) => {
const { image } = props;
const dispatch = useAppDispatch();
const { isProcessing, isConnected, canDeleteImage, shouldConfirmOnDelete } =
useAppSelector(selector);
const {
isOpen: isDeleteDialogOpen,
onOpen: onDeleteDialogOpen,
onClose: onDeleteDialogClose,
} = useDisclosure();
const { t } = useTranslation();
const handleDelete = useCallback(() => {
if (canDeleteImage && image) {
dispatch(requestedImageDeletion(image));
}
}, [image, canDeleteImage, dispatch]);
const handleInitiateDelete = useCallback(() => {
if (shouldConfirmOnDelete) {
onDeleteDialogOpen();
} else {
handleDelete();
}
}, [shouldConfirmOnDelete, onDeleteDialogOpen, handleDelete]);
useHotkeys('delete', handleInitiateDelete, [
image,
shouldConfirmOnDelete,
isConnected,
isProcessing,
]);
return (
<>
<IAIIconButton
onClick={handleInitiateDelete}
icon={<FaTrash />}
tooltip={`${t('gallery.deleteImage')} (Del)`}
aria-label={`${t('gallery.deleteImage')} (Del)`}
isDisabled={!image || !isConnected}
colorScheme="error"
/>
{image && (
<DeleteImageModal
isOpen={isDeleteDialogOpen}
onClose={onDeleteDialogClose}
handleDelete={handleDelete}
/>
)}
</>
);
};
export default memo(DeleteImageButton);

View File

@ -14,6 +14,7 @@ import { useAppToaster } from 'app/components/Toaster';
import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImage from 'common/components/IAIDndImage';
import { ImageDTO } from 'services/api'; import { ImageDTO } from 'services/api';
import { IAIImageFallback } from 'common/components/IAIImageFallback'; import { IAIImageFallback } from 'common/components/IAIImageFallback';
import { useGetIsImageInUse } from 'common/hooks/useGetIsImageInUse';
const selector = createSelector( const selector = createSelector(
[generationSelector], [generationSelector],