mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): consolidate images slice
Now that images are in a database and we can make filtered queries, we can do away with the cumbersome `resultsSlice` and `uploadsSlice`. - Remove `resultsSlice` and `uploadsSlice` entirely - Add `imagesSlice` fills the same role - Convert the application to use `imagesSlice`, reducing a lot of messy logic where we had to check which category was selected - Add a simple filter popover to the gallery, which lets you select any number of image categories
This commit is contained in:
parent
6cc00ef4b7
commit
89aa06e014
@ -1,7 +1,5 @@
|
||||
import { canvasPersistDenylist } from 'features/canvas/store/canvasPersistDenylist';
|
||||
import { galleryPersistDenylist } from 'features/gallery/store/galleryPersistDenylist';
|
||||
import { resultsPersistDenylist } from 'features/gallery/store/resultsPersistDenylist';
|
||||
import { uploadsPersistDenylist } from 'features/gallery/store/uploadsPersistDenylist';
|
||||
import { lightboxPersistDenylist } from 'features/lightbox/store/lightboxPersistDenylist';
|
||||
import { nodesPersistDenylist } from 'features/nodes/store/nodesPersistDenylist';
|
||||
import { generationPersistDenylist } from 'features/parameters/store/generationPersistDenylist';
|
||||
@ -22,11 +20,9 @@ const serializationDenylist: {
|
||||
models: modelsPersistDenylist,
|
||||
nodes: nodesPersistDenylist,
|
||||
postprocessing: postprocessingPersistDenylist,
|
||||
results: resultsPersistDenylist,
|
||||
system: systemPersistDenylist,
|
||||
// config: configPersistDenyList,
|
||||
ui: uiPersistDenylist,
|
||||
uploads: uploadsPersistDenylist,
|
||||
// hotkeys: hotkeysPersistDenylist,
|
||||
};
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { initialCanvasState } from 'features/canvas/store/canvasSlice';
|
||||
import { initialGalleryState } from 'features/gallery/store/gallerySlice';
|
||||
import { initialResultsState } from 'features/gallery/store/resultsSlice';
|
||||
import { initialUploadsState } from 'features/gallery/store/uploadsSlice';
|
||||
import { initialImagesState } from 'features/gallery/store/imagesSlice';
|
||||
import { initialLightboxState } from 'features/lightbox/store/lightboxSlice';
|
||||
import { initialNodesState } from 'features/nodes/store/nodesSlice';
|
||||
import { initialGenerationState } from 'features/parameters/store/generationSlice';
|
||||
@ -24,12 +23,11 @@ const initialStates: {
|
||||
models: initialModelsState,
|
||||
nodes: initialNodesState,
|
||||
postprocessing: initialPostprocessingState,
|
||||
results: initialResultsState,
|
||||
system: initialSystemState,
|
||||
config: initialConfigState,
|
||||
ui: initialUIState,
|
||||
uploads: initialUploadsState,
|
||||
hotkeys: initialHotkeysState,
|
||||
images: initialImagesState,
|
||||
};
|
||||
|
||||
export const unserialize: UnserializeFunction = (data, key) => {
|
||||
|
@ -59,18 +59,15 @@ import {
|
||||
addSessionCanceledPendingListener,
|
||||
addSessionCanceledRejectedListener,
|
||||
} from './listeners/sessionCanceled';
|
||||
import {
|
||||
addReceivedGalleryImagesFulfilledListener,
|
||||
addReceivedGalleryImagesRejectedListener,
|
||||
} from './listeners/receivedGalleryImages';
|
||||
import {
|
||||
addReceivedUploadImagesPageFulfilledListener,
|
||||
addReceivedUploadImagesPageRejectedListener,
|
||||
} from './listeners/receivedUploadImages';
|
||||
import {
|
||||
addImageUpdatedFulfilledListener,
|
||||
addImageUpdatedRejectedListener,
|
||||
} from './listeners/imageUpdated';
|
||||
import {
|
||||
addReceivedPageOfImagesFulfilledListener,
|
||||
addReceivedPageOfImagesRejectedListener,
|
||||
} from './listeners/receivedPageOfImages';
|
||||
import { addStagingAreaImageSavedListener } from './listeners/stagingAreaImageSaved';
|
||||
|
||||
export const listenerMiddleware = createListenerMiddleware();
|
||||
|
||||
@ -127,6 +124,7 @@ addCanvasSavedToGalleryListener();
|
||||
addCanvasDownloadedAsImageListener();
|
||||
addCanvasCopiedToClipboardListener();
|
||||
addCanvasMergedListener();
|
||||
addStagingAreaImageSavedListener();
|
||||
|
||||
// socketio
|
||||
addGeneratorProgressListener();
|
||||
@ -154,8 +152,6 @@ addSessionCanceledPendingListener();
|
||||
addSessionCanceledFulfilledListener();
|
||||
addSessionCanceledRejectedListener();
|
||||
|
||||
// Gallery pages
|
||||
addReceivedGalleryImagesFulfilledListener();
|
||||
addReceivedGalleryImagesRejectedListener();
|
||||
addReceivedUploadImagesPageFulfilledListener();
|
||||
addReceivedUploadImagesPageRejectedListener();
|
||||
// Images
|
||||
addReceivedPageOfImagesFulfilledListener();
|
||||
addReceivedPageOfImagesRejectedListener();
|
||||
|
@ -5,7 +5,7 @@ import { imageUploaded } from 'services/thunks/image';
|
||||
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { resultUpserted } from 'features/gallery/store/resultsSlice';
|
||||
import { imageUpserted } from 'features/gallery/store/imagesSlice';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
|
||||
|
||||
@ -47,7 +47,7 @@ export const addCanvasSavedToGalleryListener = () => {
|
||||
action.meta.arg.formData.file.name === filename
|
||||
);
|
||||
|
||||
dispatch(resultUpserted(uploadedImageDTO));
|
||||
dispatch(imageUpserted(uploadedImageDTO));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -5,14 +5,11 @@ import { log } from 'app/logging/useLogger';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import {
|
||||
uploadRemoved,
|
||||
uploadsAdapter,
|
||||
} from 'features/gallery/store/uploadsSlice';
|
||||
import {
|
||||
resultRemoved,
|
||||
resultsAdapter,
|
||||
} from 'features/gallery/store/resultsSlice';
|
||||
import { isUploadsImageDTO } from 'services/types/guards';
|
||||
imageRemoved,
|
||||
imagesAdapter,
|
||||
selectImagesEntities,
|
||||
selectImagesIds,
|
||||
} from 'features/gallery/store/imagesSlice';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' });
|
||||
|
||||
@ -33,19 +30,16 @@ export const addRequestedImageDeletionListener = () => {
|
||||
|
||||
const state = getState();
|
||||
const selectedImage = state.gallery.selectedImage;
|
||||
const isUserImage = isUploadsImageDTO(selectedImage);
|
||||
|
||||
if (selectedImage && selectedImage.image_name === image_name) {
|
||||
const allIds = isUserImage ? state.uploads.ids : state.results.ids;
|
||||
const ids = selectImagesIds(state);
|
||||
const entities = selectImagesEntities(state);
|
||||
|
||||
const allEntities = isUserImage
|
||||
? state.uploads.entities
|
||||
: state.results.entities;
|
||||
|
||||
const deletedImageIndex = allIds.findIndex(
|
||||
const deletedImageIndex = ids.findIndex(
|
||||
(result) => result.toString() === image_name
|
||||
);
|
||||
|
||||
const filteredIds = allIds.filter((id) => id.toString() !== image_name);
|
||||
const filteredIds = ids.filter((id) => id.toString() !== image_name);
|
||||
|
||||
const newSelectedImageIndex = clamp(
|
||||
deletedImageIndex,
|
||||
@ -55,7 +49,7 @@ export const addRequestedImageDeletionListener = () => {
|
||||
|
||||
const newSelectedImageId = filteredIds[newSelectedImageIndex];
|
||||
|
||||
const newSelectedImage = allEntities[newSelectedImageId];
|
||||
const newSelectedImage = entities[newSelectedImageId];
|
||||
|
||||
if (newSelectedImageId) {
|
||||
dispatch(imageSelected(newSelectedImage));
|
||||
@ -64,11 +58,7 @@ export const addRequestedImageDeletionListener = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (isUserImage) {
|
||||
dispatch(uploadRemoved(image_name));
|
||||
} else {
|
||||
dispatch(resultRemoved(image_name));
|
||||
}
|
||||
dispatch(imageRemoved(image_name));
|
||||
|
||||
dispatch(
|
||||
imageDeleted({ imageName: image_name, imageOrigin: image_origin })
|
||||
@ -86,12 +76,7 @@ export const addImageDeletedPendingListener = () => {
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const { imageName, imageOrigin } = action.meta.arg;
|
||||
// Preemptively remove the image from the gallery
|
||||
if (imageOrigin === 'external') {
|
||||
uploadsAdapter.removeOne(getState().uploads, imageName);
|
||||
}
|
||||
if (imageOrigin === 'internal') {
|
||||
resultsAdapter.removeOne(getState().results, imageName);
|
||||
}
|
||||
imagesAdapter.removeOne(getState().images, imageName);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { startAppListening } from '..';
|
||||
import { imageMetadataReceived } from 'services/thunks/image';
|
||||
import { resultUpserted } from 'features/gallery/store/resultsSlice';
|
||||
import { uploadUpserted } from 'features/gallery/store/uploadsSlice';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { imageUpserted } from 'features/gallery/store/imagesSlice';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'image' });
|
||||
|
||||
@ -11,16 +9,13 @@ export const addImageMetadataReceivedFulfilledListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: imageMetadataReceived.fulfilled,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const imageDTO = action.payload;
|
||||
moduleLog.debug({ data: { imageDTO } }, 'Image metadata received');
|
||||
|
||||
if (imageDTO.image_origin === 'internal') {
|
||||
dispatch(resultUpserted(imageDTO));
|
||||
}
|
||||
|
||||
if (imageDTO.image_origin === 'external') {
|
||||
dispatch(uploadUpserted(imageDTO));
|
||||
const image = action.payload;
|
||||
if (image.is_intermediate) {
|
||||
// No further actions needed for intermediate images
|
||||
return;
|
||||
}
|
||||
moduleLog.debug({ data: { image } }, 'Image metadata received');
|
||||
dispatch(imageUpserted(image));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,13 +1,8 @@
|
||||
import { startAppListening } from '..';
|
||||
import { uploadUpserted } from 'features/gallery/store/uploadsSlice';
|
||||
import {
|
||||
imageSelected,
|
||||
setCurrentCategory,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { imageUploaded } from 'services/thunks/image';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { resultUpserted } from 'features/gallery/store/resultsSlice';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { imageUpserted } from 'features/gallery/store/imagesSlice';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'image' });
|
||||
|
||||
@ -26,18 +21,8 @@ export const addImageUploadedFulfilledListener = () => {
|
||||
|
||||
const state = getState();
|
||||
|
||||
// Handle uploads
|
||||
if (image.image_category === 'user' && !image.is_intermediate) {
|
||||
dispatch(uploadUpserted(image));
|
||||
dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
|
||||
}
|
||||
|
||||
// Handle results
|
||||
// TODO: Can this ever happen? I don't think so...
|
||||
if (image.image_category !== 'user' && !image.is_intermediate) {
|
||||
dispatch(resultUpserted(image));
|
||||
dispatch(setCurrentCategory('results'));
|
||||
}
|
||||
dispatch(imageUpserted(image));
|
||||
dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { startAppListening } from '..';
|
||||
import { imageUrlsReceived } from 'services/thunks/image';
|
||||
import { resultsAdapter } from 'features/gallery/store/resultsSlice';
|
||||
import { uploadsAdapter } from 'features/gallery/store/uploadsSlice';
|
||||
import { imagesAdapter } from 'features/gallery/store/imagesSlice';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'image' });
|
||||
|
||||
@ -13,27 +12,15 @@ export const addImageUrlsReceivedFulfilledListener = () => {
|
||||
const image = action.payload;
|
||||
moduleLog.debug({ data: { image } }, 'Image URLs received');
|
||||
|
||||
const { image_origin, image_name, image_url, thumbnail_url } = image;
|
||||
const { image_name, image_url, thumbnail_url } = image;
|
||||
|
||||
if (image_origin === 'results') {
|
||||
resultsAdapter.updateOne(getState().results, {
|
||||
id: image_name,
|
||||
changes: {
|
||||
image_url,
|
||||
thumbnail_url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (image_origin === 'uploads') {
|
||||
uploadsAdapter.updateOne(getState().uploads, {
|
||||
id: image_name,
|
||||
changes: {
|
||||
image_url,
|
||||
thumbnail_url,
|
||||
},
|
||||
});
|
||||
}
|
||||
imagesAdapter.updateOne(getState().images, {
|
||||
id: image_name,
|
||||
changes: {
|
||||
image_url,
|
||||
thumbnail_url,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
||||
import { selectResultsById } from 'features/gallery/store/resultsSlice';
|
||||
import { selectUploadsById } from 'features/gallery/store/uploadsSlice';
|
||||
import { t } from 'i18next';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { startAppListening } from '..';
|
||||
@ -9,7 +7,7 @@ import {
|
||||
isImageDTO,
|
||||
} from 'features/parameters/store/actions';
|
||||
import { makeToast } from 'app/components/Toaster';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import { selectImagesById } from 'features/gallery/store/imagesSlice';
|
||||
|
||||
export const addInitialImageSelectedListener = () => {
|
||||
startAppListening({
|
||||
@ -30,16 +28,8 @@ export const addInitialImageSelectedListener = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { image_name, image_origin } = action.payload;
|
||||
|
||||
let image: ImageDTO | undefined;
|
||||
const state = getState();
|
||||
|
||||
if (image_origin === 'results') {
|
||||
image = selectResultsById(state, image_name);
|
||||
} else if (image_origin === 'uploads') {
|
||||
image = selectUploadsById(state, image_name);
|
||||
}
|
||||
const imageName = action.payload;
|
||||
const image = selectImagesById(getState(), imageName);
|
||||
|
||||
if (!image) {
|
||||
dispatch(
|
||||
|
@ -1,31 +1,31 @@
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { startAppListening } from '..';
|
||||
import { receivedGalleryImages } from 'services/thunks/gallery';
|
||||
import { serializeError } from 'serialize-error';
|
||||
import { receivedPageOfImages } from 'services/thunks/image';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'gallery' });
|
||||
|
||||
export const addReceivedGalleryImagesFulfilledListener = () => {
|
||||
export const addReceivedPageOfImagesFulfilledListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: receivedGalleryImages.fulfilled,
|
||||
actionCreator: receivedPageOfImages.fulfilled,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const page = action.payload;
|
||||
moduleLog.debug(
|
||||
{ data: { page } },
|
||||
`Received ${page.items.length} gallery images`
|
||||
`Received ${page.items.length} images`
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const addReceivedGalleryImagesRejectedListener = () => {
|
||||
export const addReceivedPageOfImagesRejectedListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: receivedGalleryImages.rejected,
|
||||
actionCreator: receivedPageOfImages.rejected,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
if (action.payload) {
|
||||
moduleLog.debug(
|
||||
{ data: { error: serializeError(action.payload.error) } },
|
||||
'Problem receiving gallery images'
|
||||
{ data: { error: serializeError(action.payload) } },
|
||||
'Problem receiving images'
|
||||
);
|
||||
}
|
||||
},
|
@ -1,33 +0,0 @@
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { startAppListening } from '..';
|
||||
import { receivedUploadImages } from 'services/thunks/gallery';
|
||||
import { serializeError } from 'serialize-error';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'gallery' });
|
||||
|
||||
export const addReceivedUploadImagesPageFulfilledListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: receivedUploadImages.fulfilled,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
const page = action.payload;
|
||||
moduleLog.debug(
|
||||
{ data: { page } },
|
||||
`Received ${page.items.length} uploaded images`
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const addReceivedUploadImagesPageRejectedListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: receivedUploadImages.rejected,
|
||||
effect: (action, { getState, dispatch }) => {
|
||||
if (action.payload) {
|
||||
moduleLog.debug(
|
||||
{ data: { error: serializeError(action.payload.error) } },
|
||||
'Problem receiving uploaded images'
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
@ -6,7 +6,6 @@ import { imageMetadataReceived } from 'services/thunks/image';
|
||||
import { sessionCanceled } from 'services/thunks/session';
|
||||
import { isImageOutput } from 'services/types/guards';
|
||||
import { progressImageSet } from 'features/system/store/systemSlice';
|
||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'socketio' });
|
||||
const nodeDenylist = ['dataURL_image'];
|
||||
|
@ -1,10 +1,7 @@
|
||||
import { startAppListening } from '../..';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { socketConnected } from 'services/events/actions';
|
||||
import {
|
||||
receivedGalleryImages,
|
||||
receivedUploadImages,
|
||||
} from 'services/thunks/gallery';
|
||||
import { receivedPageOfImages } from 'services/thunks/image';
|
||||
import { receivedModels } from 'services/thunks/model';
|
||||
import { receivedOpenAPISchema } from 'services/thunks/schema';
|
||||
|
||||
@ -18,17 +15,12 @@ export const addSocketConnectedListener = () => {
|
||||
|
||||
moduleLog.debug({ timestamp }, 'Connected');
|
||||
|
||||
const { results, uploads, models, nodes, config } = getState();
|
||||
const { models, nodes, config, images } = getState();
|
||||
|
||||
const { disabledTabs } = config;
|
||||
|
||||
// These thunks need to be dispatch in middleware; cannot handle in a reducer
|
||||
if (!results.ids.length) {
|
||||
dispatch(receivedGalleryImages());
|
||||
}
|
||||
|
||||
if (!uploads.ids.length) {
|
||||
dispatch(receivedUploadImages());
|
||||
if (!images.ids.length) {
|
||||
dispatch(receivedPageOfImages());
|
||||
}
|
||||
|
||||
if (!models.ids.length) {
|
||||
|
@ -0,0 +1,54 @@
|
||||
import { stagingAreaImageSaved } from 'features/canvas/store/actions';
|
||||
import { startAppListening } from '..';
|
||||
import { log } from 'app/logging/useLogger';
|
||||
import { imageUpdated } from 'services/thunks/image';
|
||||
import { imageUpserted } from 'features/gallery/store/imagesSlice';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
|
||||
const moduleLog = log.child({ namespace: 'canvas' });
|
||||
|
||||
export const addStagingAreaImageSavedListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: stagingAreaImageSaved,
|
||||
effect: async (action, { dispatch, getState, take }) => {
|
||||
const { image_name, image_origin } = action.payload;
|
||||
|
||||
dispatch(
|
||||
imageUpdated({
|
||||
imageName: image_name,
|
||||
imageOrigin: image_origin,
|
||||
requestBody: {
|
||||
is_intermediate: false,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const [imageUpdatedAction] = await take(
|
||||
(action) =>
|
||||
(imageUpdated.fulfilled.match(action) ||
|
||||
imageUpdated.rejected.match(action)) &&
|
||||
action.meta.arg.imageName === image_name
|
||||
);
|
||||
|
||||
if (imageUpdated.rejected.match(imageUpdatedAction)) {
|
||||
moduleLog.error(
|
||||
{ data: { arg: imageUpdatedAction.meta.arg } },
|
||||
'Image saving failed'
|
||||
);
|
||||
dispatch(
|
||||
addToast({
|
||||
title: 'Image Saving Failed',
|
||||
description: imageUpdatedAction.error.message,
|
||||
status: 'error',
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (imageUpdated.fulfilled.match(imageUpdatedAction)) {
|
||||
dispatch(imageUpserted(imageUpdatedAction.payload));
|
||||
dispatch(addToast({ title: 'Image Saved', status: 'success' }));
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
@ -10,8 +10,7 @@ import dynamicMiddlewares from 'redux-dynamic-middlewares';
|
||||
|
||||
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 imagesReducer from 'features/gallery/store/imagesSlice';
|
||||
import lightboxReducer from 'features/lightbox/store/lightboxSlice';
|
||||
import generationReducer from 'features/parameters/store/generationSlice';
|
||||
import postprocessingReducer from 'features/parameters/store/postprocessingSlice';
|
||||
@ -41,12 +40,11 @@ const allReducers = {
|
||||
models: modelsReducer,
|
||||
nodes: nodesReducer,
|
||||
postprocessing: postprocessingReducer,
|
||||
results: resultsReducer,
|
||||
system: systemReducer,
|
||||
config: configReducer,
|
||||
ui: uiReducer,
|
||||
uploads: uploadsReducer,
|
||||
hotkeys: hotkeysReducer,
|
||||
images: imagesReducer,
|
||||
// session: sessionReducer,
|
||||
};
|
||||
|
||||
@ -65,8 +63,6 @@ const rememberedKeys: (keyof typeof allReducers)[] = [
|
||||
'system',
|
||||
'ui',
|
||||
// 'hotkeys',
|
||||
// 'results',
|
||||
// 'uploads',
|
||||
// 'config',
|
||||
];
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { ButtonGroup, Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
// import { saveStagingAreaImageToGallery } from 'app/socketio/actions';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||
@ -26,6 +25,7 @@ import {
|
||||
FaPlus,
|
||||
FaSave,
|
||||
} from 'react-icons/fa';
|
||||
import { stagingAreaImageSaved } from '../store/actions';
|
||||
|
||||
const selector = createSelector(
|
||||
[canvasSelector],
|
||||
@ -157,19 +157,15 @@ const IAICanvasStagingAreaToolbar = () => {
|
||||
}
|
||||
colorScheme="accent"
|
||||
/>
|
||||
{/* <IAIIconButton
|
||||
<IAIIconButton
|
||||
tooltip={t('unifiedCanvas.saveToGallery')}
|
||||
aria-label={t('unifiedCanvas.saveToGallery')}
|
||||
icon={<FaSave />}
|
||||
onClick={() =>
|
||||
dispatch(
|
||||
saveStagingAreaImageToGallery(
|
||||
currentStagingAreaImage.image.image_url
|
||||
)
|
||||
)
|
||||
dispatch(stagingAreaImageSaved(currentStagingAreaImage.image))
|
||||
}
|
||||
colorScheme="accent"
|
||||
/> */}
|
||||
/>
|
||||
<IAIIconButton
|
||||
tooltip={t('unifiedCanvas.discardAll')}
|
||||
aria-label={t('unifiedCanvas.discardAll')}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { ImageDTO } from 'services/api';
|
||||
|
||||
export const canvasSavedToGallery = createAction('canvas/canvasSavedToGallery');
|
||||
|
||||
@ -11,3 +12,7 @@ export const canvasDownloadedAsImage = createAction(
|
||||
);
|
||||
|
||||
export const canvasMerged = createAction('canvas/canvasMerged');
|
||||
|
||||
export const stagingAreaImageSaved = createAction<ImageDTO>(
|
||||
'canvas/stagingAreaImageSaved'
|
||||
);
|
||||
|
@ -62,7 +62,6 @@ const CurrentImagePreview = () => {
|
||||
return;
|
||||
}
|
||||
e.dataTransfer.setData('invokeai/imageName', image.image_name);
|
||||
e.dataTransfer.setData('invokeai/imageOrigin', image.image_origin);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
},
|
||||
[image]
|
||||
|
@ -147,7 +147,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
const handleDragStart = useCallback(
|
||||
(e: DragEvent<HTMLDivElement>) => {
|
||||
e.dataTransfer.setData('invokeai/imageName', image.image_name);
|
||||
e.dataTransfer.setData('invokeai/imageOrigin', image.image_origin);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
},
|
||||
[image]
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {
|
||||
Box,
|
||||
ButtonGroup,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
Flex,
|
||||
FlexProps,
|
||||
Grid,
|
||||
@ -16,7 +17,6 @@ import IAIPopover from 'common/components/IAIPopover';
|
||||
import IAISlider from 'common/components/IAISlider';
|
||||
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
|
||||
import {
|
||||
setCurrentCategory,
|
||||
setGalleryImageMinimumWidth,
|
||||
setGalleryImageObjectFit,
|
||||
setShouldAutoSwitchToNewImages,
|
||||
@ -36,54 +36,53 @@ import {
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
|
||||
import { FaImage, FaUser, FaWrench } from 'react-icons/fa';
|
||||
import { FaFilter, FaWrench } from 'react-icons/fa';
|
||||
import { MdPhotoLibrary } from 'react-icons/md';
|
||||
import HoverableImage from './HoverableImage';
|
||||
|
||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
||||
import { resultsAdapter } from '../store/resultsSlice';
|
||||
import {
|
||||
receivedGalleryImages,
|
||||
receivedUploadImages,
|
||||
} from 'services/thunks/gallery';
|
||||
import { uploadsAdapter } from '../store/uploadsSlice';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { Virtuoso, VirtuosoGrid } from 'react-virtuoso';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import GalleryProgressImage from './GalleryProgressImage';
|
||||
import { uiSelector } from 'features/ui/store/uiSelectors';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import { ImageCategory, ImageDTO } from 'services/api';
|
||||
import { imageCategoriesChanged, selectImagesAll } from '../store/imagesSlice';
|
||||
import { receivedPageOfImages } from 'services/thunks/image';
|
||||
import { capitalize } from 'lodash-es';
|
||||
|
||||
const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 290;
|
||||
const PROGRESS_IMAGE_PLACEHOLDER = 'PROGRESS_IMAGE_PLACEHOLDER';
|
||||
const IMAGE_CATEGORIES: ImageCategory[] = [
|
||||
'general',
|
||||
'control',
|
||||
'mask',
|
||||
'user',
|
||||
'other',
|
||||
];
|
||||
|
||||
const categorySelector = createSelector(
|
||||
[(state: RootState) => state],
|
||||
(state) => {
|
||||
const { results, uploads, system, gallery } = state;
|
||||
const { currentCategory } = gallery;
|
||||
const { system, images } = state;
|
||||
const tempImages: (ImageDTO | typeof PROGRESS_IMAGE_PLACEHOLDER)[] = [];
|
||||
|
||||
if (currentCategory === 'results') {
|
||||
const tempImages: (ImageDTO | typeof PROGRESS_IMAGE_PLACEHOLDER)[] = [];
|
||||
|
||||
if (system.progressImage) {
|
||||
tempImages.push(PROGRESS_IMAGE_PLACEHOLDER);
|
||||
}
|
||||
|
||||
return {
|
||||
images: tempImages.concat(
|
||||
resultsAdapter.getSelectors().selectAll(results)
|
||||
),
|
||||
isLoading: results.isLoading,
|
||||
areMoreImagesAvailable: results.page < results.pages - 1,
|
||||
};
|
||||
if (system.progressImage) {
|
||||
tempImages.push(PROGRESS_IMAGE_PLACEHOLDER);
|
||||
}
|
||||
|
||||
const { categories } = images;
|
||||
|
||||
const allImages = selectImagesAll(state);
|
||||
const filteredImages = allImages.filter((i) =>
|
||||
categories.includes(i.image_category)
|
||||
);
|
||||
|
||||
return {
|
||||
images: uploadsAdapter.getSelectors().selectAll(uploads),
|
||||
isLoading: uploads.isLoading,
|
||||
areMoreImagesAvailable: uploads.page < uploads.pages - 1,
|
||||
images: tempImages.concat(filteredImages),
|
||||
isLoading: images.isLoading,
|
||||
areMoreImagesAvailable: filteredImages.length < images.total,
|
||||
categories: images.categories,
|
||||
};
|
||||
},
|
||||
defaultSelectorOptions
|
||||
@ -93,7 +92,6 @@ const mainSelector = createSelector(
|
||||
[gallerySelector, uiSelector],
|
||||
(gallery, ui) => {
|
||||
const {
|
||||
currentCategory,
|
||||
galleryImageMinimumWidth,
|
||||
galleryImageObjectFit,
|
||||
shouldAutoSwitchToNewImages,
|
||||
@ -104,7 +102,6 @@ const mainSelector = createSelector(
|
||||
const { shouldPinGallery } = ui;
|
||||
|
||||
return {
|
||||
currentCategory,
|
||||
shouldPinGallery,
|
||||
galleryImageMinimumWidth,
|
||||
galleryImageObjectFit,
|
||||
@ -120,7 +117,6 @@ const ImageGalleryContent = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const resizeObserverRef = useRef<HTMLDivElement>(null);
|
||||
const [shouldShouldIconButtons, setShouldShouldIconButtons] = useState(true);
|
||||
const rootRef = useRef(null);
|
||||
const [scroller, setScroller] = useState<HTMLElement | null>(null);
|
||||
const [initialize, osInstance] = useOverlayScrollbars({
|
||||
@ -137,7 +133,6 @@ const ImageGalleryContent = () => {
|
||||
});
|
||||
|
||||
const {
|
||||
currentCategory,
|
||||
shouldPinGallery,
|
||||
galleryImageMinimumWidth,
|
||||
galleryImageObjectFit,
|
||||
@ -146,18 +141,12 @@ const ImageGalleryContent = () => {
|
||||
selectedImage,
|
||||
} = useAppSelector(mainSelector);
|
||||
|
||||
const { images, areMoreImagesAvailable, isLoading } =
|
||||
const { images, areMoreImagesAvailable, isLoading, categories } =
|
||||
useAppSelector(categorySelector);
|
||||
|
||||
const handleClickLoadMore = () => {
|
||||
if (currentCategory === 'results') {
|
||||
dispatch(receivedGalleryImages());
|
||||
}
|
||||
|
||||
if (currentCategory === 'uploads') {
|
||||
dispatch(receivedUploadImages());
|
||||
}
|
||||
};
|
||||
const handleLoadMoreImages = useCallback(() => {
|
||||
dispatch(receivedPageOfImages());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleChangeGalleryImageMinimumWidth = (v: number) => {
|
||||
dispatch(setGalleryImageMinimumWidth(v));
|
||||
@ -168,28 +157,6 @@ const ImageGalleryContent = () => {
|
||||
dispatch(requestCanvasRescale());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!resizeObserverRef.current) {
|
||||
return;
|
||||
}
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (!resizeObserverRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
resizeObserverRef.current.clientWidth < GALLERY_SHOW_BUTTONS_MIN_WIDTH
|
||||
) {
|
||||
setShouldShouldIconButtons(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setShouldShouldIconButtons(false);
|
||||
});
|
||||
resizeObserver.observe(resizeObserverRef.current);
|
||||
return () => resizeObserver.disconnect(); // clean up
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const { current: root } = rootRef;
|
||||
if (scroller && root) {
|
||||
@ -210,12 +177,15 @@ const ImageGalleryContent = () => {
|
||||
}, []);
|
||||
|
||||
const handleEndReached = useCallback(() => {
|
||||
if (currentCategory === 'results') {
|
||||
dispatch(receivedGalleryImages());
|
||||
} else if (currentCategory === 'uploads') {
|
||||
dispatch(receivedUploadImages());
|
||||
}
|
||||
}, [dispatch, currentCategory]);
|
||||
handleLoadMoreImages();
|
||||
}, [handleLoadMoreImages]);
|
||||
|
||||
const handleCategoriesChanged = useCallback(
|
||||
(newCategories: ImageCategory[]) => {
|
||||
dispatch(imageCategoriesChanged(newCategories));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@ -232,52 +202,28 @@ const ImageGalleryContent = () => {
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<ButtonGroup
|
||||
size="sm"
|
||||
isAttached
|
||||
w="max-content"
|
||||
justifyContent="stretch"
|
||||
<IAIPopover
|
||||
triggerComponent={
|
||||
<IAIIconButton
|
||||
aria-label="Gallery Filter"
|
||||
size="sm"
|
||||
icon={<FaFilter />}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{shouldShouldIconButtons ? (
|
||||
<>
|
||||
<IAIIconButton
|
||||
aria-label={t('gallery.showGenerations')}
|
||||
tooltip={t('gallery.showGenerations')}
|
||||
isChecked={currentCategory === 'results'}
|
||||
role="radio"
|
||||
icon={<FaImage />}
|
||||
onClick={() => dispatch(setCurrentCategory('results'))}
|
||||
/>
|
||||
<IAIIconButton
|
||||
aria-label={t('gallery.showUploads')}
|
||||
tooltip={t('gallery.showUploads')}
|
||||
role="radio"
|
||||
isChecked={currentCategory === 'uploads'}
|
||||
icon={<FaUser />}
|
||||
onClick={() => dispatch(setCurrentCategory('uploads'))}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IAIButton
|
||||
size="sm"
|
||||
isChecked={currentCategory === 'results'}
|
||||
onClick={() => dispatch(setCurrentCategory('results'))}
|
||||
flexGrow={1}
|
||||
>
|
||||
{t('gallery.generations')}
|
||||
</IAIButton>
|
||||
<IAIButton
|
||||
size="sm"
|
||||
isChecked={currentCategory === 'uploads'}
|
||||
onClick={() => dispatch(setCurrentCategory('uploads'))}
|
||||
flexGrow={1}
|
||||
>
|
||||
{t('gallery.uploads')}
|
||||
</IAIButton>
|
||||
</>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
<Flex sx={{ flexDirection: 'column', gap: 2 }}>
|
||||
<CheckboxGroup
|
||||
value={categories}
|
||||
onChange={handleCategoriesChanged}
|
||||
>
|
||||
{IMAGE_CATEGORIES.map((c) => (
|
||||
<Checkbox key={c} value={c}>
|
||||
{capitalize(c)}
|
||||
</Checkbox>
|
||||
))}
|
||||
</CheckboxGroup>
|
||||
</Flex>
|
||||
</IAIPopover>
|
||||
|
||||
<Flex gap={2}>
|
||||
<IAIPopover
|
||||
@ -400,7 +346,7 @@ const ImageGalleryContent = () => {
|
||||
)}
|
||||
</Box>
|
||||
<IAIButton
|
||||
onClick={handleClickLoadMore}
|
||||
onClick={handleLoadMoreImages}
|
||||
isDisabled={!areMoreImagesAvailable}
|
||||
isLoading={isLoading}
|
||||
loadingText="Loading"
|
||||
|
@ -9,6 +9,10 @@ import { gallerySelector } from '../store/gallerySelectors';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { imageSelected } from '../store/gallerySlice';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import {
|
||||
selectFilteredImagesAsObject,
|
||||
selectFilteredImagesIds,
|
||||
} from '../store/imagesSlice';
|
||||
|
||||
const nextPrevButtonTriggerAreaStyles: ChakraProps['sx'] = {
|
||||
height: '100%',
|
||||
@ -21,9 +25,14 @@ const nextPrevButtonStyles: ChakraProps['sx'] = {
|
||||
};
|
||||
|
||||
export const nextPrevImageButtonsSelector = createSelector(
|
||||
[(state: RootState) => state, gallerySelector],
|
||||
(state, gallery) => {
|
||||
const { selectedImage, currentCategory } = gallery;
|
||||
[
|
||||
(state: RootState) => state,
|
||||
gallerySelector,
|
||||
selectFilteredImagesAsObject,
|
||||
selectFilteredImagesIds,
|
||||
],
|
||||
(state, gallery, filteredImagesAsObject, filteredImageIds) => {
|
||||
const { selectedImage } = gallery;
|
||||
|
||||
if (!selectedImage) {
|
||||
return {
|
||||
@ -32,29 +41,29 @@ export const nextPrevImageButtonsSelector = createSelector(
|
||||
};
|
||||
}
|
||||
|
||||
const currentImageIndex = state[currentCategory].ids.findIndex(
|
||||
const currentImageIndex = filteredImageIds.findIndex(
|
||||
(i) => i === selectedImage.image_name
|
||||
);
|
||||
|
||||
const nextImageIndex = clamp(
|
||||
currentImageIndex + 1,
|
||||
0,
|
||||
state[currentCategory].ids.length - 1
|
||||
filteredImageIds.length - 1
|
||||
);
|
||||
|
||||
const prevImageIndex = clamp(
|
||||
currentImageIndex - 1,
|
||||
0,
|
||||
state[currentCategory].ids.length - 1
|
||||
filteredImageIds.length - 1
|
||||
);
|
||||
|
||||
const nextImageId = state[currentCategory].ids[nextImageIndex];
|
||||
const prevImageId = state[currentCategory].ids[prevImageIndex];
|
||||
const nextImageId = filteredImageIds[nextImageIndex];
|
||||
const prevImageId = filteredImageIds[prevImageIndex];
|
||||
|
||||
const nextImage = state[currentCategory].entities[nextImageId];
|
||||
const prevImage = state[currentCategory].entities[prevImageId];
|
||||
const nextImage = filteredImagesAsObject[nextImageId];
|
||||
const prevImage = filteredImagesAsObject[prevImageId];
|
||||
|
||||
const imagesLength = state[currentCategory].ids.length;
|
||||
const imagesLength = filteredImageIds.length;
|
||||
|
||||
return {
|
||||
isOnFirstImage: currentImageIndex === 0,
|
||||
|
@ -1,33 +1,18 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { ResourceOrigin } from 'services/api';
|
||||
import { selectResultsEntities } from '../store/resultsSlice';
|
||||
import { selectUploadsEntities } from '../store/uploadsSlice';
|
||||
import { selectImagesEntities } from '../store/imagesSlice';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const useGetImageByNameSelector = createSelector(
|
||||
[selectResultsEntities, selectUploadsEntities],
|
||||
(allResults, allUploads) => {
|
||||
return { allResults, allUploads };
|
||||
}
|
||||
);
|
||||
|
||||
const useGetImageByNameAndOrigin = () => {
|
||||
const { allResults, allUploads } = useAppSelector(useGetImageByNameSelector);
|
||||
return (name: string, origin: ResourceOrigin) => {
|
||||
if (origin === 'internal') {
|
||||
const resultImagesResult = allResults[name];
|
||||
if (resultImagesResult) {
|
||||
return resultImagesResult;
|
||||
const useGetImageByName = () => {
|
||||
const images = useAppSelector(selectImagesEntities);
|
||||
return useCallback(
|
||||
(name: string | undefined) => {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (origin === 'external') {
|
||||
const userImagesResult = allUploads[name];
|
||||
if (userImagesResult) {
|
||||
return userImagesResult;
|
||||
}
|
||||
}
|
||||
};
|
||||
return images[name];
|
||||
},
|
||||
[images]
|
||||
);
|
||||
};
|
||||
|
||||
export default useGetImageByNameAndOrigin;
|
||||
export default useGetImageByName;
|
||||
|
@ -4,6 +4,5 @@ import { GalleryState } from './gallerySlice';
|
||||
* Gallery slice persist denylist
|
||||
*/
|
||||
export const galleryPersistDenylist: (keyof GalleryState)[] = [
|
||||
'currentCategory',
|
||||
'shouldAutoSwitchToNewImages',
|
||||
];
|
||||
|
@ -1,12 +1,6 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import {
|
||||
receivedGalleryImages,
|
||||
receivedUploadImages,
|
||||
} from '../../../services/thunks/gallery';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import { resultUpserted } from './resultsSlice';
|
||||
import { uploadUpserted } from './uploadsSlice';
|
||||
|
||||
type GalleryImageObjectFitType = 'contain' | 'cover';
|
||||
|
||||
@ -16,7 +10,6 @@ export interface GalleryState {
|
||||
galleryImageObjectFit: GalleryImageObjectFitType;
|
||||
shouldAutoSwitchToNewImages: boolean;
|
||||
shouldUseSingleGalleryColumn: boolean;
|
||||
currentCategory: 'results' | 'uploads';
|
||||
}
|
||||
|
||||
export const initialGalleryState: GalleryState = {
|
||||
@ -24,7 +17,6 @@ export const initialGalleryState: GalleryState = {
|
||||
galleryImageObjectFit: 'cover',
|
||||
shouldAutoSwitchToNewImages: true,
|
||||
shouldUseSingleGalleryColumn: false,
|
||||
currentCategory: 'results',
|
||||
};
|
||||
|
||||
export const gallerySlice = createSlice({
|
||||
@ -48,12 +40,6 @@ export const gallerySlice = createSlice({
|
||||
setShouldAutoSwitchToNewImages: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldAutoSwitchToNewImages = action.payload;
|
||||
},
|
||||
setCurrentCategory: (
|
||||
state,
|
||||
action: PayloadAction<'results' | 'uploads'>
|
||||
) => {
|
||||
state.currentCategory = action.payload;
|
||||
},
|
||||
setShouldUseSingleGalleryColumn: (
|
||||
state,
|
||||
action: PayloadAction<boolean>
|
||||
@ -61,55 +47,6 @@ export const gallerySlice = createSlice({
|
||||
state.shouldUseSingleGalleryColumn = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder.addCase(receivedGalleryImages.fulfilled, (state, action) => {
|
||||
// rehydrate selectedImage URL when results list comes in
|
||||
// solves case when outdated URL is in local storage
|
||||
const selectedImage = state.selectedImage;
|
||||
if (selectedImage) {
|
||||
const selectedImageInResults = action.payload.items.find(
|
||||
(image) => image.image_name === selectedImage.image_name
|
||||
);
|
||||
|
||||
if (selectedImageInResults) {
|
||||
selectedImage.image_url = selectedImageInResults.image_url;
|
||||
selectedImage.thumbnail_url = selectedImageInResults.thumbnail_url;
|
||||
state.selectedImage = selectedImage;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
builder.addCase(receivedUploadImages.fulfilled, (state, action) => {
|
||||
// rehydrate selectedImage URL when results list comes in
|
||||
// solves case when outdated URL is in local storage
|
||||
const selectedImage = state.selectedImage;
|
||||
if (selectedImage) {
|
||||
const selectedImageInResults = action.payload.items.find(
|
||||
(image) => image.image_name === selectedImage.image_name
|
||||
);
|
||||
|
||||
if (selectedImageInResults) {
|
||||
selectedImage.image_url = selectedImageInResults.image_url;
|
||||
selectedImage.thumbnail_url = selectedImageInResults.thumbnail_url;
|
||||
state.selectedImage = selectedImage;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
builder.addCase(resultUpserted, (state, action) => {
|
||||
if (state.shouldAutoSwitchToNewImages) {
|
||||
state.selectedImage = action.payload;
|
||||
state.currentCategory = 'results';
|
||||
}
|
||||
});
|
||||
|
||||
builder.addCase(uploadUpserted, (state, action) => {
|
||||
if (state.shouldAutoSwitchToNewImages) {
|
||||
state.selectedImage = action.payload;
|
||||
state.currentCategory = 'uploads';
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
@ -118,7 +55,6 @@ export const {
|
||||
setGalleryImageObjectFit,
|
||||
setShouldAutoSwitchToNewImages,
|
||||
setShouldUseSingleGalleryColumn,
|
||||
setCurrentCategory,
|
||||
} = gallerySlice.actions;
|
||||
|
||||
export default gallerySlice.reducer;
|
||||
|
127
invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts
Normal file
127
invokeai/frontend/web/src/features/gallery/store/imagesSlice.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import {
|
||||
PayloadAction,
|
||||
createEntityAdapter,
|
||||
createSelector,
|
||||
createSlice,
|
||||
} from '@reduxjs/toolkit';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { ImageCategory, ImageDTO } from 'services/api';
|
||||
import { dateComparator } from 'common/util/dateComparator';
|
||||
import { isString, keyBy } from 'lodash-es';
|
||||
import { receivedPageOfImages } from 'services/thunks/image';
|
||||
|
||||
export const imagesAdapter = createEntityAdapter<ImageDTO>({
|
||||
selectId: (image) => image.image_name,
|
||||
sortComparer: (a, b) => dateComparator(b.created_at, a.created_at),
|
||||
});
|
||||
|
||||
type AdditionaImagesState = {
|
||||
offset: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
isLoading: boolean;
|
||||
categories: ImageCategory[];
|
||||
};
|
||||
|
||||
export const initialImagesState =
|
||||
imagesAdapter.getInitialState<AdditionaImagesState>({
|
||||
offset: 0,
|
||||
limit: 0,
|
||||
total: 0,
|
||||
isLoading: false,
|
||||
categories: ['general', 'control', 'mask', 'other', 'user'],
|
||||
});
|
||||
|
||||
export type ImagesState = typeof initialImagesState;
|
||||
|
||||
const imagesSlice = createSlice({
|
||||
name: 'images',
|
||||
initialState: initialImagesState,
|
||||
reducers: {
|
||||
imageUpserted: (state, action: PayloadAction<ImageDTO>) => {
|
||||
imagesAdapter.upsertOne(state, action.payload);
|
||||
},
|
||||
imageRemoved: (state, action: PayloadAction<string | ImageDTO>) => {
|
||||
if (isString(action.payload)) {
|
||||
imagesAdapter.removeOne(state, action.payload);
|
||||
return;
|
||||
}
|
||||
|
||||
imagesAdapter.removeOne(state, action.payload.image_name);
|
||||
},
|
||||
imageCategoriesChanged: (state, action: PayloadAction<ImageCategory[]>) => {
|
||||
state.categories = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(receivedPageOfImages.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
});
|
||||
builder.addCase(receivedPageOfImages.rejected, (state) => {
|
||||
state.isLoading = false;
|
||||
});
|
||||
builder.addCase(receivedPageOfImages.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
const { items, offset, limit, total } = action.payload;
|
||||
state.offset = offset;
|
||||
state.limit = limit;
|
||||
state.total = total;
|
||||
imagesAdapter.upsertMany(state, items);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
selectAll: selectImagesAll,
|
||||
selectById: selectImagesById,
|
||||
selectEntities: selectImagesEntities,
|
||||
selectIds: selectImagesIds,
|
||||
selectTotal: selectImagesTotal,
|
||||
} = imagesAdapter.getSelectors<RootState>((state) => state.images);
|
||||
|
||||
export const { imageUpserted, imageRemoved, imageCategoriesChanged } =
|
||||
imagesSlice.actions;
|
||||
|
||||
export default imagesSlice.reducer;
|
||||
|
||||
export const selectFilteredImagesAsArray = createSelector(
|
||||
(state: RootState) => state,
|
||||
(state) => {
|
||||
const {
|
||||
images: { categories },
|
||||
} = state;
|
||||
|
||||
return selectImagesAll(state).filter((i) =>
|
||||
categories.includes(i.image_category)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const selectFilteredImagesAsObject = createSelector(
|
||||
(state: RootState) => state,
|
||||
(state) => {
|
||||
const {
|
||||
images: { categories },
|
||||
} = state;
|
||||
|
||||
return keyBy(
|
||||
selectImagesAll(state).filter((i) =>
|
||||
categories.includes(i.image_category)
|
||||
),
|
||||
'image_name'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const selectFilteredImagesIds = createSelector(
|
||||
(state: RootState) => state,
|
||||
(state) => {
|
||||
const {
|
||||
images: { categories },
|
||||
} = state;
|
||||
|
||||
return selectImagesAll(state)
|
||||
.filter((i) => categories.includes(i.image_category))
|
||||
.map((i) => i.image_name);
|
||||
}
|
||||
);
|
@ -1,8 +0,0 @@
|
||||
import { ResultsState } from './resultsSlice';
|
||||
|
||||
/**
|
||||
* Results slice persist denylist
|
||||
*
|
||||
* Currently denylisting results slice entirely, see `serialize.ts`
|
||||
*/
|
||||
export const resultsPersistDenylist: (keyof ResultsState)[] = [];
|
@ -1,91 +0,0 @@
|
||||
import {
|
||||
PayloadAction,
|
||||
createEntityAdapter,
|
||||
createSlice,
|
||||
} from '@reduxjs/toolkit';
|
||||
import { RootState } from 'app/store/store';
|
||||
import {
|
||||
receivedGalleryImages,
|
||||
IMAGES_PER_PAGE,
|
||||
} from 'services/thunks/gallery';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import { dateComparator } from 'common/util/dateComparator';
|
||||
|
||||
export type ResultsImageDTO = Omit<ImageDTO, 'image_origin'> & {
|
||||
image_origin: 'results';
|
||||
};
|
||||
|
||||
export const resultsAdapter = createEntityAdapter<ImageDTO>({
|
||||
selectId: (image) => image.image_name,
|
||||
sortComparer: (a, b) => dateComparator(b.created_at, a.created_at),
|
||||
});
|
||||
|
||||
type AdditionalResultsState = {
|
||||
page: number;
|
||||
pages: number;
|
||||
isLoading: boolean;
|
||||
nextPage: number;
|
||||
upsertedImageCount: number;
|
||||
};
|
||||
|
||||
export const initialResultsState =
|
||||
resultsAdapter.getInitialState<AdditionalResultsState>({
|
||||
page: 0,
|
||||
pages: 0,
|
||||
isLoading: false,
|
||||
nextPage: 0,
|
||||
upsertedImageCount: 0,
|
||||
});
|
||||
|
||||
export type ResultsState = typeof initialResultsState;
|
||||
|
||||
const resultsSlice = createSlice({
|
||||
name: 'results',
|
||||
initialState: initialResultsState,
|
||||
reducers: {
|
||||
resultUpserted: (state, action: PayloadAction<ImageDTO>) => {
|
||||
resultsAdapter.upsertOne(state, action.payload);
|
||||
state.upsertedImageCount += 1;
|
||||
},
|
||||
resultRemoved: (state, action: PayloadAction<string>) => {
|
||||
resultsAdapter.removeOne(state, action.payload);
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
/**
|
||||
* Received Result Images Page - PENDING
|
||||
*/
|
||||
builder.addCase(receivedGalleryImages.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
});
|
||||
|
||||
/**
|
||||
* Received Result Images Page - FULFILLED
|
||||
*/
|
||||
builder.addCase(receivedGalleryImages.fulfilled, (state, action) => {
|
||||
const { page, pages } = action.payload;
|
||||
|
||||
// We know these will all be of the results type, but it's not represented in the API types
|
||||
const items = action.payload.items;
|
||||
|
||||
resultsAdapter.setMany(state, items);
|
||||
|
||||
state.page = page;
|
||||
state.pages = pages;
|
||||
state.nextPage = items.length < IMAGES_PER_PAGE ? page : page + 1;
|
||||
state.isLoading = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
selectAll: selectResultsAll,
|
||||
selectById: selectResultsById,
|
||||
selectEntities: selectResultsEntities,
|
||||
selectIds: selectResultsIds,
|
||||
selectTotal: selectResultsTotal,
|
||||
} = resultsAdapter.getSelectors<RootState>((state) => state.results);
|
||||
|
||||
export const { resultUpserted, resultRemoved } = resultsSlice.actions;
|
||||
|
||||
export default resultsSlice.reducer;
|
@ -1,8 +0,0 @@
|
||||
import { UploadsState } from './uploadsSlice';
|
||||
|
||||
/**
|
||||
* Uploads slice persist denylist
|
||||
*
|
||||
* Currently denylisting uploads slice entirely, see `serialize.ts`
|
||||
*/
|
||||
export const uploadsPersistDenylist: (keyof UploadsState)[] = [];
|
@ -1,93 +0,0 @@
|
||||
import {
|
||||
PayloadAction,
|
||||
createEntityAdapter,
|
||||
createSlice,
|
||||
} from '@reduxjs/toolkit';
|
||||
|
||||
import { RootState } from 'app/store/store';
|
||||
import { receivedUploadImages, IMAGES_PER_PAGE } from 'services/thunks/gallery';
|
||||
import { ImageDTO } from 'services/api';
|
||||
import { dateComparator } from 'common/util/dateComparator';
|
||||
|
||||
export type UploadsImageDTO = Omit<
|
||||
ImageDTO,
|
||||
'image_origin' | 'image_category'
|
||||
> & {
|
||||
image_origin: 'external';
|
||||
image_category: 'user';
|
||||
};
|
||||
|
||||
export const uploadsAdapter = createEntityAdapter<ImageDTO>({
|
||||
selectId: (image) => image.image_name,
|
||||
sortComparer: (a, b) => dateComparator(b.created_at, a.created_at),
|
||||
});
|
||||
|
||||
type AdditionalUploadsState = {
|
||||
page: number;
|
||||
pages: number;
|
||||
isLoading: boolean;
|
||||
nextPage: number;
|
||||
upsertedImageCount: number;
|
||||
};
|
||||
|
||||
export const initialUploadsState =
|
||||
uploadsAdapter.getInitialState<AdditionalUploadsState>({
|
||||
page: 0,
|
||||
pages: 0,
|
||||
nextPage: 0,
|
||||
isLoading: false,
|
||||
upsertedImageCount: 0,
|
||||
});
|
||||
|
||||
export type UploadsState = typeof initialUploadsState;
|
||||
|
||||
const uploadsSlice = createSlice({
|
||||
name: 'uploads',
|
||||
initialState: initialUploadsState,
|
||||
reducers: {
|
||||
uploadUpserted: (state, action: PayloadAction<ImageDTO>) => {
|
||||
uploadsAdapter.upsertOne(state, action.payload);
|
||||
state.upsertedImageCount += 1;
|
||||
},
|
||||
uploadRemoved: (state, action: PayloadAction<string>) => {
|
||||
uploadsAdapter.removeOne(state, action.payload);
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
/**
|
||||
* Received Upload Images Page - PENDING
|
||||
*/
|
||||
builder.addCase(receivedUploadImages.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
});
|
||||
|
||||
/**
|
||||
* Received Upload Images Page - FULFILLED
|
||||
*/
|
||||
builder.addCase(receivedUploadImages.fulfilled, (state, action) => {
|
||||
const { page, pages } = action.payload;
|
||||
|
||||
// We know these will all be of the uploads type, but it's not represented in the API types
|
||||
const items = action.payload.items;
|
||||
|
||||
uploadsAdapter.setMany(state, items);
|
||||
|
||||
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 { uploadUpserted, uploadRemoved } = uploadsSlice.actions;
|
||||
|
||||
export default uploadsSlice.reducer;
|
@ -2,7 +2,7 @@ import { Box, Image } from '@chakra-ui/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder';
|
||||
import { useGetUrl } from 'common/util/getUrl';
|
||||
import useGetImageByNameAndOrigin from 'features/gallery/hooks/useGetImageByName';
|
||||
import useGetImageByName from 'features/gallery/hooks/useGetImageByName';
|
||||
|
||||
import { fieldValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import {
|
||||
@ -11,7 +11,6 @@ import {
|
||||
} from 'features/nodes/types/types';
|
||||
import { DragEvent, memo, useCallback, useState } from 'react';
|
||||
|
||||
import { ResourceOrigin } from 'services/api';
|
||||
import { FieldComponentProps } from './types';
|
||||
|
||||
const ImageInputFieldComponent = (
|
||||
@ -19,7 +18,7 @@ const ImageInputFieldComponent = (
|
||||
) => {
|
||||
const { nodeId, field } = props;
|
||||
|
||||
const getImageByNameAndType = useGetImageByNameAndOrigin();
|
||||
const getImageByName = useGetImageByName();
|
||||
const dispatch = useAppDispatch();
|
||||
const [url, setUrl] = useState<string | undefined>(field.value?.image_url);
|
||||
const { getUrl } = useGetUrl();
|
||||
@ -27,15 +26,7 @@ const ImageInputFieldComponent = (
|
||||
const handleDrop = useCallback(
|
||||
(e: DragEvent<HTMLDivElement>) => {
|
||||
const name = e.dataTransfer.getData('invokeai/imageName');
|
||||
const type = e.dataTransfer.getData(
|
||||
'invokeai/imageOrigin'
|
||||
) as ResourceOrigin;
|
||||
|
||||
if (!name || !type) {
|
||||
return;
|
||||
}
|
||||
|
||||
const image = getImageByNameAndType(name, type);
|
||||
const image = getImageByName(name);
|
||||
|
||||
if (!image) {
|
||||
return;
|
||||
@ -51,7 +42,7 @@ const ImageInputFieldComponent = (
|
||||
})
|
||||
);
|
||||
},
|
||||
[getImageByNameAndType, dispatch, field.name, nodeId]
|
||||
[getImageByName, dispatch, field.name, nodeId]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -26,18 +26,21 @@ const buildBaseNode = (
|
||||
| ImageToImageInvocation
|
||||
| InpaintInvocation
|
||||
| undefined => {
|
||||
const dimensionsOverride = state.canvas.boundingBoxDimensions;
|
||||
const overrides = {
|
||||
...state.canvas.boundingBoxDimensions,
|
||||
is_intermediate: true,
|
||||
};
|
||||
|
||||
if (nodeType === 'txt2img') {
|
||||
return buildTxt2ImgNode(state, dimensionsOverride);
|
||||
return buildTxt2ImgNode(state, overrides);
|
||||
}
|
||||
|
||||
if (nodeType === 'img2img') {
|
||||
return buildImg2ImgNode(state, dimensionsOverride);
|
||||
return buildImg2ImgNode(state, overrides);
|
||||
}
|
||||
|
||||
if (nodeType === 'inpaint' || nodeType === 'outpaint') {
|
||||
return buildInpaintNode(state, dimensionsOverride);
|
||||
return buildInpaintNode(state, overrides);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -5,7 +5,6 @@ import { useGetUrl } from 'common/util/getUrl';
|
||||
import { clearInitialImage } from 'features/parameters/store/generationSlice';
|
||||
import { DragEvent, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ResourceOrigin } from 'services/api';
|
||||
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
||||
import { generationSelector } from 'features/parameters/store/generationSelectors';
|
||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||
@ -55,11 +54,7 @@ const InitialImagePreview = () => {
|
||||
const handleDrop = useCallback(
|
||||
(e: DragEvent<HTMLDivElement>) => {
|
||||
const name = e.dataTransfer.getData('invokeai/imageName');
|
||||
const type = e.dataTransfer.getData(
|
||||
'invokeai/imageOrigin'
|
||||
) as ResourceOrigin;
|
||||
|
||||
dispatch(initialImageSelected({ image_name: name, image_origin: type }));
|
||||
dispatch(initialImageSelected(name));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
@ -88,7 +88,7 @@ export const useParameters = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(initialImageSelected(image));
|
||||
dispatch(initialImageSelected(image.image_name));
|
||||
toaster({
|
||||
title: t('toast.initialImageSet'),
|
||||
status: 'info',
|
||||
|
@ -26,6 +26,6 @@ export const isImageDTO = (image: any): image is ImageDTO => {
|
||||
);
|
||||
};
|
||||
|
||||
export const initialImageSelected = createAction<
|
||||
ImageDTO | ImageNameAndOrigin | undefined
|
||||
>('generation/initialImageSelected');
|
||||
export const initialImageSelected = createAction<ImageDTO | string | undefined>(
|
||||
'generation/initialImageSelected'
|
||||
);
|
||||
|
@ -1,34 +1,3 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { selectResultsById } from 'features/gallery/store/resultsSlice';
|
||||
import { selectUploadsById } from 'features/gallery/store/uploadsSlice';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
export const generationSelector = (state: RootState) => state.generation;
|
||||
|
||||
export const mayGenerateMultipleImagesSelector = createSelector(
|
||||
generationSelector,
|
||||
({ shouldRandomizeSeed, shouldGenerateVariations }) => {
|
||||
return shouldRandomizeSeed || shouldGenerateVariations;
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export const initialImageSelector = createSelector(
|
||||
[(state: RootState) => state, generationSelector],
|
||||
(state, generation) => {
|
||||
const { initialImage } = generation;
|
||||
|
||||
if (initialImage?.type === 'results') {
|
||||
return selectResultsById(state, initialImage.name);
|
||||
}
|
||||
|
||||
if (initialImage?.type === 'uploads') {
|
||||
return selectUploadsById(state, initialImage.name);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -1,64 +0,0 @@
|
||||
import { createAppAsyncThunk } from 'app/store/storeUtils';
|
||||
import { ImagesService, PaginatedResults_ImageDTO_ } from 'services/api';
|
||||
|
||||
export const IMAGES_PER_PAGE = 20;
|
||||
|
||||
type ReceivedResultImagesPageThunkConfig = {
|
||||
rejectValue: {
|
||||
error: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
export const receivedGalleryImages = createAppAsyncThunk<
|
||||
PaginatedResults_ImageDTO_,
|
||||
void,
|
||||
ReceivedResultImagesPageThunkConfig
|
||||
>(
|
||||
'results/receivedResultImagesPage',
|
||||
async (_arg, { getState, rejectWithValue }) => {
|
||||
const { page, pages, nextPage, upsertedImageCount } = getState().results;
|
||||
|
||||
// If many images have been upserted, we need to offset the page number
|
||||
// TODO: add an offset param to the list images endpoint
|
||||
const pageOffset = Math.floor(upsertedImageCount / IMAGES_PER_PAGE);
|
||||
|
||||
const response = await ImagesService.listImagesWithMetadata({
|
||||
excludeCategories: ['user'],
|
||||
isIntermediate: false,
|
||||
page: nextPage + pageOffset,
|
||||
perPage: IMAGES_PER_PAGE,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
type ReceivedUploadImagesPageThunkConfig = {
|
||||
rejectValue: {
|
||||
error: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
export const receivedUploadImages = createAppAsyncThunk<
|
||||
PaginatedResults_ImageDTO_,
|
||||
void,
|
||||
ReceivedUploadImagesPageThunkConfig
|
||||
>(
|
||||
'uploads/receivedUploadImagesPage',
|
||||
async (_arg, { getState, rejectWithValue }) => {
|
||||
const { page, pages, nextPage, upsertedImageCount } = getState().uploads;
|
||||
|
||||
// If many images have been upserted, we need to offset the page number
|
||||
// TODO: add an offset param to the list images endpoint
|
||||
const pageOffset = Math.floor(upsertedImageCount / IMAGES_PER_PAGE);
|
||||
|
||||
const response = await ImagesService.listImagesWithMetadata({
|
||||
includeCategories: ['user'],
|
||||
isIntermediate: false,
|
||||
page: nextPage + pageOffset,
|
||||
perPage: IMAGES_PER_PAGE,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
);
|
@ -1,5 +1,5 @@
|
||||
import { createAppAsyncThunk } from 'app/store/storeUtils';
|
||||
import { InvokeTabName } from 'features/ui/store/tabMap';
|
||||
import { selectImagesAll } from 'features/gallery/store/imagesSlice';
|
||||
import { ImagesService } from 'services/api';
|
||||
|
||||
type imageUrlsReceivedArg = Parameters<
|
||||
@ -71,3 +71,32 @@ export const imageUpdated = createAppAsyncThunk(
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
||||
type ImagesListedArg = Parameters<
|
||||
(typeof ImagesService)['listImagesWithMetadata']
|
||||
>[0];
|
||||
|
||||
export const IMAGES_PER_PAGE = 20;
|
||||
|
||||
/**
|
||||
* `ImagesService.listImagesWithMetadata()` thunk
|
||||
*/
|
||||
export const receivedPageOfImages = createAppAsyncThunk(
|
||||
'api/receivedPageOfImages',
|
||||
async (_, { getState }) => {
|
||||
const state = getState();
|
||||
const { categories } = state.images;
|
||||
|
||||
const totalImagesInFilter = selectImagesAll(state).filter((i) =>
|
||||
categories.includes(i.image_category)
|
||||
).length;
|
||||
|
||||
const response = await ImagesService.listImagesWithMetadata({
|
||||
categories,
|
||||
isIntermediate: false,
|
||||
offset: totalImagesInFilter,
|
||||
limit: IMAGES_PER_PAGE,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
);
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { UploadsImageDTO } from 'features/gallery/store/uploadsSlice';
|
||||
import { get, isObject, isString } from 'lodash-es';
|
||||
import {
|
||||
GraphExecutionState,
|
||||
@ -10,17 +9,9 @@ import {
|
||||
CollectInvocationOutput,
|
||||
ImageField,
|
||||
LatentsOutput,
|
||||
ImageDTO,
|
||||
ResourceOrigin,
|
||||
} from 'services/api';
|
||||
|
||||
export const isUploadsImageDTO = (
|
||||
image: ImageDTO | undefined
|
||||
): image is UploadsImageDTO =>
|
||||
image !== undefined &&
|
||||
image.image_origin === 'external' &&
|
||||
image.image_category === 'user';
|
||||
|
||||
export const isImageOutput = (
|
||||
output: GraphExecutionState['results'][string]
|
||||
): output is ImageOutput => output.type === 'image_output';
|
||||
|
Loading…
Reference in New Issue
Block a user