From 29fcc92da9e235ba1c87a2e04e69f457cdce839b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 27 May 2023 21:46:03 +1000 Subject: [PATCH] feat(ui): handle new image origin/category setup - Update all thunks & network related things - Update gallery What I have not done yet is rename the gallery tabs and the relevant slices, but I believe the functionality is all there. Also I fixed several bugs along the way but couldn't really commit them separately bc I was refactoring. Can't remember what they were, but related to the gallery image switching. --- .../middleware/listenerMiddleware/index.ts | 9 + .../listeners/canvasMerged.ts | 1 - .../listeners/canvasSavedToGallery.ts | 3 +- .../listeners/imageDeleted.ts | 41 ++- .../listeners/imageMetadataReceived.ts | 23 +- .../listeners/imageUpdated.ts | 26 ++ .../listeners/imageUploaded.ts | 27 +- .../listeners/imageUrlsReceived.ts | 6 +- .../listeners/initialImageSelected.ts | 6 +- .../listeners/socketio/invocationComplete.ts | 8 +- .../listeners/userInvokedCanvas.ts | 10 +- .../frontend/web/src/app/types/invokeai.ts | 4 +- .../src/common/components/ImageUploader.tsx | 3 +- .../web/src/common/util/parseMetadata.ts | 239 ------------------ .../components/CurrentImagePreview.tsx | 2 +- .../gallery/components/HoverableImage.tsx | 2 +- .../gallery/hooks/useGetImageByName.ts | 12 +- .../web/src/features/gallery/store/actions.ts | 4 +- .../features/gallery/store/gallerySlice.ts | 17 ++ .../features/gallery/store/resultsSlice.ts | 9 +- .../features/gallery/store/uploadsSlice.ts | 13 +- .../fields/ImageInputFieldComponent.tsx | 10 +- .../graphBuilders/buildImageToImageGraph.ts | 2 +- .../nodeBuilders/buildImageToImageNode.ts | 2 +- .../util/nodeBuilders/buildInpaintNode.ts | 2 +- .../ImageToImage/InitialImagePreview.tsx | 8 +- .../src/features/parameters/store/actions.ts | 12 +- .../web/src/services/thunks/gallery.ts | 5 +- .../frontend/web/src/services/types/guards.ts | 20 +- 29 files changed, 181 insertions(+), 345 deletions(-) create mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUpdated.ts delete mode 100644 invokeai/frontend/web/src/common/util/parseMetadata.ts diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index b669becfe6..7159957efa 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -67,6 +67,10 @@ import { addReceivedUploadImagesPageFulfilledListener, addReceivedUploadImagesPageRejectedListener, } from './listeners/receivedUploadImages'; +import { + addImageUpdatedFulfilledListener, + addImageUpdatedRejectedListener, +} from './listeners/imageUpdated'; export const listenerMiddleware = createListenerMiddleware(); @@ -90,6 +94,11 @@ export type AppListenerEffect = ListenerEffect< addImageUploadedFulfilledListener(); addImageUploadedRejectedListener(); +// Image updated +addImageUpdatedFulfilledListener(); +addImageUpdatedRejectedListener(); + +// Image selected addInitialImageSelectedListener(); // Image deleted diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts index fc4c7247cd..80865f3126 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/canvasMerged.ts @@ -57,7 +57,6 @@ export const addCanvasMergedListener = () => { }, imageCategory: 'general', isIntermediate: true, - showInGallery: false, }) ); 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 7656e58b57..01f097cdd1 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 @@ -37,8 +37,7 @@ export const addCanvasSavedToGalleryListener = () => { file: new File([blob], filename, { type: 'image/png' }), }, imageCategory: 'general', - isIntermediate: false, - showInGallery: true, + isIntermediate: true, }) ); 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 cd4771b96a..7bd92e7e13 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 @@ -4,8 +4,15 @@ import { imageDeleted } from 'services/thunks/image'; import { log } from 'app/logging/useLogger'; import { clamp } from 'lodash-es'; import { imageSelected } from 'features/gallery/store/gallerySlice'; -import { uploadsAdapter } from 'features/gallery/store/uploadsSlice'; -import { resultsAdapter } from 'features/gallery/store/resultsSlice'; +import { + uploadRemoved, + uploadsAdapter, +} from 'features/gallery/store/uploadsSlice'; +import { + resultRemoved, + resultsAdapter, +} from 'features/gallery/store/resultsSlice'; +import { isUploadsImageDTO } from 'services/types/guards'; const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' }); @@ -22,13 +29,17 @@ export const addRequestedImageDeletionListener = () => { return; } - const { image_name, image_type } = image; + const { image_name, image_origin } = image; - const selectedImageName = getState().gallery.selectedImage?.image_name; + 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; - if (selectedImageName === image_name) { - const allIds = getState()[image_type].ids; - const allEntities = getState()[image_type].entities; + const allEntities = isUserImage + ? state.uploads.entities + : state.results.entities; const deletedImageIndex = allIds.findIndex( (result) => result.toString() === image_name @@ -53,7 +64,15 @@ export const addRequestedImageDeletionListener = () => { } } - dispatch(imageDeleted({ imageName: image_name, imageType: image_type })); + if (isUserImage) { + dispatch(uploadRemoved(image_name)); + } else { + dispatch(resultRemoved(image_name)); + } + + dispatch( + imageDeleted({ imageName: image_name, imageOrigin: image_origin }) + ); }, }); }; @@ -65,12 +84,12 @@ export const addImageDeletedPendingListener = () => { startAppListening({ actionCreator: imageDeleted.pending, effect: (action, { dispatch, getState }) => { - const { imageName, imageType } = action.meta.arg; + const { imageName, imageOrigin } = action.meta.arg; // Preemptively remove the image from the gallery - if (imageType === 'uploads') { + if (imageOrigin === 'external') { uploadsAdapter.removeOne(getState().uploads, imageName); } - if (imageType === 'results') { + if (imageOrigin === 'internal') { resultsAdapter.removeOne(getState().results, 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 c93ed2820f..276ef7be6c 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,14 +1,9 @@ import { log } from 'app/logging/useLogger'; import { startAppListening } from '..'; import { imageMetadataReceived } from 'services/thunks/image'; -import { - ResultsImageDTO, - resultUpserted, -} from 'features/gallery/store/resultsSlice'; -import { - UploadsImageDTO, - uploadUpserted, -} from 'features/gallery/store/uploadsSlice'; +import { resultUpserted } from 'features/gallery/store/resultsSlice'; +import { uploadUpserted } from 'features/gallery/store/uploadsSlice'; +import { imageSelected } from 'features/gallery/store/gallerySlice'; const moduleLog = log.child({ namespace: 'image' }); @@ -16,15 +11,15 @@ export const addImageMetadataReceivedFulfilledListener = () => { startAppListening({ actionCreator: imageMetadataReceived.fulfilled, effect: (action, { getState, dispatch }) => { - const image = action.payload; - moduleLog.debug({ data: { image } }, 'Image metadata received'); + const imageDTO = action.payload; + moduleLog.debug({ data: { imageDTO } }, 'Image metadata received'); - if (image.image_type === 'results') { - dispatch(resultUpserted(action.payload as ResultsImageDTO)); + if (imageDTO.image_origin === 'internal') { + dispatch(resultUpserted(imageDTO)); } - if (image.image_type === 'uploads') { - dispatch(uploadUpserted(action.payload as UploadsImageDTO)); + if (imageDTO.image_origin === 'external') { + dispatch(uploadUpserted(imageDTO)); } }, }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUpdated.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUpdated.ts new file mode 100644 index 0000000000..6f8b46ec23 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/imageUpdated.ts @@ -0,0 +1,26 @@ +import { startAppListening } from '..'; +import { imageUpdated } from 'services/thunks/image'; +import { log } from 'app/logging/useLogger'; + +const moduleLog = log.child({ namespace: 'image' }); + +export const addImageUpdatedFulfilledListener = () => { + startAppListening({ + actionCreator: imageUpdated.fulfilled, + effect: (action, { dispatch, getState }) => { + moduleLog.debug( + { oldImage: action.meta.arg, updatedImage: action.payload }, + 'Image updated' + ); + }, + }); +}; + +export const addImageUpdatedRejectedListener = () => { + startAppListening({ + actionCreator: imageUpdated.rejected, + effect: (action, { dispatch }) => { + moduleLog.debug({ oldImage: action.meta.arg }, 'Image update failed'); + }, + }); +}; 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 3d69eb8f9a..dcce86017e 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,6 +1,9 @@ import { startAppListening } from '..'; import { uploadUpserted } from 'features/gallery/store/uploadsSlice'; -import { imageSelected } from 'features/gallery/store/gallerySlice'; +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'; @@ -10,31 +13,30 @@ const moduleLog = log.child({ namespace: 'image' }); export const addImageUploadedFulfilledListener = () => { startAppListening({ - predicate: (action): action is ReturnType => - imageUploaded.fulfilled.match(action) && - action.payload.is_intermediate === false, + actionCreator: imageUploaded.fulfilled, effect: (action, { dispatch, getState }) => { const image = action.payload; moduleLog.debug({ arg: '', image }, 'Image uploaded'); + if (action.payload.is_intermediate) { + // No further actions needed for intermediate images + return; + } + const state = getState(); // Handle uploads - if (!image.show_in_gallery && image.image_type === 'uploads') { + if (image.image_category === 'user' && !image.is_intermediate) { dispatch(uploadUpserted(image)); - dispatch(addToast({ title: 'Image Uploaded', status: 'success' })); - - if (state.gallery.shouldAutoSwitchToNewImages) { - dispatch(imageSelected(image)); - } } // Handle results // TODO: Can this ever happen? I don't think so... - if (image.show_in_gallery) { + if (image.image_category !== 'user' && !image.is_intermediate) { dispatch(resultUpserted(image)); + dispatch(setCurrentCategory('results')); } }, }); @@ -44,6 +46,9 @@ export const addImageUploadedRejectedListener = () => { startAppListening({ actionCreator: imageUploaded.rejected, effect: (action, { dispatch }) => { + const { formData, ...rest } = action.meta.arg; + const sanitizedData = { arg: { ...rest, formData: { file: '' } } }; + moduleLog.error({ data: sanitizedData }, 'Image upload failed'); dispatch( addToast({ title: 'Image Upload Failed', 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 4ff2a02118..588d7611cc 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 @@ -13,9 +13,9 @@ export const addImageUrlsReceivedFulfilledListener = () => { const image = action.payload; moduleLog.debug({ data: { image } }, 'Image URLs received'); - const { image_type, image_name, image_url, thumbnail_url } = image; + const { image_origin, image_name, image_url, thumbnail_url } = image; - if (image_type === 'results') { + if (image_origin === 'results') { resultsAdapter.updateOne(getState().results, { id: image_name, changes: { @@ -25,7 +25,7 @@ export const addImageUrlsReceivedFulfilledListener = () => { }); } - if (image_type === 'uploads') { + if (image_origin === 'uploads') { uploadsAdapter.updateOne(getState().uploads, { id: image_name, changes: { 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 d6cfc260f3..a2e783a38a 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 @@ -30,14 +30,14 @@ export const addInitialImageSelectedListener = () => { return; } - const { image_name, image_type } = action.payload; + const { image_name, image_origin } = action.payload; let image: ImageDTO | undefined; const state = getState(); - if (image_type === 'results') { + if (image_origin === 'results') { image = selectResultsById(state, image_name); - } else if (image_type === 'uploads') { + } else if (image_origin === 'uploads') { image = selectUploadsById(state, image_name); } 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 60a3cdfedf..81c0286e3b 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 @@ -34,13 +34,13 @@ export const addInvocationCompleteListener = () => { // This complete event has an associated image output if (isImageOutput(result) && !nodeDenylist.includes(node.type)) { - const { image_name, image_type } = result.image; + const { image_name, image_origin } = result.image; // Get its metadata dispatch( imageMetadataReceived({ imageName: image_name, - imageType: image_type, + imageOrigin: image_origin, }) ); @@ -48,10 +48,6 @@ export const addInvocationCompleteListener = () => { imageMetadataReceived.fulfilled.match ); - if (getState().gallery.shouldAutoSwitchToNewImages) { - dispatch(imageSelected(imageDTO)); - } - // Handle canvas image if ( graph_execution_state_id === diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCanvas.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCanvas.ts index bc1d5d5f8a..0ee3016bdb 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCanvas.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCanvas.ts @@ -103,7 +103,6 @@ export const addUserInvokedCanvasListener = () => { }, imageCategory: 'general', isIntermediate: true, - showInGallery: false, }) ); @@ -117,7 +116,7 @@ export const addUserInvokedCanvasListener = () => { // Update the base node with the image name and type baseNode.image = { image_name: baseImageDTO.image_name, - image_type: baseImageDTO.image_type, + image_origin: baseImageDTO.image_origin, }; } @@ -131,7 +130,6 @@ export const addUserInvokedCanvasListener = () => { }, imageCategory: 'mask', isIntermediate: true, - showInGallery: false, }) ); @@ -145,7 +143,7 @@ export const addUserInvokedCanvasListener = () => { // Update the base node with the image name and type baseNode.mask = { image_name: maskImageDTO.image_name, - image_type: maskImageDTO.image_type, + image_origin: maskImageDTO.image_origin, }; } @@ -162,7 +160,7 @@ export const addUserInvokedCanvasListener = () => { dispatch( imageUpdated({ imageName: baseNode.image.image_name, - imageType: baseNode.image.image_type, + imageOrigin: baseNode.image.image_origin, requestBody: { session_id: sessionId }, }) ); @@ -173,7 +171,7 @@ export const addUserInvokedCanvasListener = () => { dispatch( imageUpdated({ imageName: baseNode.mask.image_name, - imageType: baseNode.mask.image_type, + imageOrigin: baseNode.mask.image_origin, requestBody: { session_id: sessionId }, }) ); diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index 68f7568779..0de1d8c84b 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -15,7 +15,7 @@ import { SelectedImage } from 'features/parameters/store/actions'; import { InvokeTabName } from 'features/ui/store/tabMap'; import { IRect } from 'konva/lib/types'; -import { ImageResponseMetadata, ImageType } from 'services/api'; +import { ImageResponseMetadata, ResourceOrigin } from 'services/api'; import { O } from 'ts-toolbelt'; /** @@ -124,7 +124,7 @@ export type PostProcessedImageMetadata = ESRGANMetadata | FacetoolMetadata; */ // export ty`pe Image = { // name: string; -// type: ImageType; +// type: image_origin; // url: string; // thumbnail: string; // metadata: ImageResponseMetadata; diff --git a/invokeai/frontend/web/src/common/components/ImageUploader.tsx b/invokeai/frontend/web/src/common/components/ImageUploader.tsx index a4e6e52cb8..17f6d68633 100644 --- a/invokeai/frontend/web/src/common/components/ImageUploader.tsx +++ b/invokeai/frontend/web/src/common/components/ImageUploader.tsx @@ -69,9 +69,8 @@ const ImageUploader = (props: ImageUploaderProps) => { dispatch( imageUploaded({ formData: { file }, - imageCategory: 'general', + imageCategory: 'user', isIntermediate: false, - showInGallery: false, }) ); }, diff --git a/invokeai/frontend/web/src/common/util/parseMetadata.ts b/invokeai/frontend/web/src/common/util/parseMetadata.ts deleted file mode 100644 index bb3999d6d0..0000000000 --- a/invokeai/frontend/web/src/common/util/parseMetadata.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { forEach, size } from 'lodash-es'; -import { - ImageField, - LatentsField, - ConditioningField, - ControlField, -} from 'services/api'; - -const OBJECT_TYPESTRING = '[object Object]'; -const STRING_TYPESTRING = '[object String]'; -const NUMBER_TYPESTRING = '[object Number]'; -const BOOLEAN_TYPESTRING = '[object Boolean]'; -const ARRAY_TYPESTRING = '[object Array]'; - -const isObject = (obj: unknown): obj is Record => - Object.prototype.toString.call(obj) === OBJECT_TYPESTRING; - -const isString = (obj: unknown): obj is string => - Object.prototype.toString.call(obj) === STRING_TYPESTRING; - -const isNumber = (obj: unknown): obj is number => - Object.prototype.toString.call(obj) === NUMBER_TYPESTRING; - -const isBoolean = (obj: unknown): obj is boolean => - Object.prototype.toString.call(obj) === BOOLEAN_TYPESTRING; - -const isArray = (obj: unknown): obj is Array => - Object.prototype.toString.call(obj) === ARRAY_TYPESTRING; - -const parseImageField = (imageField: unknown): ImageField | undefined => { - // Must be an object - if (!isObject(imageField)) { - return; - } - - // An ImageField must have both `image_name` and `image_type` - if (!('image_name' in imageField && 'image_type' in imageField)) { - return; - } - - // An ImageField's `image_type` must be one of the allowed values - if ( - !['results', 'uploads', 'intermediates'].includes(imageField.image_type) - ) { - return; - } - - // An ImageField's `image_name` must be a string - if (typeof imageField.image_name !== 'string') { - return; - } - - // Build a valid ImageField - return { - image_type: imageField.image_type, - image_name: imageField.image_name, - }; -}; - -const parseLatentsField = (latentsField: unknown): LatentsField | undefined => { - // Must be an object - if (!isObject(latentsField)) { - return; - } - - // A LatentsField must have a `latents_name` - if (!('latents_name' in latentsField)) { - return; - } - - // A LatentsField's `latents_name` must be a string - if (typeof latentsField.latents_name !== 'string') { - return; - } - - // Build a valid LatentsField - return { - latents_name: latentsField.latents_name, - }; -}; - -const parseConditioningField = ( - conditioningField: unknown -): ConditioningField | undefined => { - // Must be an object - if (!isObject(conditioningField)) { - return; - } - - // A ConditioningField must have a `conditioning_name` - if (!('conditioning_name' in conditioningField)) { - return; - } - - // A ConditioningField's `conditioning_name` must be a string - if (typeof conditioningField.conditioning_name !== 'string') { - return; - } - - // Build a valid ConditioningField - return { - conditioning_name: conditioningField.conditioning_name, - }; -}; - -const parseControlField = (controlField: unknown): ControlField | undefined => { - // Must be an object - if (!isObject(controlField)) { - return; - } - - // A ControlField must have a `control` - if (!('control' in controlField)) { - return; - } - // console.log(typeof controlField.control); - - // Build a valid ControlField - return { - control: controlField.control, - }; -}; - -type NodeMetadata = { - [key: string]: - | string - | number - | boolean - | ImageField - | LatentsField - | ConditioningField - | ControlField; -}; - -type InvokeAIMetadata = { - session_id?: string; - node?: NodeMetadata; -}; - -export const parseNodeMetadata = ( - nodeMetadata: Record -): NodeMetadata | undefined => { - if (!isObject(nodeMetadata)) { - return; - } - - const parsed: NodeMetadata = {}; - - forEach(nodeMetadata, (nodeItem, nodeKey) => { - // `id` and `type` must be strings if they are present - if (['id', 'type'].includes(nodeKey)) { - if (isString(nodeItem)) { - parsed[nodeKey] = nodeItem; - } - return; - } - - // the only valid object types are ImageField, LatentsField, ConditioningField, ControlField - if (isObject(nodeItem)) { - if ('image_name' in nodeItem || 'image_type' in nodeItem) { - const imageField = parseImageField(nodeItem); - if (imageField) { - parsed[nodeKey] = imageField; - } - return; - } - - if ('latents_name' in nodeItem) { - const latentsField = parseLatentsField(nodeItem); - if (latentsField) { - parsed[nodeKey] = latentsField; - } - return; - } - - if ('conditioning_name' in nodeItem) { - const conditioningField = parseConditioningField(nodeItem); - if (conditioningField) { - parsed[nodeKey] = conditioningField; - } - return; - } - - if ('control' in nodeItem) { - const controlField = parseControlField(nodeItem); - if (controlField) { - parsed[nodeKey] = controlField; - } - return; - } - } - - // otherwise we accept any string, number or boolean - if (isString(nodeItem) || isNumber(nodeItem) || isBoolean(nodeItem)) { - parsed[nodeKey] = nodeItem; - return; - } - }); - - if (size(parsed) === 0) { - return; - } - - return parsed; -}; - -export const parseInvokeAIMetadata = ( - metadata: Record | undefined -): InvokeAIMetadata | undefined => { - if (metadata === undefined) { - return; - } - - if (!isObject(metadata)) { - return; - } - - const parsed: InvokeAIMetadata = {}; - - forEach(metadata, (item, key) => { - if (key === 'session_id' && isString(item)) { - parsed['session_id'] = item; - } - - if (key === 'node' && isObject(item)) { - const nodeMetadata = parseNodeMetadata(item); - - if (nodeMetadata) { - parsed['node'] = nodeMetadata; - } - } - }); - - if (size(parsed) === 0) { - return; - } - - return parsed; -}; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx index 4562e3458d..38c104a83d 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx @@ -62,7 +62,7 @@ const CurrentImagePreview = () => { return; } e.dataTransfer.setData('invokeai/imageName', image.image_name); - e.dataTransfer.setData('invokeai/imageType', image.image_type); + 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 ed427f4984..4a51580650 100644 --- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx @@ -147,7 +147,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { const handleDragStart = useCallback( (e: DragEvent) => { e.dataTransfer.setData('invokeai/imageName', image.image_name); - e.dataTransfer.setData('invokeai/imageType', image.image_type); + e.dataTransfer.setData('invokeai/imageOrigin', image.image_origin); e.dataTransfer.effectAllowed = 'move'; }, [image] diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useGetImageByName.ts b/invokeai/frontend/web/src/features/gallery/hooks/useGetImageByName.ts index ad0870e7a4..1a73971774 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useGetImageByName.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useGetImageByName.ts @@ -1,6 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { ImageType } from 'services/api'; +import { ResourceOrigin } from 'services/api'; import { selectResultsEntities } from '../store/resultsSlice'; import { selectUploadsEntities } from '../store/uploadsSlice'; @@ -11,17 +11,17 @@ const useGetImageByNameSelector = createSelector( } ); -const useGetImageByNameAndType = () => { +const useGetImageByNameAndOrigin = () => { const { allResults, allUploads } = useAppSelector(useGetImageByNameSelector); - return (name: string, type: ImageType) => { - if (type === 'results') { + return (name: string, origin: ResourceOrigin) => { + if (origin === 'internal') { const resultImagesResult = allResults[name]; if (resultImagesResult) { return resultImagesResult; } } - if (type === 'uploads') { + if (origin === 'external') { const userImagesResult = allUploads[name]; if (userImagesResult) { return userImagesResult; @@ -30,4 +30,4 @@ const useGetImageByNameAndType = () => { }; }; -export default useGetImageByNameAndType; +export default useGetImageByNameAndOrigin; diff --git a/invokeai/frontend/web/src/features/gallery/store/actions.ts b/invokeai/frontend/web/src/features/gallery/store/actions.ts index 7e071f279d..7c00201da9 100644 --- a/invokeai/frontend/web/src/features/gallery/store/actions.ts +++ b/invokeai/frontend/web/src/features/gallery/store/actions.ts @@ -1,9 +1,9 @@ import { createAction } from '@reduxjs/toolkit'; -import { ImageNameAndType } from 'features/parameters/store/actions'; +import { ImageNameAndOrigin } from 'features/parameters/store/actions'; import { ImageDTO } from 'services/api'; export const requestedImageDeletion = createAction< - ImageDTO | ImageNameAndType | undefined + ImageDTO | ImageNameAndOrigin | undefined >('gallery/requestedImageDeletion'); export const sentImageToCanvas = createAction('gallery/sentImageToCanvas'); diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index 1a49aeac1e..e904620d90 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -5,6 +5,8 @@ import { receivedUploadImages, } from '../../../services/thunks/gallery'; import { ImageDTO } from 'services/api'; +import { resultUpserted } from './resultsSlice'; +import { uploadUpserted } from './uploadsSlice'; type GalleryImageObjectFitType = 'contain' | 'cover'; @@ -76,6 +78,7 @@ export const gallerySlice = createSlice({ } } }); + builder.addCase(receivedUploadImages.fulfilled, (state, action) => { // rehydrate selectedImage URL when results list comes in // solves case when outdated URL is in local storage @@ -92,6 +95,20 @@ export const gallerySlice = createSlice({ } } }); + + 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'; + } + }); }, }); diff --git a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts index ad05284119..5bc7bd14dd 100644 --- a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts @@ -11,8 +11,8 @@ import { import { ImageDTO } from 'services/api'; import { dateComparator } from 'common/util/dateComparator'; -export type ResultsImageDTO = Omit & { - image_type: 'results'; +export type ResultsImageDTO = Omit & { + image_origin: 'results'; }; export const resultsAdapter = createEntityAdapter({ @@ -47,6 +47,9 @@ const resultsSlice = createSlice({ resultsAdapter.upsertOne(state, action.payload); state.upsertedImageCount += 1; }, + resultRemoved: (state, action: PayloadAction) => { + resultsAdapter.removeOne(state, action.payload); + }, }, extraReducers: (builder) => { /** @@ -83,6 +86,6 @@ export const { selectTotal: selectResultsTotal, } = resultsAdapter.getSelectors((state) => state.results); -export const { resultUpserted } = resultsSlice.actions; +export const { resultUpserted, resultRemoved } = resultsSlice.actions; export default resultsSlice.reducer; diff --git a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts index 49e4d7e3ff..e7620cbc31 100644 --- a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts @@ -9,8 +9,12 @@ import { receivedUploadImages, IMAGES_PER_PAGE } from 'services/thunks/gallery'; import { ImageDTO } from 'services/api'; import { dateComparator } from 'common/util/dateComparator'; -export type UploadsImageDTO = Omit & { - image_type: 'uploads'; +export type UploadsImageDTO = Omit< + ImageDTO, + 'image_origin' | 'image_category' +> & { + image_origin: 'external'; + image_category: 'user'; }; export const uploadsAdapter = createEntityAdapter({ @@ -45,6 +49,9 @@ const uploadsSlice = createSlice({ uploadsAdapter.upsertOne(state, action.payload); state.upsertedImageCount += 1; }, + uploadRemoved: (state, action: PayloadAction) => { + uploadsAdapter.removeOne(state, action.payload); + }, }, extraReducers: (builder) => { /** @@ -81,6 +88,6 @@ export const { selectTotal: selectUploadsTotal, } = uploadsAdapter.getSelectors((state) => state.uploads); -export const { uploadUpserted } = uploadsSlice.actions; +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 18be021625..e4a0f41ee1 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 useGetImageByNameAndType from 'features/gallery/hooks/useGetImageByName'; +import useGetImageByNameAndOrigin from 'features/gallery/hooks/useGetImageByName'; import { fieldValueChanged } from 'features/nodes/store/nodesSlice'; import { @@ -11,7 +11,7 @@ import { } from 'features/nodes/types/types'; import { DragEvent, memo, useCallback, useState } from 'react'; -import { ImageType } from 'services/api'; +import { ResourceOrigin } from 'services/api'; import { FieldComponentProps } from './types'; const ImageInputFieldComponent = ( @@ -19,7 +19,7 @@ const ImageInputFieldComponent = ( ) => { const { nodeId, field } = props; - const getImageByNameAndType = useGetImageByNameAndType(); + const getImageByNameAndType = useGetImageByNameAndOrigin(); const dispatch = useAppDispatch(); const [url, setUrl] = useState(field.value?.image_url); const { getUrl } = useGetUrl(); @@ -27,7 +27,9 @@ const ImageInputFieldComponent = ( const handleDrop = useCallback( (e: DragEvent) => { const name = e.dataTransfer.getData('invokeai/imageName'); - const type = e.dataTransfer.getData('invokeai/imageType') as ImageType; + const type = e.dataTransfer.getData( + 'invokeai/imageOrigin' + ) as ResourceOrigin; if (!name || !type) { return; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts index d9eb80d654..bd3d8a5460 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts @@ -64,7 +64,7 @@ export const buildImageToImageGraph = (state: RootState): Graph => { model, image: { image_name: initialImage?.image_name, - image_type: initialImage?.image_type, + image_origin: initialImage?.image_origin, }, }; diff --git a/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildImageToImageNode.ts b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildImageToImageNode.ts index 5f00d12a23..558f937837 100644 --- a/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildImageToImageNode.ts +++ b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildImageToImageNode.ts @@ -58,7 +58,7 @@ export const buildImg2ImgNode = ( imageToImageNode.image = { image_name: initialImage.name, - image_type: initialImage.type, + image_origin: initialImage.type, }; } diff --git a/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildInpaintNode.ts b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildInpaintNode.ts index b3f6cca933..0556a499be 100644 --- a/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildInpaintNode.ts +++ b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildInpaintNode.ts @@ -51,7 +51,7 @@ export const buildInpaintNode = ( inpaintNode.image = { image_name: initialImage.name, - image_type: initialImage.type, + image_origin: initialImage.type, }; } 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 be40f548e6..a5b106163f 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,7 @@ import { useGetUrl } from 'common/util/getUrl'; import { clearInitialImage } from 'features/parameters/store/generationSlice'; import { DragEvent, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { ImageType } from 'services/api'; +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,9 +55,11 @@ const InitialImagePreview = () => { const handleDrop = useCallback( (e: DragEvent) => { const name = e.dataTransfer.getData('invokeai/imageName'); - const type = e.dataTransfer.getData('invokeai/imageType') as ImageType; + const type = e.dataTransfer.getData( + 'invokeai/imageOrigin' + ) as ResourceOrigin; - dispatch(initialImageSelected({ image_name: name, image_type: type })); + dispatch(initialImageSelected({ image_name: name, image_origin: type })); }, [dispatch] ); diff --git a/invokeai/frontend/web/src/features/parameters/store/actions.ts b/invokeai/frontend/web/src/features/parameters/store/actions.ts index 853597c809..6c1030b7b0 100644 --- a/invokeai/frontend/web/src/features/parameters/store/actions.ts +++ b/invokeai/frontend/web/src/features/parameters/store/actions.ts @@ -1,10 +1,10 @@ import { createAction } from '@reduxjs/toolkit'; import { isObject } from 'lodash-es'; -import { ImageDTO, ImageType } from 'services/api'; +import { ImageDTO, ResourceOrigin } from 'services/api'; -export type ImageNameAndType = { +export type ImageNameAndOrigin = { image_name: string; - image_type: ImageType; + image_origin: ResourceOrigin; }; export const isImageDTO = (image: any): image is ImageDTO => { @@ -13,8 +13,8 @@ export const isImageDTO = (image: any): image is ImageDTO => { isObject(image) && 'image_name' in image && image?.image_name !== undefined && - 'image_type' in image && - image?.image_type !== undefined && + 'image_origin' in image && + image?.image_origin !== undefined && 'image_url' in image && image?.image_url !== undefined && 'thumbnail_url' in image && @@ -27,5 +27,5 @@ export const isImageDTO = (image: any): image is ImageDTO => { }; export const initialImageSelected = createAction< - ImageDTO | ImageNameAndType | undefined + ImageDTO | ImageNameAndOrigin | undefined >('generation/initialImageSelected'); diff --git a/invokeai/frontend/web/src/services/thunks/gallery.ts b/invokeai/frontend/web/src/services/thunks/gallery.ts index 03032a60ef..e6bb163167 100644 --- a/invokeai/frontend/web/src/services/thunks/gallery.ts +++ b/invokeai/frontend/web/src/services/thunks/gallery.ts @@ -23,8 +23,8 @@ export const receivedGalleryImages = createAppAsyncThunk< const pageOffset = Math.floor(upsertedImageCount / IMAGES_PER_PAGE); const response = await ImagesService.listImagesWithMetadata({ + excludeCategories: ['user'], isIntermediate: false, - showInGallery: true, page: nextPage + pageOffset, perPage: IMAGES_PER_PAGE, }); @@ -53,9 +53,8 @@ export const receivedUploadImages = createAppAsyncThunk< const pageOffset = Math.floor(upsertedImageCount / IMAGES_PER_PAGE); const response = await ImagesService.listImagesWithMetadata({ - imageType: 'uploads', + includeCategories: ['user'], isIntermediate: false, - showInGallery: false, page: nextPage + pageOffset, perPage: IMAGES_PER_PAGE, }); diff --git a/invokeai/frontend/web/src/services/types/guards.ts b/invokeai/frontend/web/src/services/types/guards.ts index 266e991f4d..1231a38b4d 100644 --- a/invokeai/frontend/web/src/services/types/guards.ts +++ b/invokeai/frontend/web/src/services/types/guards.ts @@ -1,4 +1,3 @@ -import { ResultsImageDTO } from 'features/gallery/store/resultsSlice'; import { UploadsImageDTO } from 'features/gallery/store/uploadsSlice'; import { get, isObject, isString } from 'lodash-es'; import { @@ -9,17 +8,18 @@ import { PromptOutput, IterateInvocationOutput, CollectInvocationOutput, - ImageType, ImageField, LatentsOutput, ImageDTO, + ResourceOrigin, } from 'services/api'; -export const isUploadsImageDTO = (image: ImageDTO): image is UploadsImageDTO => - image.image_type === 'uploads'; - -export const isResultsImageDTO = (image: ImageDTO): image is ResultsImageDTO => - image.image_type === 'results'; +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] @@ -49,10 +49,10 @@ export const isCollectOutput = ( output: GraphExecutionState['results'][string] ): output is CollectInvocationOutput => output.type === 'collect_output'; -export const isImageType = (t: unknown): t is ImageType => - isString(t) && ['results', 'uploads', 'intermediates'].includes(t); +export const isResourceOrigin = (t: unknown): t is ResourceOrigin => + isString(t) && ['internal', 'external'].includes(t); export const isImageField = (imageField: unknown): imageField is ImageField => isObject(imageField) && isString(get(imageField, 'image_name')) && - isImageType(get(imageField, 'image_type')); + isResourceOrigin(get(imageField, 'image_origin'));