From cc22427f2589c721ba45bae24efd35189afddb74 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 6 Jun 2023 14:08:04 +1000
Subject: [PATCH 01/14] feat(ui): improve UI on smaller screens
- responsive changes were causing a lot of weird layout issues, had to remove the rest of them
- canvas (non-beta) toolbar now wraps
- reduces minH for prompt boxes a bit
---
.../frontend/web/src/app/components/App.tsx | 21 ++-
.../IAICanvasToolbar/IAICanvasToolbar.tsx | 21 ++-
.../nodes/components/FieldTypeLegend.tsx | 2 +-
.../features/nodes/components/NodeEditor.tsx | 2 +-
.../Core/ParamNegativeConditioning.tsx | 1 +
.../Core/ParamPositiveConditioning.tsx | 2 +-
.../components/InvokeAILogoComponent.tsx | 22 +--
.../features/system/components/SiteHeader.tsx | 174 +++++++++++++-----
.../src/features/ui/components/InvokeTabs.tsx | 16 +-
.../components/PinParametersPanelButton.tsx | 1 -
10 files changed, 168 insertions(+), 94 deletions(-)
diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx
index 33fa57f0b3..21b3945490 100644
--- a/invokeai/frontend/web/src/app/components/App.tsx
+++ b/invokeai/frontend/web/src/app/components/App.tsx
@@ -76,18 +76,21 @@ const App = ({
{isLightboxEnabled && }
{headerComponent || }
diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx
index 69eed2b46a..30ff6fff81 100644
--- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx
+++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx
@@ -1,4 +1,4 @@
-import { ButtonGroup, Flex } from '@chakra-ui/react';
+import { Box, ButtonGroup, Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
@@ -210,16 +210,19 @@ const IAICanvasToolbar = () => {
sx={{
alignItems: 'center',
gap: 2,
+ flexWrap: 'wrap',
}}
>
-
+
+
+
diff --git a/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx b/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx
index c14c7ebccf..78316cc694 100644
--- a/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/FieldTypeLegend.tsx
@@ -6,7 +6,7 @@ import { memo } from 'react';
const FieldTypeLegend = () => {
return (
-
+
{map(FIELDS, ({ title, description, color }, key) => (
{
sx={{
position: 'relative',
width: 'full',
- height: { base: '100vh', xl: 'full' },
+ height: 'full',
borderRadius: 'md',
bg: 'base.850',
}}
diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx
index 28ab50ff82..70c342cc3b 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx
@@ -25,6 +25,7 @@ const ParamNegativeConditioning = () => {
borderColor: 'error.600',
}}
fontSize="sm"
+ minH={16}
/>
);
diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx
index 0980b84ab3..82b43517f8 100644
--- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx
+++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx
@@ -82,7 +82,7 @@ const ParamPositiveConditioning = () => {
onKeyDown={handleKeyDown}
resize="vertical"
ref={promptRef}
- minH={{ base: 20, lg: 40 }}
+ minH={32}
/>
diff --git a/invokeai/frontend/web/src/features/system/components/InvokeAILogoComponent.tsx b/invokeai/frontend/web/src/features/system/components/InvokeAILogoComponent.tsx
index f6017d02f0..bec2c32b61 100644
--- a/invokeai/frontend/web/src/features/system/components/InvokeAILogoComponent.tsx
+++ b/invokeai/frontend/web/src/features/system/components/InvokeAILogoComponent.tsx
@@ -13,22 +13,16 @@ const InvokeAILogoComponent = () => {
-
-
+ />
+
+
invoke ai
{
- const [menuOpened, setMenuOpened] = useState(false);
- const resolution = useResolution();
const { t } = useTranslation();
+ const isModelManagerEnabled =
+ useFeatureStatus('modelManager').isFeatureEnabled;
+ const isLocalizationEnabled =
+ useFeatureStatus('localization').isFeatureEnabled;
+ const isBugLinkEnabled = useFeatureStatus('bugLink').isFeatureEnabled;
+ const isDiscordLinkEnabled = useFeatureStatus('discordLink').isFeatureEnabled;
+ const isGithubLinkEnabled = useFeatureStatus('githubLink').isFeatureEnabled;
+
return (
-
-
-
-
-
-
+
+
+
- {resolution === 'desktop' ? (
-
- ) : (
+ {isModelManagerEnabled && (
+
}
- aria-label={t('accessibility.menu')}
- background={menuOpened ? 'base.800' : 'none'}
- _hover={{ background: menuOpened ? 'base.800' : 'none' }}
- onClick={() => setMenuOpened(!menuOpened)}
- p={0}
- >
- )}
-
-
- {resolution !== 'desktop' && menuOpened && (
-
-
-
+ aria-label={t('modelManager.modelManager')}
+ tooltip={t('modelManager.modelManager')}
+ size="sm"
+ variant="link"
+ data-variant="link"
+ fontSize={20}
+ icon={}
+ />
+
)}
-
+
+
+ }
+ />
+
+
+
+
+ {isLocalizationEnabled && }
+
+ {isBugLinkEnabled && (
+
+ }
+ />
+
+ )}
+
+ {isGithubLinkEnabled && (
+
+ }
+ />
+
+ )}
+
+ {isDiscordLinkEnabled && (
+
+ }
+ />
+
+ )}
+
+
+ }
+ />
+
+
);
};
diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx
index 23fc6bd192..c164b87515 100644
--- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx
@@ -152,16 +152,18 @@ const InvokeTabs = () => {
onChange={(index: number) => {
dispatch(setActiveTab(index));
}}
- flexGrow={1}
- flexDir={{ base: 'column', xl: 'row' }}
- gap={{ base: 4 }}
+ sx={{
+ flexGrow: 1,
+ gap: 4,
+ }}
isLazy
>
{tabs}
diff --git a/invokeai/frontend/web/src/features/ui/components/PinParametersPanelButton.tsx b/invokeai/frontend/web/src/features/ui/components/PinParametersPanelButton.tsx
index 46d0fa3f93..a742e2a587 100644
--- a/invokeai/frontend/web/src/features/ui/components/PinParametersPanelButton.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/PinParametersPanelButton.tsx
@@ -33,7 +33,6 @@ const PinParametersPanelButton = (props: PinParametersPanelButtonProps) => {
icon={shouldPinParametersPanel ? : }
variant="ghost"
size="sm"
- px={{ base: 10, xl: 0 }}
sx={{
color: 'base.700',
_hover: {
From 229de2dbb8caf56a0a4acee3a0cdb864151488c0 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 6 Jun 2023 11:00:15 +1000
Subject: [PATCH 02/14] feat(ui): fix canvas saving
- fix "bounding box region only" not being respected when saving
- add toasts for each action
- improve workflow `take()` predicates to use the requestId
---
.../listeners/canvasCopiedToClipboard.ts | 7 ++++++
.../listeners/canvasDownloadedAsImage.ts | 3 ++-
.../listeners/canvasMerged.ts | 24 +++++++++---------
.../listeners/canvasSavedToGallery.ts | 21 +++++++++-------
.../listeners/imageUploaded.ts | 17 ++++++++++++-
.../features/canvas/util/getBaseLayerBlob.ts | 25 +++++++++----------
.../canvas/util/getFullBaseLayerBlob.ts | 19 ++++++++++++++
7 files changed, 80 insertions(+), 36 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/canvas/util/getFullBaseLayerBlob.ts
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts
index 16642f1f32..a7ddd8e917 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasCopiedToClipboard.ts
@@ -28,6 +28,13 @@ export const addCanvasCopiedToClipboardListener = () => {
}
copyBlobToClipboard(blob);
+
+ dispatch(
+ addToast({
+ title: 'Canvas Copied to Clipboard',
+ status: 'success',
+ })
+ );
},
});
};
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts
index ef4c63b31c..c97df09cff 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasDownloadedAsImage.ts
@@ -27,7 +27,8 @@ export const addCanvasDownloadedAsImageListener = () => {
return;
}
- downloadBlob(blob, 'mergedCanvas.png');
+ downloadBlob(blob, 'canvas.png');
+ dispatch(addToast({ title: 'Canvas Downloaded', status: 'success' }));
},
});
};
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts
index 80865f3126..ed157066bb 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts
@@ -1,22 +1,20 @@
import { canvasMerged } from 'features/canvas/store/actions';
import { startAppListening } from '..';
import { log } from 'app/logging/useLogger';
-import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { addToast } from 'features/system/store/systemSlice';
import { imageUploaded } from 'services/thunks/image';
-import { v4 as uuidv4 } from 'uuid';
import { setMergedCanvas } from 'features/canvas/store/canvasSlice';
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
+import { getFullBaseLayerBlob } from 'features/canvas/util/getFullBaseLayerBlob';
const moduleLog = log.child({ namespace: 'canvasCopiedToClipboardListener' });
+export const MERGED_CANVAS_FILENAME = 'mergedCanvas.png';
export const addCanvasMergedListener = () => {
startAppListening({
actionCreator: canvasMerged,
effect: async (action, { dispatch, getState, take }) => {
- const state = getState();
-
- const blob = await getBaseLayerBlob(state, true);
+ const blob = await getFullBaseLayerBlob();
if (!blob) {
moduleLog.error('Problem getting base layer blob');
@@ -48,12 +46,12 @@ export const addCanvasMergedListener = () => {
relativeTo: canvasBaseLayer.getParent(),
});
- const filename = `mergedCanvas_${uuidv4()}.png`;
-
- dispatch(
+ const imageUploadedRequest = dispatch(
imageUploaded({
formData: {
- file: new File([blob], filename, { type: 'image/png' }),
+ file: new File([blob], MERGED_CANVAS_FILENAME, {
+ type: 'image/png',
+ }),
},
imageCategory: 'general',
isIntermediate: true,
@@ -61,9 +59,11 @@ export const addCanvasMergedListener = () => {
);
const [{ payload }] = await take(
- (action): action is ReturnType =>
- imageUploaded.fulfilled.match(action) &&
- action.meta.arg.formData.file.name === filename
+ (
+ uploadedImageAction
+ ): uploadedImageAction is ReturnType =>
+ imageUploaded.fulfilled.match(uploadedImageAction) &&
+ uploadedImageAction.meta.requestId === imageUploadedRequest.requestId
);
const mergedCanvasImage = payload;
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts
index b89620775b..2ea69df179 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasSavedToGallery.ts
@@ -4,9 +4,10 @@ import { log } from 'app/logging/useLogger';
import { imageUploaded } from 'services/thunks/image';
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
import { addToast } from 'features/system/store/systemSlice';
-import { v4 as uuidv4 } from 'uuid';
import { imageUpserted } from 'features/gallery/store/imagesSlice';
+export const SAVED_CANVAS_FILENAME = 'savedCanvas.png';
+
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
export const addCanvasSavedToGalleryListener = () => {
@@ -15,7 +16,7 @@ export const addCanvasSavedToGalleryListener = () => {
effect: async (action, { dispatch, getState, take }) => {
const state = getState();
- const blob = await getBaseLayerBlob(state, true);
+ const blob = await getBaseLayerBlob(state);
if (!blob) {
moduleLog.error('Problem getting base layer blob');
@@ -29,12 +30,12 @@ export const addCanvasSavedToGalleryListener = () => {
return;
}
- const filename = `mergedCanvas_${uuidv4()}.png`;
-
- dispatch(
+ const imageUploadedRequest = dispatch(
imageUploaded({
formData: {
- file: new File([blob], filename, { type: 'image/png' }),
+ file: new File([blob], SAVED_CANVAS_FILENAME, {
+ type: 'image/png',
+ }),
},
imageCategory: 'general',
isIntermediate: false,
@@ -42,9 +43,11 @@ export const addCanvasSavedToGalleryListener = () => {
);
const [{ payload: uploadedImageDTO }] = await take(
- (action): action is ReturnType =>
- imageUploaded.fulfilled.match(action) &&
- action.meta.arg.formData.file.name === filename
+ (
+ uploadedImageAction
+ ): uploadedImageAction is ReturnType =>
+ imageUploaded.fulfilled.match(uploadedImageAction) &&
+ uploadedImageAction.meta.requestId === imageUploadedRequest.requestId
);
dispatch(imageUpserted(uploadedImageDTO));
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts
index 6d84431f80..bfc362e48d 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUploaded.ts
@@ -3,6 +3,8 @@ import { imageUploaded } from 'services/thunks/image';
import { addToast } from 'features/system/store/systemSlice';
import { log } from 'app/logging/useLogger';
import { imageUpserted } from 'features/gallery/store/imagesSlice';
+import { SAVED_CANVAS_FILENAME } from './canvasSavedToGallery';
+import { MERGED_CANVAS_FILENAME } from './canvasMerged';
const moduleLog = log.child({ namespace: 'image' });
@@ -19,9 +21,22 @@ export const addImageUploadedFulfilledListener = () => {
return;
}
- const state = getState();
+ const originalFileName = action.meta.arg.formData.file.name;
dispatch(imageUpserted(image));
+
+ if (originalFileName === SAVED_CANVAS_FILENAME) {
+ dispatch(
+ addToast({ title: 'Canvas Saved to Gallery', status: 'success' })
+ );
+ return;
+ }
+
+ if (originalFileName === MERGED_CANVAS_FILENAME) {
+ dispatch(addToast({ title: 'Canvas Merged', status: 'success' }));
+ return;
+ }
+
dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
},
});
diff --git a/invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts b/invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts
index a576551d72..20ac482710 100644
--- a/invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts
+++ b/invokeai/frontend/web/src/features/canvas/util/getBaseLayerBlob.ts
@@ -2,10 +2,10 @@ import { getCanvasBaseLayer } from './konvaInstanceProvider';
import { RootState } from 'app/store/store';
import { konvaNodeToBlob } from './konvaNodeToBlob';
-export const getBaseLayerBlob = async (
- state: RootState,
- withoutBoundingBox?: boolean
-) => {
+/**
+ * Get the canvas base layer blob, with or without bounding box according to `shouldCropToBoundingBoxOnSave`
+ */
+export const getBaseLayerBlob = async (state: RootState) => {
const canvasBaseLayer = getCanvasBaseLayer();
if (!canvasBaseLayer) {
@@ -24,15 +24,14 @@ export const getBaseLayerBlob = async (
const absPos = clonedBaseLayer.getAbsolutePosition();
- const boundingBox =
- shouldCropToBoundingBoxOnSave && !withoutBoundingBox
- ? {
- x: boundingBoxCoordinates.x + absPos.x,
- y: boundingBoxCoordinates.y + absPos.y,
- width: boundingBoxDimensions.width,
- height: boundingBoxDimensions.height,
- }
- : clonedBaseLayer.getClientRect();
+ const boundingBox = shouldCropToBoundingBoxOnSave
+ ? {
+ x: boundingBoxCoordinates.x + absPos.x,
+ y: boundingBoxCoordinates.y + absPos.y,
+ width: boundingBoxDimensions.width,
+ height: boundingBoxDimensions.height,
+ }
+ : clonedBaseLayer.getClientRect();
return konvaNodeToBlob(clonedBaseLayer, boundingBox);
};
diff --git a/invokeai/frontend/web/src/features/canvas/util/getFullBaseLayerBlob.ts b/invokeai/frontend/web/src/features/canvas/util/getFullBaseLayerBlob.ts
new file mode 100644
index 0000000000..ba855723fb
--- /dev/null
+++ b/invokeai/frontend/web/src/features/canvas/util/getFullBaseLayerBlob.ts
@@ -0,0 +1,19 @@
+import { getCanvasBaseLayer } from './konvaInstanceProvider';
+import { konvaNodeToBlob } from './konvaNodeToBlob';
+
+/**
+ * Gets the canvas base layer blob, without bounding box
+ */
+export const getFullBaseLayerBlob = async () => {
+ const canvasBaseLayer = getCanvasBaseLayer();
+
+ if (!canvasBaseLayer) {
+ return;
+ }
+
+ const clonedBaseLayer = canvasBaseLayer.clone();
+
+ clonedBaseLayer.scale({ x: 1, y: 1 });
+
+ return konvaNodeToBlob(clonedBaseLayer, clonedBaseLayer.getClientRect());
+};
From fc5f9c30a64e12777a2e02a6dd853a981cc3cda4 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 6 Jun 2023 12:28:18 +1000
Subject: [PATCH 03/14] fix(ui): fix metadata viewer not working for canvas
images
---
.../web/src/features/gallery/components/CurrentImagePreview.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx
index 12d62ead70..5e210bf4b7 100644
--- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx
@@ -117,7 +117,7 @@ const CurrentImagePreview = () => {
/>
)}
- {shouldShowImageDetails && image && image.metadata && (
+ {shouldShowImageDetails && image && (
Date: Tue, 6 Jun 2023 12:31:10 +1000
Subject: [PATCH 04/14] fix(ui): fix canvas auto-save not working
---
.../listeners/imageMetadataReceived.ts | 19 +++++++++++++++++--
1 file changed, 17 insertions(+), 2 deletions(-)
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts
index 63aeecb95e..7d7e92ff61 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts
@@ -1,6 +1,6 @@
import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
-import { imageMetadataReceived } from 'services/thunks/image';
+import { imageMetadataReceived, imageUpdated } from 'services/thunks/image';
import { imageUpserted } from 'features/gallery/store/imagesSlice';
const moduleLog = log.child({ namespace: 'image' });
@@ -10,10 +10,25 @@ export const addImageMetadataReceivedFulfilledListener = () => {
actionCreator: imageMetadataReceived.fulfilled,
effect: (action, { getState, dispatch }) => {
const image = action.payload;
- if (image.is_intermediate) {
+
+ const state = getState();
+
+ if (
+ image.session_id === state.canvas.layerState.stagingArea.sessionId &&
+ state.canvas.shouldAutoSave
+ ) {
+ dispatch(
+ imageUpdated({
+ imageName: image.image_name,
+ imageOrigin: image.image_origin,
+ requestBody: { is_intermediate: false },
+ })
+ );
+ } else if (image.is_intermediate) {
// No further actions needed for intermediate images
return;
}
+
moduleLog.debug({ data: { image } }, 'Image metadata received');
dispatch(imageUpserted(image));
},
From 840c632c0a708c1f1f22acd78b18221211cbb7b3 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 6 Jun 2023 12:33:00 +1000
Subject: [PATCH 05/14] feat(ui): sort images by `updated_at` instead of
`created_at`
fixes issue where saved staging area images are sorted as expected in gallery.
---
invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts b/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts
index cb6469aeb4..de2cdb48b2 100644
--- a/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts
@@ -12,7 +12,7 @@ import { receivedPageOfImages } from 'services/thunks/image';
export const imagesAdapter = createEntityAdapter({
selectId: (image) => image.image_name,
- sortComparer: (a, b) => dateComparator(b.created_at, a.created_at),
+ sortComparer: (a, b) => dateComparator(b.updated_at, a.updated_at),
});
export const IMAGE_CATEGORIES: ImageCategory[] = ['general'];
From 3ff732d58310456a2954f72f7d2e90bbc05a215f Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Mon, 5 Jun 2023 16:01:35 +1000
Subject: [PATCH 06/14] feat(ui): clear controlnet image when image deleted
---
.../listeners/imageDeleted.ts | 8 +-------
.../controlNet/store/controlNetSlice.ts | 16 +++++++++++++++
.../src/features/gallery/store/imagesSlice.ts | 20 ++++++++-----------
3 files changed, 25 insertions(+), 19 deletions(-)
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts
index bf7ca4020c..e8e7a78165 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts
@@ -5,8 +5,6 @@ import { log } from 'app/logging/useLogger';
import { clamp } from 'lodash-es';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import {
- imageRemoved,
- imagesAdapter,
selectImagesEntities,
selectImagesIds,
} from 'features/gallery/store/imagesSlice';
@@ -58,8 +56,6 @@ export const addRequestedImageDeletionListener = () => {
}
}
- dispatch(imageRemoved(image_name));
-
dispatch(
imageDeleted({ imageName: image_name, imageOrigin: image_origin })
);
@@ -74,9 +70,7 @@ export const addImageDeletedPendingListener = () => {
startAppListening({
actionCreator: imageDeleted.pending,
effect: (action, { dispatch, getState }) => {
- const { imageName, imageOrigin } = action.meta.arg;
- // Preemptively remove the image from the gallery
- imagesAdapter.removeOne(getState().images, imageName);
+ //
},
});
};
diff --git a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts
index 1389457aba..40714d3ecb 100644
--- a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts
+++ b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts
@@ -13,6 +13,8 @@ import {
ControlNetModel,
} from './constants';
import { controlNetImageProcessed } from './actions';
+import { imageDeleted } from 'services/thunks/image';
+import { forEach } from 'lodash-es';
export const initialControlNet: Omit = {
isEnabled: true,
@@ -194,6 +196,20 @@ export const controlNetSlice = createSlice({
state.isProcessingControlImage = true;
}
});
+
+ builder.addCase(imageDeleted.pending, (state, action) => {
+ // Preemptively remove the image from the gallery
+ const { imageName } = action.meta.arg;
+ forEach(state.controlNets, (c) => {
+ if (c.controlImage?.image_name === imageName) {
+ c.controlImage = null;
+ c.processedControlImage = null;
+ }
+ if (c.processedControlImage?.image_name === imageName) {
+ c.processedControlImage = null;
+ }
+ });
+ });
},
});
diff --git a/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts b/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts
index de2cdb48b2..539690dcde 100644
--- a/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts
@@ -7,8 +7,8 @@ import {
import { RootState } from 'app/store/store';
import { ImageCategory, ImageDTO } from 'services/api';
import { dateComparator } from 'common/util/dateComparator';
-import { isString, keyBy } from 'lodash-es';
-import { receivedPageOfImages } from 'services/thunks/image';
+import { keyBy } from 'lodash-es';
+import { imageDeleted, receivedPageOfImages } from 'services/thunks/image';
export const imagesAdapter = createEntityAdapter({
selectId: (image) => image.image_name,
@@ -49,14 +49,6 @@ const imagesSlice = createSlice({
imageUpserted: (state, action: PayloadAction) => {
imagesAdapter.upsertOne(state, action.payload);
},
- imageRemoved: (state, action: PayloadAction) => {
- if (isString(action.payload)) {
- imagesAdapter.removeOne(state, action.payload);
- return;
- }
-
- imagesAdapter.removeOne(state, action.payload.image_name);
- },
imageCategoriesChanged: (state, action: PayloadAction) => {
state.categories = action.payload;
},
@@ -76,6 +68,11 @@ const imagesSlice = createSlice({
state.total = total;
imagesAdapter.upsertMany(state, items);
});
+ builder.addCase(imageDeleted.pending, (state, action) => {
+ // Preemptively remove the image from the gallery
+ const { imageName } = action.meta.arg;
+ imagesAdapter.removeOne(state, imageName);
+ });
},
});
@@ -87,8 +84,7 @@ export const {
selectTotal: selectImagesTotal,
} = imagesAdapter.getSelectors((state) => state.images);
-export const { imageUpserted, imageRemoved, imageCategoriesChanged } =
- imagesSlice.actions;
+export const { imageUpserted, imageCategoriesChanged } = imagesSlice.actions;
export default imagesSlice.reducer;
From 2fc0a4d53be0d762394ba5a5319982fb7400c502 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Mon, 5 Jun 2023 20:16:43 +1000
Subject: [PATCH 07/14] feat(ui): improve handling for urls/metadata received
Update images everywhere when urls or metadata is received:
- control images
- init images
- canvas
- nodes
- init image
Also renamed the variable.
---
.../listeners/imageDeleted.ts | 5 +++
.../listeners/imageMetadataReceived.ts | 4 +++
.../listeners/imageUrlsReceived.ts | 15 ++++-----
.../frontend/web/src/app/types/invokeai.ts | 1 +
.../src/features/canvas/store/canvasSlice.ts | 21 ++++++++++++
.../controlNet/store/controlNetSlice.ts | 18 ++++++++++-
.../features/gallery/store/gallerySlice.ts | 10 ++++++
.../src/features/gallery/store/imagesSlice.ts | 32 +++++++++++++++++--
.../src/features/nodes/store/nodesSlice.ts | 20 +++++++++---
.../parameters/store/generationSlice.ts | 11 +++++++
.../src/features/system/store/configSlice.ts | 1 +
11 files changed, 122 insertions(+), 16 deletions(-)
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts
index e8e7a78165..b527b5d00b 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts
@@ -5,6 +5,7 @@ import { log } from 'app/logging/useLogger';
import { clamp } from 'lodash-es';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import {
+ imageRemoved,
selectImagesEntities,
selectImagesIds,
} from 'features/gallery/store/imagesSlice';
@@ -56,6 +57,10 @@ export const addRequestedImageDeletionListener = () => {
}
}
+ // Preemptively remove from gallery
+ dispatch(imageRemoved(image_name));
+
+ // Delete from server
dispatch(
imageDeleted({ imageName: image_name, imageOrigin: image_origin })
);
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts
index 7d7e92ff61..016e3ec8a8 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts
@@ -26,6 +26,10 @@ export const addImageMetadataReceivedFulfilledListener = () => {
);
} else if (image.is_intermediate) {
// No further actions needed for intermediate images
+ moduleLog.trace(
+ { data: { image } },
+ 'Image metadata received (intermediate), skipping'
+ );
return;
}
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUrlsReceived.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUrlsReceived.ts
index fd0461f893..2e365a20ac 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUrlsReceived.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUrlsReceived.ts
@@ -1,7 +1,7 @@
import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { imageUrlsReceived } from 'services/thunks/image';
-import { imagesAdapter } from 'features/gallery/store/imagesSlice';
+import { imageUpdatedOne } from 'features/gallery/store/imagesSlice';
const moduleLog = log.child({ namespace: 'image' });
@@ -14,13 +14,12 @@ export const addImageUrlsReceivedFulfilledListener = () => {
const { image_name, image_url, thumbnail_url } = image;
- imagesAdapter.updateOne(getState().images, {
- id: image_name,
- changes: {
- image_url,
- thumbnail_url,
- },
- });
+ dispatch(
+ imageUpdatedOne({
+ id: image_name,
+ changes: { image_url, thumbnail_url },
+ })
+ );
},
});
};
diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts
index 304b094749..fa5c725a84 100644
--- a/invokeai/frontend/web/src/app/types/invokeai.ts
+++ b/invokeai/frontend/web/src/app/types/invokeai.ts
@@ -114,6 +114,7 @@ export type AppConfig = {
/**
* Whether or not we need to re-fetch images
*/
+ shouldUpdateImageUrlsOnError: boolean;
disabledTabs: InvokeTabName[];
disabledFeatures: AppFeature[];
disabledSDFeatures: SDFeature[];
diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts
index c0b73ed3ae..4742de0483 100644
--- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts
+++ b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts
@@ -31,6 +31,7 @@ import {
import { ImageDTO } from 'services/api';
import { sessionCanceled } from 'services/thunks/session';
import { setShouldUseCanvasBetaLayout } from 'features/ui/store/uiSlice';
+import { imageUrlsReceived } from 'services/thunks/image';
export const initialLayerState: CanvasLayerState = {
objects: [],
@@ -856,6 +857,26 @@ export const canvasSlice = createSlice({
builder.addCase(setShouldUseCanvasBetaLayout, (state, action) => {
state.doesCanvasNeedScaling = true;
});
+ builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
+ const { image_name, image_origin, image_url, thumbnail_url } =
+ action.payload;
+
+ state.layerState.objects.forEach((object) => {
+ if (object.kind === 'image') {
+ if (object.image.image_name === image_name) {
+ object.image.image_url = image_url;
+ object.image.thumbnail_url = thumbnail_url;
+ }
+ }
+ });
+
+ state.layerState.stagingArea.images.forEach((stagedImage) => {
+ if (stagedImage.image.image_name === image_name) {
+ stagedImage.image.image_url = image_url;
+ stagedImage.image.thumbnail_url = thumbnail_url;
+ }
+ });
+ });
},
});
diff --git a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts
index 40714d3ecb..da76ce4a8a 100644
--- a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts
+++ b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts
@@ -13,7 +13,7 @@ import {
ControlNetModel,
} from './constants';
import { controlNetImageProcessed } from './actions';
-import { imageDeleted } from 'services/thunks/image';
+import { imageDeleted, imageUrlsReceived } from 'services/thunks/image';
import { forEach } from 'lodash-es';
export const initialControlNet: Omit = {
@@ -210,6 +210,22 @@ export const controlNetSlice = createSlice({
}
});
});
+
+ builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
+ const { image_name, image_origin, image_url, thumbnail_url } =
+ action.payload;
+
+ forEach(state.controlNets, (c) => {
+ if (c.controlImage?.image_name === image_name) {
+ c.controlImage.image_url = image_url;
+ c.controlImage.thumbnail_url = thumbnail_url;
+ }
+ if (c.processedControlImage?.image_name === image_name) {
+ c.processedControlImage.image_url = image_url;
+ c.processedControlImage.thumbnail_url = thumbnail_url;
+ }
+ });
+ });
},
});
diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
index 8e5ecf64fa..b9d091305a 100644
--- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
@@ -2,6 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { ImageDTO } from 'services/api';
import { imageUpserted } from './imagesSlice';
+import { imageUrlsReceived } from 'services/thunks/image';
type GalleryImageObjectFitType = 'contain' | 'cover';
@@ -57,6 +58,15 @@ export const gallerySlice = createSlice({
state.selectedImage = action.payload;
}
});
+ builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
+ const { image_name, image_origin, image_url, thumbnail_url } =
+ action.payload;
+
+ if (state.selectedImage?.image_name === image_name) {
+ state.selectedImage.image_url = image_url;
+ state.selectedImage.thumbnail_url = thumbnail_url;
+ }
+ });
},
});
diff --git a/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts b/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts
index 539690dcde..c9fc61d10d 100644
--- a/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts
@@ -1,5 +1,6 @@
import {
PayloadAction,
+ Update,
createEntityAdapter,
createSelector,
createSlice,
@@ -8,7 +9,12 @@ import { RootState } from 'app/store/store';
import { ImageCategory, ImageDTO } from 'services/api';
import { dateComparator } from 'common/util/dateComparator';
import { keyBy } from 'lodash-es';
-import { imageDeleted, receivedPageOfImages } from 'services/thunks/image';
+import {
+ imageDeleted,
+ imageMetadataReceived,
+ imageUrlsReceived,
+ receivedPageOfImages,
+} from 'services/thunks/image';
export const imagesAdapter = createEntityAdapter({
selectId: (image) => image.image_name,
@@ -49,6 +55,12 @@ const imagesSlice = createSlice({
imageUpserted: (state, action: PayloadAction) => {
imagesAdapter.upsertOne(state, action.payload);
},
+ imageUpdatedOne: (state, action: PayloadAction>) => {
+ imagesAdapter.updateOne(state, action.payload);
+ },
+ imageRemoved: (state, action: PayloadAction) => {
+ imagesAdapter.removeOne(state, action.payload);
+ },
imageCategoriesChanged: (state, action: PayloadAction) => {
state.categories = action.payload;
},
@@ -69,10 +81,19 @@ const imagesSlice = createSlice({
imagesAdapter.upsertMany(state, items);
});
builder.addCase(imageDeleted.pending, (state, action) => {
- // Preemptively remove the image from the gallery
+ // Image deleted
const { imageName } = action.meta.arg;
imagesAdapter.removeOne(state, imageName);
});
+ builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
+ const { image_name, image_origin, image_url, thumbnail_url } =
+ action.payload;
+
+ imagesAdapter.updateOne(state, {
+ id: image_name,
+ changes: { image_url, thumbnail_url },
+ });
+ });
},
});
@@ -84,7 +105,12 @@ export const {
selectTotal: selectImagesTotal,
} = imagesAdapter.getSelectors((state) => state.images);
-export const { imageUpserted, imageCategoriesChanged } = imagesSlice.actions;
+export const {
+ imageUpserted,
+ imageUpdatedOne,
+ imageRemoved,
+ imageCategoriesChanged,
+} = imagesSlice.actions;
export default imagesSlice.reducer;
diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
index 3c93be7ac5..50c33e88b2 100644
--- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
@@ -16,9 +16,10 @@ import { receivedOpenAPISchema } from 'services/thunks/schema';
import { InvocationTemplate, InvocationValue } from '../types/types';
import { parseSchema } from '../util/parseSchema';
import { log } from 'app/logging/useLogger';
-import { size } from 'lodash-es';
+import { forEach, size } from 'lodash-es';
import { isAnyGraphBuilt } from './actions';
import { RgbaColor } from 'react-colorful';
+import { imageUrlsReceived } from 'services/thunks/image';
export type NodesState = {
nodes: Node[];
@@ -98,9 +99,20 @@ const nodesSlice = createSlice({
state.schema = action.payload;
});
- builder.addMatcher(isAnyGraphBuilt, (state, action) => {
- // TODO: Achtung! Side effect in a reducer!
- log.info({ namespace: 'nodes', data: action.payload }, 'Graph built');
+ builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
+ const { image_name, image_origin, image_url, thumbnail_url } =
+ action.payload;
+
+ state.nodes.forEach((node) => {
+ forEach(node.data.inputs, (input) => {
+ if (input.type === 'image') {
+ if (input.value?.image_name === image_name) {
+ input.value.image_url = image_url;
+ input.value.thumbnail_url = thumbnail_url;
+ }
+ }
+ });
+ });
});
},
});
diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts
index 6420950e4a..3512ded3ab 100644
--- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts
+++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts
@@ -17,6 +17,7 @@ import {
StrengthParam,
WidthParam,
} from './parameterZodSchemas';
+import { imageUrlsReceived } from 'services/thunks/image';
export interface GenerationState {
cfgScale: CfgScaleParam;
@@ -231,6 +232,16 @@ export const generationSlice = createSlice({
state.model = defaultModel;
}
});
+
+ builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
+ const { image_name, image_origin, image_url, thumbnail_url } =
+ action.payload;
+
+ if (state.initialImage?.image_name === image_name) {
+ state.initialImage.image_url = image_url;
+ state.initialImage.thumbnail_url = thumbnail_url;
+ }
+ });
},
});
diff --git a/invokeai/frontend/web/src/features/system/store/configSlice.ts b/invokeai/frontend/web/src/features/system/store/configSlice.ts
index f8cb3a483c..5e0d2ca472 100644
--- a/invokeai/frontend/web/src/features/system/store/configSlice.ts
+++ b/invokeai/frontend/web/src/features/system/store/configSlice.ts
@@ -5,6 +5,7 @@ import { merge } from 'lodash-es';
export const initialConfigState: AppConfig = {
shouldTransformUrls: false,
+ shouldUpdateImageUrlsOnError: false,
disabledTabs: [],
disabledFeatures: [],
disabledSDFeatures: [],
From 8283d23b74f8d0d997cb04d9e0e72ef5dc22d029 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Mon, 5 Jun 2023 20:24:18 +1000
Subject: [PATCH 08/14] feat(ui): remove `shouldTransformUrls`
This is no longer used.
---
.../frontend/web/src/app/types/invokeai.ts | 6 +---
.../web/src/common/components/IAIDndImage.tsx | 4 +--
.../frontend/web/src/common/util/getUrl.ts | 34 -------------------
.../components/IAICanvasObjectRenderer.tsx | 4 +--
.../components/IAICanvasStagingArea.tsx | 4 +--
.../components/CurrentImageButtons.tsx | 15 ++------
.../gallery/components/HoverableImage.tsx | 8 ++---
.../ImageMetadataViewer.tsx | 21 +-----------
.../lightbox/components/ReactPanZoomImage.tsx | 4 +--
.../ImageToImage/InitialImagePreview.tsx | 2 --
.../src/features/system/store/configSlice.ts | 1 -
11 files changed, 10 insertions(+), 93 deletions(-)
delete mode 100644 invokeai/frontend/web/src/common/util/getUrl.ts
diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts
index fa5c725a84..f202b66ca2 100644
--- a/invokeai/frontend/web/src/app/types/invokeai.ts
+++ b/invokeai/frontend/web/src/app/types/invokeai.ts
@@ -108,11 +108,7 @@ export type SDFeature =
*/
export type AppConfig = {
/**
- * Whether or not URLs should be transformed to use a different host
- */
- shouldTransformUrls: boolean;
- /**
- * Whether or not we need to re-fetch images
+ * Whether or not we should update image urls when image loading errors
*/
shouldUpdateImageUrlsOnError: boolean;
disabledTabs: InvokeTabName[];
diff --git a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx
index 5a7f93747b..f31aebf596 100644
--- a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx
+++ b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx
@@ -4,7 +4,6 @@ import { useCombinedRefs } from '@dnd-kit/utilities';
import IAIIconButton from 'common/components/IAIIconButton';
import { IAIImageFallback } from 'common/components/IAIImageFallback';
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
-import { useGetUrl } from 'common/util/getUrl';
import { AnimatePresence } from 'framer-motion';
import { ReactElement, SyntheticEvent } from 'react';
import { memo, useRef } from 'react';
@@ -45,7 +44,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {
minSize = 24,
} = props;
const dndId = useRef(uuidv4());
- const { getUrl } = useGetUrl();
const {
isOver,
setNodeRef: setDroppableRef,
@@ -100,7 +98,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
}}
>
{
- if (OpenAPI.BASE && shouldTransformUrls) {
- return [OpenAPI.BASE, url].join('/');
- }
-
- return url;
-};
-
-export const useGetUrl = () => {
- const shouldTransformUrls = useAppSelector(
- (state: RootState) => state.config.shouldTransformUrls
- );
-
- const getUrl = useCallback(
- (url?: string) => {
- if (OpenAPI.BASE && shouldTransformUrls) {
- return [OpenAPI.BASE, url].join('/');
- }
-
- return url;
- },
- [shouldTransformUrls]
- );
-
- return {
- shouldTransformUrls,
- getUrl,
- };
-};
diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx
index c99465cf40..ea04aa95c8 100644
--- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx
+++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasObjectRenderer.tsx
@@ -1,6 +1,5 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
-import { useGetUrl } from 'common/util/getUrl';
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
import { rgbaColorToString } from 'features/canvas/util/colorToString';
import { isEqual } from 'lodash-es';
@@ -33,7 +32,6 @@ const selector = createSelector(
const IAICanvasObjectRenderer = () => {
const { objects } = useAppSelector(selector);
- const { getUrl } = useGetUrl();
if (!objects) return null;
@@ -46,7 +44,7 @@ const IAICanvasObjectRenderer = () => {
key={i}
x={obj.x}
y={obj.y}
- url={getUrl(obj.image.image_url)}
+ url={obj.image.image_url}
/>
);
} else if (isCanvasBaseLine(obj)) {
diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx
index f03aeedb86..c33e0cacf5 100644
--- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx
+++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingArea.tsx
@@ -1,6 +1,5 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
-import { useGetUrl } from 'common/util/getUrl';
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
import { GroupConfig } from 'konva/lib/Group';
import { isEqual } from 'lodash-es';
@@ -56,13 +55,12 @@ const IAICanvasStagingArea = (props: Props) => {
width,
height,
} = useAppSelector(selector);
- const { getUrl } = useGetUrl();
return (
{shouldShowStagingImage && currentStagingAreaImage && (
diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx
index 91bd1a0425..6862b35fb8 100644
--- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx
@@ -47,7 +47,6 @@ import {
import { gallerySelector } from '../store/gallerySelectors';
import { useCallback } from 'react';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
-import { useGetUrl } from 'common/util/getUrl';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
import { initialImageSelected } from 'features/parameters/store/actions';
@@ -153,8 +152,6 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled;
const isFaceRestoreEnabled = useFeatureStatus('faceRestore').isFeatureEnabled;
- const { getUrl, shouldTransformUrls } = useGetUrl();
-
const {
isOpen: isDeleteDialogOpen,
onOpen: onDeleteDialogOpen,
@@ -197,10 +194,6 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
return;
}
- if (shouldTransformUrls) {
- return getUrl(image.image_url);
- }
-
if (image.image_url.startsWith('http')) {
return image.image_url;
}
@@ -229,7 +222,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
isClosable: true,
});
});
- }, [toaster, shouldTransformUrls, getUrl, t, image]);
+ }, [toaster, t, image]);
const handleClickUseAllParameters = useCallback(() => {
recallAllParameters(image);
@@ -461,11 +454,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
{t('parameters.copyImageToLink')}
-
+
} size="sm" w="100%">
{t('parameters.downloadImage')}
diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx
index 4dad27d4e8..ef4ed5be1c 100644
--- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx
@@ -21,7 +21,6 @@ import { gallerySelector } from 'features/gallery/store/gallerySelectors';
import { setActiveTab } from 'features/ui/store/uiSlice';
import { useTranslation } from 'react-i18next';
import IAIIconButton from 'common/components/IAIIconButton';
-import { useGetUrl } from 'common/util/getUrl';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
import { createSelector } from '@reduxjs/toolkit';
@@ -104,7 +103,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const { image, isSelected } = props;
const { image_url, thumbnail_url, image_name } = image;
- const { getUrl } = useGetUrl();
const [isHovered, setIsHovered] = useState(false);
@@ -208,7 +206,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
};
const handleOpenInNewTab = () => {
- window.open(getUrl(image.image_url), '_blank');
+ window.open(image.image_url, '_blank');
};
return (
@@ -296,8 +294,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
userSelect="none"
- // draggable={true}
- // onDragStart={handleDragStart}
onClick={handleSelectImage}
ref={ref}
sx={{
@@ -317,7 +313,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
shouldUseSingleGalleryColumn ? 'contain' : galleryImageObjectFit
}
rounded="md"
- src={getUrl(thumbnail_url || image_url)}
+ src={thumbnail_url || image_url}
fallback={}
sx={{
width: '100%',
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx
index 1619680ec5..1a8801fa52 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx
@@ -9,19 +9,6 @@ import {
Tooltip,
} from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
-import { useGetUrl } from 'common/util/getUrl';
-import promptToString from 'common/util/promptToString';
-import {
- setCfgScale,
- setHeight,
- setImg2imgStrength,
- setNegativePrompt,
- setPositivePrompt,
- setScheduler,
- setSeed,
- setSteps,
- setWidth,
-} from 'features/parameters/store/generationSlice';
import { setShouldShowImageDetails } from 'features/ui/store/uiSlice';
import { memo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
@@ -30,7 +17,6 @@ import { FaCopy } from 'react-icons/fa';
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { ImageDTO } from 'services/api';
-import { Scheduler } from 'app/constants';
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
type MetadataItemProps = {
@@ -146,7 +132,6 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
const metadata = image?.metadata;
const { t } = useTranslation();
- const { getUrl } = useGetUrl();
const metadataJSON = JSON.stringify(image, null, 2);
@@ -168,11 +153,7 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
>
File:
-
+
{image.image_name}
diff --git a/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx b/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx
index b1e822c309..7ec7d23371 100644
--- a/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx
+++ b/invokeai/frontend/web/src/features/lightbox/components/ReactPanZoomImage.tsx
@@ -1,6 +1,5 @@
import * as React from 'react';
import { TransformComponent, useTransformContext } from 'react-zoom-pan-pinch';
-import { useGetUrl } from 'common/util/getUrl';
import { ImageDTO } from 'services/api';
type ReactPanZoomProps = {
@@ -23,7 +22,6 @@ export default function ReactPanZoomImage({
scaleY,
}: ReactPanZoomProps) {
const { centerView } = useTransformContext();
- const { getUrl } = useGetUrl();
return (
{
const { initialImage } = useAppSelector(selector);
const { shouldFetchImages } = useAppSelector(configSelector);
- const { getUrl } = useGetUrl();
const dispatch = useAppDispatch();
const { t } = useTranslation();
const toaster = useAppToaster();
diff --git a/invokeai/frontend/web/src/features/system/store/configSlice.ts b/invokeai/frontend/web/src/features/system/store/configSlice.ts
index 5e0d2ca472..fb00a7a5d4 100644
--- a/invokeai/frontend/web/src/features/system/store/configSlice.ts
+++ b/invokeai/frontend/web/src/features/system/store/configSlice.ts
@@ -4,7 +4,6 @@ import { AppConfig, PartialAppConfig } from 'app/types/invokeai';
import { merge } from 'lodash-es';
export const initialConfigState: AppConfig = {
- shouldTransformUrls: false,
shouldUpdateImageUrlsOnError: false,
disabledTabs: [],
disabledFeatures: [],
From b20045133025b5240416350296a50a0deab41cf0 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Mon, 5 Jun 2023 22:02:23 +1000
Subject: [PATCH 09/14] feat(ui): add nodesSelector
---
invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
index 50c33e88b2..0f143b3a6a 100644
--- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
@@ -17,9 +17,9 @@ import { InvocationTemplate, InvocationValue } from '../types/types';
import { parseSchema } from '../util/parseSchema';
import { log } from 'app/logging/useLogger';
import { forEach, size } from 'lodash-es';
-import { isAnyGraphBuilt } from './actions';
import { RgbaColor } from 'react-colorful';
import { imageUrlsReceived } from 'services/thunks/image';
+import { RootState } from 'app/store/store';
export type NodesState = {
nodes: Node[];
@@ -130,3 +130,5 @@ export const {
} = nodesSlice.actions;
export default nodesSlice.reducer;
+
+export const nodesSelecter = (state: RootState) => state.nodes;
From fa338ddb6a43f03dca6cd2de4e930ac30baa44fb Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Mon, 5 Jun 2023 22:10:21 +1000
Subject: [PATCH 10/14] feat(ui): add useGetIsImageInUse
Checks if an image is currently being used eg in canvas, nodes, controlnet, init image.
---
.../src/common/hooks/useGetIsImageInUse.ts | 54 +++++++++++++++++++
1 file changed, 54 insertions(+)
create mode 100644 invokeai/frontend/web/src/common/hooks/useGetIsImageInUse.ts
diff --git a/invokeai/frontend/web/src/common/hooks/useGetIsImageInUse.ts b/invokeai/frontend/web/src/common/hooks/useGetIsImageInUse.ts
new file mode 100644
index 0000000000..ad14941d16
--- /dev/null
+++ b/invokeai/frontend/web/src/common/hooks/useGetIsImageInUse.ts
@@ -0,0 +1,54 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppSelector } from 'app/store/storeHooks';
+import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
+import { canvasSelector } from 'features/canvas/store/canvasSelectors';
+import { controlNetSelector } from 'features/controlNet/store/controlNetSlice';
+import { nodesSelecter } from 'features/nodes/store/nodesSlice';
+import { generationSelector } from 'features/parameters/store/generationSelectors';
+import { some } from 'lodash-es';
+
+const selectIsImageInUse = createSelector(
+ [
+ generationSelector,
+ canvasSelector,
+ nodesSelecter,
+ controlNetSelector,
+ (state, image_name) => image_name,
+ ],
+ (generation, canvas, nodes, controlNet, image_name) => {
+ const isInitialImage = generation.initialImage?.image_name === image_name;
+
+ const isCanvasImage = canvas.layerState.objects.some(
+ (obj) => obj.kind === 'image' && obj.image.image_name === image_name
+ );
+
+ const isNodesImage = nodes.nodes.some((node) => {
+ return some(
+ node.data.inputs,
+ (input) =>
+ input.type === 'image' && input.value?.image_name === image_name
+ );
+ });
+
+ const isControlNetImage = some(
+ controlNet.controlNets,
+ (c) =>
+ c.controlImage?.image_name === image_name ||
+ c.processedControlImage?.image_name === image_name
+ );
+
+ return {
+ isInitialImage,
+ isCanvasImage,
+ isNodesImage,
+ isControlNetImage,
+ };
+ },
+ defaultSelectorOptions
+);
+
+export const useGetIsImageInUse = (image_name?: string) => {
+ const a = useAppSelector((state) => selectIsImageInUse(state, image_name));
+
+ return a;
+};
From 3d249c4fa3bebafe744c4e9e52b3159bc3450d5a Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 6 Jun 2023 00:40:40 +1000
Subject: [PATCH 11/14] feat(ui): refactor image deletion
Add `DeleteImageContext`:
- provide a single function to delete an image
- opens the modal or immediately deletes, if confirm is off
---
.../frontend/web/src/app/components/App.tsx | 2 +
.../web/src/app/components/InvokeAIUI.tsx | 16 ++-
.../src/app/contexts/DeleteImageContext.tsx | 107 ++++++++++++++++++
.../components/CurrentImageButtons.tsx | 61 ++--------
.../components/CurrentImagePreview.tsx | 3 +
.../{DeleteImageModal.tsx => DeleteModal.tsx} | 59 +++++++---
.../gallery/components/HoverableImage.tsx | 66 +++--------
.../ImageActionButtons/DeleteImageButton.tsx | 92 ---------------
.../ImageToImage/InitialImagePreview.tsx | 1 +
9 files changed, 191 insertions(+), 216 deletions(-)
create mode 100644 invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx
rename invokeai/frontend/web/src/features/gallery/components/{DeleteImageModal.tsx => DeleteModal.tsx} (70%)
delete mode 100644 invokeai/frontend/web/src/features/gallery/components/ImageActionButtons/DeleteImageButton.tsx
diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx
index 21b3945490..67d0091261 100644
--- a/invokeai/frontend/web/src/app/components/App.tsx
+++ b/invokeai/frontend/web/src/app/components/App.tsx
@@ -21,6 +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';
const DEFAULT_CONFIG = {};
@@ -133,6 +134,7 @@ const App = ({
+
>
diff --git a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx
index c94f7624b2..0537d1de2a 100644
--- a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx
+++ b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx
@@ -17,6 +17,10 @@ import '../../i18n';
import { socketMiddleware } from 'services/events/middleware';
import { Middleware } from '@reduxjs/toolkit';
import ImageDndContext from './ImageDnd/ImageDndContext';
+import {
+ DeleteImageContext,
+ DeleteImageContextProvider,
+} from 'app/contexts/DeleteImageContext';
const App = lazy(() => import('./App'));
const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider'));
@@ -71,11 +75,13 @@ const InvokeAIUI = ({
}>
-
+
+
+
diff --git a/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx b/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx
new file mode 100644
index 0000000000..1d129d4e00
--- /dev/null
+++ b/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx
@@ -0,0 +1,107 @@
+import { useDisclosure } from '@chakra-ui/react';
+import { createSelector } from '@reduxjs/toolkit';
+import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
+import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
+import { requestedImageDeletion } from 'features/gallery/store/actions';
+import { systemSelector } from 'features/system/store/systemSelectors';
+import { PropsWithChildren, createContext, useCallback, useState } from 'react';
+import { ImageDTO } from 'services/api';
+
+type DeleteImageContextValue = {
+ /**
+ * Whether the delete image dialog is open.
+ */
+ isOpen: boolean;
+ /**
+ * Closes the delete image dialog.
+ */
+ onClose: () => void;
+ /**
+ * 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({
+ isOpen: false,
+ onClose: () => undefined,
+ onImmediatelyDelete: () => undefined,
+ onDelete: () => undefined,
+});
+
+const selector = createSelector(
+ [systemSelector],
+ (system) => {
+ const { isProcessing, isConnected, shouldConfirmOnDelete } = system;
+
+ return {
+ canDeleteImage: isConnected && !isProcessing,
+ shouldConfirmOnDelete,
+ isProcessing,
+ isConnected,
+ };
+ },
+ defaultSelectorOptions
+);
+
+type Props = PropsWithChildren;
+
+export const DeleteImageContextProvider = (props: Props) => {
+ const { canDeleteImage, shouldConfirmOnDelete } = useAppSelector(selector);
+ const [imageToDelete, setImageToDelete] = useState();
+ const dispatch = useAppDispatch();
+ const { isOpen, onOpen, onClose } = useDisclosure();
+
+ const closeAndClearImageToDelete = useCallback(() => {
+ setImageToDelete(undefined);
+ onClose();
+ }, [onClose]);
+
+ const onImmediatelyDelete = useCallback(() => {
+ if (canDeleteImage && imageToDelete) {
+ dispatch(requestedImageDeletion(imageToDelete));
+ }
+ closeAndClearImageToDelete();
+ }, [canDeleteImage, imageToDelete, closeAndClearImageToDelete, dispatch]);
+
+ const handleDelete = useCallback(
+ (image: ImageDTO) => {
+ if (shouldConfirmOnDelete) {
+ onOpen();
+ } else {
+ dispatch(requestedImageDeletion(image));
+ }
+ },
+ [shouldConfirmOnDelete, onOpen, dispatch]
+ );
+
+ const onDelete = useCallback(
+ (image?: ImageDTO) => {
+ if (!image) {
+ return;
+ }
+ setImageToDelete(image);
+ handleDelete(image);
+ },
+ [handleDelete]
+ );
+
+ return (
+
+ {props.children}
+
+ );
+};
diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx
index 6862b35fb8..333ad516ef 100644
--- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx
@@ -1,13 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { isEqual } from 'lodash-es';
-import {
- ButtonGroup,
- Flex,
- FlexProps,
- Link,
- useDisclosure,
-} from '@chakra-ui/react';
+import { ButtonGroup, Flex, FlexProps, Link } from '@chakra-ui/react';
// import { runESRGAN, runFacetool } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
@@ -45,21 +39,18 @@ import {
FaShareAlt,
} from 'react-icons/fa';
import { gallerySelector } from '../store/gallerySelectors';
-import { useCallback } from 'react';
+import { useCallback, useContext } from 'react';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
import { initialImageSelected } from 'features/parameters/store/actions';
-import {
- requestedImageDeletion,
- sentImageToCanvas,
- sentImageToImg2Img,
-} from '../store/actions';
+import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions';
import FaceRestoreSettings from 'features/parameters/components/Parameters/FaceRestore/FaceRestoreSettings';
import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/UpscaleSettings';
-import DeleteImageButton from './ImageActionButtons/DeleteImageButton';
import { useAppToaster } from 'app/components/Toaster';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
+import { DeleteImageContext } from 'app/contexts/DeleteImageContext';
+import { DeleteImageButton } from './DeleteModal';
const currentImageButtonsSelector = createSelector(
[
@@ -122,10 +113,6 @@ const currentImageButtonsSelector = createSelector(
type CurrentImageButtonsProps = FlexProps;
-/**
- * Row of buttons for common actions:
- * Use as init image, use all params, use seed, upscale, fix faces, details, delete.
- */
const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
const dispatch = useAppDispatch();
const {
@@ -137,13 +124,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
facetoolStrength,
shouldDisableToolbarButtons,
shouldShowImageDetails,
- // currentImage,
isLightboxOpen,
activeTabName,
shouldHidePreview,
image,
- canDeleteImage,
- shouldConfirmOnDelete,
shouldShowProgressInViewer,
} = useAppSelector(currentImageButtonsSelector);
@@ -152,18 +136,14 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled;
const isFaceRestoreEnabled = useFeatureStatus('faceRestore').isFeatureEnabled;
- const {
- isOpen: isDeleteDialogOpen,
- onOpen: onDeleteDialogOpen,
- onClose: onDeleteDialogClose,
- } = useDisclosure();
-
const toaster = useAppToaster();
const { t } = useTranslation();
const { recallBothPrompts, recallSeed, recallAllParameters } =
useRecallParameters();
+ const { onDelete } = useContext(DeleteImageContext);
+
// const handleCopyImage = useCallback(async () => {
// if (!image?.url) {
// return;
@@ -262,6 +242,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
// selectedImage && dispatch(runESRGAN(selectedImage));
}, []);
+ const handleDelete = useCallback(() => {
+ onDelete(image);
+ }, [image, onDelete]);
+
useHotkeys(
'Shift+U',
() => {
@@ -363,31 +347,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
[image, shouldShowImageDetails, toaster]
);
- const handleDelete = useCallback(() => {
- if (canDeleteImage && image) {
- dispatch(requestedImageDeletion(image));
- }
- }, [image, canDeleteImage, dispatch]);
-
- const handleInitiateDelete = useCallback(() => {
- if (shouldConfirmOnDelete) {
- onDeleteDialogOpen();
- } else {
- handleDelete();
- }
- }, [shouldConfirmOnDelete, onDeleteDialogOpen, handleDelete]);
-
const handleClickProgressImagesToggle = useCallback(() => {
dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer));
}, [dispatch, shouldShowProgressInViewer]);
- useHotkeys('delete', handleInitiateDelete, [
- image,
- shouldConfirmOnDelete,
- isConnected,
- isProcessing,
- ]);
-
const handleLightBox = useCallback(() => {
dispatch(setIsLightboxOpen(!isLightboxOpen));
}, [dispatch, isLightboxOpen]);
@@ -596,7 +559,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
-
+
>
diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx
index 5e210bf4b7..b8d9d6220a 100644
--- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx
@@ -15,6 +15,7 @@ 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],
@@ -54,6 +55,8 @@ 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/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/gallery/components/DeleteModal.tsx
similarity index 70%
rename from invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx
rename to invokeai/frontend/web/src/features/gallery/components/DeleteModal.tsx
index 12038f4179..ca06aa7953 100644
--- a/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/DeleteModal.tsx
@@ -9,16 +9,19 @@ import {
Text,
} from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
+import { DeleteImageContext } from 'app/contexts/DeleteImageContext';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton';
+import IAIIconButton from 'common/components/IAIIconButton';
import IAISwitch from 'common/components/IAISwitch';
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 { ChangeEvent, memo, useCallback, useRef } from 'react';
+import { ChangeEvent, memo, useCallback, useContext, useRef } from 'react';
import { useTranslation } from 'react-i18next';
+import { FaTrash } from 'react-icons/fa';
const selector = createSelector(
[systemSelector, configSelector],
@@ -34,22 +37,12 @@ const selector = createSelector(
}
);
-interface DeleteImageModalProps {
- isOpen: boolean;
- onClose: () => void;
- handleDelete: () => void;
-}
-
-const DeleteImageModal = ({
- isOpen,
- onClose,
- handleDelete,
-}: DeleteImageModalProps) => {
+const DeleteImageModal = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
+
const { shouldConfirmOnDelete, canRestoreDeletedImagesFromBin } =
useAppSelector(selector);
- const cancelRef = useRef(null);
const handleChangeShouldConfirmOnDelete = useCallback(
(e: ChangeEvent) =>
@@ -57,10 +50,10 @@ const DeleteImageModal = ({
[dispatch]
);
- const handleClickDelete = useCallback(() => {
- handleDelete();
- onClose();
- }, [handleDelete, onClose]);
+ const { isOpen, onClose, onImmediatelyDelete } =
+ useContext(DeleteImageContext);
+
+ const cancelRef = useRef(null);
return (
Cancel
-
+
Delete
@@ -107,3 +100,33 @@ const DeleteImageModal = ({
};
export default memo(DeleteImageModal);
+
+const deleteImageButtonsSelector = createSelector(
+ [systemSelector],
+ (system) => {
+ const { isProcessing, isConnected } = system;
+
+ return isConnected && !isProcessing;
+ }
+);
+
+type DeleteImageButtonProps = {
+ onClick: () => void;
+};
+
+export const DeleteImageButton = (props: DeleteImageButtonProps) => {
+ const { onClick } = props;
+ const { t } = useTranslation();
+ const canDeleteImage = useAppSelector(deleteImageButtonsSelector);
+
+ return (
+ }
+ tooltip={`${t('gallery.deleteImage')} (Del)`}
+ aria-label={`${t('gallery.deleteImage')} (Del)`}
+ isDisabled={!canDeleteImage}
+ colorScheme="error"
+ />
+ );
+};
diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx
index ef4ed5be1c..2b8f72101d 100644
--- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx
@@ -1,17 +1,8 @@
-import {
- Box,
- Flex,
- Icon,
- Image,
- MenuItem,
- MenuList,
- useDisclosure,
-} from '@chakra-ui/react';
+import { Box, Flex, Icon, Image, MenuItem, MenuList } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { imageSelected } from 'features/gallery/store/gallerySlice';
-import { DragEvent, MouseEvent, memo, useCallback, useState } from 'react';
+import { memo, useCallback, useContext, useState } from 'react';
import { FaCheck, FaExpand, FaImage, FaShare, FaTrash } from 'react-icons/fa';
-import DeleteImageModal from './DeleteImageModal';
import { ContextMenu } from 'chakra-ui-contextmenu';
import {
resizeAndScaleCanvas,
@@ -31,14 +22,11 @@ import { isEqual } from 'lodash-es';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
import { initialImageSelected } from 'features/parameters/store/actions';
-import {
- requestedImageDeletion,
- sentImageToCanvas,
- sentImageToImg2Img,
-} from '../store/actions';
+import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions';
import { useAppToaster } from 'app/components/Toaster';
import { ImageDTO } from 'services/api';
import { useDraggable } from '@dnd-kit/core';
+import { DeleteImageContext } from 'app/contexts/DeleteImageContext';
export const selector = createSelector(
[gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector],
@@ -92,27 +80,22 @@ const HoverableImage = memo((props: HoverableImageProps) => {
galleryImageMinimumWidth,
canDeleteImage,
shouldUseSingleGalleryColumn,
- shouldConfirmOnDelete,
} = useAppSelector(selector);
- const {
- isOpen: isDeleteDialogOpen,
- onOpen: onDeleteDialogOpen,
- onClose: onDeleteDialogClose,
- } = useDisclosure();
-
const { image, isSelected } = props;
const { image_url, thumbnail_url, image_name } = image;
const [isHovered, setIsHovered] = useState(false);
-
const toaster = useAppToaster();
const { t } = useTranslation();
-
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
+ const { onDelete } = useContext(DeleteImageContext);
+ const handleDelete = useCallback(() => {
+ onDelete(image);
+ }, [image, onDelete]);
const { recallBothPrompts, recallSeed, recallAllParameters } =
useRecallParameters();
@@ -126,26 +109,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const handleMouseOver = () => setIsHovered(true);
const handleMouseOut = () => setIsHovered(false);
- // Immediately deletes an image
- const handleDelete = useCallback(() => {
- if (canDeleteImage && image) {
- dispatch(requestedImageDeletion(image));
- }
- }, [dispatch, image, canDeleteImage]);
-
- // Opens the alert dialog to check if user is sure they want to delete
- const handleInitiateDelete = useCallback(
- (e: MouseEvent) => {
- e.stopPropagation();
- if (shouldConfirmOnDelete) {
- onDeleteDialogOpen();
- } else {
- handleDelete();
- }
- },
- [handleDelete, onDeleteDialogOpen, shouldConfirmOnDelete]
- );
-
const handleSelectImage = useCallback(() => {
dispatch(imageSelected(image));
}, [image, dispatch]);
@@ -281,7 +244,11 @@ const HoverableImage = memo((props: HoverableImageProps) => {
{t('parameters.sendToUnifiedCanvas')}
)}
- } onClickCapture={onDeleteDialogOpen}>
+ }
+ onClickCapture={handleDelete}
+ >
{t('gallery.deleteImage')}
@@ -357,7 +324,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
}}
>
}
size="xs"
@@ -369,11 +336,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
)}
-
);
}, memoEqualityCheck);
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageActionButtons/DeleteImageButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageActionButtons/DeleteImageButton.tsx
deleted file mode 100644
index 4b0f6e60dd..0000000000
--- a/invokeai/frontend/web/src/features/gallery/components/ImageActionButtons/DeleteImageButton.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import { createSelector } from '@reduxjs/toolkit';
-
-import { useDisclosure } from '@chakra-ui/react';
-import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import IAIIconButton from 'common/components/IAIIconButton';
-import { systemSelector } from 'features/system/store/systemSelectors';
-
-import { useHotkeys } from 'react-hotkeys-hook';
-import { useTranslation } from 'react-i18next';
-import { FaTrash } from 'react-icons/fa';
-import { memo, useCallback } from 'react';
-import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
-import DeleteImageModal from '../DeleteImageModal';
-import { requestedImageDeletion } from 'features/gallery/store/actions';
-import { ImageDTO } from 'services/api';
-
-const selector = createSelector(
- [systemSelector],
- (system) => {
- const { isProcessing, isConnected, shouldConfirmOnDelete } = system;
-
- return {
- canDeleteImage: isConnected && !isProcessing,
- shouldConfirmOnDelete,
- isProcessing,
- isConnected,
- };
- },
- defaultSelectorOptions
-);
-
-type DeleteImageButtonProps = {
- image: ImageDTO | undefined;
-};
-
-const DeleteImageButton = (props: DeleteImageButtonProps) => {
- const { image } = props;
- const dispatch = useAppDispatch();
- const { isProcessing, isConnected, canDeleteImage, shouldConfirmOnDelete } =
- useAppSelector(selector);
-
- const {
- isOpen: isDeleteDialogOpen,
- onOpen: onDeleteDialogOpen,
- onClose: onDeleteDialogClose,
- } = useDisclosure();
-
- const { t } = useTranslation();
-
- const handleDelete = useCallback(() => {
- if (canDeleteImage && image) {
- dispatch(requestedImageDeletion(image));
- }
- }, [image, canDeleteImage, dispatch]);
-
- const handleInitiateDelete = useCallback(() => {
- if (shouldConfirmOnDelete) {
- onDeleteDialogOpen();
- } else {
- handleDelete();
- }
- }, [shouldConfirmOnDelete, onDeleteDialogOpen, handleDelete]);
-
- useHotkeys('delete', handleInitiateDelete, [
- image,
- shouldConfirmOnDelete,
- isConnected,
- isProcessing,
- ]);
-
- return (
- <>
- }
- tooltip={`${t('gallery.deleteImage')} (Del)`}
- aria-label={`${t('gallery.deleteImage')} (Del)`}
- isDisabled={!image || !isConnected}
- colorScheme="error"
- />
- {image && (
-
- )}
- >
- );
-};
-
-export default memo(DeleteImageButton);
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 c006215256..73efb69728 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,6 +14,7 @@ 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],
From bf116927e10a3dd9432e3c273027cc0126b2b518 Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 6 Jun 2023 01:30:06 +1000
Subject: [PATCH 12/14] feat(ui): clear features if image used by them is
deleted
This handles the case when an image is deleted but is still in use in as eg an init image on canvas, or a control image. If we just delete the image, canvas/controlnet/etc may break (the image would just fail to load).
When an image is deleted, the app checks to see if it is in use in:
- Image to Image
- ControlNet
- Unified Canvas
- Node Editor
The delete dialog will always open if the image is in use anywhere, and the user is advised that deleting the image will reset the feature(s).
Even if the user has ticked the box to not confirm on delete, the dialog will still show if the image is in use somewhere.
---
.../frontend/web/src/app/components/App.tsx | 4 +-
.../src/app/contexts/DeleteImageContext.tsx | 71 +++++++++++++++----
...useGetIsImageInUse.ts => useImageUsage.ts} | 24 +++++--
.../controlNet/store/controlNetSlice.ts | 4 ++
.../components/CurrentImageButtons.tsx | 2 +-
.../components/CurrentImagePreview.tsx | 3 -
.../{DeleteModal.tsx => DeleteImageModal.tsx} | 68 +++++++++++++-----
.../src/features/nodes/store/nodesSlice.ts | 4 ++
.../ImageToImage/InitialImagePreview.tsx | 1 -
9 files changed, 135 insertions(+), 46 deletions(-)
rename invokeai/frontend/web/src/common/hooks/{useGetIsImageInUse.ts => useImageUsage.ts} (73%)
rename invokeai/frontend/web/src/features/gallery/components/{DeleteModal.tsx => DeleteImageModal.tsx} (68%)
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],
From bbb2a08e8f81a49a90475b5f96ae6658518fe0eb Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Tue, 6 Jun 2023 20:01:27 +1000
Subject: [PATCH 13/14] feat(ui): fix bugs with image deletion
- `imageUsage` object was always stale due to react component lifecycle, fixed this
- cleaned up the deletion listener and context
---
.../src/app/contexts/DeleteImageContext.tsx | 143 ++++++++++++------
.../listeners/imageDeleted.ts | 28 +++-
.../web/src/common/hooks/useImageUsage.ts | 66 --------
.../gallery/components/DeleteImageModal.tsx | 16 +-
.../web/src/features/gallery/store/actions.ts | 13 +-
5 files changed, 140 insertions(+), 126 deletions(-)
delete mode 100644 invokeai/frontend/web/src/common/hooks/useImageUsage.ts
diff --git a/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx b/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx
index 2f2bc4625b..8263b48114 100644
--- a/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx
+++ b/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx
@@ -4,14 +4,69 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { requestedImageDeletion } from 'features/gallery/store/actions';
import { systemSelector } from 'features/system/store/systemSelectors';
-import { PropsWithChildren, createContext, useCallback, useState } from 'react';
+import {
+ PropsWithChildren,
+ createContext,
+ useCallback,
+ useEffect,
+ useState,
+} from 'react';
import { ImageDTO } from 'services/api';
+import { RootState } from 'app/store/store';
+import { canvasSelector } from 'features/canvas/store/canvasSelectors';
+import { controlNetSelector } from 'features/controlNet/store/controlNetSlice';
+import { nodesSelecter } from 'features/nodes/store/nodesSlice';
+import { generationSelector } from 'features/parameters/store/generationSelectors';
+import { some } from 'lodash-es';
-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';
+export type ImageUsage = {
+ isInitialImage: boolean;
+ isCanvasImage: boolean;
+ isNodesImage: boolean;
+ isControlNetImage: boolean;
+};
+
+export const selectImageUsage = createSelector(
+ [
+ generationSelector,
+ canvasSelector,
+ nodesSelecter,
+ controlNetSelector,
+ (state: RootState, image_name?: string) => image_name,
+ ],
+ (generation, canvas, nodes, controlNet, image_name) => {
+ const isInitialImage = generation.initialImage?.image_name === image_name;
+
+ const isCanvasImage = canvas.layerState.objects.some(
+ (obj) => obj.kind === 'image' && obj.image.image_name === image_name
+ );
+
+ const isNodesImage = nodes.nodes.some((node) => {
+ return some(
+ node.data.inputs,
+ (input) =>
+ input.type === 'image' && input.value?.image_name === image_name
+ );
+ });
+
+ const isControlNetImage = some(
+ controlNet.controlNets,
+ (c) =>
+ c.controlImage?.image_name === image_name ||
+ c.processedControlImage?.image_name === image_name
+ );
+
+ const imageUsage: ImageUsage = {
+ isInitialImage,
+ isCanvasImage,
+ isNodesImage,
+ isControlNetImage,
+ };
+
+ return imageUsage;
+ },
+ defaultSelectorOptions
+);
type DeleteImageContextValue = {
/**
@@ -30,6 +85,10 @@ type DeleteImageContextValue = {
* The image pending deletion
*/
image?: ImageDTO;
+ /**
+ * The features in which this image is used
+ */
+ imageUsage?: ImageUsage;
/**
* Immediately deletes an image.
*
@@ -65,41 +124,28 @@ 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,
- ]
+ // Check where the image to be deleted is used (eg init image, controlnet, etc.)
+ const imageUsage = useAppSelector((state) =>
+ selectImageUsage(state, imageToDelete?.image_name)
);
+ // Clean up after deleting or dismissing the modal
const closeAndClearImageToDelete = useCallback(() => {
setImageToDelete(undefined);
onClose();
}, [onClose]);
+ // Dispatch the actual deletion action, to be handled by listener middleware
+ const handleActualDeletion = useCallback(
+ (image: ImageDTO) => {
+ dispatch(requestedImageDeletion({ image, imageUsage }));
+ closeAndClearImageToDelete();
+ },
+ [closeAndClearImageToDelete, dispatch, imageUsage]
+ );
+
+ // This is intended to be called by the delete button in the dialog
const onImmediatelyDelete = useCallback(() => {
if (canDeleteImage && imageToDelete) {
handleActualDeletion(imageToDelete);
@@ -114,25 +160,31 @@ export const DeleteImageContextProvider = (props: Props) => {
const handleGatedDeletion = useCallback(
(image: ImageDTO) => {
- if (shouldConfirmOnDelete || imageUsage) {
+ if (shouldConfirmOnDelete || some(imageUsage)) {
+ // If we should confirm on delete, or if the image is in use, open the dialog
onOpen();
} else {
handleActualDeletion(image);
}
},
- [shouldConfirmOnDelete, imageUsage, onOpen, handleActualDeletion]
+ [imageUsage, shouldConfirmOnDelete, onOpen, handleActualDeletion]
);
- const onDelete = useCallback(
- (image?: ImageDTO) => {
- if (!image) {
- return;
- }
- setImageToDelete(image);
- handleGatedDeletion(image);
- },
- [handleGatedDeletion]
- );
+ // Consumers of the context call this to delete an image
+ const onDelete = useCallback((image?: ImageDTO) => {
+ if (!image) {
+ return;
+ }
+ // Set the image to delete, then let the effect call the actual deletion
+ setImageToDelete(image);
+ }, []);
+
+ useEffect(() => {
+ // We need to use an effect here to trigger the image usage selector, else we get a stale value
+ if (imageToDelete) {
+ handleGatedDeletion(imageToDelete);
+ }
+ }, [handleGatedDeletion, imageToDelete]);
return (
{
onClose: closeAndClearImageToDelete,
onDelete,
onImmediatelyDelete,
+ imageUsage,
}}
>
{props.children}
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts
index b527b5d00b..f4376a4959 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts
@@ -9,6 +9,10 @@ import {
selectImagesEntities,
selectImagesIds,
} from 'features/gallery/store/imagesSlice';
+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';
const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' });
@@ -19,11 +23,7 @@ export const addRequestedImageDeletionListener = () => {
startAppListening({
actionCreator: requestedImageDeletion,
effect: (action, { dispatch, getState }) => {
- const image = action.payload;
- if (!image) {
- moduleLog.warn('No image provided');
- return;
- }
+ const { image, imageUsage } = action.payload;
const { image_name, image_origin } = image;
@@ -57,6 +57,24 @@ export const addRequestedImageDeletionListener = () => {
}
}
+ // We need to reset the features where the image is in use - none of these work if their image(s) don't exist
+
+ if (imageUsage.isCanvasImage) {
+ dispatch(resetCanvas());
+ }
+
+ if (imageUsage.isControlNetImage) {
+ dispatch(controlNetReset());
+ }
+
+ if (imageUsage.isInitialImage) {
+ dispatch(clearInitialImage());
+ }
+
+ if (imageUsage.isNodesImage) {
+ dispatch(nodeEditorReset());
+ }
+
// Preemptively remove from gallery
dispatch(imageRemoved(image_name));
diff --git a/invokeai/frontend/web/src/common/hooks/useImageUsage.ts b/invokeai/frontend/web/src/common/hooks/useImageUsage.ts
deleted file mode 100644
index cf762f7880..0000000000
--- a/invokeai/frontend/web/src/common/hooks/useImageUsage.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import { createSelector } from '@reduxjs/toolkit';
-import { RootState } from 'app/store/store';
-import { useAppSelector } from 'app/store/storeHooks';
-import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
-import { canvasSelector } from 'features/canvas/store/canvasSelectors';
-import { controlNetSelector } from 'features/controlNet/store/controlNetSlice';
-import { nodesSelecter } from 'features/nodes/store/nodesSlice';
-import { generationSelector } from 'features/parameters/store/generationSelectors';
-import { some } from 'lodash-es';
-
-export type ImageUsage = {
- isInitialImage: boolean;
- isCanvasImage: boolean;
- isNodesImage: boolean;
- isControlNetImage: boolean;
-};
-
-const selectImageUsage = createSelector(
- [
- generationSelector,
- canvasSelector,
- nodesSelecter,
- controlNetSelector,
- (state: RootState, image_name?: string) => image_name,
- ],
- (generation, canvas, nodes, controlNet, image_name) => {
- const isInitialImage = generation.initialImage?.image_name === image_name;
-
- const isCanvasImage = canvas.layerState.objects.some(
- (obj) => obj.kind === 'image' && obj.image.image_name === image_name
- );
-
- const isNodesImage = nodes.nodes.some((node) => {
- return some(
- node.data.inputs,
- (input) =>
- input.type === 'image' && input.value?.image_name === image_name
- );
- });
-
- const isControlNetImage = some(
- controlNet.controlNets,
- (c) =>
- c.controlImage?.image_name === image_name ||
- c.processedControlImage?.image_name === image_name
- );
-
- const imageUsage: ImageUsage = {
- isInitialImage,
- isCanvasImage,
- isNodesImage,
- isControlNetImage,
- };
-
- return imageUsage;
- },
- defaultSelectorOptions
-);
-
-export const useImageUsage = (image_name?: string) => {
- const imageUsage = useAppSelector((state) =>
- selectImageUsage(state, image_name)
- );
-
- return imageUsage;
-};
diff --git a/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx
index 335944df43..0ce7bb3666 100644
--- a/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/DeleteImageModal.tsx
@@ -12,13 +12,15 @@ import {
UnorderedList,
} from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
-import { DeleteImageContext } from 'app/contexts/DeleteImageContext';
+import {
+ DeleteImageContext,
+ ImageUsage,
+} 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';
@@ -42,9 +44,13 @@ const selector = createSelector(
defaultSelectorOptions
);
-const ImageInUseMessage = (props: { imageUsage: ImageUsage }) => {
+const ImageInUseMessage = (props: { imageUsage?: ImageUsage }) => {
const { imageUsage } = props;
+ if (!imageUsage) {
+ return null;
+ }
+
if (!some(imageUsage)) {
return null;
}
@@ -69,14 +75,12 @@ const DeleteImageModal = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
- const { isOpen, onClose, onImmediatelyDelete, image } =
+ const { isOpen, onClose, onImmediatelyDelete, image, imageUsage } =
useContext(DeleteImageContext);
const { shouldConfirmOnDelete, canRestoreDeletedImagesFromBin } =
useAppSelector(selector);
- const imageUsage = useImageUsage(image?.image_name);
-
const handleChangeShouldConfirmOnDelete = useCallback(
(e: ChangeEvent) =>
dispatch(setShouldConfirmOnDelete(!e.target.checked)),
diff --git a/invokeai/frontend/web/src/features/gallery/store/actions.ts b/invokeai/frontend/web/src/features/gallery/store/actions.ts
index 7c00201da9..8b2beb9c13 100644
--- a/invokeai/frontend/web/src/features/gallery/store/actions.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/actions.ts
@@ -1,10 +1,15 @@
import { createAction } from '@reduxjs/toolkit';
-import { ImageNameAndOrigin } from 'features/parameters/store/actions';
+import { ImageUsage } from 'app/contexts/DeleteImageContext';
import { ImageDTO } from 'services/api';
-export const requestedImageDeletion = createAction<
- ImageDTO | ImageNameAndOrigin | undefined
->('gallery/requestedImageDeletion');
+export type RequestedImageDeletionArg = {
+ image: ImageDTO;
+ imageUsage: ImageUsage;
+};
+
+export const requestedImageDeletion = createAction(
+ 'gallery/requestedImageDeletion'
+);
export const sentImageToCanvas = createAction('gallery/sentImageToCanvas');
From 454683e6eb058b4f83ee2045bb86464a6558645f Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 7 Jun 2023 00:23:51 +1000
Subject: [PATCH 14/14] feat(ui): update image urls on connect (#3507)
* feat(ui): update image urls on connect
Add `updateImageUrlsOnConnect` RTK listener:
- requests URLs for *every* image the app knows about, on connect: gallery, selectedImage, initialImage, canvas images, nodes images, controlnet images
- only fires when `shouldUpdateImagesOnConnect` config is enabled
* remove prop
---------
Co-authored-by: Mary Hipp
---
.../middleware/listenerMiddleware/index.ts | 4 +
.../listeners/updateImageUrlsOnConnect.ts | 93 +++++++++++++++++++
.../frontend/web/src/app/types/invokeai.ts | 2 +-
.../src/features/system/store/configSlice.ts | 2 +-
4 files changed, 99 insertions(+), 2 deletions(-)
create mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateImageUrlsOnConnect.ts
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
index a9349dc863..8c073e81d6 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
@@ -72,6 +72,7 @@ import { addCommitStagingAreaImageListener } from './listeners/addCommitStagingA
import { addImageCategoriesChangedListener } from './listeners/imageCategoriesChanged';
import { addControlNetImageProcessedListener } from './listeners/controlNetImageProcessed';
import { addControlNetAutoProcessListener } from './listeners/controlNetAutoProcess';
+import { addUpdateImageUrlsOnConnectListener } from './listeners/updateImageUrlsOnConnect';
export const listenerMiddleware = createListenerMiddleware();
@@ -179,3 +180,6 @@ addImageCategoriesChangedListener();
// ControlNet
addControlNetImageProcessedListener();
addControlNetAutoProcessListener();
+
+// Update image URLs on connect
+addUpdateImageUrlsOnConnectListener();
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateImageUrlsOnConnect.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateImageUrlsOnConnect.ts
new file mode 100644
index 0000000000..d02ffbe931
--- /dev/null
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateImageUrlsOnConnect.ts
@@ -0,0 +1,93 @@
+import { socketConnected } from 'services/events/actions';
+import { startAppListening } from '..';
+import { createSelector } from '@reduxjs/toolkit';
+import { generationSelector } from 'features/parameters/store/generationSelectors';
+import { canvasSelector } from 'features/canvas/store/canvasSelectors';
+import { nodesSelecter } from 'features/nodes/store/nodesSlice';
+import { controlNetSelector } from 'features/controlNet/store/controlNetSlice';
+import { ImageDTO } from 'services/api';
+import { forEach, uniqBy } from 'lodash-es';
+import { imageUrlsReceived } from 'services/thunks/image';
+import { log } from 'app/logging/useLogger';
+import { selectImagesEntities } from 'features/gallery/store/imagesSlice';
+
+const moduleLog = log.child({ namespace: 'images' });
+
+const selectAllUsedImages = createSelector(
+ [
+ generationSelector,
+ canvasSelector,
+ nodesSelecter,
+ controlNetSelector,
+ selectImagesEntities,
+ ],
+ (generation, canvas, nodes, controlNet, imageEntities) => {
+ const allUsedImages: ImageDTO[] = [];
+
+ if (generation.initialImage) {
+ allUsedImages.push(generation.initialImage);
+ }
+
+ canvas.layerState.objects.forEach((obj) => {
+ if (obj.kind === 'image') {
+ allUsedImages.push(obj.image);
+ }
+ });
+
+ nodes.nodes.forEach((node) => {
+ forEach(node.data.inputs, (input) => {
+ if (input.type === 'image' && input.value) {
+ allUsedImages.push(input.value);
+ }
+ });
+ });
+
+ forEach(controlNet.controlNets, (c) => {
+ if (c.controlImage) {
+ allUsedImages.push(c.controlImage);
+ }
+ if (c.processedControlImage) {
+ allUsedImages.push(c.processedControlImage);
+ }
+ });
+
+ forEach(imageEntities, (image) => {
+ if (image) {
+ allUsedImages.push(image);
+ }
+ });
+
+ const uniqueImages = uniqBy(allUsedImages, 'image_name');
+
+ return uniqueImages;
+ }
+);
+
+export const addUpdateImageUrlsOnConnectListener = () => {
+ startAppListening({
+ actionCreator: socketConnected,
+ effect: async (action, { dispatch, getState, take }) => {
+ const state = getState();
+
+ if (!state.config.shouldUpdateImagesOnConnect) {
+ return;
+ }
+
+ const allUsedImages = selectAllUsedImages(state);
+
+ moduleLog.trace(
+ { data: allUsedImages },
+ `Fetching new image URLs for ${allUsedImages.length} images`
+ );
+
+ allUsedImages.forEach(({ image_name, image_origin }) => {
+ dispatch(
+ imageUrlsReceived({
+ imageName: image_name,
+ imageOrigin: image_origin,
+ })
+ );
+ });
+ },
+ });
+};
diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts
index f202b66ca2..4931c498bf 100644
--- a/invokeai/frontend/web/src/app/types/invokeai.ts
+++ b/invokeai/frontend/web/src/app/types/invokeai.ts
@@ -110,7 +110,7 @@ export type AppConfig = {
/**
* Whether or not we should update image urls when image loading errors
*/
- shouldUpdateImageUrlsOnError: boolean;
+ shouldUpdateImagesOnConnect: boolean;
disabledTabs: InvokeTabName[];
disabledFeatures: AppFeature[];
disabledSDFeatures: SDFeature[];
diff --git a/invokeai/frontend/web/src/features/system/store/configSlice.ts b/invokeai/frontend/web/src/features/system/store/configSlice.ts
index fb00a7a5d4..5f4dd68959 100644
--- a/invokeai/frontend/web/src/features/system/store/configSlice.ts
+++ b/invokeai/frontend/web/src/features/system/store/configSlice.ts
@@ -4,7 +4,7 @@ import { AppConfig, PartialAppConfig } from 'app/types/invokeai';
import { merge } from 'lodash-es';
export const initialConfigState: AppConfig = {
- shouldUpdateImageUrlsOnError: false,
+ shouldUpdateImagesOnConnect: false,
disabledTabs: [],
disabledFeatures: [],
disabledSDFeatures: [],