diff --git a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/serialize.ts b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/serialize.ts
index 52995e0da3..9fb4ceae32 100644
--- a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/serialize.ts
+++ b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/serialize.ts
@@ -1,7 +1,5 @@
import { canvasPersistDenylist } from 'features/canvas/store/canvasPersistDenylist';
import { galleryPersistDenylist } from 'features/gallery/store/galleryPersistDenylist';
-import { resultsPersistDenylist } from 'features/gallery/store/resultsPersistDenylist';
-import { uploadsPersistDenylist } from 'features/gallery/store/uploadsPersistDenylist';
import { lightboxPersistDenylist } from 'features/lightbox/store/lightboxPersistDenylist';
import { nodesPersistDenylist } from 'features/nodes/store/nodesPersistDenylist';
import { generationPersistDenylist } from 'features/parameters/store/generationPersistDenylist';
@@ -22,11 +20,9 @@ const serializationDenylist: {
models: modelsPersistDenylist,
nodes: nodesPersistDenylist,
postprocessing: postprocessingPersistDenylist,
- results: resultsPersistDenylist,
system: systemPersistDenylist,
// config: configPersistDenyList,
ui: uiPersistDenylist,
- uploads: uploadsPersistDenylist,
// hotkeys: hotkeysPersistDenylist,
};
diff --git a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/unserialize.ts b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/unserialize.ts
index 155a7786b3..c6ae4946f2 100644
--- a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/unserialize.ts
+++ b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/unserialize.ts
@@ -1,7 +1,6 @@
import { initialCanvasState } from 'features/canvas/store/canvasSlice';
import { initialGalleryState } from 'features/gallery/store/gallerySlice';
-import { initialResultsState } from 'features/gallery/store/resultsSlice';
-import { initialUploadsState } from 'features/gallery/store/uploadsSlice';
+import { initialImagesState } from 'features/gallery/store/imagesSlice';
import { initialLightboxState } from 'features/lightbox/store/lightboxSlice';
import { initialNodesState } from 'features/nodes/store/nodesSlice';
import { initialGenerationState } from 'features/parameters/store/generationSlice';
@@ -24,12 +23,11 @@ const initialStates: {
models: initialModelsState,
nodes: initialNodesState,
postprocessing: initialPostprocessingState,
- results: initialResultsState,
system: initialSystemState,
config: initialConfigState,
ui: initialUIState,
- uploads: initialUploadsState,
hotkeys: initialHotkeysState,
+ images: initialImagesState,
};
export const unserialize: UnserializeFunction = (data, key) => {
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 7159957efa..6cc9867bfd 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts
@@ -59,18 +59,15 @@ import {
addSessionCanceledPendingListener,
addSessionCanceledRejectedListener,
} from './listeners/sessionCanceled';
-import {
- addReceivedGalleryImagesFulfilledListener,
- addReceivedGalleryImagesRejectedListener,
-} from './listeners/receivedGalleryImages';
-import {
- addReceivedUploadImagesPageFulfilledListener,
- addReceivedUploadImagesPageRejectedListener,
-} from './listeners/receivedUploadImages';
import {
addImageUpdatedFulfilledListener,
addImageUpdatedRejectedListener,
} from './listeners/imageUpdated';
+import {
+ addReceivedPageOfImagesFulfilledListener,
+ addReceivedPageOfImagesRejectedListener,
+} from './listeners/receivedPageOfImages';
+import { addStagingAreaImageSavedListener } from './listeners/stagingAreaImageSaved';
export const listenerMiddleware = createListenerMiddleware();
@@ -127,6 +124,7 @@ addCanvasSavedToGalleryListener();
addCanvasDownloadedAsImageListener();
addCanvasCopiedToClipboardListener();
addCanvasMergedListener();
+addStagingAreaImageSavedListener();
// socketio
addGeneratorProgressListener();
@@ -154,8 +152,6 @@ addSessionCanceledPendingListener();
addSessionCanceledFulfilledListener();
addSessionCanceledRejectedListener();
-// Gallery pages
-addReceivedGalleryImagesFulfilledListener();
-addReceivedGalleryImagesRejectedListener();
-addReceivedUploadImagesPageFulfilledListener();
-addReceivedUploadImagesPageRejectedListener();
+// Images
+addReceivedPageOfImagesFulfilledListener();
+addReceivedPageOfImagesRejectedListener();
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 01f097cdd1..a692a90670 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
@@ -5,7 +5,7 @@ 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 { resultUpserted } from 'features/gallery/store/resultsSlice';
+import { imageUpserted } from 'features/gallery/store/imagesSlice';
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
@@ -47,7 +47,7 @@ export const addCanvasSavedToGalleryListener = () => {
action.meta.arg.formData.file.name === filename
);
- dispatch(resultUpserted(uploadedImageDTO));
+ 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 7bd92e7e13..bf7ca4020c 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,14 +5,11 @@ import { log } from 'app/logging/useLogger';
import { clamp } from 'lodash-es';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import {
- uploadRemoved,
- uploadsAdapter,
-} from 'features/gallery/store/uploadsSlice';
-import {
- resultRemoved,
- resultsAdapter,
-} from 'features/gallery/store/resultsSlice';
-import { isUploadsImageDTO } from 'services/types/guards';
+ imageRemoved,
+ imagesAdapter,
+ selectImagesEntities,
+ selectImagesIds,
+} from 'features/gallery/store/imagesSlice';
const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' });
@@ -33,19 +30,16 @@ export const addRequestedImageDeletionListener = () => {
const state = getState();
const selectedImage = state.gallery.selectedImage;
- const isUserImage = isUploadsImageDTO(selectedImage);
+
if (selectedImage && selectedImage.image_name === image_name) {
- const allIds = isUserImage ? state.uploads.ids : state.results.ids;
+ const ids = selectImagesIds(state);
+ const entities = selectImagesEntities(state);
- const allEntities = isUserImage
- ? state.uploads.entities
- : state.results.entities;
-
- const deletedImageIndex = allIds.findIndex(
+ const deletedImageIndex = ids.findIndex(
(result) => result.toString() === image_name
);
- const filteredIds = allIds.filter((id) => id.toString() !== image_name);
+ const filteredIds = ids.filter((id) => id.toString() !== image_name);
const newSelectedImageIndex = clamp(
deletedImageIndex,
@@ -55,7 +49,7 @@ export const addRequestedImageDeletionListener = () => {
const newSelectedImageId = filteredIds[newSelectedImageIndex];
- const newSelectedImage = allEntities[newSelectedImageId];
+ const newSelectedImage = entities[newSelectedImageId];
if (newSelectedImageId) {
dispatch(imageSelected(newSelectedImage));
@@ -64,11 +58,7 @@ export const addRequestedImageDeletionListener = () => {
}
}
- if (isUserImage) {
- dispatch(uploadRemoved(image_name));
- } else {
- dispatch(resultRemoved(image_name));
- }
+ dispatch(imageRemoved(image_name));
dispatch(
imageDeleted({ imageName: image_name, imageOrigin: image_origin })
@@ -86,12 +76,7 @@ export const addImageDeletedPendingListener = () => {
effect: (action, { dispatch, getState }) => {
const { imageName, imageOrigin } = action.meta.arg;
// Preemptively remove the image from the gallery
- if (imageOrigin === 'external') {
- uploadsAdapter.removeOne(getState().uploads, imageName);
- }
- if (imageOrigin === 'internal') {
- resultsAdapter.removeOne(getState().results, imageName);
- }
+ 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 276ef7be6c..63aeecb95e 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,9 +1,7 @@
import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { imageMetadataReceived } from 'services/thunks/image';
-import { resultUpserted } from 'features/gallery/store/resultsSlice';
-import { uploadUpserted } from 'features/gallery/store/uploadsSlice';
-import { imageSelected } from 'features/gallery/store/gallerySlice';
+import { imageUpserted } from 'features/gallery/store/imagesSlice';
const moduleLog = log.child({ namespace: 'image' });
@@ -11,16 +9,13 @@ export const addImageMetadataReceivedFulfilledListener = () => {
startAppListening({
actionCreator: imageMetadataReceived.fulfilled,
effect: (action, { getState, dispatch }) => {
- const imageDTO = action.payload;
- moduleLog.debug({ data: { imageDTO } }, 'Image metadata received');
-
- if (imageDTO.image_origin === 'internal') {
- dispatch(resultUpserted(imageDTO));
- }
-
- if (imageDTO.image_origin === 'external') {
- dispatch(uploadUpserted(imageDTO));
+ const image = action.payload;
+ if (image.is_intermediate) {
+ // No further actions needed for intermediate images
+ 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 dcce86017e..6d84431f80 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
@@ -1,13 +1,8 @@
import { startAppListening } from '..';
-import { uploadUpserted } from 'features/gallery/store/uploadsSlice';
-import {
- imageSelected,
- setCurrentCategory,
-} from 'features/gallery/store/gallerySlice';
import { imageUploaded } from 'services/thunks/image';
import { addToast } from 'features/system/store/systemSlice';
-import { resultUpserted } from 'features/gallery/store/resultsSlice';
import { log } from 'app/logging/useLogger';
+import { imageUpserted } from 'features/gallery/store/imagesSlice';
const moduleLog = log.child({ namespace: 'image' });
@@ -26,18 +21,8 @@ export const addImageUploadedFulfilledListener = () => {
const state = getState();
- // Handle uploads
- if (image.image_category === 'user' && !image.is_intermediate) {
- dispatch(uploadUpserted(image));
- dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
- }
-
- // Handle results
- // TODO: Can this ever happen? I don't think so...
- if (image.image_category !== 'user' && !image.is_intermediate) {
- dispatch(resultUpserted(image));
- dispatch(setCurrentCategory('results'));
- }
+ dispatch(imageUpserted(image));
+ 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 588d7611cc..fd0461f893 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,8 +1,7 @@
import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
import { imageUrlsReceived } from 'services/thunks/image';
-import { resultsAdapter } from 'features/gallery/store/resultsSlice';
-import { uploadsAdapter } from 'features/gallery/store/uploadsSlice';
+import { imagesAdapter } from 'features/gallery/store/imagesSlice';
const moduleLog = log.child({ namespace: 'image' });
@@ -13,27 +12,15 @@ export const addImageUrlsReceivedFulfilledListener = () => {
const image = action.payload;
moduleLog.debug({ data: { image } }, 'Image URLs received');
- const { image_origin, image_name, image_url, thumbnail_url } = image;
+ const { image_name, image_url, thumbnail_url } = image;
- if (image_origin === 'results') {
- resultsAdapter.updateOne(getState().results, {
- id: image_name,
- changes: {
- image_url,
- thumbnail_url,
- },
- });
- }
-
- if (image_origin === 'uploads') {
- uploadsAdapter.updateOne(getState().uploads, {
- id: image_name,
- changes: {
- image_url,
- thumbnail_url,
- },
- });
- }
+ imagesAdapter.updateOne(getState().images, {
+ id: image_name,
+ changes: {
+ image_url,
+ thumbnail_url,
+ },
+ });
},
});
};
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts
index a2e783a38a..940cc84c1e 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/initialImageSelected.ts
@@ -1,6 +1,4 @@
import { initialImageChanged } from 'features/parameters/store/generationSlice';
-import { selectResultsById } from 'features/gallery/store/resultsSlice';
-import { selectUploadsById } from 'features/gallery/store/uploadsSlice';
import { t } from 'i18next';
import { addToast } from 'features/system/store/systemSlice';
import { startAppListening } from '..';
@@ -9,7 +7,7 @@ import {
isImageDTO,
} from 'features/parameters/store/actions';
import { makeToast } from 'app/components/Toaster';
-import { ImageDTO } from 'services/api';
+import { selectImagesById } from 'features/gallery/store/imagesSlice';
export const addInitialImageSelectedListener = () => {
startAppListening({
@@ -30,16 +28,8 @@ export const addInitialImageSelectedListener = () => {
return;
}
- const { image_name, image_origin } = action.payload;
-
- let image: ImageDTO | undefined;
- const state = getState();
-
- if (image_origin === 'results') {
- image = selectResultsById(state, image_name);
- } else if (image_origin === 'uploads') {
- image = selectUploadsById(state, image_name);
- }
+ const imageName = action.payload;
+ const image = selectImagesById(getState(), imageName);
if (!image) {
dispatch(
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/receivedGalleryImages.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/receivedPageOfImages.ts
similarity index 53%
rename from invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/receivedGalleryImages.ts
rename to invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/receivedPageOfImages.ts
index aba81e1e72..9a2ec0e7a5 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/receivedGalleryImages.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/receivedPageOfImages.ts
@@ -1,31 +1,31 @@
import { log } from 'app/logging/useLogger';
import { startAppListening } from '..';
-import { receivedGalleryImages } from 'services/thunks/gallery';
import { serializeError } from 'serialize-error';
+import { receivedPageOfImages } from 'services/thunks/image';
const moduleLog = log.child({ namespace: 'gallery' });
-export const addReceivedGalleryImagesFulfilledListener = () => {
+export const addReceivedPageOfImagesFulfilledListener = () => {
startAppListening({
- actionCreator: receivedGalleryImages.fulfilled,
+ actionCreator: receivedPageOfImages.fulfilled,
effect: (action, { getState, dispatch }) => {
const page = action.payload;
moduleLog.debug(
{ data: { page } },
- `Received ${page.items.length} gallery images`
+ `Received ${page.items.length} images`
);
},
});
};
-export const addReceivedGalleryImagesRejectedListener = () => {
+export const addReceivedPageOfImagesRejectedListener = () => {
startAppListening({
- actionCreator: receivedGalleryImages.rejected,
+ actionCreator: receivedPageOfImages.rejected,
effect: (action, { getState, dispatch }) => {
if (action.payload) {
moduleLog.debug(
- { data: { error: serializeError(action.payload.error) } },
- 'Problem receiving gallery images'
+ { data: { error: serializeError(action.payload) } },
+ 'Problem receiving images'
);
}
},
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/receivedUploadImages.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/receivedUploadImages.ts
deleted file mode 100644
index 602fccf847..0000000000
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/receivedUploadImages.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { log } from 'app/logging/useLogger';
-import { startAppListening } from '..';
-import { receivedUploadImages } from 'services/thunks/gallery';
-import { serializeError } from 'serialize-error';
-
-const moduleLog = log.child({ namespace: 'gallery' });
-
-export const addReceivedUploadImagesPageFulfilledListener = () => {
- startAppListening({
- actionCreator: receivedUploadImages.fulfilled,
- effect: (action, { getState, dispatch }) => {
- const page = action.payload;
- moduleLog.debug(
- { data: { page } },
- `Received ${page.items.length} uploaded images`
- );
- },
- });
-};
-
-export const addReceivedUploadImagesPageRejectedListener = () => {
- startAppListening({
- actionCreator: receivedUploadImages.rejected,
- effect: (action, { getState, dispatch }) => {
- if (action.payload) {
- moduleLog.debug(
- { data: { error: serializeError(action.payload.error) } },
- 'Problem receiving uploaded images'
- );
- }
- },
- });
-};
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/invocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/invocationComplete.ts
index 81c0286e3b..fb2056ae35 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/invocationComplete.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/invocationComplete.ts
@@ -6,7 +6,6 @@ import { imageMetadataReceived } from 'services/thunks/image';
import { sessionCanceled } from 'services/thunks/session';
import { isImageOutput } from 'services/types/guards';
import { progressImageSet } from 'features/system/store/systemSlice';
-import { imageSelected } from 'features/gallery/store/gallerySlice';
const moduleLog = log.child({ namespace: 'socketio' });
const nodeDenylist = ['dataURL_image'];
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected.ts
index 650918ba3c..85035e6bf9 100644
--- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected.ts
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/socketio/socketConnected.ts
@@ -1,10 +1,7 @@
import { startAppListening } from '../..';
import { log } from 'app/logging/useLogger';
import { socketConnected } from 'services/events/actions';
-import {
- receivedGalleryImages,
- receivedUploadImages,
-} from 'services/thunks/gallery';
+import { receivedPageOfImages } from 'services/thunks/image';
import { receivedModels } from 'services/thunks/model';
import { receivedOpenAPISchema } from 'services/thunks/schema';
@@ -18,17 +15,12 @@ export const addSocketConnectedListener = () => {
moduleLog.debug({ timestamp }, 'Connected');
- const { results, uploads, models, nodes, config } = getState();
+ const { models, nodes, config, images } = getState();
const { disabledTabs } = config;
- // These thunks need to be dispatch in middleware; cannot handle in a reducer
- if (!results.ids.length) {
- dispatch(receivedGalleryImages());
- }
-
- if (!uploads.ids.length) {
- dispatch(receivedUploadImages());
+ if (!images.ids.length) {
+ dispatch(receivedPageOfImages());
}
if (!models.ids.length) {
diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts
new file mode 100644
index 0000000000..9bd3cd6dd2
--- /dev/null
+++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/stagingAreaImageSaved.ts
@@ -0,0 +1,54 @@
+import { stagingAreaImageSaved } from 'features/canvas/store/actions';
+import { startAppListening } from '..';
+import { log } from 'app/logging/useLogger';
+import { imageUpdated } from 'services/thunks/image';
+import { imageUpserted } from 'features/gallery/store/imagesSlice';
+import { addToast } from 'features/system/store/systemSlice';
+
+const moduleLog = log.child({ namespace: 'canvas' });
+
+export const addStagingAreaImageSavedListener = () => {
+ startAppListening({
+ actionCreator: stagingAreaImageSaved,
+ effect: async (action, { dispatch, getState, take }) => {
+ const { image_name, image_origin } = action.payload;
+
+ dispatch(
+ imageUpdated({
+ imageName: image_name,
+ imageOrigin: image_origin,
+ requestBody: {
+ is_intermediate: false,
+ },
+ })
+ );
+
+ const [imageUpdatedAction] = await take(
+ (action) =>
+ (imageUpdated.fulfilled.match(action) ||
+ imageUpdated.rejected.match(action)) &&
+ action.meta.arg.imageName === image_name
+ );
+
+ if (imageUpdated.rejected.match(imageUpdatedAction)) {
+ moduleLog.error(
+ { data: { arg: imageUpdatedAction.meta.arg } },
+ 'Image saving failed'
+ );
+ dispatch(
+ addToast({
+ title: 'Image Saving Failed',
+ description: imageUpdatedAction.error.message,
+ status: 'error',
+ })
+ );
+ return;
+ }
+
+ if (imageUpdated.fulfilled.match(imageUpdatedAction)) {
+ dispatch(imageUpserted(imageUpdatedAction.payload));
+ dispatch(addToast({ title: 'Image Saved', status: 'success' }));
+ }
+ },
+ });
+};
diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts
index 4e9c154f3a..521610adcc 100644
--- a/invokeai/frontend/web/src/app/store/store.ts
+++ b/invokeai/frontend/web/src/app/store/store.ts
@@ -10,8 +10,7 @@ import dynamicMiddlewares from 'redux-dynamic-middlewares';
import canvasReducer from 'features/canvas/store/canvasSlice';
import galleryReducer from 'features/gallery/store/gallerySlice';
-import resultsReducer from 'features/gallery/store/resultsSlice';
-import uploadsReducer from 'features/gallery/store/uploadsSlice';
+import imagesReducer from 'features/gallery/store/imagesSlice';
import lightboxReducer from 'features/lightbox/store/lightboxSlice';
import generationReducer from 'features/parameters/store/generationSlice';
import postprocessingReducer from 'features/parameters/store/postprocessingSlice';
@@ -41,12 +40,11 @@ const allReducers = {
models: modelsReducer,
nodes: nodesReducer,
postprocessing: postprocessingReducer,
- results: resultsReducer,
system: systemReducer,
config: configReducer,
ui: uiReducer,
- uploads: uploadsReducer,
hotkeys: hotkeysReducer,
+ images: imagesReducer,
// session: sessionReducer,
};
@@ -65,8 +63,6 @@ const rememberedKeys: (keyof typeof allReducers)[] = [
'system',
'ui',
// 'hotkeys',
- // 'results',
- // 'uploads',
// 'config',
];
diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx
index 64c752fce0..68bc15bbaa 100644
--- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx
+++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasStagingAreaToolbar.tsx
@@ -1,6 +1,5 @@
import { ButtonGroup, Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
-// import { saveStagingAreaImageToGallery } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
@@ -26,6 +25,7 @@ import {
FaPlus,
FaSave,
} from 'react-icons/fa';
+import { stagingAreaImageSaved } from '../store/actions';
const selector = createSelector(
[canvasSelector],
@@ -157,19 +157,15 @@ const IAICanvasStagingAreaToolbar = () => {
}
colorScheme="accent"
/>
- {/* }
onClick={() =>
- dispatch(
- saveStagingAreaImageToGallery(
- currentStagingAreaImage.image.image_url
- )
- )
+ dispatch(stagingAreaImageSaved(currentStagingAreaImage.image))
}
colorScheme="accent"
- /> */}
+ />
(
+ 'canvas/stagingAreaImageSaved'
+);
diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx
index 38c104a83d..280d859b87 100644
--- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx
@@ -62,7 +62,6 @@ const CurrentImagePreview = () => {
return;
}
e.dataTransfer.setData('invokeai/imageName', image.image_name);
- e.dataTransfer.setData('invokeai/imageOrigin', image.image_origin);
e.dataTransfer.effectAllowed = 'move';
},
[image]
diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx
index 4a51580650..94b653af1c 100644
--- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx
@@ -147,7 +147,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const handleDragStart = useCallback(
(e: DragEvent) => {
e.dataTransfer.setData('invokeai/imageName', image.image_name);
- e.dataTransfer.setData('invokeai/imageOrigin', image.image_origin);
e.dataTransfer.effectAllowed = 'move';
},
[image]
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx
index 7c7fd29038..4b1786168d 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx
@@ -1,6 +1,7 @@
import {
Box,
- ButtonGroup,
+ Checkbox,
+ CheckboxGroup,
Flex,
FlexProps,
Grid,
@@ -16,7 +17,6 @@ import IAIPopover from 'common/components/IAIPopover';
import IAISlider from 'common/components/IAISlider';
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
import {
- setCurrentCategory,
setGalleryImageMinimumWidth,
setGalleryImageObjectFit,
setShouldAutoSwitchToNewImages,
@@ -36,54 +36,53 @@ import {
} from 'react';
import { useTranslation } from 'react-i18next';
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
-import { FaImage, FaUser, FaWrench } from 'react-icons/fa';
+import { FaFilter, FaWrench } from 'react-icons/fa';
import { MdPhotoLibrary } from 'react-icons/md';
import HoverableImage from './HoverableImage';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
-import { resultsAdapter } from '../store/resultsSlice';
-import {
- receivedGalleryImages,
- receivedUploadImages,
-} from 'services/thunks/gallery';
-import { uploadsAdapter } from '../store/uploadsSlice';
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store';
import { Virtuoso, VirtuosoGrid } from 'react-virtuoso';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import GalleryProgressImage from './GalleryProgressImage';
import { uiSelector } from 'features/ui/store/uiSelectors';
-import { ImageDTO } from 'services/api';
+import { ImageCategory, ImageDTO } from 'services/api';
+import { imageCategoriesChanged, selectImagesAll } from '../store/imagesSlice';
+import { receivedPageOfImages } from 'services/thunks/image';
+import { capitalize } from 'lodash-es';
-const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 290;
const PROGRESS_IMAGE_PLACEHOLDER = 'PROGRESS_IMAGE_PLACEHOLDER';
+const IMAGE_CATEGORIES: ImageCategory[] = [
+ 'general',
+ 'control',
+ 'mask',
+ 'user',
+ 'other',
+];
const categorySelector = createSelector(
[(state: RootState) => state],
(state) => {
- const { results, uploads, system, gallery } = state;
- const { currentCategory } = gallery;
+ const { system, images } = state;
+ const tempImages: (ImageDTO | typeof PROGRESS_IMAGE_PLACEHOLDER)[] = [];
- if (currentCategory === 'results') {
- const tempImages: (ImageDTO | typeof PROGRESS_IMAGE_PLACEHOLDER)[] = [];
-
- if (system.progressImage) {
- tempImages.push(PROGRESS_IMAGE_PLACEHOLDER);
- }
-
- return {
- images: tempImages.concat(
- resultsAdapter.getSelectors().selectAll(results)
- ),
- isLoading: results.isLoading,
- areMoreImagesAvailable: results.page < results.pages - 1,
- };
+ if (system.progressImage) {
+ tempImages.push(PROGRESS_IMAGE_PLACEHOLDER);
}
+ const { categories } = images;
+
+ const allImages = selectImagesAll(state);
+ const filteredImages = allImages.filter((i) =>
+ categories.includes(i.image_category)
+ );
+
return {
- images: uploadsAdapter.getSelectors().selectAll(uploads),
- isLoading: uploads.isLoading,
- areMoreImagesAvailable: uploads.page < uploads.pages - 1,
+ images: tempImages.concat(filteredImages),
+ isLoading: images.isLoading,
+ areMoreImagesAvailable: filteredImages.length < images.total,
+ categories: images.categories,
};
},
defaultSelectorOptions
@@ -93,7 +92,6 @@ const mainSelector = createSelector(
[gallerySelector, uiSelector],
(gallery, ui) => {
const {
- currentCategory,
galleryImageMinimumWidth,
galleryImageObjectFit,
shouldAutoSwitchToNewImages,
@@ -104,7 +102,6 @@ const mainSelector = createSelector(
const { shouldPinGallery } = ui;
return {
- currentCategory,
shouldPinGallery,
galleryImageMinimumWidth,
galleryImageObjectFit,
@@ -120,7 +117,6 @@ const ImageGalleryContent = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const resizeObserverRef = useRef(null);
- const [shouldShouldIconButtons, setShouldShouldIconButtons] = useState(true);
const rootRef = useRef(null);
const [scroller, setScroller] = useState(null);
const [initialize, osInstance] = useOverlayScrollbars({
@@ -137,7 +133,6 @@ const ImageGalleryContent = () => {
});
const {
- currentCategory,
shouldPinGallery,
galleryImageMinimumWidth,
galleryImageObjectFit,
@@ -146,18 +141,12 @@ const ImageGalleryContent = () => {
selectedImage,
} = useAppSelector(mainSelector);
- const { images, areMoreImagesAvailable, isLoading } =
+ const { images, areMoreImagesAvailable, isLoading, categories } =
useAppSelector(categorySelector);
- const handleClickLoadMore = () => {
- if (currentCategory === 'results') {
- dispatch(receivedGalleryImages());
- }
-
- if (currentCategory === 'uploads') {
- dispatch(receivedUploadImages());
- }
- };
+ const handleLoadMoreImages = useCallback(() => {
+ dispatch(receivedPageOfImages());
+ }, [dispatch]);
const handleChangeGalleryImageMinimumWidth = (v: number) => {
dispatch(setGalleryImageMinimumWidth(v));
@@ -168,28 +157,6 @@ const ImageGalleryContent = () => {
dispatch(requestCanvasRescale());
};
- useEffect(() => {
- if (!resizeObserverRef.current) {
- return;
- }
- const resizeObserver = new ResizeObserver(() => {
- if (!resizeObserverRef.current) {
- return;
- }
-
- if (
- resizeObserverRef.current.clientWidth < GALLERY_SHOW_BUTTONS_MIN_WIDTH
- ) {
- setShouldShouldIconButtons(true);
- return;
- }
-
- setShouldShouldIconButtons(false);
- });
- resizeObserver.observe(resizeObserverRef.current);
- return () => resizeObserver.disconnect(); // clean up
- }, []);
-
useEffect(() => {
const { current: root } = rootRef;
if (scroller && root) {
@@ -210,12 +177,15 @@ const ImageGalleryContent = () => {
}, []);
const handleEndReached = useCallback(() => {
- if (currentCategory === 'results') {
- dispatch(receivedGalleryImages());
- } else if (currentCategory === 'uploads') {
- dispatch(receivedUploadImages());
- }
- }, [dispatch, currentCategory]);
+ handleLoadMoreImages();
+ }, [handleLoadMoreImages]);
+
+ const handleCategoriesChanged = useCallback(
+ (newCategories: ImageCategory[]) => {
+ dispatch(imageCategoriesChanged(newCategories));
+ },
+ [dispatch]
+ );
return (
{
alignItems="center"
justifyContent="space-between"
>
- }
+ />
+ }
>
- {shouldShouldIconButtons ? (
- <>
- }
- onClick={() => dispatch(setCurrentCategory('results'))}
- />
- }
- onClick={() => dispatch(setCurrentCategory('uploads'))}
- />
- >
- ) : (
- <>
- dispatch(setCurrentCategory('results'))}
- flexGrow={1}
- >
- {t('gallery.generations')}
-
- dispatch(setCurrentCategory('uploads'))}
- flexGrow={1}
- >
- {t('gallery.uploads')}
-
- >
- )}
-
+
+
+ {IMAGE_CATEGORIES.map((c) => (
+
+ {capitalize(c)}
+
+ ))}
+
+
+
{
)}
state, gallerySelector],
- (state, gallery) => {
- const { selectedImage, currentCategory } = gallery;
+ [
+ (state: RootState) => state,
+ gallerySelector,
+ selectFilteredImagesAsObject,
+ selectFilteredImagesIds,
+ ],
+ (state, gallery, filteredImagesAsObject, filteredImageIds) => {
+ const { selectedImage } = gallery;
if (!selectedImage) {
return {
@@ -32,29 +41,29 @@ export const nextPrevImageButtonsSelector = createSelector(
};
}
- const currentImageIndex = state[currentCategory].ids.findIndex(
+ const currentImageIndex = filteredImageIds.findIndex(
(i) => i === selectedImage.image_name
);
const nextImageIndex = clamp(
currentImageIndex + 1,
0,
- state[currentCategory].ids.length - 1
+ filteredImageIds.length - 1
);
const prevImageIndex = clamp(
currentImageIndex - 1,
0,
- state[currentCategory].ids.length - 1
+ filteredImageIds.length - 1
);
- const nextImageId = state[currentCategory].ids[nextImageIndex];
- const prevImageId = state[currentCategory].ids[prevImageIndex];
+ const nextImageId = filteredImageIds[nextImageIndex];
+ const prevImageId = filteredImageIds[prevImageIndex];
- const nextImage = state[currentCategory].entities[nextImageId];
- const prevImage = state[currentCategory].entities[prevImageId];
+ const nextImage = filteredImagesAsObject[nextImageId];
+ const prevImage = filteredImagesAsObject[prevImageId];
- const imagesLength = state[currentCategory].ids.length;
+ const imagesLength = filteredImageIds.length;
return {
isOnFirstImage: currentImageIndex === 0,
diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGetImageByName.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGetImageByName.ts
index 1a73971774..89709b322a 100644
--- a/invokeai/frontend/web/src/features/gallery/hooks/useGetImageByName.ts
+++ b/invokeai/frontend/web/src/features/gallery/hooks/useGetImageByName.ts
@@ -1,33 +1,18 @@
-import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
-import { ResourceOrigin } from 'services/api';
-import { selectResultsEntities } from '../store/resultsSlice';
-import { selectUploadsEntities } from '../store/uploadsSlice';
+import { selectImagesEntities } from '../store/imagesSlice';
+import { useCallback } from 'react';
-const useGetImageByNameSelector = createSelector(
- [selectResultsEntities, selectUploadsEntities],
- (allResults, allUploads) => {
- return { allResults, allUploads };
- }
-);
-
-const useGetImageByNameAndOrigin = () => {
- const { allResults, allUploads } = useAppSelector(useGetImageByNameSelector);
- return (name: string, origin: ResourceOrigin) => {
- if (origin === 'internal') {
- const resultImagesResult = allResults[name];
- if (resultImagesResult) {
- return resultImagesResult;
+const useGetImageByName = () => {
+ const images = useAppSelector(selectImagesEntities);
+ return useCallback(
+ (name: string | undefined) => {
+ if (!name) {
+ return;
}
- }
-
- if (origin === 'external') {
- const userImagesResult = allUploads[name];
- if (userImagesResult) {
- return userImagesResult;
- }
- }
- };
+ return images[name];
+ },
+ [images]
+ );
};
-export default useGetImageByNameAndOrigin;
+export default useGetImageByName;
diff --git a/invokeai/frontend/web/src/features/gallery/store/galleryPersistDenylist.ts b/invokeai/frontend/web/src/features/gallery/store/galleryPersistDenylist.ts
index 49f51d5a80..44e03f9f71 100644
--- a/invokeai/frontend/web/src/features/gallery/store/galleryPersistDenylist.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/galleryPersistDenylist.ts
@@ -4,6 +4,5 @@ import { GalleryState } from './gallerySlice';
* Gallery slice persist denylist
*/
export const galleryPersistDenylist: (keyof GalleryState)[] = [
- 'currentCategory',
'shouldAutoSwitchToNewImages',
];
diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
index e904620d90..16121b6e38 100644
--- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
+++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts
@@ -1,12 +1,6 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
-import {
- receivedGalleryImages,
- receivedUploadImages,
-} from '../../../services/thunks/gallery';
import { ImageDTO } from 'services/api';
-import { resultUpserted } from './resultsSlice';
-import { uploadUpserted } from './uploadsSlice';
type GalleryImageObjectFitType = 'contain' | 'cover';
@@ -16,7 +10,6 @@ export interface GalleryState {
galleryImageObjectFit: GalleryImageObjectFitType;
shouldAutoSwitchToNewImages: boolean;
shouldUseSingleGalleryColumn: boolean;
- currentCategory: 'results' | 'uploads';
}
export const initialGalleryState: GalleryState = {
@@ -24,7 +17,6 @@ export const initialGalleryState: GalleryState = {
galleryImageObjectFit: 'cover',
shouldAutoSwitchToNewImages: true,
shouldUseSingleGalleryColumn: false,
- currentCategory: 'results',
};
export const gallerySlice = createSlice({
@@ -48,12 +40,6 @@ export const gallerySlice = createSlice({
setShouldAutoSwitchToNewImages: (state, action: PayloadAction) => {
state.shouldAutoSwitchToNewImages = action.payload;
},
- setCurrentCategory: (
- state,
- action: PayloadAction<'results' | 'uploads'>
- ) => {
- state.currentCategory = action.payload;
- },
setShouldUseSingleGalleryColumn: (
state,
action: PayloadAction
@@ -61,55 +47,6 @@ export const gallerySlice = createSlice({
state.shouldUseSingleGalleryColumn = action.payload;
},
},
- extraReducers(builder) {
- builder.addCase(receivedGalleryImages.fulfilled, (state, action) => {
- // rehydrate selectedImage URL when results list comes in
- // solves case when outdated URL is in local storage
- const selectedImage = state.selectedImage;
- if (selectedImage) {
- const selectedImageInResults = action.payload.items.find(
- (image) => image.image_name === selectedImage.image_name
- );
-
- if (selectedImageInResults) {
- selectedImage.image_url = selectedImageInResults.image_url;
- selectedImage.thumbnail_url = selectedImageInResults.thumbnail_url;
- state.selectedImage = selectedImage;
- }
- }
- });
-
- builder.addCase(receivedUploadImages.fulfilled, (state, action) => {
- // rehydrate selectedImage URL when results list comes in
- // solves case when outdated URL is in local storage
- const selectedImage = state.selectedImage;
- if (selectedImage) {
- const selectedImageInResults = action.payload.items.find(
- (image) => image.image_name === selectedImage.image_name
- );
-
- if (selectedImageInResults) {
- selectedImage.image_url = selectedImageInResults.image_url;
- selectedImage.thumbnail_url = selectedImageInResults.thumbnail_url;
- state.selectedImage = selectedImage;
- }
- }
- });
-
- builder.addCase(resultUpserted, (state, action) => {
- if (state.shouldAutoSwitchToNewImages) {
- state.selectedImage = action.payload;
- state.currentCategory = 'results';
- }
- });
-
- builder.addCase(uploadUpserted, (state, action) => {
- if (state.shouldAutoSwitchToNewImages) {
- state.selectedImage = action.payload;
- state.currentCategory = 'uploads';
- }
- });
- },
});
export const {
@@ -118,7 +55,6 @@ export const {
setGalleryImageObjectFit,
setShouldAutoSwitchToNewImages,
setShouldUseSingleGalleryColumn,
- setCurrentCategory,
} = gallerySlice.actions;
export default gallerySlice.reducer;
diff --git a/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts b/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts
new file mode 100644
index 0000000000..8ab34fccf0
--- /dev/null
+++ b/invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts
@@ -0,0 +1,127 @@
+import {
+ PayloadAction,
+ createEntityAdapter,
+ createSelector,
+ createSlice,
+} from '@reduxjs/toolkit';
+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';
+
+export const imagesAdapter = createEntityAdapter({
+ selectId: (image) => image.image_name,
+ sortComparer: (a, b) => dateComparator(b.created_at, a.created_at),
+});
+
+type AdditionaImagesState = {
+ offset: number;
+ limit: number;
+ total: number;
+ isLoading: boolean;
+ categories: ImageCategory[];
+};
+
+export const initialImagesState =
+ imagesAdapter.getInitialState({
+ offset: 0,
+ limit: 0,
+ total: 0,
+ isLoading: false,
+ categories: ['general', 'control', 'mask', 'other', 'user'],
+ });
+
+export type ImagesState = typeof initialImagesState;
+
+const imagesSlice = createSlice({
+ name: 'images',
+ initialState: initialImagesState,
+ reducers: {
+ 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;
+ },
+ },
+ extraReducers: (builder) => {
+ builder.addCase(receivedPageOfImages.pending, (state) => {
+ state.isLoading = true;
+ });
+ builder.addCase(receivedPageOfImages.rejected, (state) => {
+ state.isLoading = false;
+ });
+ builder.addCase(receivedPageOfImages.fulfilled, (state, action) => {
+ state.isLoading = false;
+ const { items, offset, limit, total } = action.payload;
+ state.offset = offset;
+ state.limit = limit;
+ state.total = total;
+ imagesAdapter.upsertMany(state, items);
+ });
+ },
+});
+
+export const {
+ selectAll: selectImagesAll,
+ selectById: selectImagesById,
+ selectEntities: selectImagesEntities,
+ selectIds: selectImagesIds,
+ selectTotal: selectImagesTotal,
+} = imagesAdapter.getSelectors((state) => state.images);
+
+export const { imageUpserted, imageRemoved, imageCategoriesChanged } =
+ imagesSlice.actions;
+
+export default imagesSlice.reducer;
+
+export const selectFilteredImagesAsArray = createSelector(
+ (state: RootState) => state,
+ (state) => {
+ const {
+ images: { categories },
+ } = state;
+
+ return selectImagesAll(state).filter((i) =>
+ categories.includes(i.image_category)
+ );
+ }
+);
+
+export const selectFilteredImagesAsObject = createSelector(
+ (state: RootState) => state,
+ (state) => {
+ const {
+ images: { categories },
+ } = state;
+
+ return keyBy(
+ selectImagesAll(state).filter((i) =>
+ categories.includes(i.image_category)
+ ),
+ 'image_name'
+ );
+ }
+);
+
+export const selectFilteredImagesIds = createSelector(
+ (state: RootState) => state,
+ (state) => {
+ const {
+ images: { categories },
+ } = state;
+
+ return selectImagesAll(state)
+ .filter((i) => categories.includes(i.image_category))
+ .map((i) => i.image_name);
+ }
+);
diff --git a/invokeai/frontend/web/src/features/gallery/store/resultsPersistDenylist.ts b/invokeai/frontend/web/src/features/gallery/store/resultsPersistDenylist.ts
deleted file mode 100644
index 1c3d8aaaec..0000000000
--- a/invokeai/frontend/web/src/features/gallery/store/resultsPersistDenylist.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { ResultsState } from './resultsSlice';
-
-/**
- * Results slice persist denylist
- *
- * Currently denylisting results slice entirely, see `serialize.ts`
- */
-export const resultsPersistDenylist: (keyof ResultsState)[] = [];
diff --git a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts
deleted file mode 100644
index 5bc7bd14dd..0000000000
--- a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import {
- PayloadAction,
- createEntityAdapter,
- createSlice,
-} from '@reduxjs/toolkit';
-import { RootState } from 'app/store/store';
-import {
- receivedGalleryImages,
- IMAGES_PER_PAGE,
-} from 'services/thunks/gallery';
-import { ImageDTO } from 'services/api';
-import { dateComparator } from 'common/util/dateComparator';
-
-export type ResultsImageDTO = Omit & {
- image_origin: 'results';
-};
-
-export const resultsAdapter = createEntityAdapter({
- selectId: (image) => image.image_name,
- sortComparer: (a, b) => dateComparator(b.created_at, a.created_at),
-});
-
-type AdditionalResultsState = {
- page: number;
- pages: number;
- isLoading: boolean;
- nextPage: number;
- upsertedImageCount: number;
-};
-
-export const initialResultsState =
- resultsAdapter.getInitialState({
- page: 0,
- pages: 0,
- isLoading: false,
- nextPage: 0,
- upsertedImageCount: 0,
- });
-
-export type ResultsState = typeof initialResultsState;
-
-const resultsSlice = createSlice({
- name: 'results',
- initialState: initialResultsState,
- reducers: {
- resultUpserted: (state, action: PayloadAction) => {
- resultsAdapter.upsertOne(state, action.payload);
- state.upsertedImageCount += 1;
- },
- resultRemoved: (state, action: PayloadAction) => {
- resultsAdapter.removeOne(state, action.payload);
- },
- },
- extraReducers: (builder) => {
- /**
- * Received Result Images Page - PENDING
- */
- builder.addCase(receivedGalleryImages.pending, (state) => {
- state.isLoading = true;
- });
-
- /**
- * Received Result Images Page - FULFILLED
- */
- builder.addCase(receivedGalleryImages.fulfilled, (state, action) => {
- const { page, pages } = action.payload;
-
- // We know these will all be of the results type, but it's not represented in the API types
- const items = action.payload.items;
-
- resultsAdapter.setMany(state, items);
-
- state.page = page;
- state.pages = pages;
- state.nextPage = items.length < IMAGES_PER_PAGE ? page : page + 1;
- state.isLoading = false;
- });
- },
-});
-
-export const {
- selectAll: selectResultsAll,
- selectById: selectResultsById,
- selectEntities: selectResultsEntities,
- selectIds: selectResultsIds,
- selectTotal: selectResultsTotal,
-} = resultsAdapter.getSelectors((state) => state.results);
-
-export const { resultUpserted, resultRemoved } = resultsSlice.actions;
-
-export default resultsSlice.reducer;
diff --git a/invokeai/frontend/web/src/features/gallery/store/uploadsPersistDenylist.ts b/invokeai/frontend/web/src/features/gallery/store/uploadsPersistDenylist.ts
deleted file mode 100644
index 296e8b2057..0000000000
--- a/invokeai/frontend/web/src/features/gallery/store/uploadsPersistDenylist.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { UploadsState } from './uploadsSlice';
-
-/**
- * Uploads slice persist denylist
- *
- * Currently denylisting uploads slice entirely, see `serialize.ts`
- */
-export const uploadsPersistDenylist: (keyof UploadsState)[] = [];
diff --git a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts
deleted file mode 100644
index e7620cbc31..0000000000
--- a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import {
- PayloadAction,
- createEntityAdapter,
- createSlice,
-} from '@reduxjs/toolkit';
-
-import { RootState } from 'app/store/store';
-import { receivedUploadImages, IMAGES_PER_PAGE } from 'services/thunks/gallery';
-import { ImageDTO } from 'services/api';
-import { dateComparator } from 'common/util/dateComparator';
-
-export type UploadsImageDTO = Omit<
- ImageDTO,
- 'image_origin' | 'image_category'
-> & {
- image_origin: 'external';
- image_category: 'user';
-};
-
-export const uploadsAdapter = createEntityAdapter({
- selectId: (image) => image.image_name,
- sortComparer: (a, b) => dateComparator(b.created_at, a.created_at),
-});
-
-type AdditionalUploadsState = {
- page: number;
- pages: number;
- isLoading: boolean;
- nextPage: number;
- upsertedImageCount: number;
-};
-
-export const initialUploadsState =
- uploadsAdapter.getInitialState({
- page: 0,
- pages: 0,
- nextPage: 0,
- isLoading: false,
- upsertedImageCount: 0,
- });
-
-export type UploadsState = typeof initialUploadsState;
-
-const uploadsSlice = createSlice({
- name: 'uploads',
- initialState: initialUploadsState,
- reducers: {
- uploadUpserted: (state, action: PayloadAction) => {
- uploadsAdapter.upsertOne(state, action.payload);
- state.upsertedImageCount += 1;
- },
- uploadRemoved: (state, action: PayloadAction) => {
- uploadsAdapter.removeOne(state, action.payload);
- },
- },
- extraReducers: (builder) => {
- /**
- * Received Upload Images Page - PENDING
- */
- builder.addCase(receivedUploadImages.pending, (state) => {
- state.isLoading = true;
- });
-
- /**
- * Received Upload Images Page - FULFILLED
- */
- builder.addCase(receivedUploadImages.fulfilled, (state, action) => {
- const { page, pages } = action.payload;
-
- // We know these will all be of the uploads type, but it's not represented in the API types
- const items = action.payload.items;
-
- uploadsAdapter.setMany(state, items);
-
- state.page = page;
- state.pages = pages;
- state.nextPage = items.length < IMAGES_PER_PAGE ? page : page + 1;
- state.isLoading = false;
- });
- },
-});
-
-export const {
- selectAll: selectUploadsAll,
- selectById: selectUploadsById,
- selectEntities: selectUploadsEntities,
- selectIds: selectUploadsIds,
- selectTotal: selectUploadsTotal,
-} = uploadsAdapter.getSelectors((state) => state.uploads);
-
-export const { uploadUpserted, uploadRemoved } = uploadsSlice.actions;
-
-export default uploadsSlice.reducer;
diff --git a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx
index e4a0f41ee1..57cefb0a9c 100644
--- a/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/fields/ImageInputFieldComponent.tsx
@@ -2,7 +2,7 @@ import { Box, Image } from '@chakra-ui/react';
import { useAppDispatch } from 'app/store/storeHooks';
import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder';
import { useGetUrl } from 'common/util/getUrl';
-import useGetImageByNameAndOrigin from 'features/gallery/hooks/useGetImageByName';
+import useGetImageByName from 'features/gallery/hooks/useGetImageByName';
import { fieldValueChanged } from 'features/nodes/store/nodesSlice';
import {
@@ -11,7 +11,6 @@ import {
} from 'features/nodes/types/types';
import { DragEvent, memo, useCallback, useState } from 'react';
-import { ResourceOrigin } from 'services/api';
import { FieldComponentProps } from './types';
const ImageInputFieldComponent = (
@@ -19,7 +18,7 @@ const ImageInputFieldComponent = (
) => {
const { nodeId, field } = props;
- const getImageByNameAndType = useGetImageByNameAndOrigin();
+ const getImageByName = useGetImageByName();
const dispatch = useAppDispatch();
const [url, setUrl] = useState(field.value?.image_url);
const { getUrl } = useGetUrl();
@@ -27,15 +26,7 @@ const ImageInputFieldComponent = (
const handleDrop = useCallback(
(e: DragEvent) => {
const name = e.dataTransfer.getData('invokeai/imageName');
- const type = e.dataTransfer.getData(
- 'invokeai/imageOrigin'
- ) as ResourceOrigin;
-
- if (!name || !type) {
- return;
- }
-
- const image = getImageByNameAndType(name, type);
+ const image = getImageByName(name);
if (!image) {
return;
@@ -51,7 +42,7 @@ const ImageInputFieldComponent = (
})
);
},
- [getImageByNameAndType, dispatch, field.name, nodeId]
+ [getImageByName, dispatch, field.name, nodeId]
);
return (
diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasGraph.ts
index 3615f7d298..2e741443cf 100644
--- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasGraph.ts
+++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildCanvasGraph.ts
@@ -26,18 +26,21 @@ const buildBaseNode = (
| ImageToImageInvocation
| InpaintInvocation
| undefined => {
- const dimensionsOverride = state.canvas.boundingBoxDimensions;
+ const overrides = {
+ ...state.canvas.boundingBoxDimensions,
+ is_intermediate: true,
+ };
if (nodeType === 'txt2img') {
- return buildTxt2ImgNode(state, dimensionsOverride);
+ return buildTxt2ImgNode(state, overrides);
}
if (nodeType === 'img2img') {
- return buildImg2ImgNode(state, dimensionsOverride);
+ return buildImg2ImgNode(state, overrides);
}
if (nodeType === 'inpaint' || nodeType === 'outpaint') {
- return buildInpaintNode(state, dimensionsOverride);
+ return buildInpaintNode(state, overrides);
}
};
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 a5b106163f..cfe1513420 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
@@ -5,7 +5,6 @@ import { useGetUrl } from 'common/util/getUrl';
import { clearInitialImage } from 'features/parameters/store/generationSlice';
import { DragEvent, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
-import { ResourceOrigin } from 'services/api';
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
import { generationSelector } from 'features/parameters/store/generationSelectors';
import { initialImageSelected } from 'features/parameters/store/actions';
@@ -55,11 +54,7 @@ const InitialImagePreview = () => {
const handleDrop = useCallback(
(e: DragEvent) => {
const name = e.dataTransfer.getData('invokeai/imageName');
- const type = e.dataTransfer.getData(
- 'invokeai/imageOrigin'
- ) as ResourceOrigin;
-
- dispatch(initialImageSelected({ image_name: name, image_origin: type }));
+ dispatch(initialImageSelected(name));
},
[dispatch]
);
diff --git a/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts b/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts
index 27ae63e5dd..ca9826693d 100644
--- a/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts
+++ b/invokeai/frontend/web/src/features/parameters/hooks/useParameters.ts
@@ -88,7 +88,7 @@ export const useParameters = () => {
return;
}
- dispatch(initialImageSelected(image));
+ dispatch(initialImageSelected(image.image_name));
toaster({
title: t('toast.initialImageSet'),
status: 'info',
diff --git a/invokeai/frontend/web/src/features/parameters/store/actions.ts b/invokeai/frontend/web/src/features/parameters/store/actions.ts
index 6c1030b7b0..e9b90134e1 100644
--- a/invokeai/frontend/web/src/features/parameters/store/actions.ts
+++ b/invokeai/frontend/web/src/features/parameters/store/actions.ts
@@ -26,6 +26,6 @@ export const isImageDTO = (image: any): image is ImageDTO => {
);
};
-export const initialImageSelected = createAction<
- ImageDTO | ImageNameAndOrigin | undefined
->('generation/initialImageSelected');
+export const initialImageSelected = createAction(
+ 'generation/initialImageSelected'
+);
diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSelectors.ts b/invokeai/frontend/web/src/features/parameters/store/generationSelectors.ts
index dbf5eec791..b7322740ef 100644
--- a/invokeai/frontend/web/src/features/parameters/store/generationSelectors.ts
+++ b/invokeai/frontend/web/src/features/parameters/store/generationSelectors.ts
@@ -1,34 +1,3 @@
-import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store/store';
-import { selectResultsById } from 'features/gallery/store/resultsSlice';
-import { selectUploadsById } from 'features/gallery/store/uploadsSlice';
-import { isEqual } from 'lodash-es';
export const generationSelector = (state: RootState) => state.generation;
-
-export const mayGenerateMultipleImagesSelector = createSelector(
- generationSelector,
- ({ shouldRandomizeSeed, shouldGenerateVariations }) => {
- return shouldRandomizeSeed || shouldGenerateVariations;
- },
- {
- memoizeOptions: {
- resultEqualityCheck: isEqual,
- },
- }
-);
-
-export const initialImageSelector = createSelector(
- [(state: RootState) => state, generationSelector],
- (state, generation) => {
- const { initialImage } = generation;
-
- if (initialImage?.type === 'results') {
- return selectResultsById(state, initialImage.name);
- }
-
- if (initialImage?.type === 'uploads') {
- return selectUploadsById(state, initialImage.name);
- }
- }
-);
diff --git a/invokeai/frontend/web/src/services/thunks/gallery.ts b/invokeai/frontend/web/src/services/thunks/gallery.ts
deleted file mode 100644
index e6bb163167..0000000000
--- a/invokeai/frontend/web/src/services/thunks/gallery.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { createAppAsyncThunk } from 'app/store/storeUtils';
-import { ImagesService, PaginatedResults_ImageDTO_ } from 'services/api';
-
-export const IMAGES_PER_PAGE = 20;
-
-type ReceivedResultImagesPageThunkConfig = {
- rejectValue: {
- error: unknown;
- };
-};
-
-export const receivedGalleryImages = createAppAsyncThunk<
- PaginatedResults_ImageDTO_,
- void,
- ReceivedResultImagesPageThunkConfig
->(
- 'results/receivedResultImagesPage',
- async (_arg, { getState, rejectWithValue }) => {
- const { page, pages, nextPage, upsertedImageCount } = getState().results;
-
- // If many images have been upserted, we need to offset the page number
- // TODO: add an offset param to the list images endpoint
- const pageOffset = Math.floor(upsertedImageCount / IMAGES_PER_PAGE);
-
- const response = await ImagesService.listImagesWithMetadata({
- excludeCategories: ['user'],
- isIntermediate: false,
- page: nextPage + pageOffset,
- perPage: IMAGES_PER_PAGE,
- });
-
- return response;
- }
-);
-
-type ReceivedUploadImagesPageThunkConfig = {
- rejectValue: {
- error: unknown;
- };
-};
-
-export const receivedUploadImages = createAppAsyncThunk<
- PaginatedResults_ImageDTO_,
- void,
- ReceivedUploadImagesPageThunkConfig
->(
- 'uploads/receivedUploadImagesPage',
- async (_arg, { getState, rejectWithValue }) => {
- const { page, pages, nextPage, upsertedImageCount } = getState().uploads;
-
- // If many images have been upserted, we need to offset the page number
- // TODO: add an offset param to the list images endpoint
- const pageOffset = Math.floor(upsertedImageCount / IMAGES_PER_PAGE);
-
- const response = await ImagesService.listImagesWithMetadata({
- includeCategories: ['user'],
- isIntermediate: false,
- page: nextPage + pageOffset,
- perPage: IMAGES_PER_PAGE,
- });
-
- return response;
- }
-);
diff --git a/invokeai/frontend/web/src/services/thunks/image.ts b/invokeai/frontend/web/src/services/thunks/image.ts
index f324edad2b..87832c6b1e 100644
--- a/invokeai/frontend/web/src/services/thunks/image.ts
+++ b/invokeai/frontend/web/src/services/thunks/image.ts
@@ -1,5 +1,5 @@
import { createAppAsyncThunk } from 'app/store/storeUtils';
-import { InvokeTabName } from 'features/ui/store/tabMap';
+import { selectImagesAll } from 'features/gallery/store/imagesSlice';
import { ImagesService } from 'services/api';
type imageUrlsReceivedArg = Parameters<
@@ -71,3 +71,32 @@ export const imageUpdated = createAppAsyncThunk(
return response;
}
);
+
+type ImagesListedArg = Parameters<
+ (typeof ImagesService)['listImagesWithMetadata']
+>[0];
+
+export const IMAGES_PER_PAGE = 20;
+
+/**
+ * `ImagesService.listImagesWithMetadata()` thunk
+ */
+export const receivedPageOfImages = createAppAsyncThunk(
+ 'api/receivedPageOfImages',
+ async (_, { getState }) => {
+ const state = getState();
+ const { categories } = state.images;
+
+ const totalImagesInFilter = selectImagesAll(state).filter((i) =>
+ categories.includes(i.image_category)
+ ).length;
+
+ const response = await ImagesService.listImagesWithMetadata({
+ categories,
+ isIntermediate: false,
+ offset: totalImagesInFilter,
+ limit: IMAGES_PER_PAGE,
+ });
+ return response;
+ }
+);
diff --git a/invokeai/frontend/web/src/services/types/guards.ts b/invokeai/frontend/web/src/services/types/guards.ts
index 1231a38b4d..4d33cfa246 100644
--- a/invokeai/frontend/web/src/services/types/guards.ts
+++ b/invokeai/frontend/web/src/services/types/guards.ts
@@ -1,4 +1,3 @@
-import { UploadsImageDTO } from 'features/gallery/store/uploadsSlice';
import { get, isObject, isString } from 'lodash-es';
import {
GraphExecutionState,
@@ -10,17 +9,9 @@ import {
CollectInvocationOutput,
ImageField,
LatentsOutput,
- ImageDTO,
ResourceOrigin,
} from 'services/api';
-export const isUploadsImageDTO = (
- image: ImageDTO | undefined
-): image is UploadsImageDTO =>
- image !== undefined &&
- image.image_origin === 'external' &&
- image.image_category === 'user';
-
export const isImageOutput = (
output: GraphExecutionState['results'][string]
): output is ImageOutput => output.type === 'image_output';