diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 67d0091261..bb2f140716 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -21,7 +21,7 @@ import { ReactNode, memo, useCallback, useEffect, useState } from 'react'; import { APP_HEIGHT, APP_WIDTH } from 'theme/util/constants'; import GlobalHotkeys from './GlobalHotkeys'; import Toaster from './Toaster'; -import DeleteModal from 'features/gallery/components/DeleteModal'; +import DeleteImageModal from 'features/gallery/components/DeleteImageModal'; const DEFAULT_CONFIG = {}; @@ -134,7 +134,7 @@ const App = ({ - + diff --git a/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx b/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx index 1d129d4e00..2f2bc4625b 100644 --- a/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx +++ b/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx @@ -7,6 +7,12 @@ import { systemSelector } from 'features/system/store/systemSelectors'; import { PropsWithChildren, createContext, useCallback, useState } from 'react'; import { ImageDTO } from 'services/api'; +import { useImageUsage } from 'common/hooks/useImageUsage'; +import { resetCanvas } from 'features/canvas/store/canvasSlice'; +import { controlNetReset } from 'features/controlNet/store/controlNetSlice'; +import { clearInitialImage } from 'features/parameters/store/generationSlice'; +import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; + type DeleteImageContextValue = { /** * Whether the delete image dialog is open. @@ -16,16 +22,20 @@ type DeleteImageContextValue = { * Closes the delete image dialog. */ onClose: () => void; + /** + * Opens the delete image dialog and handles all deletion-related checks. + */ + onDelete: (image?: ImageDTO) => void; + /** + * The image pending deletion + */ + image?: ImageDTO; /** * 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({ @@ -43,8 +53,6 @@ const selector = createSelector( return { canDeleteImage: isConnected && !isProcessing, shouldConfirmOnDelete, - isProcessing, - isConnected, }; }, defaultSelectorOptions @@ -57,6 +65,35 @@ export const DeleteImageContextProvider = (props: Props) => { const [imageToDelete, setImageToDelete] = useState(); const dispatch = useAppDispatch(); const { isOpen, onOpen, onClose } = useDisclosure(); + const imageUsage = useImageUsage(imageToDelete?.image_name); + + const handleActualDeletion = useCallback( + (image: ImageDTO) => { + dispatch(requestedImageDeletion(image)); + + if (imageUsage.isCanvasImage) { + dispatch(resetCanvas()); + } + + if (imageUsage.isControlNetImage) { + dispatch(controlNetReset()); + } + + if (imageUsage.isInitialImage) { + dispatch(clearInitialImage()); + } + + if (imageUsage.isControlNetImage) { + dispatch(nodeEditorReset()); + } + }, + [ + dispatch, + imageUsage.isCanvasImage, + imageUsage.isControlNetImage, + imageUsage.isInitialImage, + ] + ); const closeAndClearImageToDelete = useCallback(() => { setImageToDelete(undefined); @@ -65,20 +102,25 @@ export const DeleteImageContextProvider = (props: Props) => { const onImmediatelyDelete = useCallback(() => { if (canDeleteImage && imageToDelete) { - dispatch(requestedImageDeletion(imageToDelete)); + handleActualDeletion(imageToDelete); } closeAndClearImageToDelete(); - }, [canDeleteImage, imageToDelete, closeAndClearImageToDelete, dispatch]); + }, [ + canDeleteImage, + imageToDelete, + closeAndClearImageToDelete, + handleActualDeletion, + ]); - const handleDelete = useCallback( + const handleGatedDeletion = useCallback( (image: ImageDTO) => { - if (shouldConfirmOnDelete) { + if (shouldConfirmOnDelete || imageUsage) { onOpen(); } else { - dispatch(requestedImageDeletion(image)); + handleActualDeletion(image); } }, - [shouldConfirmOnDelete, onOpen, dispatch] + [shouldConfirmOnDelete, imageUsage, onOpen, handleActualDeletion] ); const onDelete = useCallback( @@ -87,15 +129,16 @@ export const DeleteImageContextProvider = (props: Props) => { return; } setImageToDelete(image); - handleDelete(image); + handleGatedDeletion(image); }, - [handleDelete] + [handleGatedDeletion] ); return ( image_name, + (state: RootState, image_name?: string) => image_name, ], (generation, canvas, nodes, controlNet, image_name) => { const isInitialImage = generation.initialImage?.image_name === image_name; @@ -37,18 +45,22 @@ const selectIsImageInUse = createSelector( c.processedControlImage?.image_name === image_name ); - return { + const imageUsage: ImageUsage = { isInitialImage, isCanvasImage, isNodesImage, isControlNetImage, }; + + return imageUsage; }, defaultSelectorOptions ); -export const useGetIsImageInUse = (image_name?: string) => { - const a = useAppSelector((state) => selectIsImageInUse(state, image_name)); +export const useImageUsage = (image_name?: string) => { + const imageUsage = useAppSelector((state) => + selectImageUsage(state, image_name) + ); - return a; + return imageUsage; }; diff --git a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts index da76ce4a8a..92d6c302e9 100644 --- a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts +++ b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts @@ -187,6 +187,9 @@ export const controlNetSlice = createSlice({ processorType ].default as RequiredControlNetProcessorNode; }, + controlNetReset: () => { + return { ...initialControlNetState }; + }, }, extraReducers: (builder) => { builder.addCase(controlNetImageProcessed, (state, action) => { @@ -243,6 +246,7 @@ export const { controlNetEndStepPctChanged, controlNetProcessorParamsChanged, controlNetProcessorTypeChanged, + controlNetReset, } = controlNetSlice.actions; export default controlNetSlice.reducer; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx index 333ad516ef..a5eaeb4c71 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx @@ -50,7 +50,7 @@ import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/U import { useAppToaster } from 'app/components/Toaster'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { DeleteImageContext } from 'app/contexts/DeleteImageContext'; -import { DeleteImageButton } from './DeleteModal'; +import { DeleteImageButton } from './DeleteImageModal'; const currentImageButtonsSelector = createSelector( [ diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx index b8d9d6220a..5e210bf4b7 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx @@ -15,7 +15,6 @@ import { imageSelected } from '../store/gallerySlice'; import IAIDndImage from 'common/components/IAIDndImage'; import { ImageDTO } from 'services/api'; import { IAIImageFallback } from 'common/components/IAIImageFallback'; -import { useGetIsImageInUse } from 'common/hooks/useGetIsImageInUse'; export const imagesSelector = createSelector( [uiSelector, gallerySelector, systemSelector], @@ -55,8 +54,6 @@ const CurrentImagePreview = () => { const toaster = useAppToaster(); const dispatch = useAppDispatch(); - const isImageInUse = useGetIsImageInUse(image?.image_name); - console.log(isImageInUse); const handleError = useCallback(() => { dispatch(imageSelected()); if (shouldFetchImages) { diff --git a/invokeai/frontend/web/src/features/gallery/components/DeleteModal.tsx b/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx similarity index 68% rename from invokeai/frontend/web/src/features/gallery/components/DeleteModal.tsx rename to invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx index ca06aa7953..335944df43 100644 --- a/invokeai/frontend/web/src/features/gallery/components/DeleteModal.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx @@ -5,19 +5,24 @@ import { AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, + Divider, Flex, + ListItem, Text, + UnorderedList, } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { DeleteImageContext } from 'app/contexts/DeleteImageContext'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import IAIButton from 'common/components/IAIButton'; import IAIIconButton from 'common/components/IAIIconButton'; import IAISwitch from 'common/components/IAISwitch'; +import { ImageUsage, useImageUsage } from 'common/hooks/useImageUsage'; import { configSelector } from 'features/system/store/configSelectors'; import { systemSelector } from 'features/system/store/systemSelectors'; import { setShouldConfirmOnDelete } from 'features/system/store/systemSlice'; -import { isEqual } from 'lodash-es'; +import { some } from 'lodash-es'; import { ChangeEvent, memo, useCallback, useContext, useRef } from 'react'; import { useTranslation } from 'react-i18next'; @@ -28,31 +33,56 @@ const selector = createSelector( (system, config) => { const { shouldConfirmOnDelete } = system; const { canRestoreDeletedImagesFromBin } = config; - return { shouldConfirmOnDelete, canRestoreDeletedImagesFromBin }; + + return { + shouldConfirmOnDelete, + canRestoreDeletedImagesFromBin, + }; }, - { - memoizeOptions: { - resultEqualityCheck: isEqual, - }, - } + defaultSelectorOptions ); +const ImageInUseMessage = (props: { imageUsage: ImageUsage }) => { + const { imageUsage } = props; + + if (!some(imageUsage)) { + return null; + } + + return ( + <> + This image is currently in use in the following features: + + {imageUsage.isInitialImage && Image to Image} + {imageUsage.isCanvasImage && Unified Canvas} + {imageUsage.isControlNetImage && ControlNet} + {imageUsage.isNodesImage && Node Editor} + + + If you delete this image, those features will immediately be reset. + + + ); +}; + const DeleteImageModal = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); + const { isOpen, onClose, onImmediatelyDelete, image } = + useContext(DeleteImageContext); + const { shouldConfirmOnDelete, canRestoreDeletedImagesFromBin } = useAppSelector(selector); + const imageUsage = useImageUsage(image?.image_name); + const handleChangeShouldConfirmOnDelete = useCallback( (e: ChangeEvent) => dispatch(setShouldConfirmOnDelete(!e.target.checked)), [dispatch] ); - const { isOpen, onClose, onImmediatelyDelete } = - useContext(DeleteImageContext); - const cancelRef = useRef(null); return ( @@ -69,15 +99,15 @@ const DeleteImageModal = () => { - - - {t('common.areYouSure')} - - {canRestoreDeletedImagesFromBin - ? t('gallery.deleteImageBin') - : t('gallery.deleteImagePermanent')} - - + + + + + {canRestoreDeletedImagesFromBin + ? t('gallery.deleteImageBin') + : t('gallery.deleteImagePermanent')} + + {t('common.areYouSure')} { + return { ...initialNodesState }; + }, }, extraReducers(builder) { builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => { @@ -127,6 +130,7 @@ export const { connectionEnded, shouldShowGraphOverlayChanged, parsedOpenAPISchema, + nodeEditorReset, } = nodesSlice.actions; export default nodesSlice.reducer; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx index 73efb69728..c006215256 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx @@ -14,7 +14,6 @@ import { useAppToaster } from 'app/components/Toaster'; import IAIDndImage from 'common/components/IAIDndImage'; import { ImageDTO } from 'services/api'; import { IAIImageFallback } from 'common/components/IAIImageFallback'; -import { useGetIsImageInUse } from 'common/hooks/useGetIsImageInUse'; const selector = createSelector( [generationSelector],