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>
|
</ButtonGroup>
|
||||||
|
|
||||||
{/* <DeleteImageModal image={selectedImage}>
|
<DeleteImageModal image={selectedImage}>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
icon={<FaTrash />}
|
icon={<FaTrash />}
|
||||||
tooltip={`${t('parameters.deleteImage')} (Del)`}
|
tooltip={`${t('parameters.deleteImage')} (Del)`}
|
||||||
aria-label={`${t('parameters.deleteImage')} (Del)`}
|
aria-label={`${t('parameters.deleteImage')} (Del)`}
|
||||||
isDisabled={!selectedImage || !isConnected || isProcessing}
|
isDisabled={!selectedImage || !isConnected}
|
||||||
colorScheme="error"
|
colorScheme="error"
|
||||||
/>
|
/>
|
||||||
</DeleteImageModal> */}
|
</DeleteImageModal>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -31,6 +31,7 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { imageDeleted } from 'services/thunks/image';
|
||||||
|
|
||||||
const deleteImageModalSelector = createSelector(
|
const deleteImageModalSelector = createSelector(
|
||||||
systemSelector,
|
systemSelector,
|
||||||
@ -52,7 +53,7 @@ interface DeleteImageModalProps {
|
|||||||
/**
|
/**
|
||||||
* The image to delete.
|
* The image to delete.
|
||||||
*/
|
*/
|
||||||
image?: InvokeAI._Image;
|
image?: InvokeAI.Image;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,7 +78,9 @@ const DeleteImageModal = forwardRef(
|
|||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (isConnected && !isProcessing && image) {
|
if (isConnected && !isProcessing && image) {
|
||||||
dispatch(deleteImage(image));
|
dispatch(
|
||||||
|
imageDeleted({ imageType: image.type, imageName: image.name })
|
||||||
|
);
|
||||||
}
|
}
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
Image,
|
Image,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuList,
|
MenuList,
|
||||||
|
Text,
|
||||||
useTheme,
|
useTheme,
|
||||||
useToast,
|
useToast,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
@ -20,7 +21,14 @@ import {
|
|||||||
setSeed,
|
setSeed,
|
||||||
} from 'features/parameters/store/generationSlice';
|
} from 'features/parameters/store/generationSlice';
|
||||||
import { DragEvent, memo, useState } from 'react';
|
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 DeleteImageModal from './DeleteImageModal';
|
||||||
import { ContextMenu } from 'chakra-ui-contextmenu';
|
import { ContextMenu } from 'chakra-ui-contextmenu';
|
||||||
import * as InvokeAI from 'app/invokeai';
|
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 { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import { useGetUrl } from 'common/util/getUrl';
|
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 {
|
interface HoverableImageProps {
|
||||||
image: InvokeAI.Image;
|
image: InvokeAI.Image;
|
||||||
@ -177,15 +188,19 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
menuProps={{ size: 'sm', isLazy: true }}
|
menuProps={{ size: 'sm', isLazy: true }}
|
||||||
renderMenu={() => (
|
renderMenu={() => (
|
||||||
<MenuList>
|
<MenuList>
|
||||||
<MenuItem onClickCapture={handleOpenInNewTab}>
|
<MenuItem
|
||||||
|
icon={<ExternalLinkIcon />}
|
||||||
|
onClickCapture={handleOpenInNewTab}
|
||||||
|
>
|
||||||
{t('common.openInNewTab')}
|
{t('common.openInNewTab')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{!disabledFeatures.includes('lightbox') && (
|
{!disabledFeatures.includes('lightbox') && (
|
||||||
<MenuItem onClickCapture={handleLightBox}>
|
<MenuItem icon={<FaExpand />} onClickCapture={handleLightBox}>
|
||||||
{t('parameters.openInViewer')}
|
{t('parameters.openInViewer')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
icon={<IoArrowUndoCircleOutline />}
|
||||||
onClickCapture={handleUsePrompt}
|
onClickCapture={handleUsePrompt}
|
||||||
isDisabled={image?.metadata?.sd_metadata?.prompt === undefined}
|
isDisabled={image?.metadata?.sd_metadata?.prompt === undefined}
|
||||||
>
|
>
|
||||||
@ -193,12 +208,21 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
icon={<IoArrowUndoCircleOutline />}
|
||||||
onClickCapture={handleUseSeed}
|
onClickCapture={handleUseSeed}
|
||||||
isDisabled={image?.metadata?.sd_metadata?.seed === undefined}
|
isDisabled={image?.metadata?.sd_metadata?.seed === undefined}
|
||||||
>
|
>
|
||||||
{t('parameters.useSeed')}
|
{t('parameters.useSeed')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
icon={<IoArrowUndoCircleOutline />}
|
||||||
|
onClickCapture={handleUseInitialImage}
|
||||||
|
isDisabled={image?.metadata?.sd_metadata?.type !== 'img2img'}
|
||||||
|
>
|
||||||
|
{t('parameters.useInitImg')}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
icon={<IoArrowUndoCircleOutline />}
|
||||||
onClickCapture={handleUseAllParameters}
|
onClickCapture={handleUseAllParameters}
|
||||||
isDisabled={
|
isDisabled={
|
||||||
!['txt2img', 'img2img'].includes(
|
!['txt2img', 'img2img'].includes(
|
||||||
@ -209,21 +233,18 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
{t('parameters.useAll')}
|
{t('parameters.useAll')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClickCapture={handleUseInitialImage}
|
icon={<FaShare />}
|
||||||
isDisabled={image?.metadata?.sd_metadata?.type !== 'img2img'}
|
onClickCapture={handleSendToImageToImage}
|
||||||
>
|
>
|
||||||
{t('parameters.useInitImg')}
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem onClickCapture={handleSendToImageToImage}>
|
|
||||||
{t('parameters.sendToImg2Img')}
|
{t('parameters.sendToImg2Img')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClickCapture={handleSendToCanvas}>
|
<MenuItem icon={<FaShare />} onClickCapture={handleSendToCanvas}>
|
||||||
{t('parameters.sendToUnifiedCanvas')}
|
{t('parameters.sendToUnifiedCanvas')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem data-warning>
|
<MenuItem icon={<FaTrash />}>
|
||||||
{/* <DeleteImageModal image={image}>
|
<DeleteImageModal image={image}>
|
||||||
<p>{t('parameters.deleteImage')}</p>
|
<Text>{t('parameters.deleteImage')}</Text>
|
||||||
</DeleteImageModal> */}
|
</DeleteImageModal>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuList>
|
</MenuList>
|
||||||
)}
|
)}
|
||||||
@ -302,15 +323,15 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
insetInlineEnd: 1,
|
insetInlineEnd: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* <DeleteImageModal image={image}>
|
<DeleteImageModal image={image}>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label={t('parameters.deleteImage')}
|
aria-label={t('parameters.deleteImage')}
|
||||||
icon={<FaTrashAlt />}
|
icon={<FaTrash />}
|
||||||
size="xs"
|
size="xs"
|
||||||
fontSize={14}
|
fontSize={14}
|
||||||
isDisabled={!mayDeleteImage}
|
isDisabled={!mayDeleteImage}
|
||||||
/>
|
/>
|
||||||
</DeleteImageModal> */}
|
</DeleteImageModal>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -6,8 +6,8 @@ import { InvokeTabName } from 'features/ui/store/tabMap';
|
|||||||
import { IRect } from 'konva/lib/types';
|
import { IRect } from 'konva/lib/types';
|
||||||
import { clamp } from 'lodash';
|
import { clamp } from 'lodash';
|
||||||
import { isImageOutput } from 'services/types/guards';
|
import { isImageOutput } from 'services/types/guards';
|
||||||
import { imageUploaded } from 'services/thunks/image';
|
|
||||||
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
|
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
|
||||||
|
import { imageUploaded } from 'services/thunks/image';
|
||||||
|
|
||||||
export type GalleryCategory = 'user' | 'result';
|
export type GalleryCategory = 'user' | 'result';
|
||||||
|
|
||||||
|
@ -13,7 +13,11 @@ import {
|
|||||||
extractTimestampFromImageName,
|
extractTimestampFromImageName,
|
||||||
} from 'services/util/deserializeImageField';
|
} from 'services/util/deserializeImageField';
|
||||||
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
|
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>({
|
export const resultsAdapter = createEntityAdapter<Image>({
|
||||||
selectId: (image) => image.name,
|
selectId: (image) => image.name,
|
||||||
@ -107,6 +111,9 @@ const resultsSlice = createSlice({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image Received - FULFILLED
|
||||||
|
*/
|
||||||
builder.addCase(imageReceived.fulfilled, (state, action) => {
|
builder.addCase(imageReceived.fulfilled, (state, action) => {
|
||||||
const { imagePath } = action.payload;
|
const { imagePath } = action.payload;
|
||||||
const { imageName } = action.meta.arg;
|
const { imageName } = action.meta.arg;
|
||||||
@ -119,17 +126,31 @@ const resultsSlice = createSlice({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thumbnail Received - FULFILLED
|
||||||
|
*/
|
||||||
builder.addCase(thumbnailReceived.fulfilled, (state, action) => {
|
builder.addCase(thumbnailReceived.fulfilled, (state, action) => {
|
||||||
const { thumbnailPath } = action.payload;
|
const { thumbnailPath } = action.payload;
|
||||||
const { imageName } = action.meta.arg;
|
const { thumbnailName } = action.meta.arg;
|
||||||
|
|
||||||
resultsAdapter.updateOne(state, {
|
resultsAdapter.updateOne(state, {
|
||||||
id: imageName,
|
id: thumbnailName,
|
||||||
changes: {
|
changes: {
|
||||||
thumbnail: thumbnailPath,
|
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,
|
receivedUploadImagesPage,
|
||||||
IMAGES_PER_PAGE,
|
IMAGES_PER_PAGE,
|
||||||
} from 'services/thunks/gallery';
|
} from 'services/thunks/gallery';
|
||||||
import { imageUploaded } from 'services/thunks/image';
|
import { imageDeleted, imageUploaded } from 'services/thunks/image';
|
||||||
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
|
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
|
||||||
|
|
||||||
export const uploadsAdapter = createEntityAdapter<Image>({
|
export const uploadsAdapter = createEntityAdapter<Image>({
|
||||||
@ -71,6 +71,17 @@ const uploadsSlice = createSlice({
|
|||||||
|
|
||||||
uploadsAdapter.addOne(state, uploadedImage);
|
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 { createAppAsyncThunk } from 'app/storeUtils';
|
||||||
|
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||||
|
import { clamp } from 'lodash';
|
||||||
import { ImagesService } from 'services/api';
|
import { ImagesService } from 'services/api';
|
||||||
import { getHeaders } from 'services/util/getHeaders';
|
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
|
* Function to check if an action is a fulfilled `ImagesService.uploadImage()` thunk
|
||||||
*/
|
*/
|
||||||
export const isFulfilledImageUploadedAction = isFulfilled(imageUploaded);
|
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