feat(ui): wip gallery migration

This commit is contained in:
psychedelicious 2023-04-04 18:34:50 +10:00
parent b7de3162c3
commit cfe86ec541
23 changed files with 244 additions and 147 deletions

View File

@ -113,7 +113,7 @@ export declare type Metadata = SystemGenerationMetadata & {
}; };
// An Image has a UUID, url, modified timestamp, width, height and maybe metadata // An Image has a UUID, url, modified timestamp, width, height and maybe metadata
export declare type Image = { export declare type _Image = {
uuid: string; uuid: string;
url: string; url: string;
thumbnail: string; thumbnail: string;
@ -130,7 +130,7 @@ export declare type Image = {
/** /**
* ResultImage * ResultImage
*/ */
export declare type ResultImage = { export declare type Image = {
name: string; name: string;
url: string; url: string;
thumbnail: string; thumbnail: string;
@ -142,7 +142,7 @@ export declare type ResultImage = {
// 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>;
}; };
/** /**
@ -289,7 +289,7 @@ export declare type SystemStatusResponse = SystemStatus;
export declare type SystemConfigResponse = SystemConfig; export declare type SystemConfigResponse = SystemConfig;
export declare type ImageResultResponse = Omit<Image, 'uuid'> & { export declare type ImageResultResponse = Omit<_Image, 'uuid'> & {
boundingBox?: IRect; boundingBox?: IRect;
generationMode: InvokeTabName; generationMode: InvokeTabName;
}; };
@ -310,7 +310,7 @@ export declare type ErrorResponse = {
}; };
export declare type GalleryImagesResponse = { export declare type GalleryImagesResponse = {
images: Array<Omit<Image, 'uuid'>>; images: Array<Omit<_Image, 'uuid'>>;
areMoreImagesAvailable: boolean; areMoreImagesAvailable: boolean;
category: GalleryCategory; category: GalleryCategory;
}; };

View File

@ -34,8 +34,11 @@ import {
} from 'services/apiSlice'; } from 'services/apiSlice';
import { emitUnsubscribe } from './actions'; import { emitUnsubscribe } from './actions';
import { resultAdded } from 'features/gallery/store/resultsSlice'; import { resultAdded } from 'features/gallery/store/resultsSlice';
import { getInitialResultsPage } from 'services/thunks/gallery'; import {
import { prepareResultImage } from 'services/util/prepareResultImage'; getNextResultsPage,
getNextUploadsPage,
} from 'services/thunks/gallery';
import { processImageField } from 'services/util/processImageField';
/** /**
* Returns an object containing listener callbacks * Returns an object containing listener callbacks
@ -54,10 +57,12 @@ const makeSocketIOListeners = (
dispatch(setIsConnected(true)); dispatch(setIsConnected(true));
dispatch(setCurrentStatus(i18n.t('common.statusConnected'))); dispatch(setCurrentStatus(i18n.t('common.statusConnected')));
// fetch more results, but only if we don't already have results // fetch more images, but only if we don't already have images
// maybe we should have a different thunk for `onConnect` vs when you click 'Load More'?
if (!getState().results.ids.length) { if (!getState().results.ids.length) {
dispatch(getInitialResultsPage()); dispatch(getNextResultsPage());
}
if (!getState().uploads.ids.length) {
dispatch(getNextUploadsPage());
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -94,7 +99,7 @@ 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 resultImage = prepareResultImage(data.result.image); const resultImage = processImageField(data.result.image);
dispatch(resultAdded(resultImage)); dispatch(resultAdded(resultImage));
// // need to update the type for this or figure out how to get these values // // need to update the type for this or figure out how to get these values

View File

@ -13,9 +13,13 @@ import { InvokeTabName } from 'features/ui/store/tabMap';
export const generateImage = createAction<InvokeTabName>( export const generateImage = createAction<InvokeTabName>(
'socketio/generateImage' 'socketio/generateImage'
); );
export const runESRGAN = createAction<InvokeAI.Image>('socketio/runESRGAN'); export const runESRGAN = createAction<InvokeAI._Image>('socketio/runESRGAN');
export const runFacetool = createAction<InvokeAI.Image>('socketio/runFacetool'); export const runFacetool = createAction<InvokeAI._Image>(
export const deleteImage = createAction<InvokeAI.Image>('socketio/deleteImage'); 'socketio/runFacetool'
);
export const deleteImage = createAction<InvokeAI._Image>(
'socketio/deleteImage'
);
export const requestImages = createAction<GalleryCategory>( export const requestImages = createAction<GalleryCategory>(
'socketio/requestImages' 'socketio/requestImages'
); );

View File

@ -91,7 +91,7 @@ const makeSocketIOEmitters = (
}) })
); );
}, },
emitRunESRGAN: (imageToProcess: InvokeAI.Image) => { emitRunESRGAN: (imageToProcess: InvokeAI._Image) => {
dispatch(setIsProcessing(true)); dispatch(setIsProcessing(true));
const { const {
@ -119,7 +119,7 @@ const makeSocketIOEmitters = (
}) })
); );
}, },
emitRunFacetool: (imageToProcess: InvokeAI.Image) => { emitRunFacetool: (imageToProcess: InvokeAI._Image) => {
dispatch(setIsProcessing(true)); dispatch(setIsProcessing(true));
const { const {
@ -150,7 +150,7 @@ const makeSocketIOEmitters = (
}) })
); );
}, },
emitDeleteImage: (imageToDelete: InvokeAI.Image) => { emitDeleteImage: (imageToDelete: InvokeAI._Image) => {
const { url, uuid, category, thumbnail } = imageToDelete; const { url, uuid, category, thumbnail } = imageToDelete;
dispatch(removeImage(imageToDelete)); dispatch(removeImage(imageToDelete));
socketio.emit('deleteImage', url, thumbnail, uuid, category); socketio.emit('deleteImage', url, thumbnail, uuid, category);

View File

@ -262,7 +262,7 @@ const makeSocketIOListeners = (
*/ */
// Generate a UUID for each image // Generate a UUID for each image
const preparedImages = images.map((image): InvokeAI.Image => { const preparedImages = images.map((image): InvokeAI._Image => {
return { return {
uuid: uuidv4(), uuid: uuidv4(),
...image, ...image,
@ -334,7 +334,7 @@ const makeSocketIOListeners = (
if ( if (
initialImage === url || initialImage === url ||
(initialImage as InvokeAI.Image)?.url === url (initialImage as InvokeAI._Image)?.url === url
) { ) {
dispatch(clearInitialImage()); dispatch(clearInitialImage());
} }

View File

@ -8,6 +8,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 resultsReducer from 'features/gallery/store/resultsSlice';
import uploadsReducer from 'features/gallery/store/uploadsSlice';
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';
@ -82,6 +83,7 @@ const rootReducer = combineReducers({
lightbox: lightboxReducer, lightbox: lightboxReducer,
api: apiReducer, api: apiReducer,
results: resultsReducer, results: resultsReducer,
uploads: uploadsReducer,
}); });
const rootPersistConfig = getPersistConfig({ const rootPersistConfig = getPersistConfig({
@ -94,8 +96,9 @@ const rootPersistConfig = getPersistConfig({
...galleryBlacklist, ...galleryBlacklist,
...lightboxBlacklist, ...lightboxBlacklist,
...apiBlacklist, ...apiBlacklist,
// for now, never persist the results slice // for now, never persist the results/uploads slices
'results', 'results',
'uploads',
], ],
debounce: 300, debounce: 300,
}); });

View File

@ -6,7 +6,7 @@ import {
UpscaleInvocation, UpscaleInvocation,
} from 'services/api'; } from 'services/api';
import { Image } from 'app/invokeai'; import { _Image } from 'app/invokeai';
// fe todo fix model type (frontend uses null, backend uses undefined) // fe todo fix model type (frontend uses null, backend uses undefined)
// fe todo update front end to store to have whole image field (vs just name) // fe todo update front end to store to have whole image field (vs just name)
@ -83,7 +83,8 @@ export function buildImg2ImgNode(
model, model,
progress_images: shouldDisplayInProgressType === 'full-res', progress_images: shouldDisplayInProgressType === 'full-res',
image: { image: {
image_name: (initialImage as Image).name, image_name: (initialImage as _Image).name!,
image_type: 'result',
}, },
strength, strength,
fit, fit,
@ -104,7 +105,9 @@ export function buildFacetoolNode(
type: 'restore_face', type: 'restore_face',
image: { image: {
image_name: image_name:
typeof initialImage === 'string' ? initialImage : initialImage?.url, (typeof initialImage === 'string' ? initialImage : initialImage?.url) ||
'',
image_type: 'result',
}, },
strength, strength,
}; };
@ -125,7 +128,9 @@ export function buildUpscaleNode(
type: 'upscale', type: 'upscale',
image: { image: {
image_name: image_name:
typeof initialImage === 'string' ? initialImage : initialImage?.url, (typeof initialImage === 'string' ? initialImage : initialImage?.url) ||
'',
image_type: 'result',
}, },
strength, strength,
level, level,

View File

@ -156,7 +156,7 @@ export const canvasSlice = createSlice({
setCursorPosition: (state, action: PayloadAction<Vector2d | null>) => { setCursorPosition: (state, action: PayloadAction<Vector2d | null>) => {
state.cursorPosition = action.payload; state.cursorPosition = action.payload;
}, },
setInitialCanvasImage: (state, action: PayloadAction<InvokeAI.Image>) => { setInitialCanvasImage: (state, action: PayloadAction<InvokeAI._Image>) => {
const image = action.payload; const image = action.payload;
const { stageDimensions } = state; const { stageDimensions } = state;
@ -291,7 +291,7 @@ export const canvasSlice = createSlice({
state, state,
action: PayloadAction<{ action: PayloadAction<{
boundingBox: IRect; boundingBox: IRect;
image: InvokeAI.Image; image: InvokeAI._Image;
}> }>
) => { ) => {
const { boundingBox, image } = action.payload; const { boundingBox, image } = action.payload;

View File

@ -37,7 +37,7 @@ export type CanvasImage = {
y: number; y: number;
width: number; width: number;
height: number; height: number;
image: InvokeAI.Image; image: InvokeAI._Image;
}; };
export type CanvasMaskLine = { export type CanvasMaskLine = {
@ -125,7 +125,7 @@ export interface CanvasState {
cursorPosition: Vector2d | null; cursorPosition: Vector2d | null;
doesCanvasNeedScaling: boolean; doesCanvasNeedScaling: boolean;
futureLayerStates: CanvasLayerState[]; futureLayerStates: CanvasLayerState[];
intermediateImage?: InvokeAI.Image; intermediateImage?: InvokeAI._Image;
isCanvasInitialized: boolean; isCanvasInitialized: boolean;
isDrawing: boolean; isDrawing: boolean;
isMaskEnabled: boolean; isMaskEnabled: boolean;

View File

@ -105,7 +105,7 @@ export const mergeAndUploadCanvas =
const { url, width, height } = image; const { url, width, height } = image;
const newImage: InvokeAI.Image = { const newImage: InvokeAI._Image = {
uuid: uuidv4(), uuid: uuidv4(),
category: shouldSaveToGallery ? 'result' : 'user', category: shouldSaveToGallery ? 'result' : 'user',
...image, ...image,

View File

@ -52,7 +52,7 @@ interface DeleteImageModalProps {
/** /**
* The image to delete. * The image to delete.
*/ */
image?: InvokeAI.Image; image?: InvokeAI._Image;
} }
/** /**

View File

@ -33,7 +33,7 @@ import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
interface HoverableImageProps { interface HoverableImageProps {
image: InvokeAI.Image; image: InvokeAI._Image;
isSelected: boolean; isSelected: boolean;
} }

View File

@ -25,11 +25,44 @@ 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, selectResultsTotal } from '../store/resultsSlice'; import {
import { getNextResultsPage } from 'services/thunks/gallery'; resultsAdapter,
selectResultsAll,
selectResultsTotal,
} from '../store/resultsSlice';
import {
getNextResultsPage,
getNextUploadsPage,
} from 'services/thunks/gallery';
import { selectUploadsAll, uploadsAdapter } from '../store/uploadsSlice';
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store';
const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 290; const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 290;
const gallerySelector = createSelector(
[
(state: RootState) => state.uploads,
(state: RootState) => state.results,
(state: RootState) => state.gallery,
],
(uploads, results, gallery) => {
const { currentCategory } = gallery;
return currentCategory === 'result'
? {
images: resultsAdapter.getSelectors().selectAll(results),
isLoading: results.isLoading,
areMoreImagesAvailable: results.page < results.pages - 1,
}
: {
images: uploadsAdapter.getSelectors().selectAll(uploads),
isLoading: uploads.isLoading,
areMoreImagesAvailable: uploads.page < uploads.pages - 1,
};
}
);
const ImageGalleryContent = () => { const ImageGalleryContent = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
@ -37,7 +70,7 @@ const ImageGalleryContent = () => {
const [shouldShouldIconButtons, setShouldShouldIconButtons] = useState(true); const [shouldShouldIconButtons, setShouldShouldIconButtons] = useState(true);
const { const {
images, // images,
currentCategory, currentCategory,
currentImageUuid, currentImageUuid,
shouldPinGallery, shouldPinGallery,
@ -45,20 +78,24 @@ const ImageGalleryContent = () => {
galleryGridTemplateColumns, galleryGridTemplateColumns,
galleryImageObjectFit, galleryImageObjectFit,
shouldAutoSwitchToNewImages, shouldAutoSwitchToNewImages,
areMoreImagesAvailable, // areMoreImagesAvailable,
shouldUseSingleGalleryColumn, shouldUseSingleGalleryColumn,
} = useAppSelector(imageGallerySelector); } = useAppSelector(imageGallerySelector);
const allResultImages = useAppSelector(selectResultsAll); const { images, areMoreImagesAvailable, isLoading } =
const currentResultsPage = useAppSelector((state) => state.results.page); useAppSelector(gallerySelector);
const totalResultsPages = useAppSelector((state) => state.results.pages);
const isLoadingResults = useAppSelector((state) => state.results.isLoading);
// const handleClickLoadMore = () => { // const handleClickLoadMore = () => {
// dispatch(requestImages(currentCategory)); // dispatch(requestImages(currentCategory));
// }; // };
const handleClickLoadMore = () => { const handleClickLoadMore = () => {
if (currentCategory === 'result') {
dispatch(getNextResultsPage()); dispatch(getNextResultsPage());
}
if (currentCategory === 'user') {
dispatch(getNextUploadsPage());
}
}; };
const handleChangeGalleryImageMinimumWidth = (v: number) => { const handleChangeGalleryImageMinimumWidth = (v: number) => {
@ -223,17 +260,17 @@ const ImageGalleryContent = () => {
/> />
); );
})} */} })} */}
{allResultImages.map((image) => ( {images.map((image) => (
<Image key={image.name} src={image.thumbnail} /> <Image key={image.name} src={image.thumbnail} />
))} ))}
</Grid> </Grid>
<IAIButton <IAIButton
onClick={handleClickLoadMore} onClick={handleClickLoadMore}
isDisabled={currentResultsPage === totalResultsPages - 1} isDisabled={!areMoreImagesAvailable}
isLoading={isLoadingResults} isLoading={isLoading}
flexShrink={0} flexShrink={0}
> >
{currentResultsPage !== totalResultsPages - 1 {areMoreImagesAvailable
? t('gallery.loadMore') ? t('gallery.loadMore')
: t('gallery.allImagesLoaded')} : t('gallery.allImagesLoaded')}
</IAIButton> </IAIButton>

View File

@ -113,7 +113,7 @@ const MetadataItem = ({
}; };
type ImageMetadataViewerProps = { type ImageMetadataViewerProps = {
image: InvokeAI.Image; image: InvokeAI._Image;
}; };
// TODO: I don't know if this is needed. // TODO: I don't know if this is needed.

View File

@ -8,7 +8,7 @@ import { clamp } from 'lodash';
export type GalleryCategory = 'user' | 'result'; export type GalleryCategory = 'user' | 'result';
export type AddImagesPayload = { export type AddImagesPayload = {
images: Array<InvokeAI.Image>; images: Array<InvokeAI._Image>;
areMoreImagesAvailable: boolean; areMoreImagesAvailable: boolean;
category: GalleryCategory; category: GalleryCategory;
}; };
@ -16,16 +16,16 @@ export type AddImagesPayload = {
type GalleryImageObjectFitType = 'contain' | 'cover'; type GalleryImageObjectFitType = 'contain' | 'cover';
export type Gallery = { export type Gallery = {
images: InvokeAI.Image[]; images: InvokeAI._Image[];
latest_mtime?: number; latest_mtime?: number;
earliest_mtime?: number; earliest_mtime?: number;
areMoreImagesAvailable: boolean; areMoreImagesAvailable: boolean;
}; };
export interface GalleryState { export interface GalleryState {
currentImage?: InvokeAI.Image; currentImage?: InvokeAI._Image;
currentImageUuid: string; currentImageUuid: string;
intermediateImage?: InvokeAI.Image & { intermediateImage?: InvokeAI._Image & {
boundingBox?: IRect; boundingBox?: IRect;
generationMode?: InvokeTabName; generationMode?: InvokeTabName;
}; };
@ -69,7 +69,7 @@ export const gallerySlice = createSlice({
name: 'gallery', name: 'gallery',
initialState, initialState,
reducers: { reducers: {
setCurrentImage: (state, action: PayloadAction<InvokeAI.Image>) => { setCurrentImage: (state, action: PayloadAction<InvokeAI._Image>) => {
state.currentImage = action.payload; state.currentImage = action.payload;
state.currentImageUuid = action.payload.uuid; state.currentImageUuid = action.payload.uuid;
}, },
@ -124,7 +124,7 @@ export const gallerySlice = createSlice({
addImage: ( addImage: (
state, state,
action: PayloadAction<{ action: PayloadAction<{
image: InvokeAI.Image; image: InvokeAI._Image;
category: GalleryCategory; category: GalleryCategory;
}> }>
) => { ) => {
@ -150,7 +150,10 @@ export const gallerySlice = createSlice({
setIntermediateImage: ( setIntermediateImage: (
state, state,
action: PayloadAction< action: PayloadAction<
InvokeAI.Image & { boundingBox?: IRect; generationMode?: InvokeTabName } InvokeAI._Image & {
boundingBox?: IRect;
generationMode?: InvokeTabName;
}
> >
) => { ) => {
state.intermediateImage = action.payload; state.intermediateImage = action.payload;

View File

@ -1,17 +1,15 @@
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
import { ResultImage } from 'app/invokeai'; import { Image } from 'app/invokeai';
import { RootState } from 'app/store'; import { RootState } from 'app/store';
import { map } from 'lodash'; import { getNextResultsPage, IMAGES_PER_PAGE } from 'services/thunks/gallery';
import { getNextResultsPage } from 'services/thunks/gallery'; import { processImageField } from 'services/util/processImageField';
import { isImageOutput } from 'services/types/guards';
import { prepareResultImage } from 'services/util/prepareResultImage';
// use `createEntityAdapter` to create a slice for results images // use `createEntityAdapter` to create a slice for results images
// https://redux-toolkit.js.org/api/createEntityAdapter#overview // https://redux-toolkit.js.org/api/createEntityAdapter#overview
// the "Entity" is InvokeAI.ResultImage, while the "entities" are instances of that type // the "Entity" is InvokeAI.ResultImage, while the "entities" are instances of that type
const resultsAdapter = createEntityAdapter<ResultImage>({ export const resultsAdapter = createEntityAdapter<Image>({
// Provide a callback to get a stable, unique identifier for each entity. This defaults to // Provide a callback to get a stable, unique identifier for each entity. This defaults to
// `(item) => item.id`, but for our result images, the `name` is the unique identifier. // `(item) => item.id`, but for our result images, the `name` is the unique identifier.
selectId: (image) => image.name, selectId: (image) => image.name,
@ -26,6 +24,7 @@ type AdditionalResultsState = {
page: number; // current page we are on page: number; // current page we are on
pages: number; // the total number of pages available pages: number; // the total number of pages available
isLoading: boolean; // whether we are loading more images or not, mostly a placeholder isLoading: boolean; // whether we are loading more images or not, mostly a placeholder
nextPage: number; // the next page to request
}; };
const resultsSlice = createSlice({ const resultsSlice = createSlice({
@ -35,6 +34,7 @@ const resultsSlice = createSlice({
page: 0, page: 0,
pages: 0, pages: 0,
isLoading: false, isLoading: false,
nextPage: 0,
}), }),
reducers: { reducers: {
// the adapter provides some helper reducers; see the docs for all of them // the adapter provides some helper reducers; see the docs for all of them
@ -47,28 +47,20 @@ const resultsSlice = createSlice({
extraReducers: (builder) => { extraReducers: (builder) => {
// here we can respond to a fulfilled call of the `getNextResultsPage` thunk // here we can respond to a fulfilled call of the `getNextResultsPage` thunk
// because we pass in the fulfilled thunk action creator, everything is typed // because we pass in the fulfilled thunk action creator, everything is typed
builder.addCase(getNextResultsPage.pending, (state, action) => { builder.addCase(getNextResultsPage.pending, (state) => {
state.isLoading = true; state.isLoading = true;
}); });
builder.addCase(getNextResultsPage.fulfilled, (state, action) => { builder.addCase(getNextResultsPage.fulfilled, (state, action) => {
const { items, page, pages } = action.payload; const { items, page, pages } = action.payload;
// build flattened array of results ojects, use lodash `map()` to make results object an array const resultImages = items.map((image) => processImageField(image));
const allResults = items.flatMap((session) => map(session.results));
// filter out non-image-outputs (eg latents, prompts, etc) // use the adapter reducer to append all the results to state
const imageOutputResults = allResults.filter(isImageOutput);
// map results to ResultImage objects
const resultImages = imageOutputResults.map((result) =>
prepareResultImage(result.image)
);
// use the adapter reducer to add all the results to resultsSlice state
resultsAdapter.addMany(state, resultImages); resultsAdapter.addMany(state, resultImages);
state.page = page; state.page = page;
state.pages = pages; state.pages = pages;
state.nextPage = items.length < IMAGES_PER_PAGE ? page : page + 1;
state.isLoading = false; state.isLoading = false;
}); });
}, },

View File

@ -0,0 +1,60 @@
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
import { Image } from 'app/invokeai';
import { RootState } from 'app/store';
import { getNextUploadsPage, IMAGES_PER_PAGE } from 'services/thunks/gallery';
import { processImageField } from 'services/util/processImageField';
export const uploadsAdapter = createEntityAdapter<Image>({
selectId: (image) => image.name,
sortComparer: (a, b) => b.timestamp - a.timestamp,
});
type AdditionalUploadsState = {
page: number;
pages: number;
isLoading: boolean;
nextPage: number;
};
const uploadsSlice = createSlice({
name: 'uploads',
initialState: uploadsAdapter.getInitialState<AdditionalUploadsState>({
page: 0,
pages: 0,
nextPage: 0,
isLoading: false,
}),
reducers: {
uploadAdded: uploadsAdapter.addOne,
},
extraReducers: (builder) => {
builder.addCase(getNextUploadsPage.pending, (state) => {
state.isLoading = true;
});
builder.addCase(getNextUploadsPage.fulfilled, (state, action) => {
const { items, page, pages } = action.payload;
const images = items.map((image) => processImageField(image));
uploadsAdapter.addMany(state, images);
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<RootState>((state) => state.uploads);
export const { uploadAdded } = uploadsSlice.actions;
export default uploadsSlice.reducer;

View File

@ -3,7 +3,7 @@ import { TransformComponent, useTransformContext } from 'react-zoom-pan-pinch';
import * as InvokeAI from 'app/invokeai'; import * as InvokeAI from 'app/invokeai';
type ReactPanZoomProps = { type ReactPanZoomProps = {
image: InvokeAI.Image; image: InvokeAI._Image;
styleClass?: string; styleClass?: string;
alt?: string; alt?: string;
ref?: React.Ref<HTMLImageElement>; ref?: React.Ref<HTMLImageElement>;

View File

@ -11,7 +11,7 @@ export interface GenerationState {
height: number; height: number;
img2imgStrength: number; img2imgStrength: number;
infillMethod: string; infillMethod: string;
initialImage?: InvokeAI.Image | string; // can be an Image or url initialImage?: InvokeAI._Image | string; // can be an Image or url
iterations: number; iterations: number;
maskPath: string; maskPath: string;
perlin: number; perlin: number;
@ -319,7 +319,7 @@ export const generationSlice = createSlice({
}, },
setInitialImage: ( setInitialImage: (
state, state,
action: PayloadAction<InvokeAI.Image | string> action: PayloadAction<InvokeAI._Image | string>
) => { ) => {
state.initialImage = action.payload; state.initialImage = action.payload;
}, },

View File

@ -36,7 +36,7 @@ export const invokeMiddleware: Middleware =
console.log('uploadImage.fulfilled'); console.log('uploadImage.fulfilled');
// TODO: actually get correct attributes here // TODO: actually get correct attributes here
const newImage: InvokeAI.Image = { const newImage: InvokeAI._Image = {
uuid: uuidv4(), uuid: uuidv4(),
category: 'user', category: 'user',
url: uploadLocation, url: uploadLocation,

View File

@ -1,41 +1,28 @@
import { createAppAsyncThunk } from 'app/storeUtils'; import { createAppAsyncThunk } from 'app/storeUtils';
import { SessionsService } from 'services/api'; import { ImagesService } from 'services/api';
export const IMAGES_PER_PAGE = 20;
/**
* Get the last 10 sessions' worth of images.
*
* This should be at most 10 images so long as we continue to make a new session for every
* generation.
*
* If a session was created but no image generated, this will be < 10 images.
*
* When we allow more images per sesssion, this is kinda no longer a viable way to grab results,
* because a session could have many, many images. In that situation, barring a change to the api,
* we have to keep track of images we've grabbed and the session they came from, so that when we
* want to load more, we can "resume" fetching images from that session.
*
* The API should change.
*/
export const getNextResultsPage = createAppAsyncThunk( export const getNextResultsPage = createAppAsyncThunk(
'results/getMoreResultsImages', 'results/getInitialResultsPage',
async (_arg, { getState }) => { async (_arg, { getState }) => {
const { page } = getState().results; const response = await ImagesService.listImages({
imageType: 'results',
const response = await SessionsService.listSessions({ page: getState().results.nextPage,
page: page + 1, perPage: IMAGES_PER_PAGE,
perPage: 10,
}); });
return response; return response;
} }
); );
export const getInitialResultsPage = createAppAsyncThunk( export const getNextUploadsPage = createAppAsyncThunk(
'results/getMoreResultsImages', 'uploads/getNextUploadsPage',
async (_arg) => { async (_arg, { getState }) => {
const response = await SessionsService.listSessions({ const response = await ImagesService.listImages({
page: 0, imageType: 'uploads',
perPage: 10, page: getState().uploads.nextPage,
perPage: IMAGES_PER_PAGE,
}); });
return response; return response;

View File

@ -1,45 +0,0 @@
import { ResultImage } from 'app/invokeai';
import { ImageField, 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,
};
};
export const extractTimestampFromResultImageName = (imageName: string) => {
const timestamp = imageName.split('_')?.pop()?.split('.')[0];
if (timestamp === undefined) {
return 0;
}
return Number(timestamp);
};
export const prepareResultImage = (image: ImageField): ResultImage => {
const name = image.image_name;
const { imageUrl, thumbnailUrl } = buildImageUrls('results', name);
const timestamp = extractTimestampFromResultImageName(name);
return {
name,
url: imageUrl,
thumbnail: thumbnailUrl,
timestamp,
height: 512,
width: 512,
};
};

View File

@ -0,0 +1,46 @@
import { Image } from 'app/invokeai';
import { ImageField, ImageType } from 'services/api';
export const buildImageUrls = (
imageType: ImageType,
imageName: string
): { url: string; thumbnail: string } => {
const url = `api/v1/images/${imageType}/${imageName}`;
const thumbnail = `api/v1/images/${imageType}/thumbnails/${
imageName.split('.')[0]
}.webp`;
return {
url,
thumbnail,
};
};
export const extractTimestampFromImageName = (imageName: string) => {
const timestamp = imageName.split('_')?.pop()?.split('.')[0];
if (timestamp === undefined) {
return 0;
}
return Number(timestamp);
};
export const processImageField = (image: ImageField): Image => {
const name = image.image_name;
const type = image.image_type;
const { url, thumbnail } = buildImageUrls(type, name);
const timestamp = extractTimestampFromImageName(name);
return {
name,
url,
thumbnail,
timestamp,
height: 512,
width: 512,
};
};