mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): restore image deletion functionality
This commit is contained in:
parent
2e54da13d8
commit
75d25dd5cc
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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();
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user