feat(ui): restore image deletion functionality

This commit is contained in:
psychedelicious 2023-04-26 22:54:41 +10:00
parent 2e54da13d8
commit 75d25dd5cc
7 changed files with 133 additions and 27 deletions

View File

@ -643,15 +643,15 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
/>
</ButtonGroup>
{/* <DeleteImageModal image={selectedImage}>
<DeleteImageModal image={selectedImage}>
<IAIIconButton
icon={<FaTrash />}
tooltip={`${t('parameters.deleteImage')} (Del)`}
aria-label={`${t('parameters.deleteImage')} (Del)`}
isDisabled={!selectedImage || !isConnected || isProcessing}
isDisabled={!selectedImage || !isConnected}
colorScheme="error"
/>
</DeleteImageModal> */}
</DeleteImageModal>
</Flex>
);
};

View File

@ -31,6 +31,7 @@ import {
useRef,
} from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { imageDeleted } from 'services/thunks/image';
const deleteImageModalSelector = createSelector(
systemSelector,
@ -52,7 +53,7 @@ interface DeleteImageModalProps {
/**
* The image to delete.
*/
image?: InvokeAI._Image;
image?: InvokeAI.Image;
}
/**
@ -77,7 +78,9 @@ const DeleteImageModal = forwardRef(
const handleDelete = () => {
if (isConnected && !isProcessing && image) {
dispatch(deleteImage(image));
dispatch(
imageDeleted({ imageType: image.type, imageName: image.name })
);
}
onClose();
};

View File

@ -5,6 +5,7 @@ import {
Image,
MenuItem,
MenuList,
Text,
useTheme,
useToast,
} from '@chakra-ui/react';
@ -20,7 +21,14 @@ import {
setSeed,
} from 'features/parameters/store/generationSlice';
import { DragEvent, memo, useState } from 'react';
import { FaCheck, FaTrashAlt } from 'react-icons/fa';
import {
FaCheck,
FaExpand,
FaLink,
FaShare,
FaTrash,
FaTrashAlt,
} from 'react-icons/fa';
import DeleteImageModal from './DeleteImageModal';
import { ContextMenu } from 'chakra-ui-contextmenu';
import * as InvokeAI from 'app/invokeai';
@ -35,6 +43,9 @@ import useSetBothPrompts from 'features/parameters/hooks/usePrompt';
import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
import IAIIconButton from 'common/components/IAIIconButton';
import { useGetUrl } from 'common/util/getUrl';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import { BiZoomIn } from 'react-icons/bi';
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
interface HoverableImageProps {
image: InvokeAI.Image;
@ -177,15 +188,19 @@ const HoverableImage = memo((props: HoverableImageProps) => {
menuProps={{ size: 'sm', isLazy: true }}
renderMenu={() => (
<MenuList>
<MenuItem onClickCapture={handleOpenInNewTab}>
<MenuItem
icon={<ExternalLinkIcon />}
onClickCapture={handleOpenInNewTab}
>
{t('common.openInNewTab')}
</MenuItem>
{!disabledFeatures.includes('lightbox') && (
<MenuItem onClickCapture={handleLightBox}>
<MenuItem icon={<FaExpand />} onClickCapture={handleLightBox}>
{t('parameters.openInViewer')}
</MenuItem>
)}
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleUsePrompt}
isDisabled={image?.metadata?.sd_metadata?.prompt === undefined}
>
@ -193,12 +208,21 @@ const HoverableImage = memo((props: HoverableImageProps) => {
</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(
@ -209,21 +233,18 @@ const HoverableImage = memo((props: HoverableImageProps) => {
{t('parameters.useAll')}
</MenuItem>
<MenuItem
onClickCapture={handleUseInitialImage}
isDisabled={image?.metadata?.sd_metadata?.type !== 'img2img'}
icon={<FaShare />}
onClickCapture={handleSendToImageToImage}
>
{t('parameters.useInitImg')}
</MenuItem>
<MenuItem onClickCapture={handleSendToImageToImage}>
{t('parameters.sendToImg2Img')}
</MenuItem>
<MenuItem onClickCapture={handleSendToCanvas}>
<MenuItem icon={<FaShare />} onClickCapture={handleSendToCanvas}>
{t('parameters.sendToUnifiedCanvas')}
</MenuItem>
<MenuItem data-warning>
{/* <DeleteImageModal image={image}>
<p>{t('parameters.deleteImage')}</p>
</DeleteImageModal> */}
<MenuItem icon={<FaTrash />}>
<DeleteImageModal image={image}>
<Text>{t('parameters.deleteImage')}</Text>
</DeleteImageModal>
</MenuItem>
</MenuList>
)}
@ -302,15 +323,15 @@ const HoverableImage = memo((props: HoverableImageProps) => {
insetInlineEnd: 1,
}}
>
{/* <DeleteImageModal image={image}>
<DeleteImageModal image={image}>
<IAIIconButton
aria-label={t('parameters.deleteImage')}
icon={<FaTrashAlt />}
icon={<FaTrash />}
size="xs"
fontSize={14}
isDisabled={!mayDeleteImage}
/>
</DeleteImageModal> */}
</DeleteImageModal>
</Box>
)}
</Box>

View File

@ -6,8 +6,8 @@ import { InvokeTabName } from 'features/ui/store/tabMap';
import { IRect } from 'konva/lib/types';
import { clamp } from 'lodash';
import { isImageOutput } from 'services/types/guards';
import { imageUploaded } from 'services/thunks/image';
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
import { imageUploaded } from 'services/thunks/image';
export type GalleryCategory = 'user' | 'result';

View File

@ -13,7 +13,11 @@ import {
extractTimestampFromImageName,
} from 'services/util/deserializeImageField';
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
import { imageReceived, thumbnailReceived } from 'services/thunks/image';
import {
imageDeleted,
imageReceived,
thumbnailReceived,
} from 'services/thunks/image';
export const resultsAdapter = createEntityAdapter<Image>({
selectId: (image) => image.name,
@ -107,6 +111,9 @@ const resultsSlice = createSlice({
}
});
/**
* Image Received - FULFILLED
*/
builder.addCase(imageReceived.fulfilled, (state, action) => {
const { imagePath } = action.payload;
const { imageName } = action.meta.arg;
@ -119,17 +126,31 @@ const resultsSlice = createSlice({
});
});
/**
* Thumbnail Received - FULFILLED
*/
builder.addCase(thumbnailReceived.fulfilled, (state, action) => {
const { thumbnailPath } = action.payload;
const { imageName } = action.meta.arg;
const { thumbnailName } = action.meta.arg;
resultsAdapter.updateOne(state, {
id: imageName,
id: thumbnailName,
changes: {
thumbnail: thumbnailPath,
},
});
});
/**
* Delete Image - FULFILLED
*/
builder.addCase(imageDeleted.fulfilled, (state, action) => {
const { imageType, imageName } = action.meta.arg;
if (imageType === 'results') {
resultsAdapter.removeOne(state, imageName);
}
});
},
});

View File

@ -6,7 +6,7 @@ import {
receivedUploadImagesPage,
IMAGES_PER_PAGE,
} from 'services/thunks/gallery';
import { imageUploaded } from 'services/thunks/image';
import { imageDeleted, imageUploaded } from 'services/thunks/image';
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
export const uploadsAdapter = createEntityAdapter<Image>({
@ -71,6 +71,17 @@ const uploadsSlice = createSlice({
uploadsAdapter.addOne(state, uploadedImage);
});
/**
* Delete Image - FULFILLED
*/
builder.addCase(imageDeleted.fulfilled, (state, action) => {
const { imageType, imageName } = action.meta.arg;
if (imageType === 'uploads') {
uploadsAdapter.removeOne(state, imageName);
}
});
},
});

View File

@ -1,5 +1,7 @@
import { isFulfilled } from '@reduxjs/toolkit';
import { isFulfilled, isRejected } from '@reduxjs/toolkit';
import { createAppAsyncThunk } from 'app/storeUtils';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { clamp } from 'lodash';
import { ImagesService } from 'services/api';
import { getHeaders } from 'services/util/getHeaders';
@ -49,3 +51,51 @@ export const imageUploaded = createAppAsyncThunk(
* Function to check if an action is a fulfilled `ImagesService.uploadImage()` thunk
*/
export const isFulfilledImageUploadedAction = isFulfilled(imageUploaded);
type ImageDeletedArg = Parameters<(typeof ImagesService)['deleteImage']>[0];
/**
* `ImagesService.deleteImage()` thunk
*/
export const imageDeleted = createAppAsyncThunk(
'api/imageDeleted',
async (arg: ImageDeletedArg, { getState, dispatch }) => {
const { imageType, imageName } = arg;
if (imageType !== 'uploads' && imageType !== 'results') {
return;
}
// TODO: move this logic to another thunk?
// Determine which image should replace the deleted image, if the deleted image is the selected image.
// Unfortunately, we have to do this here, because the resultsSlice and uploadsSlice cannot change
// the selected image.
const selectedImageName = getState().gallery.selectedImageName;
if (selectedImageName === imageName) {
const allIds = getState()[imageType].ids;
const deletedImageIndex = allIds.findIndex(
(result) => result.toString() === imageName
);
const filteredIds = allIds.filter((id) => id.toString() !== imageName);
const newSelectedImageIndex = clamp(
deletedImageIndex,
0,
filteredIds.length - 1
);
const newSelectedImageId = filteredIds[newSelectedImageIndex];
dispatch(
imageSelected(newSelectedImageId ? newSelectedImageId.toString() : '')
);
}
const response = await ImagesService.deleteImage(arg);
return response;
}
);