diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx
index 33fa57f0b3..bb2f140716 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 DeleteImageModal from 'features/gallery/components/DeleteImageModal';
const DEFAULT_CONFIG = {};
@@ -76,18 +77,21 @@ const App = ({
{isLightboxEnabled && }
{headerComponent || }
@@ -130,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..8263b48114
--- /dev/null
+++ b/invokeai/frontend/web/src/app/contexts/DeleteImageContext.tsx
@@ -0,0 +1,203 @@
+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,
+ 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';
+
+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 = {
+ /**
+ * Whether the delete image dialog is open.
+ */
+ isOpen: boolean;
+ /**
+ * 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;
+ /**
+ * The features in which this image is used
+ */
+ imageUsage?: ImageUsage;
+ /**
+ * Immediately deletes an image.
+ *
+ * You probably don't want to use this - use `onDelete` instead.
+ */
+ onImmediatelyDelete: () => 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,
+ };
+ },
+ 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();
+
+ // 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);
+ }
+ closeAndClearImageToDelete();
+ }, [
+ canDeleteImage,
+ imageToDelete,
+ closeAndClearImageToDelete,
+ handleActualDeletion,
+ ]);
+
+ const handleGatedDeletion = useCallback(
+ (image: ImageDTO) => {
+ if (shouldConfirmOnDelete || some(imageUsage)) {
+ // If we should confirm on delete, or if the image is in use, open the dialog
+ onOpen();
+ } else {
+ handleActualDeletion(image);
+ }
+ },
+ [imageUsage, shouldConfirmOnDelete, onOpen, handleActualDeletion]
+ );
+
+ // 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 (
+
+ {props.children}
+
+ );
+};
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/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/imageDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageDeleted.ts
index bf7ca4020c..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
@@ -6,10 +6,13 @@ import { clamp } from 'lodash-es';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import {
imageRemoved,
- imagesAdapter,
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' });
@@ -20,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;
@@ -58,8 +57,28 @@ 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));
+ // Delete from server
dispatch(
imageDeleted({ imageName: image_name, imageOrigin: image_origin })
);
@@ -74,9 +93,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/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageMetadataReceived.ts
index 63aeecb95e..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
@@ -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,29 @@ 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
+ moduleLog.trace(
+ { data: { image } },
+ 'Image metadata received (intermediate), skipping'
+ );
return;
}
+
moduleLog.debug({ data: { image } }, 'Image metadata received');
dispatch(imageUpserted(image));
},
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/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/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 304b094749..4931c498bf 100644
--- a/invokeai/frontend/web/src/app/types/invokeai.ts
+++ b/invokeai/frontend/web/src/app/types/invokeai.ts
@@ -108,12 +108,9 @@ 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
*/
+ shouldUpdateImagesOnConnect: boolean;
disabledTabs: InvokeTabName[];
disabledFeatures: AppFeature[];
disabledSDFeatures: SDFeature[];
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/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/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/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());
+};
diff --git a/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts b/invokeai/frontend/web/src/features/controlNet/store/controlNetSlice.ts
index 1389457aba..92d6c302e9 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, imageUrlsReceived } from 'services/thunks/image';
+import { forEach } from 'lodash-es';
export const initialControlNet: Omit = {
isEnabled: true,
@@ -185,6 +187,9 @@ export const controlNetSlice = createSlice({
processorType
].default as RequiredControlNetProcessorNode;
},
+ controlNetReset: () => {
+ return { ...initialControlNetState };
+ },
},
extraReducers: (builder) => {
builder.addCase(controlNetImageProcessed, (state, action) => {
@@ -194,6 +199,36 @@ 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;
+ }
+ });
+ });
+
+ 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;
+ }
+ });
+ });
},
});
@@ -211,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 91bd1a0425..a5eaeb4c71 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,22 +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 { 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';
-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 './DeleteImageModal';
const currentImageButtonsSelector = createSelector(
[
@@ -123,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 {
@@ -138,13 +124,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
facetoolStrength,
shouldDisableToolbarButtons,
shouldShowImageDetails,
- // currentImage,
isLightboxOpen,
activeTabName,
shouldHidePreview,
image,
- canDeleteImage,
- shouldConfirmOnDelete,
shouldShowProgressInViewer,
} = useAppSelector(currentImageButtonsSelector);
@@ -153,20 +136,14 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled;
const isFaceRestoreEnabled = useFeatureStatus('faceRestore').isFeatureEnabled;
- const { getUrl, shouldTransformUrls } = useGetUrl();
-
- 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;
@@ -197,10 +174,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 +202,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
isClosable: true,
});
});
- }, [toaster, shouldTransformUrls, getUrl, t, image]);
+ }, [toaster, t, image]);
const handleClickUseAllParameters = useCallback(() => {
recallAllParameters(image);
@@ -269,6 +242,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
// selectedImage && dispatch(runESRGAN(selectedImage));
}, []);
+ const handleDelete = useCallback(() => {
+ onDelete(image);
+ }, [image, onDelete]);
+
useHotkeys(
'Shift+U',
() => {
@@ -370,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]);
@@ -461,11 +417,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
{t('parameters.copyImageToLink')}
-
+
} size="sm" w="100%">
{t('parameters.downloadImage')}
@@ -607,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 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 && (
{
const { shouldConfirmOnDelete } = system;
const { canRestoreDeletedImagesFromBin } = config;
- return { shouldConfirmOnDelete, canRestoreDeletedImagesFromBin };
+
+ return {
+ shouldConfirmOnDelete,
+ canRestoreDeletedImagesFromBin,
+ };
},
- {
- memoizeOptions: {
- resultEqualityCheck: isEqual,
- },
- }
+ defaultSelectorOptions
);
-interface DeleteImageModalProps {
- isOpen: boolean;
- onClose: () => void;
- handleDelete: () => void;
-}
+const ImageInUseMessage = (props: { imageUsage?: ImageUsage }) => {
+ const { imageUsage } = props;
-const DeleteImageModal = ({
- isOpen,
- onClose,
- handleDelete,
-}: DeleteImageModalProps) => {
+ if (!imageUsage) {
+ return null;
+ }
+
+ 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, imageUsage } =
+ useContext(DeleteImageContext);
+
const { shouldConfirmOnDelete, canRestoreDeletedImagesFromBin } =
useAppSelector(selector);
- const cancelRef = useRef(null);
const handleChangeShouldConfirmOnDelete = useCallback(
(e: ChangeEvent) =>
@@ -57,10 +87,7 @@ const DeleteImageModal = ({
[dispatch]
);
- const handleClickDelete = useCallback(() => {
- handleDelete();
- onClose();
- }, [handleDelete, onClose]);
+ const cancelRef = useRef(null);
return (
-
-
- {t('common.areYouSure')}
-
- {canRestoreDeletedImagesFromBin
- ? t('gallery.deleteImageBin')
- : t('gallery.deleteImagePermanent')}
-
-
+
+
+
+
+ {canRestoreDeletedImagesFromBin
+ ? t('gallery.deleteImageBin')
+ : t('gallery.deleteImagePermanent')}
+
+ {t('common.areYouSure')}
Cancel
-
+
Delete
@@ -107,3 +134,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 4dad27d4e8..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,
@@ -21,7 +12,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';
@@ -32,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],
@@ -93,28 +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 { getUrl } = useGetUrl();
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();
@@ -128,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]);
@@ -208,7 +169,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
};
const handleOpenInNewTab = () => {
- window.open(getUrl(image.image_url), '_blank');
+ window.open(image.image_url, '_blank');
};
return (
@@ -283,7 +244,11 @@ const HoverableImage = memo((props: HoverableImageProps) => {
{t('parameters.sendToUnifiedCanvas')}
)}
- } onClickCapture={onDeleteDialogOpen}>
+ }
+ onClickCapture={handleDelete}
+ >
{t('gallery.deleteImage')}
@@ -296,8 +261,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
userSelect="none"
- // draggable={true}
- // onDragStart={handleDragStart}
onClick={handleSelectImage}
ref={ref}
sx={{
@@ -317,7 +280,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%',
@@ -361,7 +324,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
}}
>
}
size="xs"
@@ -373,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/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/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');
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 cb6469aeb4..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,
@@ -7,12 +8,17 @@ 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,
+ imageMetadataReceived,
+ imageUrlsReceived,
+ 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'];
@@ -49,13 +55,11 @@ 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);
+ 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;
@@ -76,6 +80,20 @@ const imagesSlice = createSlice({
state.total = total;
imagesAdapter.upsertMany(state, items);
});
+ builder.addCase(imageDeleted.pending, (state, action) => {
+ // 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 },
+ });
+ });
},
});
@@ -87,8 +105,12 @@ export const {
selectTotal: selectImagesTotal,
} = imagesAdapter.getSelectors((state) => state.images);
-export const { imageUpserted, imageRemoved, imageCategoriesChanged } =
- imagesSlice.actions;
+export const {
+ imageUpserted,
+ imageUpdatedOne,
+ imageRemoved,
+ imageCategoriesChanged,
+} = imagesSlice.actions;
export default imagesSlice.reducer;
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 (
{
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/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
index 3c93be7ac5..403a9292e1 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 { isAnyGraphBuilt } from './actions';
+import { forEach, size } from 'lodash-es';
import { RgbaColor } from 'react-colorful';
+import { imageUrlsReceived } from 'services/thunks/image';
+import { RootState } from 'app/store/store';
export type NodesState = {
nodes: Node[];
@@ -92,15 +93,29 @@ const nodesSlice = createSlice({
console.error(err);
}
},
+ nodeEditorReset: () => {
+ return { ...initialNodesState };
+ },
},
extraReducers(builder) {
builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => {
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;
+ }
+ }
+ });
+ });
});
},
});
@@ -115,6 +130,9 @@ export const {
connectionEnded,
shouldShowGraphOverlayChanged,
parsedOpenAPISchema,
+ nodeEditorReset,
} = nodesSlice.actions;
export default nodesSlice.reducer;
+
+export const nodesSelecter = (state: RootState) => state.nodes;
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/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx
index 72dec6c149..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
@@ -1,7 +1,6 @@
import { Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
-import { useGetUrl } from 'common/util/getUrl';
import {
clearInitialImage,
initialImageChanged,
@@ -30,7 +29,6 @@ const selector = createSelector(
const InitialImagePreview = () => {
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/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/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/system/store/configSlice.ts b/invokeai/frontend/web/src/features/system/store/configSlice.ts
index f8cb3a483c..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 = {
- shouldTransformUrls: false,
+ shouldUpdateImagesOnConnect: false,
disabledTabs: [],
disabledFeatures: [],
disabledSDFeatures: [],
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: {