Files
InvokeAI/invokeai/frontend/web/src/services/api/thunks/image.ts
psychedelicious 90aa97edd4 feat(ui): add multi-select and batch capabilities
This introduces the core functionality for batch operations on images and multiple selection in the gallery/batch manager.

A number of other substantial changes are included:
- `imagesSlice` is consolidated into `gallerySlice`, allowing for simpler selection of filtered images
- `batchSlice` is added to manage the batch
- The wonky context pattern for image deletion has been changed, much simpler now using a `imageDeletionSlice` and redux listeners; this needs to be implemented still for the other image modals
- Minimum gallery size in px implemented as a hook
- Many style fixes & several bug fixes

TODO:
- The UI and UX need to be figured out, especially for controlnet
- Batch processing is not hooked up; generation does not do anything with batch
- Routes to support batch image operations, specifically delete and add/remove to/from boards
2023-07-04 00:18:27 +10:00

330 lines
7.4 KiB
TypeScript

import queryString from 'query-string';
import { createAppAsyncThunk } from 'app/store/storeUtils';
import { selectImagesAll } from 'features/gallery/store/gallerySlice';
import { size } from 'lodash-es';
import { paths } from 'services/api/schema';
import { $client } from 'services/api/client';
type GetImageUrlsArg =
paths['/api/v1/images/{image_name}/urls']['get']['parameters']['path'];
type GetImageUrlsResponse =
paths['/api/v1/images/{image_name}/urls']['get']['responses']['200']['content']['application/json'];
type GetImageUrlsThunkConfig = {
rejectValue: {
arg: GetImageUrlsArg;
error: unknown;
};
};
/**
* Thunk to get image URLs
*/
export const imageUrlsReceived = createAppAsyncThunk<
GetImageUrlsResponse,
GetImageUrlsArg,
GetImageUrlsThunkConfig
>('api/imageUrlsReceived', async (arg, { rejectWithValue }) => {
const { image_name } = arg;
const { get } = $client.get();
const { data, error, response } = await get(
'/api/v1/images/{image_name}/urls',
{
params: {
path: {
image_name,
},
},
}
);
if (error) {
return rejectWithValue({ arg, error });
}
return data;
});
type GetImageMetadataArg =
paths['/api/v1/images/{image_name}/metadata']['get']['parameters']['path'];
type GetImageMetadataResponse =
paths['/api/v1/images/{image_name}/metadata']['get']['responses']['200']['content']['application/json'];
type GetImageMetadataThunkConfig = {
rejectValue: {
arg: GetImageMetadataArg;
error: unknown;
};
};
export const imageMetadataReceived = createAppAsyncThunk<
GetImageMetadataResponse,
GetImageMetadataArg,
GetImageMetadataThunkConfig
>('api/imageMetadataReceived', async (arg, { rejectWithValue }) => {
const { image_name } = arg;
const { get } = $client.get();
const { data, error, response } = await get(
'/api/v1/images/{image_name}/metadata',
{
params: {
path: { image_name },
},
}
);
if (error) {
return rejectWithValue({ arg, error });
}
return data;
});
type ControlNetAction = {
type: 'SET_CONTROLNET_IMAGE';
controlNetId: string;
};
type InitialImageAction = {
type: 'SET_INITIAL_IMAGE';
};
type NodesAction = {
type: 'SET_NODES_IMAGE';
nodeId: string;
fieldName: string;
};
type CanvasInitialImageAction = {
type: 'SET_CANVAS_INITIAL_IMAGE';
};
type CanvasMergedAction = {
type: 'TOAST_CANVAS_MERGED';
};
type CanvasSavedToGalleryAction = {
type: 'TOAST_CANVAS_SAVED_TO_GALLERY';
};
type UploadedToastAction = {
type: 'TOAST_UPLOADED';
};
type AddToBatchAction = {
type: 'ADD_TO_BATCH';
};
export type PostUploadAction =
| ControlNetAction
| InitialImageAction
| NodesAction
| CanvasInitialImageAction
| CanvasMergedAction
| CanvasSavedToGalleryAction
| UploadedToastAction
| AddToBatchAction;
type UploadImageArg =
paths['/api/v1/images/']['post']['parameters']['query'] & {
file: File;
postUploadAction?: PostUploadAction;
};
type UploadImageResponse =
paths['/api/v1/images/']['post']['responses']['201']['content']['application/json'];
type UploadImageThunkConfig = {
rejectValue: {
arg: UploadImageArg;
error: unknown;
};
};
/**
* `ImagesService.uploadImage()` thunk
*/
export const imageUploaded = createAppAsyncThunk<
UploadImageResponse,
UploadImageArg,
UploadImageThunkConfig
>('api/imageUploaded', async (arg, { rejectWithValue }) => {
const {
postUploadAction,
file,
image_category,
is_intermediate,
session_id,
} = arg;
const { post } = $client.get();
const formData = new FormData();
formData.append('file', file);
const { data, error, response } = await post('/api/v1/images/', {
params: {
query: {
image_category,
is_intermediate,
session_id,
},
},
// TODO: Proper handling of `multipart/form-data` is coming soon, will fix type issues
// https://github.com/drwpow/openapi-typescript/issues/1123
// @ts-ignore
body: formData,
});
if (error) {
return rejectWithValue({ arg, error });
}
return data;
});
type DeleteImageArg =
paths['/api/v1/images/{image_name}']['delete']['parameters']['path'];
type DeleteImageResponse =
paths['/api/v1/images/{image_name}']['delete']['responses']['200']['content']['application/json'];
type DeleteImageThunkConfig = {
rejectValue: {
arg: DeleteImageArg;
error: unknown;
};
};
/**
* `ImagesService.deleteImage()` thunk
*/
export const imageDeleted = createAppAsyncThunk<
DeleteImageResponse,
DeleteImageArg,
DeleteImageThunkConfig
>('api/imageDeleted', async (arg, { rejectWithValue }) => {
const { image_name } = arg;
const { del } = $client.get();
const { data, error, response } = await del('/api/v1/images/{image_name}', {
params: {
path: {
image_name,
},
},
});
if (error) {
return rejectWithValue({ arg, error });
}
});
type UpdateImageArg =
paths['/api/v1/images/{image_name}']['patch']['requestBody']['content']['application/json'] &
paths['/api/v1/images/{image_name}']['patch']['parameters']['path'];
type UpdateImageResponse =
paths['/api/v1/images/{image_name}']['patch']['responses']['200']['content']['application/json'];
type UpdateImageThunkConfig = {
rejectValue: {
arg: UpdateImageArg;
error: unknown;
};
};
/**
* `ImagesService.updateImage()` thunk
*/
export const imageUpdated = createAppAsyncThunk<
UpdateImageResponse,
UpdateImageArg,
UpdateImageThunkConfig
>('api/imageUpdated', async (arg, { rejectWithValue }) => {
const { image_name, image_category, is_intermediate, session_id } = arg;
const { patch } = $client.get();
const { data, error, response } = await patch('/api/v1/images/{image_name}', {
params: {
path: {
image_name,
},
},
body: {
image_category,
is_intermediate,
session_id,
},
});
if (error) {
return rejectWithValue({ arg, error });
}
return data;
});
export const IMAGES_PER_PAGE = 20;
const DEFAULT_IMAGES_LISTED_ARG = {
limit: IMAGES_PER_PAGE,
};
type ListImagesArg = NonNullable<
paths['/api/v1/images/']['get']['parameters']['query']
>;
type ListImagesResponse =
paths['/api/v1/images/']['get']['responses']['200']['content']['application/json'];
type ListImagesThunkConfig = {
rejectValue: {
arg: ListImagesArg;
error: unknown;
};
};
/**
* `ImagesService.listImagesWithMetadata()` thunk
*/
export const receivedPageOfImages = createAppAsyncThunk<
ListImagesResponse,
ListImagesArg,
ListImagesThunkConfig
>('api/receivedPageOfImages', async (arg, { getState, rejectWithValue }) => {
const { get } = $client.get();
const state = getState();
const { categories, selectedBoardId } = state.gallery;
const images = selectImagesAll(state).filter((i) => {
const isInCategory = categories.includes(i.image_category);
const isInSelectedBoard = selectedBoardId
? i.board_id === selectedBoardId
: true;
return isInCategory && isInSelectedBoard;
});
let query: ListImagesArg = {};
if (size(arg)) {
query = {
...DEFAULT_IMAGES_LISTED_ARG,
offset: images.length,
...arg,
};
} else {
query = {
...DEFAULT_IMAGES_LISTED_ARG,
categories,
offset: images.length,
};
}
const { data, error, response } = await get('/api/v1/images/', {
params: {
query,
},
querySerializer: (q) => queryString.stringify(q, { arrayFormat: 'none' }),
});
if (error) {
return rejectWithValue({ arg, error });
}
return data;
});