From 7ca32ce9f333b79d82eeba7836de8c1b52c4eb7e Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 3 Apr 2023 19:05:33 +1000 Subject: [PATCH] feat(ui): begin migrating gallery to nodes Along the way, migrate to use RTK `createEntityAdapter` for gallery images, and separate `results` and `uploads` into separate slices. Much cleaner this way. --- invokeai/frontend/web/src/app/invokeai.d.ts | 13 +++ .../web/src/app/nodesSocketio/listeners.ts | 51 ++++++++---- invokeai/frontend/web/src/app/store.ts | 2 + .../components/ImageGalleryContent.tsx | 12 ++- .../features/gallery/store/resultsSlice.ts | 35 ++++++++ .../frontend/web/src/services/thunks/extra.ts | 82 +++++++++++++------ .../web/src/services/util/buildImageUrls.ts | 17 ++++ .../extractTimestampFromResultImageName.ts | 9 ++ .../{util.ts => util/makeGraphOfXImages.ts} | 2 +- 9 files changed, 180 insertions(+), 43 deletions(-) create mode 100644 invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts create mode 100644 invokeai/frontend/web/src/services/util/buildImageUrls.ts create mode 100644 invokeai/frontend/web/src/services/util/extractTimestampFromResultImageName.ts rename invokeai/frontend/web/src/services/{util.ts => util/makeGraphOfXImages.ts} (90%) diff --git a/invokeai/frontend/web/src/app/invokeai.d.ts b/invokeai/frontend/web/src/app/invokeai.d.ts index d6cfbf209e..a8d0315ad7 100644 --- a/invokeai/frontend/web/src/app/invokeai.d.ts +++ b/invokeai/frontend/web/src/app/invokeai.d.ts @@ -127,6 +127,19 @@ export declare type Image = { name?: string; }; +/** + * ResultImage + */ +export declare type ResultImage = { + name: string; + url: string; + thumbnail: string; + width: number; + height: number; + timestamp: number; + metadata?: Metadata; +}; + // GalleryImages is an array of Image. export declare type GalleryImages = { images: Array; diff --git a/invokeai/frontend/web/src/app/nodesSocketio/listeners.ts b/invokeai/frontend/web/src/app/nodesSocketio/listeners.ts index 38828c145c..febf1109cf 100644 --- a/invokeai/frontend/web/src/app/nodesSocketio/listeners.ts +++ b/invokeai/frontend/web/src/app/nodesSocketio/listeners.ts @@ -34,6 +34,9 @@ import { } from 'services/apiSlice'; import { emitUnsubscribe } from './actions'; import { getGalleryImages } from 'services/thunks/extra'; +import { resultAdded } from 'features/gallery/store/resultsSlice'; +import { buildImageUrls } from 'services/util/buildImageUrls'; +import { extractTimestampFromResultImageName } from 'services/util/extractTimestampFromResultImageName'; /** * Returns an object containing listener callbacks @@ -87,28 +90,46 @@ const makeSocketIOListeners = ( try { const sessionId = data.graph_execution_state_id; if (data.result.type === 'image') { - const url = `api/v1/images/${data.result.image.image_type}/${data.result.image.image_name}`; + const { image_name: imageName } = data.result.image; + + const { imageUrl, thumbnailUrl } = buildImageUrls( + 'results', + imageName + ); + + const timestamp = extractTimestampFromResultImageName(imageName); + + // // need to update the type for this or figure out how to get these values + // dispatch( + // addImage({ + // category: 'result', + // image: { + // uuid: uuidv4(), + // url: imageUrl, + // thumbnail: '', + // width: 512, + // height: 512, + // category: 'result', + // name: imageName, + // mtime: new Date().getTime(), + // }, + // }) + // ); - // need to update the type for this or figure out how to get these values dispatch( - addImage({ - category: 'result', - image: { - uuid: uuidv4(), - url, - thumbnail: '', - width: 512, - height: 512, - category: 'result', - name: data.result.image.image_name, - mtime: new Date().getTime(), - }, + resultAdded({ + name: imageName, + url: imageUrl, + thumbnail: thumbnailUrl, + width: 512, + height: 512, + timestamp, }) ); dispatch( addLogEntry({ timestamp: dateFormat(new Date(), 'isoDateTime'), - message: `Generated: ${data.result.image.image_name}`, + message: `Generated: ${imageName}`, }) ); dispatch(setIsProcessing(false)); diff --git a/invokeai/frontend/web/src/app/store.ts b/invokeai/frontend/web/src/app/store.ts index 42306d0a82..1d469d329a 100644 --- a/invokeai/frontend/web/src/app/store.ts +++ b/invokeai/frontend/web/src/app/store.ts @@ -7,6 +7,7 @@ import { getPersistConfig } from 'redux-deep-persist'; import canvasReducer from 'features/canvas/store/canvasSlice'; import galleryReducer from 'features/gallery/store/gallerySlice'; +import resultsReducer from 'features/gallery/store/resultsSlice'; import lightboxReducer from 'features/lightbox/store/lightboxSlice'; import generationReducer from 'features/parameters/store/generationSlice'; import postprocessingReducer from 'features/parameters/store/postprocessingSlice'; @@ -80,6 +81,7 @@ const rootReducer = combineReducers({ ui: uiReducer, lightbox: lightboxReducer, api: apiReducer, + results: resultsReducer, }); const rootPersistConfig = getPersistConfig({ diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx index 8ddf862c47..7599ee8abf 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx @@ -1,4 +1,4 @@ -import { ButtonGroup, Flex, Grid, Icon, Text } from '@chakra-ui/react'; +import { ButtonGroup, Flex, Grid, Icon, Image, Text } from '@chakra-ui/react'; import { requestImages } from 'app/socketio/actions'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import IAIButton from 'common/components/IAIButton'; @@ -25,6 +25,7 @@ import HoverableImage from './HoverableImage'; import Scrollable from 'features/ui/components/common/Scrollable'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; +import { selectResultsAll } from '../store/resultsSlice'; const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 290; @@ -47,6 +48,8 @@ const ImageGalleryContent = () => { shouldUseSingleGalleryColumn, } = useAppSelector(imageGallerySelector); + const allResultImages = useAppSelector(selectResultsAll); + const handleClickLoadMore = () => { dispatch(requestImages(currentCategory)); }; @@ -202,7 +205,7 @@ const ImageGalleryContent = () => { gap={2} style={{ gridTemplateColumns: galleryGridTemplateColumns }} > - {images.map((image) => { + {/* {images.map((image) => { const { uuid } = image; const isSelected = currentImageUuid === uuid; return ( @@ -212,7 +215,10 @@ const ImageGalleryContent = () => { isSelected={isSelected} /> ); - })} + })} */} + {allResultImages.map((image) => ( + + ))} ({ + // Image IDs are just their filename + selectId: (image) => image.name, + // Keep the "all IDs" array sorted based on result timestamps + sortComparer: (a, b) => b.timestamp - a.timestamp, +}); + +const resultsSlice = createSlice({ + name: 'results', + initialState: resultsAdapter.getInitialState(), + reducers: { + // Can pass adapter functions directly as case reducers. Because we're passing this + // as a value, `createSlice` will auto-generate the action type / creator + resultAdded: resultsAdapter.addOne, + resultsReceived: resultsAdapter.setAll, + }, +}); + +// Can create a set of memoized selectors based on the location of this entity state +export const { + selectAll: selectResultsAll, + selectById: selectResultsById, + selectEntities: selectResultsEntities, + selectIds: selectResultsIds, + selectTotal: selectResultsTotal, +} = resultsAdapter.getSelectors((state) => state.results); + +export const { resultAdded, resultsReceived } = resultsSlice.actions; + +export default resultsSlice.reducer; diff --git a/invokeai/frontend/web/src/services/thunks/extra.ts b/invokeai/frontend/web/src/services/thunks/extra.ts index 7de1384ec2..5a3aaef0b4 100644 --- a/invokeai/frontend/web/src/services/thunks/extra.ts +++ b/invokeai/frontend/web/src/services/thunks/extra.ts @@ -1,9 +1,10 @@ import { createAppAsyncThunk } from 'app/storeUtils'; -import { addImage } from 'features/gallery/store/gallerySlice'; -import { forEach } from 'lodash'; +import { map } from 'lodash'; import { SessionsService } from 'services/api'; import { isImageOutput } from 'services/types/guards'; -import { v4 as uuidv4 } from 'uuid'; +import { buildImageUrls } from 'services/util/buildImageUrls'; +import { extractTimestampFromResultImageName } from 'services/util/extractTimestampFromResultImageName'; +import { resultsReceived } from 'features/gallery/store/resultsSlice'; type GetGalleryImagesArg = { count: number; @@ -30,28 +31,61 @@ export const getGalleryImages = createAppAsyncThunk( perPage: 20, }); - response.items.forEach((session) => { - forEach(session.results, (result) => { - if (isImageOutput(result)) { - const url = `api/v1/images/${result.image.image_type}/${result.image.image_name}`; + // build flattened array of results ojects, use lodash `map()` to make results object an array + const allResults = response.items.flatMap((session) => + map(session.results) + ); - dispatch( - addImage({ - category: 'result', - image: { - uuid: uuidv4(), - url, - thumbnail: '', - width: 512, - height: 512, - category: 'result', - name: result.image.image_name, - mtime: new Date().getTime(), - }, - }) - ); - } - }); + // filter out non-image-outputs (eg latents, prompts, etc) + const imageOutputResults = allResults.filter(isImageOutput); + + // build ResultImage objects + const resultImages = imageOutputResults.map((result) => { + const name = result.image.image_name; + + const { imageUrl, thumbnailUrl } = buildImageUrls('results', name); + const timestamp = extractTimestampFromResultImageName(name); + + return { + name, + url: imageUrl, + thumbnail: thumbnailUrl, + timestamp, + height: 512, + width: 512, + }; }); + + // update the results slice + dispatch(resultsReceived(resultImages)); + + // response.items.forEach((session) => { + // forEach(session.results, (result) => { + // if (isImageOutput(result)) { + // const { imageUrl, thumbnailUrl } = buildImageUrls( + // result.image.image_type!, // fix the generated types to avoid non-null assertion + // result.image.image_name! // fix the generated types to avoid non-null assertion + // ); + + // dispatch + + // dispatch( + // addImage({ + // category: 'result', + // image: { + // uuid: uuidv4(), + // url: imageUrl, + // thumbnail: , + // width: 512, + // height: 512, + // category: 'result', + // name: result.image.image_name, + // mtime: new Date().getTime(), + // }, + // }) + // ); + // } + // }); + // }); } ); diff --git a/invokeai/frontend/web/src/services/util/buildImageUrls.ts b/invokeai/frontend/web/src/services/util/buildImageUrls.ts new file mode 100644 index 0000000000..3f2b1860f3 --- /dev/null +++ b/invokeai/frontend/web/src/services/util/buildImageUrls.ts @@ -0,0 +1,17 @@ +import { ImageType } from 'services/api'; + +export const buildImageUrls = ( + imageType: ImageType, + imageName: string +): { imageUrl: string; thumbnailUrl: string } => { + const imageUrl = `api/v1/images/${imageType}/${imageName}`; + + const thumbnailUrl = `api/v1/images/${imageType}/thumbnails/${ + imageName.split('.')[0] + }.webp`; + + return { + imageUrl, + thumbnailUrl, + }; +}; diff --git a/invokeai/frontend/web/src/services/util/extractTimestampFromResultImageName.ts b/invokeai/frontend/web/src/services/util/extractTimestampFromResultImageName.ts new file mode 100644 index 0000000000..fe3dc65ec5 --- /dev/null +++ b/invokeai/frontend/web/src/services/util/extractTimestampFromResultImageName.ts @@ -0,0 +1,9 @@ +export const extractTimestampFromResultImageName = (imageName: string) => { + const timestamp = imageName.split('_')?.pop()?.split('.')[0]; + + if (timestamp === undefined) { + return 0; + } + + return Number(timestamp); +}; diff --git a/invokeai/frontend/web/src/services/util.ts b/invokeai/frontend/web/src/services/util/makeGraphOfXImages.ts similarity index 90% rename from invokeai/frontend/web/src/services/util.ts rename to invokeai/frontend/web/src/services/util/makeGraphOfXImages.ts index 79859150fd..386ca972b1 100644 --- a/invokeai/frontend/web/src/services/util.ts +++ b/invokeai/frontend/web/src/services/util/makeGraphOfXImages.ts @@ -1,4 +1,4 @@ -import { Graph, TextToImageInvocation } from './api'; +import { Graph, TextToImageInvocation } from '../api'; /** * Make a graph of however many images