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
|
// 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;
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
@ -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'
|
||||||
);
|
);
|
||||||
|
@ -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);
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -52,7 +52,7 @@ interface DeleteImageModalProps {
|
|||||||
/**
|
/**
|
||||||
* The image to delete.
|
* 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';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
|
|
||||||
interface HoverableImageProps {
|
interface HoverableImageProps {
|
||||||
image: InvokeAI.Image;
|
image: InvokeAI._Image;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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.
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -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';
|
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>;
|
||||||
|
@ -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;
|
||||||
},
|
},
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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