mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
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:
parent
4fe7e52111
commit
7ca32ce9f3
13
invokeai/frontend/web/src/app/invokeai.d.ts
vendored
13
invokeai/frontend/web/src/app/invokeai.d.ts
vendored
@ -127,6 +127,19 @@ export declare type Image = {
|
|||||||
name?: string;
|
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.
|
// GalleryImages is an array of Image.
|
||||||
export declare type GalleryImages = {
|
export declare type GalleryImages = {
|
||||||
images: Array<Image>;
|
images: Array<Image>;
|
||||||
|
@ -34,6 +34,9 @@ import {
|
|||||||
} from 'services/apiSlice';
|
} from 'services/apiSlice';
|
||||||
import { emitUnsubscribe } from './actions';
|
import { emitUnsubscribe } from './actions';
|
||||||
import { getGalleryImages } from 'services/thunks/extra';
|
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
|
* Returns an object containing listener callbacks
|
||||||
@ -87,28 +90,46 @@ const makeSocketIOListeners = (
|
|||||||
try {
|
try {
|
||||||
const sessionId = data.graph_execution_state_id;
|
const sessionId = data.graph_execution_state_id;
|
||||||
if (data.result.type === 'image') {
|
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(
|
dispatch(
|
||||||
addImage({
|
resultAdded({
|
||||||
category: 'result',
|
name: imageName,
|
||||||
image: {
|
url: imageUrl,
|
||||||
uuid: uuidv4(),
|
thumbnail: thumbnailUrl,
|
||||||
url,
|
width: 512,
|
||||||
thumbnail: '',
|
height: 512,
|
||||||
width: 512,
|
timestamp,
|
||||||
height: 512,
|
|
||||||
category: 'result',
|
|
||||||
name: data.result.image.image_name,
|
|
||||||
mtime: new Date().getTime(),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
dispatch(
|
dispatch(
|
||||||
addLogEntry({
|
addLogEntry({
|
||||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||||
message: `Generated: ${data.result.image.image_name}`,
|
message: `Generated: ${imageName}`,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
dispatch(setIsProcessing(false));
|
dispatch(setIsProcessing(false));
|
||||||
|
@ -7,6 +7,7 @@ import { getPersistConfig } from 'redux-deep-persist';
|
|||||||
|
|
||||||
import canvasReducer from 'features/canvas/store/canvasSlice';
|
import canvasReducer from 'features/canvas/store/canvasSlice';
|
||||||
import galleryReducer from 'features/gallery/store/gallerySlice';
|
import galleryReducer from 'features/gallery/store/gallerySlice';
|
||||||
|
import resultsReducer from 'features/gallery/store/resultsSlice';
|
||||||
import lightboxReducer from 'features/lightbox/store/lightboxSlice';
|
import lightboxReducer from 'features/lightbox/store/lightboxSlice';
|
||||||
import generationReducer from 'features/parameters/store/generationSlice';
|
import generationReducer from 'features/parameters/store/generationSlice';
|
||||||
import postprocessingReducer from 'features/parameters/store/postprocessingSlice';
|
import postprocessingReducer from 'features/parameters/store/postprocessingSlice';
|
||||||
@ -80,6 +81,7 @@ const rootReducer = combineReducers({
|
|||||||
ui: uiReducer,
|
ui: uiReducer,
|
||||||
lightbox: lightboxReducer,
|
lightbox: lightboxReducer,
|
||||||
api: apiReducer,
|
api: apiReducer,
|
||||||
|
results: resultsReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
const rootPersistConfig = getPersistConfig({
|
const rootPersistConfig = getPersistConfig({
|
||||||
|
@ -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 { requestImages } from 'app/socketio/actions';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||||
import IAIButton from 'common/components/IAIButton';
|
import IAIButton from 'common/components/IAIButton';
|
||||||
@ -25,6 +25,7 @@ import HoverableImage from './HoverableImage';
|
|||||||
|
|
||||||
import Scrollable from 'features/ui/components/common/Scrollable';
|
import Scrollable from 'features/ui/components/common/Scrollable';
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||||
|
import { selectResultsAll } from '../store/resultsSlice';
|
||||||
|
|
||||||
const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 290;
|
const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 290;
|
||||||
|
|
||||||
@ -47,6 +48,8 @@ const ImageGalleryContent = () => {
|
|||||||
shouldUseSingleGalleryColumn,
|
shouldUseSingleGalleryColumn,
|
||||||
} = useAppSelector(imageGallerySelector);
|
} = useAppSelector(imageGallerySelector);
|
||||||
|
|
||||||
|
const allResultImages = useAppSelector(selectResultsAll);
|
||||||
|
|
||||||
const handleClickLoadMore = () => {
|
const handleClickLoadMore = () => {
|
||||||
dispatch(requestImages(currentCategory));
|
dispatch(requestImages(currentCategory));
|
||||||
};
|
};
|
||||||
@ -202,7 +205,7 @@ const ImageGalleryContent = () => {
|
|||||||
gap={2}
|
gap={2}
|
||||||
style={{ gridTemplateColumns: galleryGridTemplateColumns }}
|
style={{ gridTemplateColumns: galleryGridTemplateColumns }}
|
||||||
>
|
>
|
||||||
{images.map((image) => {
|
{/* {images.map((image) => {
|
||||||
const { uuid } = image;
|
const { uuid } = image;
|
||||||
const isSelected = currentImageUuid === uuid;
|
const isSelected = currentImageUuid === uuid;
|
||||||
return (
|
return (
|
||||||
@ -212,7 +215,10 @@ const ImageGalleryContent = () => {
|
|||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})} */}
|
||||||
|
{allResultImages.map((image) => (
|
||||||
|
<Image key={image.name} src={image.thumbnail} />
|
||||||
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
<IAIButton
|
<IAIButton
|
||||||
onClick={handleClickLoadMore}
|
onClick={handleClickLoadMore}
|
||||||
|
@ -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;
|
@ -1,9 +1,10 @@
|
|||||||
import { createAppAsyncThunk } from 'app/storeUtils';
|
import { createAppAsyncThunk } from 'app/storeUtils';
|
||||||
import { addImage } from 'features/gallery/store/gallerySlice';
|
import { map } from 'lodash';
|
||||||
import { forEach } from 'lodash';
|
|
||||||
import { SessionsService } from 'services/api';
|
import { SessionsService } from 'services/api';
|
||||||
import { isImageOutput } from 'services/types/guards';
|
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 = {
|
type GetGalleryImagesArg = {
|
||||||
count: number;
|
count: number;
|
||||||
@ -30,28 +31,61 @@ export const getGalleryImages = createAppAsyncThunk(
|
|||||||
perPage: 20,
|
perPage: 20,
|
||||||
});
|
});
|
||||||
|
|
||||||
response.items.forEach((session) => {
|
// build flattened array of results ojects, use lodash `map()` to make results object an array
|
||||||
forEach(session.results, (result) => {
|
const allResults = response.items.flatMap((session) =>
|
||||||
if (isImageOutput(result)) {
|
map(session.results)
|
||||||
const url = `api/v1/images/${result.image.image_type}/${result.image.image_name}`;
|
);
|
||||||
|
|
||||||
dispatch(
|
// filter out non-image-outputs (eg latents, prompts, etc)
|
||||||
addImage({
|
const imageOutputResults = allResults.filter(isImageOutput);
|
||||||
category: 'result',
|
|
||||||
image: {
|
// build ResultImage objects
|
||||||
uuid: uuidv4(),
|
const resultImages = imageOutputResults.map((result) => {
|
||||||
url,
|
const name = result.image.image_name;
|
||||||
thumbnail: '',
|
|
||||||
width: 512,
|
const { imageUrl, thumbnailUrl } = buildImageUrls('results', name);
|
||||||
height: 512,
|
const timestamp = extractTimestampFromResultImageName(name);
|
||||||
category: 'result',
|
|
||||||
name: result.image.image_name,
|
return {
|
||||||
mtime: new Date().getTime(),
|
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(),
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
17
invokeai/frontend/web/src/services/util/buildImageUrls.ts
Normal file
17
invokeai/frontend/web/src/services/util/buildImageUrls.ts
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,9 @@
|
|||||||
|
export const extractTimestampFromResultImageName = (imageName: string) => {
|
||||||
|
const timestamp = imageName.split('_')?.pop()?.split('.')[0];
|
||||||
|
|
||||||
|
if (timestamp === undefined) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(timestamp);
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import { Graph, TextToImageInvocation } from './api';
|
import { Graph, TextToImageInvocation } from '../api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make a graph of however many images
|
* Make a graph of however many images
|
Loading…
Reference in New Issue
Block a user