mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
refactor(ui): async modal pattern; use for deleting images
This was needed for a canvas flow change which is currently paused, but the new API is much much nicer to use, so I am keeping it.
This commit is contained in:
@@ -6,7 +6,7 @@ import {
|
||||
NewGallerySessionDialog,
|
||||
} from 'features/controlLayers/components/NewSessionConfirmationAlertDialog';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
|
||||
import { DeleteImageModal } from 'features/deleteImageModal/components/DeleteImageModal';
|
||||
import { FullscreenDropzone } from 'features/dnd/FullscreenDropzone';
|
||||
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
|
||||
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
|
||||
|
||||
@@ -9,15 +9,14 @@ import { addDeleteBoardAndImagesFulfilledListener } from 'app/store/middleware/l
|
||||
import { addBoardIdSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/boardIdSelected';
|
||||
import { addBulkDownloadListeners } from 'app/store/middleware/listenerMiddleware/listeners/bulkDownload';
|
||||
import { addEnqueueRequestedLinear } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear';
|
||||
import { addEnsureImageIsSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/ensureImageIsSelectedListener';
|
||||
import { addGalleryImageClickedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked';
|
||||
import { addGalleryOffsetChangedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryOffsetChanged';
|
||||
import { addGetOpenAPISchemaListener } from 'app/store/middleware/listenerMiddleware/listeners/getOpenAPISchema';
|
||||
import { addImageAddedToBoardFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageAddedToBoard';
|
||||
import { addImageDeletionListeners } from 'app/store/middleware/listenerMiddleware/listeners/imageDeletionListeners';
|
||||
import { addImageRemovedFromBoardFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageRemovedFromBoard';
|
||||
import { addImagesStarredListener } from 'app/store/middleware/listenerMiddleware/listeners/imagesStarred';
|
||||
import { addImagesUnstarredListener } from 'app/store/middleware/listenerMiddleware/listeners/imagesUnstarred';
|
||||
import { addImageToDeleteSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/imageToDeleteSelected';
|
||||
import { addImageUploadedFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/imageUploaded';
|
||||
import { addModelSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelSelected';
|
||||
import { addModelsLoadedListener } from 'app/store/middleware/listenerMiddleware/listeners/modelsLoaded';
|
||||
@@ -46,9 +45,7 @@ export const addAppListener = addListener.withTypes<RootState, AppDispatch>();
|
||||
addImageUploadedFulfilledListener(startAppListening);
|
||||
|
||||
// Image deleted
|
||||
addImageDeletionListeners(startAppListening);
|
||||
addDeleteBoardAndImagesFulfilledListener(startAppListening);
|
||||
addImageToDeleteSelectedListener(startAppListening);
|
||||
|
||||
// Image starred
|
||||
addImagesStarredListener(startAppListening);
|
||||
@@ -91,3 +88,5 @@ addAppConfigReceivedListener(startAppListening);
|
||||
addAdHocPostProcessingRequestedListener(startAppListening);
|
||||
|
||||
addSetDefaultSettingsListener(startAppListening);
|
||||
|
||||
addEnsureImageIsSelectedListener(startAppListening);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { getImageUsage } from 'features/deleteImageModal/store/selectors';
|
||||
import { getImageUsage } from 'features/deleteImageModal/store/state';
|
||||
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
|
||||
export const addEnsureImageIsSelectedListener = (startAppListening: AppStartListening) => {
|
||||
// When we list images, if no images is selected, select the first one.
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.listImages.matchFulfilled,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const selection = getState().gallery.selection;
|
||||
if (selection.length === 0) {
|
||||
dispatch(imageSelected(action.payload.items[0] ?? null));
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,221 +0,0 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import type { AppDispatch, RootState } from 'app/store/store';
|
||||
import { entityDeleted, referenceImageIPAdapterImageChanged } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
|
||||
import { isModalOpenChanged } from 'features/deleteImageModal/store/slice';
|
||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { fieldImageCollectionValueChanged, fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { isImageFieldCollectionInputInstance, isImageFieldInputInstance } from 'features/nodes/types/field';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { forEach, intersectionBy } from 'lodash-es';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import type { Param0 } from 'tsafe';
|
||||
|
||||
const log = logger('gallery');
|
||||
|
||||
//TODO(psyche): handle image deletion (canvas staging area?)
|
||||
|
||||
// Some utils to delete images from different parts of the app
|
||||
const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||
const actions: Param0<typeof dispatch>[] = [];
|
||||
state.nodes.present.nodes.forEach((node) => {
|
||||
if (!isInvocationNode(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
forEach(node.data.inputs, (input) => {
|
||||
if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) {
|
||||
actions.push(
|
||||
fieldImageValueChanged({
|
||||
nodeId: node.data.id,
|
||||
fieldName: input.name,
|
||||
value: undefined,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isImageFieldCollectionInputInstance(input)) {
|
||||
actions.push(
|
||||
fieldImageCollectionValueChanged({
|
||||
nodeId: node.data.id,
|
||||
fieldName: input.name,
|
||||
value: input.value?.filter((value) => value?.image_name !== imageDTO.image_name),
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
actions.forEach(dispatch);
|
||||
};
|
||||
|
||||
const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||
selectCanvasSlice(state).controlLayers.entities.forEach(({ id, objects }) => {
|
||||
let shouldDelete = false;
|
||||
for (const obj of objects) {
|
||||
if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === imageDTO.image_name) {
|
||||
shouldDelete = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (shouldDelete) {
|
||||
dispatch(entityDeleted({ entityIdentifier: { id, type: 'control_layer' } }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const deleteReferenceImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||
selectCanvasSlice(state).referenceImages.entities.forEach((entity) => {
|
||||
if (entity.ipAdapter.image?.image_name === imageDTO.image_name) {
|
||||
dispatch(referenceImageIPAdapterImageChanged({ entityIdentifier: getEntityIdentifier(entity), imageDTO: null }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const deleteRasterLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||
selectCanvasSlice(state).rasterLayers.entities.forEach(({ id, objects }) => {
|
||||
let shouldDelete = false;
|
||||
for (const obj of objects) {
|
||||
if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === imageDTO.image_name) {
|
||||
shouldDelete = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (shouldDelete) {
|
||||
dispatch(entityDeleted({ entityIdentifier: { id, type: 'raster_layer' } }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const addImageDeletionListeners = (startAppListening: AppStartListening) => {
|
||||
// Handle single image deletion
|
||||
startAppListening({
|
||||
actionCreator: imageDeletionConfirmed,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
const { imageDTOs, imagesUsage } = action.payload;
|
||||
|
||||
if (imageDTOs.length !== 1 || imagesUsage.length !== 1) {
|
||||
// handle multiples in separate listener
|
||||
return;
|
||||
}
|
||||
|
||||
const imageDTO = imageDTOs[0];
|
||||
const imageUsage = imagesUsage[0];
|
||||
|
||||
if (!imageDTO || !imageUsage) {
|
||||
// satisfy noUncheckedIndexedAccess
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const state = getState();
|
||||
await dispatch(imagesApi.endpoints.deleteImage.initiate(imageDTO)).unwrap();
|
||||
|
||||
if (state.gallery.selection.some((i) => i.image_name === imageDTO.image_name)) {
|
||||
// The deleted image was a selected image, we need to select the next image
|
||||
const newSelection = state.gallery.selection.filter((i) => i.image_name !== imageDTO.image_name);
|
||||
|
||||
if (newSelection.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current list of images and select the same index
|
||||
const baseQueryArgs = selectListImagesQueryArgs(state);
|
||||
const data = imagesApi.endpoints.listImages.select(baseQueryArgs)(state).data;
|
||||
|
||||
if (data) {
|
||||
const deletedImageIndex = data.items.findIndex((i) => i.image_name === imageDTO.image_name);
|
||||
const nextImage = data.items[deletedImageIndex + 1] ?? data.items[0] ?? null;
|
||||
if (nextImage?.image_name === imageDTO.image_name) {
|
||||
// If the next image is the same as the deleted one, it means it was the last image, reset selection
|
||||
dispatch(imageSelected(null));
|
||||
} else {
|
||||
dispatch(imageSelected(nextImage));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deleteNodesImages(state, dispatch, imageDTO);
|
||||
deleteReferenceImages(state, dispatch, imageDTO);
|
||||
deleteRasterLayerImages(state, dispatch, imageDTO);
|
||||
deleteControlLayerImages(state, dispatch, imageDTO);
|
||||
} catch {
|
||||
// no-op
|
||||
} finally {
|
||||
dispatch(isModalOpenChanged(false));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Handle multiple image deletion
|
||||
startAppListening({
|
||||
actionCreator: imageDeletionConfirmed,
|
||||
effect: async (action, { dispatch, getState }) => {
|
||||
const { imageDTOs, imagesUsage } = action.payload;
|
||||
|
||||
if (imageDTOs.length <= 1 || imagesUsage.length <= 1) {
|
||||
// handle singles in separate listener
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const state = getState();
|
||||
await dispatch(imagesApi.endpoints.deleteImages.initiate({ imageDTOs })).unwrap();
|
||||
|
||||
if (intersectionBy(state.gallery.selection, imageDTOs, 'image_name').length > 0) {
|
||||
// Some selected images were deleted, need to select the next image
|
||||
const queryArgs = selectListImagesQueryArgs(state);
|
||||
const { data } = imagesApi.endpoints.listImages.select(queryArgs)(state);
|
||||
if (data) {
|
||||
// When we delete multiple images, we clear the selection. Then, the the next time we load images, we will
|
||||
// select the first one. This is handled below in the listener for `imagesApi.endpoints.listImages.matchFulfilled`.
|
||||
dispatch(imageSelected(null));
|
||||
}
|
||||
}
|
||||
|
||||
// We need to reset the features where the image is in use - none of these work if their image(s) don't exist
|
||||
|
||||
imageDTOs.forEach((imageDTO) => {
|
||||
deleteNodesImages(state, dispatch, imageDTO);
|
||||
deleteControlLayerImages(state, dispatch, imageDTO);
|
||||
deleteReferenceImages(state, dispatch, imageDTO);
|
||||
deleteRasterLayerImages(state, dispatch, imageDTO);
|
||||
});
|
||||
} catch {
|
||||
// no-op
|
||||
} finally {
|
||||
dispatch(isModalOpenChanged(false));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// When we list images, if no images is selected, select the first one.
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.listImages.matchFulfilled,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const selection = getState().gallery.selection;
|
||||
if (selection.length === 0) {
|
||||
dispatch(imageSelected(action.payload.items[0] ?? null));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.deleteImage.matchFulfilled,
|
||||
effect: (action) => {
|
||||
log.debug({ imageDTO: action.meta.arg.originalArgs }, 'Image deleted');
|
||||
},
|
||||
});
|
||||
|
||||
startAppListening({
|
||||
matcher: imagesApi.endpoints.deleteImage.matchRejected,
|
||||
effect: (action) => {
|
||||
log.debug({ imageDTO: action.meta.arg.originalArgs }, 'Unable to delete image');
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
|
||||
import { selectImageUsage } from 'features/deleteImageModal/store/selectors';
|
||||
import { imagesToDeleteSelected, isModalOpenChanged } from 'features/deleteImageModal/store/slice';
|
||||
|
||||
export const addImageToDeleteSelectedListener = (startAppListening: AppStartListening) => {
|
||||
startAppListening({
|
||||
actionCreator: imagesToDeleteSelected,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const imageDTOs = action.payload;
|
||||
const state = getState();
|
||||
const { shouldConfirmOnDelete } = state.system;
|
||||
const imagesUsage = selectImageUsage(getState());
|
||||
|
||||
const isImageInUse =
|
||||
imagesUsage.some((i) => i.isRasterLayerImage) ||
|
||||
imagesUsage.some((i) => i.isControlLayerImage) ||
|
||||
imagesUsage.some((i) => i.isReferenceImage) ||
|
||||
imagesUsage.some((i) => i.isInpaintMaskImage) ||
|
||||
imagesUsage.some((i) => i.isUpscaleImage) ||
|
||||
imagesUsage.some((i) => i.isNodesImage) ||
|
||||
imagesUsage.some((i) => i.isRegionalGuidanceImage);
|
||||
|
||||
if (shouldConfirmOnDelete || isImageInUse) {
|
||||
dispatch(isModalOpenChanged(true));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(imageDeletionConfirmed({ imageDTOs, imagesUsage }));
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { lorasPersistConfig, lorasSlice } from 'features/controlLayers/store/lorasSlice';
|
||||
import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||
import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice';
|
||||
import { dynamicPromptsPersistConfig, dynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||
import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import { hrfPersistConfig, hrfSlice } from 'features/hrf/store/hrfSlice';
|
||||
@@ -54,7 +53,6 @@ const allReducers = {
|
||||
[configSlice.name]: configSlice.reducer,
|
||||
[uiSlice.name]: uiSlice.reducer,
|
||||
[dynamicPromptsSlice.name]: dynamicPromptsSlice.reducer,
|
||||
[deleteImageModalSlice.name]: deleteImageModalSlice.reducer,
|
||||
[changeBoardModalSlice.name]: changeBoardModalSlice.reducer,
|
||||
[modelManagerV2Slice.name]: modelManagerV2Slice.reducer,
|
||||
[queueSlice.name]: queueSlice.reducer,
|
||||
|
||||
@@ -1,98 +1,38 @@
|
||||
import { ConfirmationAlertDialog, Divider, Flex, FormControl, FormLabel, Switch, Text } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { imageDeletionConfirmed } from 'features/deleteImageModal/store/actions';
|
||||
import { getImageUsage, selectImageUsage } from 'features/deleteImageModal/store/selectors';
|
||||
import {
|
||||
imageDeletionCanceled,
|
||||
isModalOpenChanged,
|
||||
selectDeleteImageModalSlice,
|
||||
} from 'features/deleteImageModal/store/slice';
|
||||
import type { ImageUsage } from 'features/deleteImageModal/store/types';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
|
||||
import { selectSystemSlice, setShouldConfirmOnDelete } from 'features/system/store/systemSlice';
|
||||
import { some } from 'lodash-es';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage';
|
||||
import { useDeleteImageModalApi, useDeleteImageModalState } from 'features/deleteImageModal/store/state';
|
||||
import { selectSystemShouldConfirmOnDelete, setShouldConfirmOnDelete } from 'features/system/store/systemSlice';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ImageUsageMessage from './ImageUsageMessage';
|
||||
|
||||
const selectImageUsages = createMemoizedSelector(
|
||||
[selectDeleteImageModalSlice, selectNodesSlice, selectCanvasSlice, selectImageUsage, selectUpscaleSlice],
|
||||
(deleteImageModal, nodes, canvas, imagesUsage, upscale) => {
|
||||
const { imagesToDelete } = deleteImageModal;
|
||||
|
||||
const allImageUsage = (imagesToDelete ?? []).map(({ image_name }) =>
|
||||
getImageUsage(nodes, canvas, upscale, image_name)
|
||||
);
|
||||
|
||||
const imageUsageSummary: ImageUsage = {
|
||||
isUpscaleImage: some(allImageUsage, (i) => i.isUpscaleImage),
|
||||
isRasterLayerImage: some(allImageUsage, (i) => i.isRasterLayerImage),
|
||||
isInpaintMaskImage: some(allImageUsage, (i) => i.isInpaintMaskImage),
|
||||
isRegionalGuidanceImage: some(allImageUsage, (i) => i.isRegionalGuidanceImage),
|
||||
isNodesImage: some(allImageUsage, (i) => i.isNodesImage),
|
||||
isControlLayerImage: some(allImageUsage, (i) => i.isControlLayerImage),
|
||||
isReferenceImage: some(allImageUsage, (i) => i.isReferenceImage),
|
||||
};
|
||||
|
||||
return {
|
||||
imagesToDelete,
|
||||
imagesUsage,
|
||||
imageUsageSummary,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const selectShouldConfirmOnDelete = createSelector(selectSystemSlice, (system) => system.shouldConfirmOnDelete);
|
||||
const selectIsModalOpen = createSelector(
|
||||
selectDeleteImageModalSlice,
|
||||
(deleteImageModal) => deleteImageModal.isModalOpen
|
||||
);
|
||||
|
||||
const DeleteImageModal = () => {
|
||||
useAssertSingleton('DeleteImageModal');
|
||||
const dispatch = useAppDispatch();
|
||||
export const DeleteImageModal = memo(() => {
|
||||
const state = useDeleteImageModalState();
|
||||
const api = useDeleteImageModalApi();
|
||||
const { dispatch } = useAppStore();
|
||||
const { t } = useTranslation();
|
||||
const shouldConfirmOnDelete = useAppSelector(selectShouldConfirmOnDelete);
|
||||
const isModalOpen = useAppSelector(selectIsModalOpen);
|
||||
const { imagesToDelete, imagesUsage, imageUsageSummary } = useAppSelector(selectImageUsages);
|
||||
const shouldConfirmOnDelete = useAppSelector(selectSystemShouldConfirmOnDelete);
|
||||
|
||||
const handleChangeShouldConfirmOnDelete = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(setShouldConfirmOnDelete(!e.target.checked)),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
dispatch(imageDeletionCanceled());
|
||||
dispatch(isModalOpenChanged(false));
|
||||
}, [dispatch]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
if (!imagesToDelete.length || !imagesUsage.length) {
|
||||
return;
|
||||
}
|
||||
dispatch(imageDeletionCanceled());
|
||||
dispatch(imageDeletionConfirmed({ imageDTOs: imagesToDelete, imagesUsage }));
|
||||
}, [dispatch, imagesToDelete, imagesUsage]);
|
||||
|
||||
return (
|
||||
<ConfirmationAlertDialog
|
||||
title={t('gallery.deleteImage', { count: imagesToDelete.length })}
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleClose}
|
||||
title={`${t('gallery.deleteImage', { count: state.imageDTOs.length })}2`}
|
||||
isOpen={state.isOpen}
|
||||
onClose={api.close}
|
||||
cancelButtonText={t('common.cancel')}
|
||||
acceptButtonText={t('common.delete')}
|
||||
acceptCallback={handleDelete}
|
||||
acceptCallback={api.confirm}
|
||||
cancelCallback={api.cancel}
|
||||
useInert={false}
|
||||
>
|
||||
<Flex direction="column" gap={3}>
|
||||
<ImageUsageMessage imageUsage={imageUsageSummary} />
|
||||
<ImageUsageMessage imageUsage={state.usageSummary} />
|
||||
<Divider />
|
||||
<Text>{t('gallery.deleteImagePermanent')}</Text>
|
||||
<Text>{t('common.areYouSure')}</Text>
|
||||
@@ -103,6 +43,5 @@ const DeleteImageModal = () => {
|
||||
</Flex>
|
||||
</ConfirmationAlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(DeleteImageModal);
|
||||
});
|
||||
DeleteImageModal.displayName = 'DeleteImageModal';
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
import type { ImageUsage } from './types';
|
||||
|
||||
export const imageDeletionConfirmed = createAction<{
|
||||
imageDTOs: ImageDTO[];
|
||||
imagesUsage: ImageUsage[];
|
||||
}>('deleteImageModal/imageDeletionConfirmed');
|
||||
@@ -1,6 +0,0 @@
|
||||
import type { DeleteImageState } from './types';
|
||||
|
||||
export const initialDeleteImageState: DeleteImageState = {
|
||||
imagesToDelete: [],
|
||||
isModalOpen: false,
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasState } from 'features/controlLayers/store/types';
|
||||
import { selectDeleteImageModalSlice } from 'features/deleteImageModal/store/slice';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import type { NodesState } from 'features/nodes/store/types';
|
||||
import { isImageFieldCollectionInputInstance, isImageFieldInputInstance } from 'features/nodes/types/field';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import type { UpscaleState } from 'features/parameters/store/upscaleSlice';
|
||||
import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
|
||||
import { some } from 'lodash-es';
|
||||
|
||||
import type { ImageUsage } from './types';
|
||||
// TODO(psyche): handle image deletion (canvas staging area?)
|
||||
export const getImageUsage = (nodes: NodesState, canvas: CanvasState, upscale: UpscaleState, image_name: string) => {
|
||||
const isNodesImage = nodes.nodes.filter(isInvocationNode).some((node) =>
|
||||
some(node.data.inputs, (input) => {
|
||||
if (isImageFieldInputInstance(input)) {
|
||||
if (input.value?.image_name === image_name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isImageFieldCollectionInputInstance(input)) {
|
||||
if (input.value?.some((value) => value?.image_name === image_name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
);
|
||||
|
||||
const isUpscaleImage = upscale.upscaleInitialImage?.image_name === image_name;
|
||||
|
||||
const isReferenceImage = canvas.referenceImages.entities.some(
|
||||
({ ipAdapter }) => ipAdapter.image?.image_name === image_name
|
||||
);
|
||||
|
||||
const isRasterLayerImage = canvas.rasterLayers.entities.some(({ objects }) =>
|
||||
objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name)
|
||||
);
|
||||
|
||||
const isControlLayerImage = canvas.controlLayers.entities.some(({ objects }) =>
|
||||
objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name)
|
||||
);
|
||||
|
||||
const isInpaintMaskImage = canvas.inpaintMasks.entities.some(({ objects }) =>
|
||||
objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name)
|
||||
);
|
||||
|
||||
const isRegionalGuidanceImage = canvas.regionalGuidance.entities.some(({ referenceImages }) =>
|
||||
referenceImages.some(({ ipAdapter }) => ipAdapter.image?.image_name === image_name)
|
||||
);
|
||||
|
||||
const imageUsage: ImageUsage = {
|
||||
isUpscaleImage,
|
||||
isRasterLayerImage,
|
||||
isInpaintMaskImage,
|
||||
isRegionalGuidanceImage,
|
||||
isNodesImage,
|
||||
isControlLayerImage,
|
||||
isReferenceImage,
|
||||
};
|
||||
|
||||
return imageUsage;
|
||||
};
|
||||
|
||||
export const selectImageUsage = createMemoizedSelector(
|
||||
selectDeleteImageModalSlice,
|
||||
selectNodesSlice,
|
||||
selectCanvasSlice,
|
||||
selectUpscaleSlice,
|
||||
(deleteImageModal, nodes, canvas, upscale) => {
|
||||
const { imagesToDelete } = deleteImageModal;
|
||||
|
||||
if (!imagesToDelete.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const imagesUsage = imagesToDelete.map((i) => getImageUsage(nodes, canvas, upscale, i.image_name));
|
||||
|
||||
return imagesUsage;
|
||||
}
|
||||
);
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { RootState } from 'app/store/store';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
import { initialDeleteImageState } from './initialState';
|
||||
|
||||
export const deleteImageModalSlice = createSlice({
|
||||
name: 'deleteImageModal',
|
||||
initialState: initialDeleteImageState,
|
||||
reducers: {
|
||||
isModalOpenChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.isModalOpen = action.payload;
|
||||
},
|
||||
imagesToDeleteSelected: (state, action: PayloadAction<ImageDTO[]>) => {
|
||||
state.imagesToDelete = action.payload;
|
||||
},
|
||||
imageDeletionCanceled: (state) => {
|
||||
state.imagesToDelete = [];
|
||||
state.isModalOpen = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { isModalOpenChanged, imagesToDeleteSelected, imageDeletionCanceled } = deleteImageModalSlice.actions;
|
||||
|
||||
export const selectDeleteImageModalSlice = (state: RootState) => state.deleteImageModal;
|
||||
@@ -0,0 +1,298 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { getStore, useAppStore } from 'app/store/nanostores/store';
|
||||
import type { AppDispatch, AppGetState, RootState } from 'app/store/store';
|
||||
import { entityDeleted, referenceImageIPAdapterImageChanged } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { type CanvasState, getEntityIdentifier } from 'features/controlLayers/store/types';
|
||||
import type { ImageUsage } from 'features/deleteImageModal/store/types';
|
||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { fieldImageCollectionValueChanged, fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import type { NodesState } from 'features/nodes/store/types';
|
||||
import { isImageFieldCollectionInputInstance, isImageFieldInputInstance } from 'features/nodes/types/field';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { selectUpscaleSlice, type UpscaleState } from 'features/parameters/store/upscaleSlice';
|
||||
import { selectSystemShouldConfirmOnDelete } from 'features/system/store/systemSlice';
|
||||
import { forEach, intersectionBy, some } from 'lodash-es';
|
||||
import { atom } from 'nanostores';
|
||||
import { useMemo } from 'react';
|
||||
import { imagesApi } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
import type { Param0 } from 'tsafe';
|
||||
|
||||
// Implements an awaitable modal dialog for deleting images
|
||||
|
||||
type DeleteImagesModalState = {
|
||||
imageDTOs: ImageDTO[];
|
||||
usagePerImage: ImageUsage[];
|
||||
usageSummary: ImageUsage;
|
||||
isOpen: boolean;
|
||||
resolve?: () => void;
|
||||
reject?: (reason?: string) => void;
|
||||
};
|
||||
|
||||
const getInitialState = (): DeleteImagesModalState => ({
|
||||
imageDTOs: [],
|
||||
usagePerImage: [],
|
||||
usageSummary: {
|
||||
isControlLayerImage: false,
|
||||
isInpaintMaskImage: false,
|
||||
isNodesImage: false,
|
||||
isRasterLayerImage: false,
|
||||
isRegionalGuidanceImage: false,
|
||||
isReferenceImage: false,
|
||||
isUpscaleImage: false,
|
||||
},
|
||||
isOpen: false,
|
||||
});
|
||||
|
||||
const $deleteModalState = atom<DeleteImagesModalState>(getInitialState());
|
||||
|
||||
const deleteImagesWithDialog = async (imageDTOs: ImageDTO[]): Promise<void> => {
|
||||
const { getState, dispatch } = getStore();
|
||||
const imageUsage = getImageUsageFromImageDTOs(imageDTOs, getState());
|
||||
const shouldConfirmOnDelete = selectSystemShouldConfirmOnDelete(getState());
|
||||
|
||||
if (!shouldConfirmOnDelete && !isAnyImageInUse(imageUsage)) {
|
||||
// If we don't need to confirm and the images are not in use, delete them directly
|
||||
await handleDeletions(imageDTOs, dispatch, getState);
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
$deleteModalState.set({
|
||||
usagePerImage: imageUsage,
|
||||
usageSummary: getImageUsageSummary(imageUsage),
|
||||
imageDTOs,
|
||||
isOpen: true,
|
||||
resolve,
|
||||
reject,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeletions = async (imageDTOs: ImageDTO[], dispatch: AppDispatch, getState: AppGetState) => {
|
||||
try {
|
||||
const state = getState();
|
||||
await dispatch(imagesApi.endpoints.deleteImages.initiate({ imageDTOs })).unwrap();
|
||||
|
||||
if (intersectionBy(state.gallery.selection, imageDTOs, 'image_name').length > 0) {
|
||||
// Some selected images were deleted, need to select the next image
|
||||
const queryArgs = selectListImagesQueryArgs(state);
|
||||
const { data } = imagesApi.endpoints.listImages.select(queryArgs)(state);
|
||||
if (data) {
|
||||
// When we delete multiple images, we clear the selection. Then, the the next time we load images, we will
|
||||
// select the first one. This is handled below in the listener for `imagesApi.endpoints.listImages.matchFulfilled`.
|
||||
dispatch(imageSelected(null));
|
||||
}
|
||||
}
|
||||
|
||||
// We need to reset the features where the image is in use - none of these work if their image(s) don't exist
|
||||
for (const imageDTO of imageDTOs) {
|
||||
deleteNodesImages(state, dispatch, imageDTO);
|
||||
deleteControlLayerImages(state, dispatch, imageDTO);
|
||||
deleteReferenceImages(state, dispatch, imageDTO);
|
||||
deleteRasterLayerImages(state, dispatch, imageDTO);
|
||||
}
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDeletion = async (dispatch: AppDispatch, getState: AppGetState) => {
|
||||
const state = $deleteModalState.get();
|
||||
await handleDeletions(state.imageDTOs, dispatch, getState);
|
||||
state.resolve?.();
|
||||
closeSilently();
|
||||
};
|
||||
|
||||
const cancelDeletion = () => {
|
||||
const state = $deleteModalState.get();
|
||||
state.reject?.('User canceled');
|
||||
closeSilently();
|
||||
};
|
||||
|
||||
const closeSilently = () => {
|
||||
$deleteModalState.set(getInitialState());
|
||||
};
|
||||
|
||||
export const useDeleteImageModalState = () => {
|
||||
const state = useStore($deleteModalState);
|
||||
return state;
|
||||
};
|
||||
|
||||
export const useDeleteImageModalApi = () => {
|
||||
const { dispatch, getState } = useAppStore();
|
||||
const api = useMemo(
|
||||
() => ({
|
||||
delete: deleteImagesWithDialog,
|
||||
confirm: () => confirmDeletion(dispatch, getState),
|
||||
cancel: cancelDeletion,
|
||||
close: closeSilently,
|
||||
getUsageSummary: getImageUsageSummary,
|
||||
}),
|
||||
[dispatch, getState]
|
||||
);
|
||||
|
||||
return api;
|
||||
};
|
||||
|
||||
const getImageUsageFromImageDTOs = (imageDTOs: ImageDTO[], state: RootState): ImageUsage[] => {
|
||||
if (imageDTOs.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const nodes = selectNodesSlice(state);
|
||||
const canvas = selectCanvasSlice(state);
|
||||
const upscale = selectUpscaleSlice(state);
|
||||
|
||||
return imageDTOs.map(({ image_name }) => getImageUsage(nodes, canvas, upscale, image_name));
|
||||
};
|
||||
|
||||
const getImageUsageSummary = (imageUsage: ImageUsage[]): ImageUsage => ({
|
||||
isUpscaleImage: some(imageUsage, (i) => i.isUpscaleImage),
|
||||
isRasterLayerImage: some(imageUsage, (i) => i.isRasterLayerImage),
|
||||
isInpaintMaskImage: some(imageUsage, (i) => i.isInpaintMaskImage),
|
||||
isRegionalGuidanceImage: some(imageUsage, (i) => i.isRegionalGuidanceImage),
|
||||
isNodesImage: some(imageUsage, (i) => i.isNodesImage),
|
||||
isControlLayerImage: some(imageUsage, (i) => i.isControlLayerImage),
|
||||
isReferenceImage: some(imageUsage, (i) => i.isReferenceImage),
|
||||
});
|
||||
|
||||
const isAnyImageInUse = (imageUsage: ImageUsage[]): boolean =>
|
||||
imageUsage.some(
|
||||
(i) =>
|
||||
i.isRasterLayerImage ||
|
||||
i.isControlLayerImage ||
|
||||
i.isReferenceImage ||
|
||||
i.isInpaintMaskImage ||
|
||||
i.isUpscaleImage ||
|
||||
i.isNodesImage ||
|
||||
i.isRegionalGuidanceImage
|
||||
);
|
||||
|
||||
// Some utils to delete images from different parts of the app
|
||||
const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||
const actions: Param0<typeof dispatch>[] = [];
|
||||
state.nodes.present.nodes.forEach((node) => {
|
||||
if (!isInvocationNode(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
forEach(node.data.inputs, (input) => {
|
||||
if (isImageFieldInputInstance(input) && input.value?.image_name === imageDTO.image_name) {
|
||||
actions.push(
|
||||
fieldImageValueChanged({
|
||||
nodeId: node.data.id,
|
||||
fieldName: input.name,
|
||||
value: undefined,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isImageFieldCollectionInputInstance(input)) {
|
||||
actions.push(
|
||||
fieldImageCollectionValueChanged({
|
||||
nodeId: node.data.id,
|
||||
fieldName: input.name,
|
||||
value: input.value?.filter((value) => value?.image_name !== imageDTO.image_name),
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
actions.forEach(dispatch);
|
||||
};
|
||||
|
||||
const deleteControlLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||
selectCanvasSlice(state).controlLayers.entities.forEach(({ id, objects }) => {
|
||||
let shouldDelete = false;
|
||||
for (const obj of objects) {
|
||||
if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === imageDTO.image_name) {
|
||||
shouldDelete = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (shouldDelete) {
|
||||
dispatch(entityDeleted({ entityIdentifier: { id, type: 'control_layer' } }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const deleteReferenceImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||
selectCanvasSlice(state).referenceImages.entities.forEach((entity) => {
|
||||
if (entity.ipAdapter.image?.image_name === imageDTO.image_name) {
|
||||
dispatch(referenceImageIPAdapterImageChanged({ entityIdentifier: getEntityIdentifier(entity), imageDTO: null }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const deleteRasterLayerImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {
|
||||
selectCanvasSlice(state).rasterLayers.entities.forEach(({ id, objects }) => {
|
||||
let shouldDelete = false;
|
||||
for (const obj of objects) {
|
||||
if (obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === imageDTO.image_name) {
|
||||
shouldDelete = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (shouldDelete) {
|
||||
dispatch(entityDeleted({ entityIdentifier: { id, type: 'raster_layer' } }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const getImageUsage = (nodes: NodesState, canvas: CanvasState, upscale: UpscaleState, image_name: string) => {
|
||||
const isNodesImage = nodes.nodes.filter(isInvocationNode).some((node) =>
|
||||
some(node.data.inputs, (input) => {
|
||||
if (isImageFieldInputInstance(input)) {
|
||||
if (input.value?.image_name === image_name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isImageFieldCollectionInputInstance(input)) {
|
||||
if (input.value?.some((value) => value?.image_name === image_name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
);
|
||||
|
||||
const isUpscaleImage = upscale.upscaleInitialImage?.image_name === image_name;
|
||||
|
||||
const isReferenceImage = canvas.referenceImages.entities.some(
|
||||
({ ipAdapter }) => ipAdapter.image?.image_name === image_name
|
||||
);
|
||||
|
||||
const isRasterLayerImage = canvas.rasterLayers.entities.some(({ objects }) =>
|
||||
objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name)
|
||||
);
|
||||
|
||||
const isControlLayerImage = canvas.controlLayers.entities.some(({ objects }) =>
|
||||
objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name)
|
||||
);
|
||||
|
||||
const isInpaintMaskImage = canvas.inpaintMasks.entities.some(({ objects }) =>
|
||||
objects.some((obj) => obj.type === 'image' && 'image_name' in obj.image && obj.image.image_name === image_name)
|
||||
);
|
||||
|
||||
const isRegionalGuidanceImage = canvas.regionalGuidance.entities.some(({ referenceImages }) =>
|
||||
referenceImages.some(({ ipAdapter }) => ipAdapter.image?.image_name === image_name)
|
||||
);
|
||||
|
||||
const imageUsage: ImageUsage = {
|
||||
isUpscaleImage,
|
||||
isRasterLayerImage,
|
||||
isInpaintMaskImage,
|
||||
isRegionalGuidanceImage,
|
||||
isNodesImage,
|
||||
isControlLayerImage,
|
||||
isReferenceImage,
|
||||
};
|
||||
|
||||
return imageUsage;
|
||||
};
|
||||
@@ -17,7 +17,7 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage';
|
||||
import { getImageUsage } from 'features/deleteImageModal/store/selectors';
|
||||
import { getImageUsage } from 'features/deleteImageModal/store/state';
|
||||
import type { ImageUsage } from 'features/deleteImageModal/store/types';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { IconMenuItem } from 'common/components/IconMenuItem';
|
||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -8,12 +7,16 @@ import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||
|
||||
export const ImageMenuItemDelete = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const deleteImageModal = useDeleteImageModalApi();
|
||||
const imageDTO = useImageDTOContext();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(imagesToDeleteSelected([imageDTO]));
|
||||
}, [dispatch, imageDTO]);
|
||||
const onClick = useCallback(async () => {
|
||||
try {
|
||||
await deleteImageModal.delete([imageDTO]);
|
||||
} catch {
|
||||
// noop;
|
||||
}
|
||||
}, [deleteImageModal, imageDTO]);
|
||||
|
||||
return (
|
||||
<IconMenuItem
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useStore } from '@nanostores/react';
|
||||
import { $customStarUI } from 'app/store/nanostores/customStarUI';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoardModal/store/slice';
|
||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -19,6 +19,7 @@ const MultipleSelectionMenuItems = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const selection = useAppSelector((s) => s.gallery.selection);
|
||||
const customStarUi = useStore($customStarUI);
|
||||
const deleteImageModal = useDeleteImageModalApi();
|
||||
|
||||
const isBulkDownloadEnabled = useFeatureStatus('bulkDownload');
|
||||
|
||||
@@ -32,8 +33,8 @@ const MultipleSelectionMenuItems = () => {
|
||||
}, [dispatch, selection]);
|
||||
|
||||
const handleDeleteSelection = useCallback(() => {
|
||||
dispatch(imagesToDeleteSelected(selection));
|
||||
}, [dispatch, selection]);
|
||||
deleteImageModal.delete(selection);
|
||||
}, [deleteImageModal, selection]);
|
||||
|
||||
const handleStarSelection = useCallback(() => {
|
||||
starImages({ imageDTOs: selection });
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useShiftModifier } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
|
||||
import { DndImageIcon } from 'features/dnd/DndImageIcon';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
@@ -15,16 +14,17 @@ type Props = {
|
||||
export const GalleryImageDeleteIconButton = memo(({ imageDTO }: Props) => {
|
||||
const shift = useShiftModifier();
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const deleteImageModal = useDeleteImageModalApi();
|
||||
|
||||
const onClick = useCallback(
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
if (!imageDTO) {
|
||||
return;
|
||||
}
|
||||
dispatch(imagesToDeleteSelected([imageDTO]));
|
||||
deleteImageModal.delete([imageDTO]);
|
||||
},
|
||||
[dispatch, imageDTO]
|
||||
[deleteImageModal, imageDTO]
|
||||
);
|
||||
|
||||
if (!shift) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useIsRegionFocused } from 'common/hooks/focus';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
|
||||
import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation';
|
||||
import { useGalleryPagination } from 'features/gallery/hooks/useGalleryPagination';
|
||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
@@ -16,7 +16,6 @@ import { useListImagesQuery } from 'services/api/endpoints/images';
|
||||
export const useGalleryHotkeys = () => {
|
||||
useAssertSingleton('useGalleryHotkeys');
|
||||
const { goNext, goPrev, isNextEnabled, isPrevEnabled } = useGalleryPagination();
|
||||
const dispatch = useAppDispatch();
|
||||
const selection = useAppSelector((s) => s.gallery.selection);
|
||||
const queryArgs = useAppSelector(selectListImagesQueryArgs);
|
||||
const queryResult = useListImagesQuery(queryArgs);
|
||||
@@ -25,6 +24,7 @@ export const useGalleryHotkeys = () => {
|
||||
const isWorkflowsFocused = useIsRegionFocused('workflows');
|
||||
const isGalleryFocused = useIsRegionFocused('gallery');
|
||||
const isImageViewerFocused = useIsRegionFocused('viewer');
|
||||
const deleteImageModal = useDeleteImageModalApi();
|
||||
|
||||
// When we are on the canvas tab, we need to disable the delete hotkey when the user is focused on the layers tab in
|
||||
// the right hand panel, because the same hotkey is used to delete layers.
|
||||
@@ -209,7 +209,7 @@ export const useGalleryHotkeys = () => {
|
||||
if (!selection.length) {
|
||||
return;
|
||||
}
|
||||
dispatch(imagesToDeleteSelected(selection));
|
||||
deleteImageModal.delete(selection);
|
||||
},
|
||||
options: {
|
||||
enabled: (isGalleryFocused || isImageViewerFocused) && isDeleteEnabledByTab && !isWorkflowsFocused,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react';
|
||||
import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||
import { useDeleteImageModalApi } from 'features/deleteImageModal/store/state';
|
||||
import {
|
||||
handlers,
|
||||
parseAndRecallAllMetadata,
|
||||
@@ -34,6 +34,7 @@ export const useImageActions = (imageDTO: ImageDTO) => {
|
||||
const [hasSeed, setHasSeed] = useState(false);
|
||||
const [hasPrompts, setHasPrompts] = useState(false);
|
||||
const hasTemplates = useStore($hasTemplates);
|
||||
const deleteImageModal = useDeleteImageModalApi();
|
||||
|
||||
useEffect(() => {
|
||||
const parseMetadata = async () => {
|
||||
@@ -169,8 +170,8 @@ export const useImageActions = (imageDTO: ImageDTO) => {
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
const _delete = useCallback(() => {
|
||||
dispatch(imagesToDeleteSelected([imageDTO]));
|
||||
}, [dispatch, imageDTO]);
|
||||
deleteImageModal.delete([imageDTO]);
|
||||
}, [deleteImageModal, imageDTO]);
|
||||
|
||||
return {
|
||||
hasMetadata,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { $authToken } from 'app/store/nanostores/authToken';
|
||||
import { getStore } from 'app/store/nanostores/store';
|
||||
import type { BoardId } from 'features/gallery/store/types';
|
||||
import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||
import { uniqBy } from 'lodash-es';
|
||||
import type { components, paths } from 'services/api/schema';
|
||||
import type {
|
||||
DeleteBoardResult,
|
||||
@@ -14,6 +15,7 @@ import type {
|
||||
UploadImageArg,
|
||||
} from 'services/api/types';
|
||||
import { getCategories, getListImagesUrl } from 'services/api/util';
|
||||
import stableHash from 'stable-hash';
|
||||
import type { Param0 } from 'tsafe';
|
||||
import type { JsonObject } from 'type-fest';
|
||||
|
||||
@@ -133,11 +135,12 @@ export const imagesApi = api.injectEndpoints({
|
||||
};
|
||||
},
|
||||
invalidatesTags: (result, error, { imageDTOs }) => {
|
||||
if (imageDTOs[0]) {
|
||||
const categories = getCategories(imageDTOs[0]);
|
||||
const boardId = imageDTOs[0].board_id ?? 'none';
|
||||
const tags: ApiTagDescription[] = [];
|
||||
for (const imageDTO of imageDTOs) {
|
||||
const categories = getCategories(imageDTO);
|
||||
const boardId = imageDTO.board_id ?? 'none';
|
||||
|
||||
const tags: ApiTagDescription[] = [
|
||||
tags.push(
|
||||
{
|
||||
type: 'ImageList',
|
||||
id: getListImagesUrl({
|
||||
@@ -152,12 +155,12 @@ export const imagesApi = api.injectEndpoints({
|
||||
{
|
||||
type: 'BoardImagesTotal',
|
||||
id: boardId,
|
||||
},
|
||||
];
|
||||
|
||||
return tags;
|
||||
}
|
||||
);
|
||||
}
|
||||
return [];
|
||||
|
||||
const dedupedTags = uniqBy(tags, stableHash);
|
||||
return dedupedTags;
|
||||
},
|
||||
}),
|
||||
deleteUncategorizedImages: build.mutation<components['schemas']['DeleteImagesFromListResult'], void>({
|
||||
|
||||
Reference in New Issue
Block a user