mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): wip gallery migration
This commit is contained in:
parent
b7de3162c3
commit
cfe86ec541
10
invokeai/frontend/web/src/app/invokeai.d.ts
vendored
10
invokeai/frontend/web/src/app/invokeai.d.ts
vendored
@ -113,7 +113,7 @@ export declare type Metadata = SystemGenerationMetadata & {
|
||||
};
|
||||
|
||||
// An Image has a UUID, url, modified timestamp, width, height and maybe metadata
|
||||
export declare type Image = {
|
||||
export declare type _Image = {
|
||||
uuid: string;
|
||||
url: string;
|
||||
thumbnail: string;
|
||||
@ -130,7 +130,7 @@ export declare type Image = {
|
||||
/**
|
||||
* ResultImage
|
||||
*/
|
||||
export declare type ResultImage = {
|
||||
export declare type Image = {
|
||||
name: string;
|
||||
url: string;
|
||||
thumbnail: string;
|
||||
@ -142,7 +142,7 @@ export declare type ResultImage = {
|
||||
|
||||
// GalleryImages is an array of Image.
|
||||
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 ImageResultResponse = Omit<Image, 'uuid'> & {
|
||||
export declare type ImageResultResponse = Omit<_Image, 'uuid'> & {
|
||||
boundingBox?: IRect;
|
||||
generationMode: InvokeTabName;
|
||||
};
|
||||
@ -310,7 +310,7 @@ export declare type ErrorResponse = {
|
||||
};
|
||||
|
||||
export declare type GalleryImagesResponse = {
|
||||
images: Array<Omit<Image, 'uuid'>>;
|
||||
images: Array<Omit<_Image, 'uuid'>>;
|
||||
areMoreImagesAvailable: boolean;
|
||||
category: GalleryCategory;
|
||||
};
|
||||
|
@ -34,8 +34,11 @@ import {
|
||||
} from 'services/apiSlice';
|
||||
import { emitUnsubscribe } from './actions';
|
||||
import { resultAdded } from 'features/gallery/store/resultsSlice';
|
||||
import { getInitialResultsPage } from 'services/thunks/gallery';
|
||||
import { prepareResultImage } from 'services/util/prepareResultImage';
|
||||
import {
|
||||
getNextResultsPage,
|
||||
getNextUploadsPage,
|
||||
} from 'services/thunks/gallery';
|
||||
import { processImageField } from 'services/util/processImageField';
|
||||
|
||||
/**
|
||||
* Returns an object containing listener callbacks
|
||||
@ -54,10 +57,12 @@ const makeSocketIOListeners = (
|
||||
dispatch(setIsConnected(true));
|
||||
dispatch(setCurrentStatus(i18n.t('common.statusConnected')));
|
||||
|
||||
// fetch more results, but only if we don't already have results
|
||||
// maybe we should have a different thunk for `onConnect` vs when you click 'Load More'?
|
||||
// fetch more images, but only if we don't already have images
|
||||
if (!getState().results.ids.length) {
|
||||
dispatch(getInitialResultsPage());
|
||||
dispatch(getNextResultsPage());
|
||||
}
|
||||
if (!getState().uploads.ids.length) {
|
||||
dispatch(getNextUploadsPage());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@ -94,7 +99,7 @@ const makeSocketIOListeners = (
|
||||
try {
|
||||
const sessionId = data.graph_execution_state_id;
|
||||
if (data.result.type === 'image') {
|
||||
const resultImage = prepareResultImage(data.result.image);
|
||||
const resultImage = processImageField(data.result.image);
|
||||
|
||||
dispatch(resultAdded(resultImage));
|
||||
// // need to update the type for this or figure out how to get these values
|
||||
|
@ -13,9 +13,13 @@ import { InvokeTabName } from 'features/ui/store/tabMap';
|
||||
export const generateImage = createAction<InvokeTabName>(
|
||||
'socketio/generateImage'
|
||||
);
|
||||
export const runESRGAN = createAction<InvokeAI.Image>('socketio/runESRGAN');
|
||||
export const runFacetool = createAction<InvokeAI.Image>('socketio/runFacetool');
|
||||
export const deleteImage = createAction<InvokeAI.Image>('socketio/deleteImage');
|
||||
export const runESRGAN = createAction<InvokeAI._Image>('socketio/runESRGAN');
|
||||
export const runFacetool = createAction<InvokeAI._Image>(
|
||||
'socketio/runFacetool'
|
||||
);
|
||||
export const deleteImage = createAction<InvokeAI._Image>(
|
||||
'socketio/deleteImage'
|
||||
);
|
||||
export const requestImages = createAction<GalleryCategory>(
|
||||
'socketio/requestImages'
|
||||
);
|
||||
|
@ -91,7 +91,7 @@ const makeSocketIOEmitters = (
|
||||
})
|
||||
);
|
||||
},
|
||||
emitRunESRGAN: (imageToProcess: InvokeAI.Image) => {
|
||||
emitRunESRGAN: (imageToProcess: InvokeAI._Image) => {
|
||||
dispatch(setIsProcessing(true));
|
||||
|
||||
const {
|
||||
@ -119,7 +119,7 @@ const makeSocketIOEmitters = (
|
||||
})
|
||||
);
|
||||
},
|
||||
emitRunFacetool: (imageToProcess: InvokeAI.Image) => {
|
||||
emitRunFacetool: (imageToProcess: InvokeAI._Image) => {
|
||||
dispatch(setIsProcessing(true));
|
||||
|
||||
const {
|
||||
@ -150,7 +150,7 @@ const makeSocketIOEmitters = (
|
||||
})
|
||||
);
|
||||
},
|
||||
emitDeleteImage: (imageToDelete: InvokeAI.Image) => {
|
||||
emitDeleteImage: (imageToDelete: InvokeAI._Image) => {
|
||||
const { url, uuid, category, thumbnail } = imageToDelete;
|
||||
dispatch(removeImage(imageToDelete));
|
||||
socketio.emit('deleteImage', url, thumbnail, uuid, category);
|
||||
|
@ -262,7 +262,7 @@ const makeSocketIOListeners = (
|
||||
*/
|
||||
|
||||
// Generate a UUID for each image
|
||||
const preparedImages = images.map((image): InvokeAI.Image => {
|
||||
const preparedImages = images.map((image): InvokeAI._Image => {
|
||||
return {
|
||||
uuid: uuidv4(),
|
||||
...image,
|
||||
@ -334,7 +334,7 @@ const makeSocketIOListeners = (
|
||||
|
||||
if (
|
||||
initialImage === url ||
|
||||
(initialImage as InvokeAI.Image)?.url === url
|
||||
(initialImage as InvokeAI._Image)?.url === url
|
||||
) {
|
||||
dispatch(clearInitialImage());
|
||||
}
|
||||
|
@ -8,6 +8,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 uploadsReducer from 'features/gallery/store/uploadsSlice';
|
||||
import lightboxReducer from 'features/lightbox/store/lightboxSlice';
|
||||
import generationReducer from 'features/parameters/store/generationSlice';
|
||||
import postprocessingReducer from 'features/parameters/store/postprocessingSlice';
|
||||
@ -82,6 +83,7 @@ const rootReducer = combineReducers({
|
||||
lightbox: lightboxReducer,
|
||||
api: apiReducer,
|
||||
results: resultsReducer,
|
||||
uploads: uploadsReducer,
|
||||
});
|
||||
|
||||
const rootPersistConfig = getPersistConfig({
|
||||
@ -94,8 +96,9 @@ const rootPersistConfig = getPersistConfig({
|
||||
...galleryBlacklist,
|
||||
...lightboxBlacklist,
|
||||
...apiBlacklist,
|
||||
// for now, never persist the results slice
|
||||
// for now, never persist the results/uploads slices
|
||||
'results',
|
||||
'uploads',
|
||||
],
|
||||
debounce: 300,
|
||||
});
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
UpscaleInvocation,
|
||||
} 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 update front end to store to have whole image field (vs just name)
|
||||
@ -83,7 +83,8 @@ export function buildImg2ImgNode(
|
||||
model,
|
||||
progress_images: shouldDisplayInProgressType === 'full-res',
|
||||
image: {
|
||||
image_name: (initialImage as Image).name,
|
||||
image_name: (initialImage as _Image).name!,
|
||||
image_type: 'result',
|
||||
},
|
||||
strength,
|
||||
fit,
|
||||
@ -104,7 +105,9 @@ export function buildFacetoolNode(
|
||||
type: 'restore_face',
|
||||
image: {
|
||||
image_name:
|
||||
typeof initialImage === 'string' ? initialImage : initialImage?.url,
|
||||
(typeof initialImage === 'string' ? initialImage : initialImage?.url) ||
|
||||
'',
|
||||
image_type: 'result',
|
||||
},
|
||||
strength,
|
||||
};
|
||||
@ -125,7 +128,9 @@ export function buildUpscaleNode(
|
||||
type: 'upscale',
|
||||
image: {
|
||||
image_name:
|
||||
typeof initialImage === 'string' ? initialImage : initialImage?.url,
|
||||
(typeof initialImage === 'string' ? initialImage : initialImage?.url) ||
|
||||
'',
|
||||
image_type: 'result',
|
||||
},
|
||||
strength,
|
||||
level,
|
||||
|
@ -156,7 +156,7 @@ export const canvasSlice = createSlice({
|
||||
setCursorPosition: (state, action: PayloadAction<Vector2d | null>) => {
|
||||
state.cursorPosition = action.payload;
|
||||
},
|
||||
setInitialCanvasImage: (state, action: PayloadAction<InvokeAI.Image>) => {
|
||||
setInitialCanvasImage: (state, action: PayloadAction<InvokeAI._Image>) => {
|
||||
const image = action.payload;
|
||||
const { stageDimensions } = state;
|
||||
|
||||
@ -291,7 +291,7 @@ export const canvasSlice = createSlice({
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
boundingBox: IRect;
|
||||
image: InvokeAI.Image;
|
||||
image: InvokeAI._Image;
|
||||
}>
|
||||
) => {
|
||||
const { boundingBox, image } = action.payload;
|
||||
|
@ -37,7 +37,7 @@ export type CanvasImage = {
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
image: InvokeAI.Image;
|
||||
image: InvokeAI._Image;
|
||||
};
|
||||
|
||||
export type CanvasMaskLine = {
|
||||
@ -125,7 +125,7 @@ export interface CanvasState {
|
||||
cursorPosition: Vector2d | null;
|
||||
doesCanvasNeedScaling: boolean;
|
||||
futureLayerStates: CanvasLayerState[];
|
||||
intermediateImage?: InvokeAI.Image;
|
||||
intermediateImage?: InvokeAI._Image;
|
||||
isCanvasInitialized: boolean;
|
||||
isDrawing: boolean;
|
||||
isMaskEnabled: boolean;
|
||||
|
@ -105,7 +105,7 @@ export const mergeAndUploadCanvas =
|
||||
|
||||
const { url, width, height } = image;
|
||||
|
||||
const newImage: InvokeAI.Image = {
|
||||
const newImage: InvokeAI._Image = {
|
||||
uuid: uuidv4(),
|
||||
category: shouldSaveToGallery ? 'result' : 'user',
|
||||
...image,
|
||||
|
@ -52,7 +52,7 @@ interface DeleteImageModalProps {
|
||||
/**
|
||||
* The image to delete.
|
||||
*/
|
||||
image?: InvokeAI.Image;
|
||||
image?: InvokeAI._Image;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -33,7 +33,7 @@ import { setIsLightboxOpen } from 'features/lightbox/store/lightboxSlice';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
|
||||
interface HoverableImageProps {
|
||||
image: InvokeAI.Image;
|
||||
image: InvokeAI._Image;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
|
@ -25,11 +25,44 @@ import HoverableImage from './HoverableImage';
|
||||
|
||||
import Scrollable from 'features/ui/components/common/Scrollable';
|
||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||
import { selectResultsAll, selectResultsTotal } from '../store/resultsSlice';
|
||||
import { getNextResultsPage } from 'services/thunks/gallery';
|
||||
import {
|
||||
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 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 dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
@ -37,7 +70,7 @@ const ImageGalleryContent = () => {
|
||||
const [shouldShouldIconButtons, setShouldShouldIconButtons] = useState(true);
|
||||
|
||||
const {
|
||||
images,
|
||||
// images,
|
||||
currentCategory,
|
||||
currentImageUuid,
|
||||
shouldPinGallery,
|
||||
@ -45,20 +78,24 @@ const ImageGalleryContent = () => {
|
||||
galleryGridTemplateColumns,
|
||||
galleryImageObjectFit,
|
||||
shouldAutoSwitchToNewImages,
|
||||
areMoreImagesAvailable,
|
||||
// areMoreImagesAvailable,
|
||||
shouldUseSingleGalleryColumn,
|
||||
} = useAppSelector(imageGallerySelector);
|
||||
|
||||
const allResultImages = useAppSelector(selectResultsAll);
|
||||
const currentResultsPage = useAppSelector((state) => state.results.page);
|
||||
const totalResultsPages = useAppSelector((state) => state.results.pages);
|
||||
const isLoadingResults = useAppSelector((state) => state.results.isLoading);
|
||||
const { images, areMoreImagesAvailable, isLoading } =
|
||||
useAppSelector(gallerySelector);
|
||||
|
||||
// const handleClickLoadMore = () => {
|
||||
// dispatch(requestImages(currentCategory));
|
||||
// };
|
||||
const handleClickLoadMore = () => {
|
||||
dispatch(getNextResultsPage());
|
||||
if (currentCategory === 'result') {
|
||||
dispatch(getNextResultsPage());
|
||||
}
|
||||
|
||||
if (currentCategory === 'user') {
|
||||
dispatch(getNextUploadsPage());
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangeGalleryImageMinimumWidth = (v: number) => {
|
||||
@ -223,17 +260,17 @@ const ImageGalleryContent = () => {
|
||||
/>
|
||||
);
|
||||
})} */}
|
||||
{allResultImages.map((image) => (
|
||||
{images.map((image) => (
|
||||
<Image key={image.name} src={image.thumbnail} />
|
||||
))}
|
||||
</Grid>
|
||||
<IAIButton
|
||||
onClick={handleClickLoadMore}
|
||||
isDisabled={currentResultsPage === totalResultsPages - 1}
|
||||
isLoading={isLoadingResults}
|
||||
isDisabled={!areMoreImagesAvailable}
|
||||
isLoading={isLoading}
|
||||
flexShrink={0}
|
||||
>
|
||||
{currentResultsPage !== totalResultsPages - 1
|
||||
{areMoreImagesAvailable
|
||||
? t('gallery.loadMore')
|
||||
: t('gallery.allImagesLoaded')}
|
||||
</IAIButton>
|
||||
|
@ -113,7 +113,7 @@ const MetadataItem = ({
|
||||
};
|
||||
|
||||
type ImageMetadataViewerProps = {
|
||||
image: InvokeAI.Image;
|
||||
image: InvokeAI._Image;
|
||||
};
|
||||
|
||||
// TODO: I don't know if this is needed.
|
||||
|
@ -8,7 +8,7 @@ import { clamp } from 'lodash';
|
||||
export type GalleryCategory = 'user' | 'result';
|
||||
|
||||
export type AddImagesPayload = {
|
||||
images: Array<InvokeAI.Image>;
|
||||
images: Array<InvokeAI._Image>;
|
||||
areMoreImagesAvailable: boolean;
|
||||
category: GalleryCategory;
|
||||
};
|
||||
@ -16,16 +16,16 @@ export type AddImagesPayload = {
|
||||
type GalleryImageObjectFitType = 'contain' | 'cover';
|
||||
|
||||
export type Gallery = {
|
||||
images: InvokeAI.Image[];
|
||||
images: InvokeAI._Image[];
|
||||
latest_mtime?: number;
|
||||
earliest_mtime?: number;
|
||||
areMoreImagesAvailable: boolean;
|
||||
};
|
||||
|
||||
export interface GalleryState {
|
||||
currentImage?: InvokeAI.Image;
|
||||
currentImage?: InvokeAI._Image;
|
||||
currentImageUuid: string;
|
||||
intermediateImage?: InvokeAI.Image & {
|
||||
intermediateImage?: InvokeAI._Image & {
|
||||
boundingBox?: IRect;
|
||||
generationMode?: InvokeTabName;
|
||||
};
|
||||
@ -69,7 +69,7 @@ export const gallerySlice = createSlice({
|
||||
name: 'gallery',
|
||||
initialState,
|
||||
reducers: {
|
||||
setCurrentImage: (state, action: PayloadAction<InvokeAI.Image>) => {
|
||||
setCurrentImage: (state, action: PayloadAction<InvokeAI._Image>) => {
|
||||
state.currentImage = action.payload;
|
||||
state.currentImageUuid = action.payload.uuid;
|
||||
},
|
||||
@ -124,7 +124,7 @@ export const gallerySlice = createSlice({
|
||||
addImage: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
image: InvokeAI.Image;
|
||||
image: InvokeAI._Image;
|
||||
category: GalleryCategory;
|
||||
}>
|
||||
) => {
|
||||
@ -150,7 +150,10 @@ export const gallerySlice = createSlice({
|
||||
setIntermediateImage: (
|
||||
state,
|
||||
action: PayloadAction<
|
||||
InvokeAI.Image & { boundingBox?: IRect; generationMode?: InvokeTabName }
|
||||
InvokeAI._Image & {
|
||||
boundingBox?: IRect;
|
||||
generationMode?: InvokeTabName;
|
||||
}
|
||||
>
|
||||
) => {
|
||||
state.intermediateImage = action.payload;
|
||||
|
@ -1,17 +1,15 @@
|
||||
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
|
||||
import { ResultImage } from 'app/invokeai';
|
||||
import { Image } from 'app/invokeai';
|
||||
|
||||
import { RootState } from 'app/store';
|
||||
import { map } from 'lodash';
|
||||
import { getNextResultsPage } from 'services/thunks/gallery';
|
||||
import { isImageOutput } from 'services/types/guards';
|
||||
import { prepareResultImage } from 'services/util/prepareResultImage';
|
||||
import { getNextResultsPage, IMAGES_PER_PAGE } from 'services/thunks/gallery';
|
||||
import { processImageField } from 'services/util/processImageField';
|
||||
|
||||
// use `createEntityAdapter` to create a slice for results images
|
||||
// https://redux-toolkit.js.org/api/createEntityAdapter#overview
|
||||
|
||||
// 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
|
||||
// `(item) => item.id`, but for our result images, the `name` is the unique identifier.
|
||||
selectId: (image) => image.name,
|
||||
@ -26,6 +24,7 @@ type AdditionalResultsState = {
|
||||
page: number; // current page we are on
|
||||
pages: number; // the total number of pages available
|
||||
isLoading: boolean; // whether we are loading more images or not, mostly a placeholder
|
||||
nextPage: number; // the next page to request
|
||||
};
|
||||
|
||||
const resultsSlice = createSlice({
|
||||
@ -35,6 +34,7 @@ const resultsSlice = createSlice({
|
||||
page: 0,
|
||||
pages: 0,
|
||||
isLoading: false,
|
||||
nextPage: 0,
|
||||
}),
|
||||
reducers: {
|
||||
// the adapter provides some helper reducers; see the docs for all of them
|
||||
@ -47,28 +47,20 @@ const resultsSlice = createSlice({
|
||||
extraReducers: (builder) => {
|
||||
// here we can respond to a fulfilled call of the `getNextResultsPage` thunk
|
||||
// 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;
|
||||
});
|
||||
builder.addCase(getNextResultsPage.fulfilled, (state, action) => {
|
||||
const { items, page, pages } = action.payload;
|
||||
|
||||
// build flattened array of results ojects, use lodash `map()` to make results object an array
|
||||
const allResults = items.flatMap((session) => map(session.results));
|
||||
const resultImages = items.map((image) => processImageField(image));
|
||||
|
||||
// filter out non-image-outputs (eg latents, prompts, etc)
|
||||
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
|
||||
// use the adapter reducer to append all the results to state
|
||||
resultsAdapter.addMany(state, resultImages);
|
||||
|
||||
state.page = page;
|
||||
state.pages = pages;
|
||||
state.nextPage = items.length < IMAGES_PER_PAGE ? page : page + 1;
|
||||
state.isLoading = false;
|
||||
});
|
||||
},
|
||||
|
@ -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;
|
@ -3,7 +3,7 @@ import { TransformComponent, useTransformContext } from 'react-zoom-pan-pinch';
|
||||
import * as InvokeAI from 'app/invokeai';
|
||||
|
||||
type ReactPanZoomProps = {
|
||||
image: InvokeAI.Image;
|
||||
image: InvokeAI._Image;
|
||||
styleClass?: string;
|
||||
alt?: string;
|
||||
ref?: React.Ref<HTMLImageElement>;
|
||||
|
@ -11,7 +11,7 @@ export interface GenerationState {
|
||||
height: number;
|
||||
img2imgStrength: number;
|
||||
infillMethod: string;
|
||||
initialImage?: InvokeAI.Image | string; // can be an Image or url
|
||||
initialImage?: InvokeAI._Image | string; // can be an Image or url
|
||||
iterations: number;
|
||||
maskPath: string;
|
||||
perlin: number;
|
||||
@ -319,7 +319,7 @@ export const generationSlice = createSlice({
|
||||
},
|
||||
setInitialImage: (
|
||||
state,
|
||||
action: PayloadAction<InvokeAI.Image | string>
|
||||
action: PayloadAction<InvokeAI._Image | string>
|
||||
) => {
|
||||
state.initialImage = action.payload;
|
||||
},
|
||||
|
@ -36,7 +36,7 @@ export const invokeMiddleware: Middleware =
|
||||
console.log('uploadImage.fulfilled');
|
||||
|
||||
// TODO: actually get correct attributes here
|
||||
const newImage: InvokeAI.Image = {
|
||||
const newImage: InvokeAI._Image = {
|
||||
uuid: uuidv4(),
|
||||
category: 'user',
|
||||
url: uploadLocation,
|
||||
|
@ -1,41 +1,28 @@
|
||||
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(
|
||||
'results/getMoreResultsImages',
|
||||
'results/getInitialResultsPage',
|
||||
async (_arg, { getState }) => {
|
||||
const { page } = getState().results;
|
||||
|
||||
const response = await SessionsService.listSessions({
|
||||
page: page + 1,
|
||||
perPage: 10,
|
||||
const response = await ImagesService.listImages({
|
||||
imageType: 'results',
|
||||
page: getState().results.nextPage,
|
||||
perPage: IMAGES_PER_PAGE,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
export const getInitialResultsPage = createAppAsyncThunk(
|
||||
'results/getMoreResultsImages',
|
||||
async (_arg) => {
|
||||
const response = await SessionsService.listSessions({
|
||||
page: 0,
|
||||
perPage: 10,
|
||||
export const getNextUploadsPage = createAppAsyncThunk(
|
||||
'uploads/getNextUploadsPage',
|
||||
async (_arg, { getState }) => {
|
||||
const response = await ImagesService.listImages({
|
||||
imageType: 'uploads',
|
||||
page: getState().uploads.nextPage,
|
||||
perPage: IMAGES_PER_PAGE,
|
||||
});
|
||||
|
||||
return response;
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
46
invokeai/frontend/web/src/services/util/processImageField.ts
Normal file
46
invokeai/frontend/web/src/services/util/processImageField.ts
Normal 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,
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue
Block a user