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.
This commit is contained in:
psychedelicious 2023-04-03 19:05:33 +10:00
parent 4fe7e52111
commit 7ca32ce9f3
9 changed files with 180 additions and 43 deletions

View File

@ -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<Image>;

View File

@ -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: '',
resultAdded({
name: imageName,
url: imageUrl,
thumbnail: thumbnailUrl,
width: 512,
height: 512,
category: 'result',
name: data.result.image.image_name,
mtime: new Date().getTime(),
},
timestamp,
})
);
dispatch(
addLogEntry({
timestamp: dateFormat(new Date(), 'isoDateTime'),
message: `Generated: ${data.result.image.image_name}`,
message: `Generated: ${imageName}`,
})
);
dispatch(setIsProcessing(false));

View File

@ -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({

View File

@ -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 key={image.name} src={image.thumbnail} />
))}
</Grid>
<IAIButton
onClick={handleClickLoadMore}

View File

@ -0,0 +1,35 @@
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
import * as InvokeAI from 'app/invokeai';
import { RootState } from 'app/store';
const resultsAdapter = createEntityAdapter<InvokeAI.ResultImage>({
// 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<RootState>((state) => state.results);
export const { resultAdded, resultsReceived } = resultsSlice.actions;
export default resultsSlice.reducer;

View File

@ -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,
// 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,
category: 'result',
name: result.image.image_name,
mtime: new Date().getTime(),
},
})
);
}
});
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(),
// },
// })
// );
// }
// });
// });
}
);

View File

@ -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,
};
};

View File

@ -0,0 +1,9 @@
export const extractTimestampFromResultImageName = (imageName: string) => {
const timestamp = imageName.split('_')?.pop()?.split('.')[0];
if (timestamp === undefined) {
return 0;
}
return Number(timestamp);
};

View File

@ -1,4 +1,4 @@
import { Graph, TextToImageInvocation } from './api';
import { Graph, TextToImageInvocation } from '../api';
/**
* Make a graph of however many images