feat(ui): refactor DeleteImageModal

- refactor the component
- use translations
- add config for systems where deleted images are not sent to bin (only changes the messaging)
This commit is contained in:
psychedelicious 2023-04-27 12:37:53 +10:00
parent 0cc739afc8
commit 99392debe8
7 changed files with 573 additions and 528 deletions

View File

@ -63,7 +63,7 @@
"postProcessDesc3": "The Invoke AI Command Line Interface offers various other features including Embiggen.", "postProcessDesc3": "The Invoke AI Command Line Interface offers various other features including Embiggen.",
"training": "Training", "training": "Training",
"trainingDesc1": "A dedicated workflow for training your own embeddings and checkpoints using Textual Inversion and Dreambooth from the web interface.", "trainingDesc1": "A dedicated workflow for training your own embeddings and checkpoints using Textual Inversion and Dreambooth from the web interface.",
"trainingDesc2": "InvokeAI already supports training custom embeddings using Textual Inversion using the main script.", "trainingDesc2": "InvokeAI already supports training custom embeddourings using Textual Inversion using the main script.",
"upload": "Upload", "upload": "Upload",
"close": "Close", "close": "Close",
"cancel": "Cancel", "cancel": "Cancel",
@ -100,7 +100,9 @@
"loadingInvokeAI": "Loading Invoke AI", "loadingInvokeAI": "Loading Invoke AI",
"random": "Random", "random": "Random",
"generate": "Generate", "generate": "Generate",
"openInNewTab": "Open in New Tab" "openInNewTab": "Open in New Tab",
"dontAskMeAgain": "Don't ask me again",
"areYouSure": "Are you sure?"
}, },
"gallery": { "gallery": {
"generations": "Generations", "generations": "Generations",
@ -116,7 +118,10 @@
"pinGallery": "Pin Gallery", "pinGallery": "Pin Gallery",
"allImagesLoaded": "All Images Loaded", "allImagesLoaded": "All Images Loaded",
"loadMore": "Load More", "loadMore": "Load More",
"noImagesInGallery": "No Images In Gallery" "noImagesInGallery": "No Images In Gallery",
"deleteImage": "Delete Image",
"deleteImageBin": "Deleted images will be sent to your operating system's Bin.",
"deleteImagePermanent": "Deleted images cannot be restored."
}, },
"hotkeys": { "hotkeys": {
"keyboardShortcuts": "Keyboard Shortcuts", "keyboardShortcuts": "Keyboard Shortcuts",
@ -508,7 +513,6 @@
"useAll": "Use All", "useAll": "Use All",
"useInitImg": "Use Initial Image", "useInitImg": "Use Initial Image",
"info": "Info", "info": "Info",
"deleteImage": "Delete Image",
"initialImage": "Initial Image", "initialImage": "Initial Image",
"showOptionsPanel": "Show Options Panel", "showOptionsPanel": "Show Options Panel",
"hidePreview": "Hide Preview", "hidePreview": "Hide Preview",

View File

@ -375,6 +375,7 @@ export declare type AppConfig = {
shouldFetchImages: boolean; shouldFetchImages: boolean;
disabledTabs: InvokeTabName[]; disabledTabs: InvokeTabName[];
disabledFeatures: AppFeature[]; disabledFeatures: AppFeature[];
canRestoreDeletedImagesFromBin: boolean;
sd: { sd: {
iterations: { iterations: {
initial: number; initial: number;

View File

@ -7,6 +7,7 @@ import {
FlexProps, FlexProps,
FormControl, FormControl,
Link, Link,
useDisclosure,
useToast, useToast,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { runESRGAN, runFacetool } from 'app/socketio/actions'; import { runESRGAN, runFacetool } from 'app/socketio/actions';
@ -66,6 +67,7 @@ import useSetBothPrompts from 'features/parameters/hooks/usePrompt';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import { useGetUrl } from 'common/util/getUrl'; import { useGetUrl } from 'common/util/getUrl';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { imageDeleted } from 'services/thunks/image';
const currentImageButtonsSelector = createSelector( const currentImageButtonsSelector = createSelector(
[ [
@ -77,17 +79,14 @@ const currentImageButtonsSelector = createSelector(
activeTabNameSelector, activeTabNameSelector,
selectedImageSelector, selectedImageSelector,
], ],
( (system, gallery, postprocessing, ui, lightbox, activeTabName, image) => {
system, const {
gallery, isProcessing,
postprocessing, isConnected,
ui, isGFPGANAvailable,
lightbox, isESRGANAvailable,
activeTabName, shouldConfirmOnDelete,
selectedImage } = system;
) => {
const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } =
system;
const { upscalingLevel, facetoolStrength } = postprocessing; const { upscalingLevel, facetoolStrength } = postprocessing;
@ -98,6 +97,8 @@ const currentImageButtonsSelector = createSelector(
const { intermediateImage, currentImage } = gallery; const { intermediateImage, currentImage } = gallery;
return { return {
canDeleteImage: isConnected && !isProcessing,
shouldConfirmOnDelete,
isProcessing, isProcessing,
isConnected, isConnected,
isGFPGANAvailable, isGFPGANAvailable,
@ -110,7 +111,7 @@ const currentImageButtonsSelector = createSelector(
activeTabName, activeTabName,
isLightboxOpen, isLightboxOpen,
shouldHidePreview, shouldHidePreview,
selectedImage, image,
}; };
}, },
{ {
@ -141,7 +142,9 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
isLightboxOpen, isLightboxOpen,
activeTabName, activeTabName,
shouldHidePreview, shouldHidePreview,
selectedImage, image,
canDeleteImage,
shouldConfirmOnDelete,
} = useAppSelector(currentImageButtonsSelector); } = useAppSelector(currentImageButtonsSelector);
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled; const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
@ -150,25 +153,31 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
const { getUrl, shouldTransformUrls } = useGetUrl(); const { getUrl, shouldTransformUrls } = useGetUrl();
const {
isOpen: isDeleteDialogOpen,
onOpen: onDeleteDialogOpen,
onClose: onDeleteDialogClose,
} = useDisclosure();
const toast = useToast(); const toast = useToast();
const { t } = useTranslation(); const { t } = useTranslation();
const setBothPrompts = useSetBothPrompts(); const setBothPrompts = useSetBothPrompts();
const handleClickUseAsInitialImage = () => { const handleClickUseAsInitialImage = () => {
if (!selectedImage) return; if (!image) return;
if (isLightboxOpen) dispatch(setIsLightboxOpen(false)); if (isLightboxOpen) dispatch(setIsLightboxOpen(false));
dispatch(initialImageSelected(selectedImage.name)); dispatch(initialImageSelected(image.name));
// dispatch(setInitialImage(currentImage)); // dispatch(setInitialImage(currentImage));
// dispatch(setActiveTab('img2img')); // dispatch(setActiveTab('img2img'));
}; };
const handleCopyImage = async () => { const handleCopyImage = async () => {
if (!selectedImage?.url) { if (!image?.url) {
return; return;
} }
const url = getUrl(selectedImage.url); const url = getUrl(image.url);
if (!url) { if (!url) {
return; return;
@ -188,10 +197,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
}; };
const handleCopyImageLink = () => { const handleCopyImageLink = () => {
const url = selectedImage const url = image
? shouldTransformUrls ? shouldTransformUrls
? getUrl(selectedImage.url) ? getUrl(image.url)
: window.location.toString() + selectedImage.url : window.location.toString() + image.url
: ''; : '';
if (!url) { if (!url) {
@ -211,7 +220,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
useHotkeys( useHotkeys(
'shift+i', 'shift+i',
() => { () => {
if (selectedImage) { if (image) {
handleClickUseAsInitialImage(); handleClickUseAsInitialImage();
toast({ toast({
title: t('toast.sentToImageToImage'), title: t('toast.sentToImageToImage'),
@ -229,7 +238,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
}); });
} }
}, },
[selectedImage] [image]
); );
const handlePreviewVisibility = () => { const handlePreviewVisibility = () => {
@ -237,7 +246,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
}; };
const handleClickUseAllParameters = () => { const handleClickUseAllParameters = () => {
if (!selectedImage) return; if (!image) return;
// selectedImage.metadata && // selectedImage.metadata &&
// dispatch(setAllParameters(selectedImage.metadata)); // dispatch(setAllParameters(selectedImage.metadata));
// if (selectedImage.metadata?.image.type === 'img2img') { // if (selectedImage.metadata?.image.type === 'img2img') {
@ -250,11 +259,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
useHotkeys( useHotkeys(
'a', 'a',
() => { () => {
if ( if (['txt2img', 'img2img'].includes(image?.metadata?.sd_metadata?.type)) {
['txt2img', 'img2img'].includes(
selectedImage?.metadata?.sd_metadata?.type
)
) {
handleClickUseAllParameters(); handleClickUseAllParameters();
toast({ toast({
title: t('toast.parametersSet'), title: t('toast.parametersSet'),
@ -272,18 +277,17 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
}); });
} }
}, },
[selectedImage] [image]
); );
const handleClickUseSeed = () => { const handleClickUseSeed = () => {
selectedImage?.metadata && image?.metadata && dispatch(setSeed(image.metadata.sd_metadata.seed));
dispatch(setSeed(selectedImage.metadata.sd_metadata.seed));
}; };
useHotkeys( useHotkeys(
's', 's',
() => { () => {
if (selectedImage?.metadata?.sd_metadata?.seed) { if (image?.metadata?.sd_metadata?.seed) {
handleClickUseSeed(); handleClickUseSeed();
toast({ toast({
title: t('toast.seedSet'), title: t('toast.seedSet'),
@ -301,19 +305,19 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
}); });
} }
}, },
[selectedImage] [image]
); );
const handleClickUsePrompt = useCallback(() => { const handleClickUsePrompt = useCallback(() => {
if (selectedImage?.metadata?.sd_metadata?.prompt) { if (image?.metadata?.sd_metadata?.prompt) {
setBothPrompts(selectedImage?.metadata?.sd_metadata?.prompt); setBothPrompts(image?.metadata?.sd_metadata?.prompt);
} }
}, [selectedImage?.metadata?.sd_metadata?.prompt, setBothPrompts]); }, [image?.metadata?.sd_metadata?.prompt, setBothPrompts]);
useHotkeys( useHotkeys(
'p', 'p',
() => { () => {
if (selectedImage?.metadata?.sd_metadata?.prompt) { if (image?.metadata?.sd_metadata?.prompt) {
handleClickUsePrompt(); handleClickUsePrompt();
toast({ toast({
title: t('toast.promptSet'), title: t('toast.promptSet'),
@ -331,7 +335,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
}); });
} }
}, },
[selectedImage] [image]
); );
const handleClickUpscale = () => { const handleClickUpscale = () => {
@ -356,7 +360,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
}, },
[ [
isUpscalingEnabled, isUpscalingEnabled,
selectedImage, image,
isESRGANAvailable, isESRGANAvailable,
shouldDisableToolbarButtons, shouldDisableToolbarButtons,
isConnected, isConnected,
@ -388,7 +392,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
[ [
isFaceRestoreEnabled, isFaceRestoreEnabled,
selectedImage, image,
isGFPGANAvailable, isGFPGANAvailable,
shouldDisableToolbarButtons, shouldDisableToolbarButtons,
isConnected, isConnected,
@ -401,7 +405,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
dispatch(setShouldShowImageDetails(!shouldShowImageDetails)); dispatch(setShouldShowImageDetails(!shouldShowImageDetails));
const handleSendToCanvas = () => { const handleSendToCanvas = () => {
if (!selectedImage) return; if (!image) return;
if (isLightboxOpen) dispatch(setIsLightboxOpen(false)); if (isLightboxOpen) dispatch(setIsLightboxOpen(false));
// dispatch(setInitialCanvasImage(selectedImage)); // dispatch(setInitialCanvasImage(selectedImage));
@ -422,7 +426,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
useHotkeys( useHotkeys(
'i', 'i',
() => { () => {
if (selectedImage) { if (image) {
handleClickShowImageDetails(); handleClickShowImageDetails();
} else { } else {
toast({ toast({
@ -433,226 +437,255 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
}); });
} }
}, },
[selectedImage, shouldShowImageDetails] [image, shouldShowImageDetails]
); );
const handleInitiateDelete = () => {
if (shouldConfirmOnDelete) {
onDeleteDialogOpen();
} else {
handleDelete();
}
};
const handleDelete = () => {
if (canDeleteImage && image) {
dispatch(imageDeleted({ imageType: image.type, imageName: image.name }));
}
};
useHotkeys('delete', handleInitiateDelete, [
image,
shouldConfirmOnDelete,
isConnected,
isProcessing,
]);
const handleLightBox = () => { const handleLightBox = () => {
dispatch(setIsLightboxOpen(!isLightboxOpen)); dispatch(setIsLightboxOpen(!isLightboxOpen));
}; };
return ( return (
<Flex <>
sx={{ <Flex
flexWrap: 'wrap', sx={{
justifyContent: 'center', flexWrap: 'wrap',
alignItems: 'center', justifyContent: 'center',
gap: 2, alignItems: 'center',
}} gap: 2,
{...props} }}
> {...props}
<ButtonGroup isAttached={true}> >
<IAIPopover <ButtonGroup isAttached={true}>
triggerComponent={ <IAIPopover
<IAIIconButton triggerComponent={
isDisabled={!selectedImage} <IAIIconButton
aria-label={`${t('parameters.sendTo')}...`} isDisabled={!image}
icon={<FaShareAlt />} aria-label={`${t('parameters.sendTo')}...`}
/> icon={<FaShareAlt />}
} />
> }
<Flex
sx={{
flexDirection: 'column',
rowGap: 2,
}}
> >
<IAIButton <Flex
size="sm" sx={{
onClick={handleClickUseAsInitialImage} flexDirection: 'column',
leftIcon={<FaShare />} rowGap: 2,
}}
> >
{t('parameters.sendToImg2Img')} <IAIButton
</IAIButton> size="sm"
<IAIButton onClick={handleClickUseAsInitialImage}
size="sm" leftIcon={<FaShare />}
onClick={handleSendToCanvas} >
leftIcon={<FaShare />} {t('parameters.sendToImg2Img')}
>
{t('parameters.sendToUnifiedCanvas')}
</IAIButton>
<IAIButton
size="sm"
onClick={handleCopyImage}
leftIcon={<FaCopy />}
>
{t('parameters.copyImage')}
</IAIButton>
<IAIButton
size="sm"
onClick={handleCopyImageLink}
leftIcon={<FaCopy />}
>
{t('parameters.copyImageToLink')}
</IAIButton>
<Link download={true} href={getUrl(selectedImage?.url ?? '')}>
<IAIButton leftIcon={<FaDownload />} size="sm" w="100%">
{t('parameters.downloadImage')}
</IAIButton> </IAIButton>
</Link> <IAIButton
</Flex> size="sm"
</IAIPopover> onClick={handleSendToCanvas}
<IAIIconButton leftIcon={<FaShare />}
icon={shouldHidePreview ? <FaEyeSlash /> : <FaEye />} >
tooltip={ {t('parameters.sendToUnifiedCanvas')}
!shouldHidePreview </IAIButton>
? t('parameters.hidePreview')
: t('parameters.showPreview') <IAIButton
} size="sm"
aria-label={ onClick={handleCopyImage}
!shouldHidePreview leftIcon={<FaCopy />}
? t('parameters.hidePreview') >
: t('parameters.showPreview') {t('parameters.copyImage')}
} </IAIButton>
isChecked={shouldHidePreview} <IAIButton
onClick={handlePreviewVisibility} size="sm"
/> onClick={handleCopyImageLink}
{isLightboxEnabled && ( leftIcon={<FaCopy />}
>
{t('parameters.copyImageToLink')}
</IAIButton>
<Link download={true} href={getUrl(image?.url ?? '')}>
<IAIButton leftIcon={<FaDownload />} size="sm" w="100%">
{t('parameters.downloadImage')}
</IAIButton>
</Link>
</Flex>
</IAIPopover>
<IAIIconButton <IAIIconButton
icon={<FaExpand />} icon={shouldHidePreview ? <FaEyeSlash /> : <FaEye />}
tooltip={ tooltip={
!isLightboxOpen !shouldHidePreview
? `${t('parameters.openInViewer')} (Z)` ? t('parameters.hidePreview')
: `${t('parameters.closeViewer')} (Z)` : t('parameters.showPreview')
} }
aria-label={ aria-label={
!isLightboxOpen !shouldHidePreview
? `${t('parameters.openInViewer')} (Z)` ? t('parameters.hidePreview')
: `${t('parameters.closeViewer')} (Z)` : t('parameters.showPreview')
} }
isChecked={isLightboxOpen} isChecked={shouldHidePreview}
onClick={handleLightBox} onClick={handlePreviewVisibility}
/> />
)} {isLightboxEnabled && (
</ButtonGroup> <IAIIconButton
icon={<FaExpand />}
<ButtonGroup isAttached={true}> tooltip={
<IAIIconButton !isLightboxOpen
icon={<FaQuoteRight />} ? `${t('parameters.openInViewer')} (Z)`
tooltip={`${t('parameters.usePrompt')} (P)`} : `${t('parameters.closeViewer')} (Z)`
aria-label={`${t('parameters.usePrompt')} (P)`}
isDisabled={!selectedImage?.metadata?.sd_metadata?.prompt}
onClick={handleClickUsePrompt}
/>
<IAIIconButton
icon={<FaSeedling />}
tooltip={`${t('parameters.useSeed')} (S)`}
aria-label={`${t('parameters.useSeed')} (S)`}
isDisabled={!selectedImage?.metadata?.sd_metadata?.seed}
onClick={handleClickUseSeed}
/>
<IAIIconButton
icon={<FaAsterisk />}
tooltip={`${t('parameters.useAll')} (A)`}
aria-label={`${t('parameters.useAll')} (A)`}
isDisabled={
!['txt2img', 'img2img'].includes(
selectedImage?.metadata?.sd_metadata?.type
)
}
onClick={handleClickUseAllParameters}
/>
</ButtonGroup>
{(isUpscalingEnabled || isFaceRestoreEnabled) && (
<ButtonGroup isAttached={true}>
{isFaceRestoreEnabled && (
<IAIPopover
triggerComponent={
<IAIIconButton
icon={<FaGrinStars />}
aria-label={t('parameters.restoreFaces')}
/>
} }
> aria-label={
<Flex !isLightboxOpen
sx={{ ? `${t('parameters.openInViewer')} (Z)`
flexDirection: 'column', : `${t('parameters.closeViewer')} (Z)`
rowGap: 4,
}}
>
<FaceRestoreSettings />
<IAIButton
isDisabled={
!isGFPGANAvailable ||
!selectedImage ||
!(isConnected && !isProcessing) ||
!facetoolStrength
}
onClick={handleClickFixFaces}
>
{t('parameters.restoreFaces')}
</IAIButton>
</Flex>
</IAIPopover>
)}
{isUpscalingEnabled && (
<IAIPopover
triggerComponent={
<IAIIconButton
icon={<FaExpandArrowsAlt />}
aria-label={t('parameters.upscale')}
/>
} }
> isChecked={isLightboxOpen}
<Flex onClick={handleLightBox}
sx={{ />
flexDirection: 'column',
gap: 4,
}}
>
<UpscaleSettings />
<IAIButton
isDisabled={
!isESRGANAvailable ||
!selectedImage ||
!(isConnected && !isProcessing) ||
!upscalingLevel
}
onClick={handleClickUpscale}
>
{t('parameters.upscaleImage')}
</IAIButton>
</Flex>
</IAIPopover>
)} )}
</ButtonGroup> </ButtonGroup>
)}
<ButtonGroup isAttached={true}> <ButtonGroup isAttached={true}>
<IAIIconButton <IAIIconButton
icon={<FaCode />} icon={<FaQuoteRight />}
tooltip={`${t('parameters.info')} (I)`} tooltip={`${t('parameters.usePrompt')} (P)`}
aria-label={`${t('parameters.info')} (I)`} aria-label={`${t('parameters.usePrompt')} (P)`}
isChecked={shouldShowImageDetails} isDisabled={!image?.metadata?.sd_metadata?.prompt}
onClick={handleClickShowImageDetails} onClick={handleClickUsePrompt}
/> />
</ButtonGroup>
<IAIIconButton
icon={<FaSeedling />}
tooltip={`${t('parameters.useSeed')} (S)`}
aria-label={`${t('parameters.useSeed')} (S)`}
isDisabled={!image?.metadata?.sd_metadata?.seed}
onClick={handleClickUseSeed}
/>
<IAIIconButton
icon={<FaAsterisk />}
tooltip={`${t('parameters.useAll')} (A)`}
aria-label={`${t('parameters.useAll')} (A)`}
isDisabled={
!['txt2img', 'img2img'].includes(
image?.metadata?.sd_metadata?.type
)
}
onClick={handleClickUseAllParameters}
/>
</ButtonGroup>
{(isUpscalingEnabled || isFaceRestoreEnabled) && (
<ButtonGroup isAttached={true}>
{isFaceRestoreEnabled && (
<IAIPopover
triggerComponent={
<IAIIconButton
icon={<FaGrinStars />}
aria-label={t('parameters.restoreFaces')}
/>
}
>
<Flex
sx={{
flexDirection: 'column',
rowGap: 4,
}}
>
<FaceRestoreSettings />
<IAIButton
isDisabled={
!isGFPGANAvailable ||
!image ||
!(isConnected && !isProcessing) ||
!facetoolStrength
}
onClick={handleClickFixFaces}
>
{t('parameters.restoreFaces')}
</IAIButton>
</Flex>
</IAIPopover>
)}
{isUpscalingEnabled && (
<IAIPopover
triggerComponent={
<IAIIconButton
icon={<FaExpandArrowsAlt />}
aria-label={t('parameters.upscale')}
/>
}
>
<Flex
sx={{
flexDirection: 'column',
gap: 4,
}}
>
<UpscaleSettings />
<IAIButton
isDisabled={
!isESRGANAvailable ||
!image ||
!(isConnected && !isProcessing) ||
!upscalingLevel
}
onClick={handleClickUpscale}
>
{t('parameters.upscaleImage')}
</IAIButton>
</Flex>
</IAIPopover>
)}
</ButtonGroup>
)}
<ButtonGroup isAttached={true}>
<IAIIconButton
icon={<FaCode />}
tooltip={`${t('parameters.info')} (I)`}
aria-label={`${t('parameters.info')} (I)`}
isChecked={shouldShowImageDetails}
onClick={handleClickShowImageDetails}
/>
</ButtonGroup>
<DeleteImageModal image={selectedImage}>
<IAIIconButton <IAIIconButton
onClick={handleInitiateDelete}
icon={<FaTrash />} icon={<FaTrash />}
tooltip={`${t('parameters.deleteImage')} (Del)`} tooltip={`${t('gallery.deleteImage')} (Del)`}
aria-label={`${t('parameters.deleteImage')} (Del)`} aria-label={`${t('gallery.deleteImage')} (Del)`}
isDisabled={!selectedImage || !isConnected} isDisabled={!image || !isConnected}
colorScheme="error" colorScheme="error"
/> />
</DeleteImageModal> </Flex>
</Flex> {image && (
<DeleteImageModal
isOpen={isDeleteDialogOpen}
onClose={onDeleteDialogClose}
handleDelete={handleDelete}
/>
)}
</>
); );
}; };

View File

@ -5,39 +5,27 @@ import {
AlertDialogFooter, AlertDialogFooter,
AlertDialogHeader, AlertDialogHeader,
AlertDialogOverlay, AlertDialogOverlay,
forwardRef,
Flex, Flex,
Text, Text,
useDisclosure,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import * as InvokeAI from 'app/invokeai';
import { deleteImage } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIButton from 'common/components/IAIButton'; import IAIButton from 'common/components/IAIButton';
import IAISwitch from 'common/components/IAISwitch'; import IAISwitch from 'common/components/IAISwitch';
import { configSelector } from 'features/system/store/configSelectors';
import { systemSelector } from 'features/system/store/systemSelectors'; import { systemSelector } from 'features/system/store/systemSelectors';
import { import { setShouldConfirmOnDelete } from 'features/system/store/systemSlice';
setShouldConfirmOnDelete,
SystemState,
} from 'features/system/store/systemSlice';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { import { ChangeEvent, memo, useCallback, useRef } from 'react';
ChangeEvent, import { useTranslation } from 'react-i18next';
cloneElement,
ReactElement,
SyntheticEvent,
useRef,
} from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { imageDeleted } from 'services/thunks/image';
const deleteImageModalSelector = createSelector( const selector = createSelector(
systemSelector, [systemSelector, configSelector],
(system: SystemState) => { (system, config) => {
const { shouldConfirmOnDelete, isConnected, isProcessing } = system; const { shouldConfirmOnDelete } = system;
return { shouldConfirmOnDelete, isConnected, isProcessing }; const { canRestoreDeletedImagesFromBin } = config;
return { shouldConfirmOnDelete, canRestoreDeletedImagesFromBin };
}, },
{ {
memoizeOptions: { memoizeOptions: {
@ -45,106 +33,77 @@ const deleteImageModalSelector = createSelector(
}, },
} }
); );
interface DeleteImageModalProps { interface DeleteImageModalProps {
/** isOpen: boolean;
* Component which, on click, should delete the image/open the modal. onClose: () => void;
*/ handleDelete: () => void;
children: ReactElement;
/**
* The image to delete.
*/
image?: InvokeAI.Image;
} }
/** const DeleteImageModal = ({
* Needs a child, which will act as the button to delete an image. isOpen,
* If system.shouldConfirmOnDelete is true, a confirmation modal is displayed. onClose,
* If it is false, the image is deleted immediately. handleDelete,
* The confirmation modal has a "Don't ask me again" switch to set the boolean. }: DeleteImageModalProps) => {
*/ const dispatch = useAppDispatch();
const DeleteImageModal = forwardRef( const { t } = useTranslation();
({ image, children }: DeleteImageModalProps, ref) => { const { shouldConfirmOnDelete, canRestoreDeletedImagesFromBin } =
const { isOpen, onOpen, onClose } = useDisclosure(); useAppSelector(selector);
const dispatch = useAppDispatch(); const cancelRef = useRef<HTMLButtonElement>(null);
const { shouldConfirmOnDelete, isConnected, isProcessing } = useAppSelector(
deleteImageModalSelector
);
const cancelRef = useRef<HTMLButtonElement>(null);
const handleClickDelete = (e: SyntheticEvent) => { const handleChangeShouldConfirmOnDelete = useCallback(
e.stopPropagation(); (e: ChangeEvent<HTMLInputElement>) =>
shouldConfirmOnDelete ? onOpen() : handleDelete(); dispatch(setShouldConfirmOnDelete(!e.target.checked)),
}; [dispatch]
);
const handleDelete = () => { const handleClickDelete = useCallback(() => {
if (isConnected && !isProcessing && image) { handleDelete();
dispatch( onClose();
imageDeleted({ imageType: image.type, imageName: image.name }) }, [handleDelete, onClose]);
);
}
onClose();
};
useHotkeys( return (
'delete', <AlertDialog
() => { isOpen={isOpen}
shouldConfirmOnDelete ? onOpen() : handleDelete(); leastDestructiveRef={cancelRef}
}, onClose={onClose}
[image, shouldConfirmOnDelete, isConnected, isProcessing] isCentered
); >
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
{t('gallery.deleteImage')}
</AlertDialogHeader>
const handleChangeShouldConfirmOnDelete = ( <AlertDialogBody>
e: ChangeEvent<HTMLInputElement> <Flex direction="column" gap={5}>
) => dispatch(setShouldConfirmOnDelete(!e.target.checked)); <Flex direction="column" gap={2}>
<Text>{t('common.areYouSure')}</Text>
<Text>
{canRestoreDeletedImagesFromBin
? t('gallery.deleteImageBin')
: t('gallery.deleteImagePermanent')}
</Text>
</Flex>
<IAISwitch
label={t('common.dontAskMeAgain')}
isChecked={!shouldConfirmOnDelete}
onChange={handleChangeShouldConfirmOnDelete}
/>
</Flex>
</AlertDialogBody>
<AlertDialogFooter>
<IAIButton ref={cancelRef} onClick={onClose}>
Cancel
</IAIButton>
<IAIButton colorScheme="error" onClick={handleClickDelete} ml={3}>
Delete
</IAIButton>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
);
};
return ( export default memo(DeleteImageModal);
<>
{cloneElement(children, {
// TODO: This feels wrong.
onClick: image ? handleClickDelete : undefined,
ref: ref,
})}
<AlertDialog
isOpen={isOpen}
leastDestructiveRef={cancelRef}
onClose={onClose}
>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
Delete image
</AlertDialogHeader>
<AlertDialogBody>
<Flex direction="column" gap={5}>
<Text>
Are you sure? Deleted images will be sent to the Bin. You
can restore from there if you wish to.
</Text>
<IAISwitch
label="Don't ask me again"
isChecked={!shouldConfirmOnDelete}
onChange={handleChangeShouldConfirmOnDelete}
/>
</Flex>
</AlertDialogBody>
<AlertDialogFooter>
<IAIButton ref={cancelRef} onClick={onClose}>
Cancel
</IAIButton>
<IAIButton colorScheme="error" onClick={handleDelete} ml={3}>
Delete
</IAIButton>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
</>
);
}
);
DeleteImageModal.displayName = 'DeleteImageModal';
export default DeleteImageModal;

View File

@ -6,6 +6,7 @@ import {
MenuItem, MenuItem,
MenuList, MenuList,
Text, Text,
useDisclosure,
useTheme, useTheme,
useToast, useToast,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
@ -36,7 +37,7 @@ import {
resizeAndScaleCanvas, resizeAndScaleCanvas,
setInitialCanvasImage, setInitialCanvasImage,
} from 'features/canvas/store/canvasSlice'; } from 'features/canvas/store/canvasSlice';
import { hoverableImageSelector } from 'features/gallery/store/gallerySelectors'; import { gallerySelector } from 'features/gallery/store/gallerySelectors';
import { setActiveTab } from 'features/ui/store/uiSlice'; import { setActiveTab } from 'features/ui/store/uiSlice';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import useSetBothPrompts from 'features/parameters/hooks/usePrompt'; import useSetBothPrompts from 'features/parameters/hooks/usePrompt';
@ -46,6 +47,50 @@ import { useGetUrl } from 'common/util/getUrl';
import { ExternalLinkIcon } from '@chakra-ui/icons'; import { ExternalLinkIcon } from '@chakra-ui/icons';
import { BiZoomIn } from 'react-icons/bi'; import { BiZoomIn } from 'react-icons/bi';
import { IoArrowUndoCircleOutline } from 'react-icons/io5'; import { IoArrowUndoCircleOutline } from 'react-icons/io5';
import { imageDeleted } from 'services/thunks/image';
import { createSelector } from '@reduxjs/toolkit';
import { systemSelector } from 'features/system/store/systemSelectors';
import { configSelector } from 'features/system/store/configSelectors';
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { isEqual } from 'lodash';
export const selector = createSelector(
[
gallerySelector,
systemSelector,
configSelector,
lightboxSelector,
activeTabNameSelector,
],
(gallery, system, config, lightbox, activeTabName) => {
const {
galleryImageObjectFit,
galleryImageMinimumWidth,
shouldUseSingleGalleryColumn,
} = gallery;
const { isLightboxOpen } = lightbox;
const { disabledFeatures } = config;
const { isConnected, isProcessing, shouldConfirmOnDelete } = system;
return {
canDeleteImage: isConnected && !isProcessing,
shouldConfirmOnDelete,
galleryImageObjectFit,
galleryImageMinimumWidth,
shouldUseSingleGalleryColumn,
activeTabName,
isLightboxOpen,
disabledFeatures,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
interface HoverableImageProps { interface HoverableImageProps {
image: InvokeAI.Image; image: InvokeAI.Image;
@ -66,10 +111,16 @@ const HoverableImage = memo((props: HoverableImageProps) => {
activeTabName, activeTabName,
galleryImageObjectFit, galleryImageObjectFit,
galleryImageMinimumWidth, galleryImageMinimumWidth,
mayDeleteImage, canDeleteImage,
shouldUseSingleGalleryColumn, shouldUseSingleGalleryColumn,
disabledFeatures, disabledFeatures,
} = useAppSelector(hoverableImageSelector); shouldConfirmOnDelete,
} = useAppSelector(selector);
const {
isOpen: isDeleteDialogOpen,
onOpen: onDeleteDialogOpen,
onClose: onDeleteDialogClose,
} = useDisclosure();
const { image, isSelected } = props; const { image, isSelected } = props;
const { url, thumbnail, name, metadata } = image; const { url, thumbnail, name, metadata } = image;
const { getUrl } = useGetUrl(); const { getUrl } = useGetUrl();
@ -85,6 +136,20 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const handleMouseOut = () => setIsHovered(false); const handleMouseOut = () => setIsHovered(false);
const handleInitiateDelete = () => {
if (shouldConfirmOnDelete) {
onDeleteDialogOpen();
} else {
handleDelete();
}
};
const handleDelete = () => {
if (canDeleteImage && image) {
dispatch(imageDeleted({ imageType: image.type, imageName: image.name }));
}
};
const handleUsePrompt = () => { const handleUsePrompt = () => {
if (image.metadata?.sd_metadata?.prompt) { if (image.metadata?.sd_metadata?.prompt) {
setBothPrompts(image.metadata?.sd_metadata?.prompt); setBothPrompts(image.metadata?.sd_metadata?.prompt);
@ -184,159 +249,167 @@ const HoverableImage = memo((props: HoverableImageProps) => {
}; };
return ( return (
<ContextMenu<HTMLDivElement> <>
menuProps={{ size: 'sm', isLazy: true }} <ContextMenu<HTMLDivElement>
renderMenu={() => ( menuProps={{ size: 'sm', isLazy: true }}
<MenuList> renderMenu={() => (
<MenuItem <MenuList>
icon={<ExternalLinkIcon />} <MenuItem
onClickCapture={handleOpenInNewTab} icon={<ExternalLinkIcon />}
> onClickCapture={handleOpenInNewTab}
{t('common.openInNewTab')} >
</MenuItem> {t('common.openInNewTab')}
{!disabledFeatures.includes('lightbox') && (
<MenuItem icon={<FaExpand />} onClickCapture={handleLightBox}>
{t('parameters.openInViewer')}
</MenuItem> </MenuItem>
)} {!disabledFeatures.includes('lightbox') && (
<MenuItem <MenuItem icon={<FaExpand />} onClickCapture={handleLightBox}>
icon={<IoArrowUndoCircleOutline />} {t('parameters.openInViewer')}
onClickCapture={handleUsePrompt} </MenuItem>
isDisabled={image?.metadata?.sd_metadata?.prompt === undefined}
>
{t('parameters.usePrompt')}
</MenuItem>
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleUseSeed}
isDisabled={image?.metadata?.sd_metadata?.seed === undefined}
>
{t('parameters.useSeed')}
</MenuItem>
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleUseInitialImage}
isDisabled={image?.metadata?.sd_metadata?.type !== 'img2img'}
>
{t('parameters.useInitImg')}
</MenuItem>
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleUseAllParameters}
isDisabled={
!['txt2img', 'img2img'].includes(
image?.metadata?.sd_metadata?.type
)
}
>
{t('parameters.useAll')}
</MenuItem>
<MenuItem
icon={<FaShare />}
onClickCapture={handleSendToImageToImage}
>
{t('parameters.sendToImg2Img')}
</MenuItem>
<MenuItem icon={<FaShare />} onClickCapture={handleSendToCanvas}>
{t('parameters.sendToUnifiedCanvas')}
</MenuItem>
<MenuItem icon={<FaTrash />}>
<DeleteImageModal image={image}>
<Text>{t('parameters.deleteImage')}</Text>
</DeleteImageModal>
</MenuItem>
</MenuList>
)}
>
{(ref) => (
<Box
position="relative"
key={name}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
userSelect="none"
draggable={true}
onDragStart={handleDragStart}
ref={ref}
sx={{
padding: 2,
display: 'flex',
justifyContent: 'center',
transition: 'transform 0.2s ease-out',
_hover: {
cursor: 'pointer',
zIndex: 2,
},
_before: { content: '""', display: 'block', paddingBottom: '100%' },
}}
>
<Image
objectFit={
shouldUseSingleGalleryColumn ? 'contain' : galleryImageObjectFit
}
rounded="md"
src={getUrl(thumbnail || url)}
loading="lazy"
sx={{
position: 'absolute',
width: '100%',
height: '100%',
maxWidth: '100%',
maxHeight: '100%',
top: '50%',
transform: 'translate(-50%,-50%)',
...(direction === 'rtl'
? { insetInlineEnd: '50%' }
: { insetInlineStart: '50%' }),
}}
/>
<Flex
onClick={handleSelectImage}
sx={{
position: 'absolute',
top: '0',
insetInlineStart: '0',
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
}}
>
{isSelected && (
<Icon
as={FaCheck}
sx={{
width: '50%',
height: '50%',
fill: 'ok.500',
}}
/>
)} )}
</Flex> <MenuItem
{isHovered && galleryImageMinimumWidth >= 64 && ( icon={<IoArrowUndoCircleOutline />}
<Box onClickCapture={handleUsePrompt}
isDisabled={image?.metadata?.sd_metadata?.prompt === undefined}
>
{t('parameters.usePrompt')}
</MenuItem>
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleUseSeed}
isDisabled={image?.metadata?.sd_metadata?.seed === undefined}
>
{t('parameters.useSeed')}
</MenuItem>
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleUseInitialImage}
isDisabled={image?.metadata?.sd_metadata?.type !== 'img2img'}
>
{t('parameters.useInitImg')}
</MenuItem>
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleUseAllParameters}
isDisabled={
!['txt2img', 'img2img'].includes(
image?.metadata?.sd_metadata?.type
)
}
>
{t('parameters.useAll')}
</MenuItem>
<MenuItem
icon={<FaShare />}
onClickCapture={handleSendToImageToImage}
>
{t('parameters.sendToImg2Img')}
</MenuItem>
<MenuItem icon={<FaShare />} onClickCapture={handleSendToCanvas}>
{t('parameters.sendToUnifiedCanvas')}
</MenuItem>
<MenuItem icon={<FaTrash />} onClickCapture={onDeleteDialogOpen}>
{t('gallery.deleteImage')}
</MenuItem>
</MenuList>
)}
>
{(ref) => (
<Box
position="relative"
key={name}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
userSelect="none"
draggable={true}
onDragStart={handleDragStart}
ref={ref}
sx={{
padding: 2,
display: 'flex',
justifyContent: 'center',
transition: 'transform 0.2s ease-out',
_hover: {
cursor: 'pointer',
zIndex: 2,
},
_before: {
content: '""',
display: 'block',
paddingBottom: '100%',
},
}}
>
<Image
objectFit={
shouldUseSingleGalleryColumn ? 'contain' : galleryImageObjectFit
}
rounded="md"
src={getUrl(thumbnail || url)}
loading="lazy"
sx={{ sx={{
position: 'absolute', position: 'absolute',
top: 1, width: '100%',
insetInlineEnd: 1, height: '100%',
maxWidth: '100%',
maxHeight: '100%',
top: '50%',
transform: 'translate(-50%,-50%)',
...(direction === 'rtl'
? { insetInlineEnd: '50%' }
: { insetInlineStart: '50%' }),
}}
/>
<Flex
onClick={handleSelectImage}
sx={{
position: 'absolute',
top: '0',
insetInlineStart: '0',
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
}} }}
> >
<DeleteImageModal image={image}> {isSelected && (
<Icon
as={FaCheck}
sx={{
width: '50%',
height: '50%',
fill: 'ok.500',
}}
/>
)}
</Flex>
{isHovered && galleryImageMinimumWidth >= 64 && (
<Box
sx={{
position: 'absolute',
top: 1,
insetInlineEnd: 1,
}}
>
<IAIIconButton <IAIIconButton
aria-label={t('parameters.deleteImage')} onClickCapture={handleInitiateDelete}
aria-label={t('gallery.deleteImage')}
icon={<FaTrash />} icon={<FaTrash />}
size="xs" size="xs"
fontSize={14} fontSize={14}
isDisabled={!mayDeleteImage} isDisabled={!canDeleteImage}
/> />
</DeleteImageModal> </Box>
</Box> )}
)} </Box>
</Box> )}
)} </ContextMenu>
</ContextMenu> <DeleteImageModal
isOpen={isDeleteDialogOpen}
onClose={onDeleteDialogClose}
handleDelete={handleDelete}
/>
</>
); );
}, memoEqualityCheck); }, memoEqualityCheck);

View File

@ -68,32 +68,6 @@ export const imageGallerySelector = createSelector(
} }
); );
export const hoverableImageSelector = createSelector(
[
gallerySelector,
systemSelector,
configSelector,
lightboxSelector,
activeTabNameSelector,
],
(gallery, system, config, lightbox, activeTabName) => {
return {
mayDeleteImage: system.isConnected && !system.isProcessing,
galleryImageObjectFit: gallery.galleryImageObjectFit,
galleryImageMinimumWidth: gallery.galleryImageMinimumWidth,
shouldUseSingleGalleryColumn: gallery.shouldUseSingleGalleryColumn,
activeTabName,
isLightboxOpen: lightbox.isLightboxOpen,
disabledFeatures: config.disabledFeatures,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
export const selectedImageSelector = createSelector( export const selectedImageSelector = createSelector(
[gallerySelector, selectResultsEntities, selectUploadsEntities], [gallerySelector, selectResultsEntities, selectUploadsEntities],
(gallery, allResults, allUploads) => { (gallery, allResults, allUploads) => {

View File

@ -8,6 +8,7 @@ const initialConfigState: AppConfig = {
shouldFetchImages: false, shouldFetchImages: false,
disabledTabs: [], disabledTabs: [],
disabledFeatures: [], disabledFeatures: [],
canRestoreDeletedImagesFromBin: true,
sd: { sd: {
iterations: { iterations: {
initial: 1, initial: 1,