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';