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 { canvasPersistDenylist } from 'features/canvas/store/canvasPersistDenylist';
|
||||||
import { galleryPersistDenylist } from 'features/gallery/store/galleryPersistDenylist';
|
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 { lightboxPersistDenylist } from 'features/lightbox/store/lightboxPersistDenylist';
|
||||||
import { nodesPersistDenylist } from 'features/nodes/store/nodesPersistDenylist';
|
import { nodesPersistDenylist } from 'features/nodes/store/nodesPersistDenylist';
|
||||||
import { generationPersistDenylist } from 'features/parameters/store/generationPersistDenylist';
|
import { generationPersistDenylist } from 'features/parameters/store/generationPersistDenylist';
|
||||||
@ -22,11 +20,9 @@ const serializationDenylist: {
|
|||||||
models: modelsPersistDenylist,
|
models: modelsPersistDenylist,
|
||||||
nodes: nodesPersistDenylist,
|
nodes: nodesPersistDenylist,
|
||||||
postprocessing: postprocessingPersistDenylist,
|
postprocessing: postprocessingPersistDenylist,
|
||||||
results: resultsPersistDenylist,
|
|
||||||
system: systemPersistDenylist,
|
system: systemPersistDenylist,
|
||||||
// config: configPersistDenyList,
|
// config: configPersistDenyList,
|
||||||
ui: uiPersistDenylist,
|
ui: uiPersistDenylist,
|
||||||
uploads: uploadsPersistDenylist,
|
|
||||||
// hotkeys: hotkeysPersistDenylist,
|
// hotkeys: hotkeysPersistDenylist,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { initialCanvasState } from 'features/canvas/store/canvasSlice';
|
import { initialCanvasState } from 'features/canvas/store/canvasSlice';
|
||||||
import { initialGalleryState } from 'features/gallery/store/gallerySlice';
|
import { initialGalleryState } from 'features/gallery/store/gallerySlice';
|
||||||
import { initialResultsState } from 'features/gallery/store/resultsSlice';
|
import { initialImagesState } from 'features/gallery/store/imagesSlice';
|
||||||
import { initialUploadsState } from 'features/gallery/store/uploadsSlice';
|
|
||||||
import { initialLightboxState } from 'features/lightbox/store/lightboxSlice';
|
import { initialLightboxState } from 'features/lightbox/store/lightboxSlice';
|
||||||
import { initialNodesState } from 'features/nodes/store/nodesSlice';
|
import { initialNodesState } from 'features/nodes/store/nodesSlice';
|
||||||
import { initialGenerationState } from 'features/parameters/store/generationSlice';
|
import { initialGenerationState } from 'features/parameters/store/generationSlice';
|
||||||
@ -24,12 +23,11 @@ const initialStates: {
|
|||||||
models: initialModelsState,
|
models: initialModelsState,
|
||||||
nodes: initialNodesState,
|
nodes: initialNodesState,
|
||||||
postprocessing: initialPostprocessingState,
|
postprocessing: initialPostprocessingState,
|
||||||
results: initialResultsState,
|
|
||||||
system: initialSystemState,
|
system: initialSystemState,
|
||||||
config: initialConfigState,
|
config: initialConfigState,
|
||||||
ui: initialUIState,
|
ui: initialUIState,
|
||||||
uploads: initialUploadsState,
|
|
||||||
hotkeys: initialHotkeysState,
|
hotkeys: initialHotkeysState,
|
||||||
|
images: initialImagesState,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const unserialize: UnserializeFunction = (data, key) => {
|
export const unserialize: UnserializeFunction = (data, key) => {
|
||||||
|
@ -59,18 +59,15 @@ import {
|
|||||||
addSessionCanceledPendingListener,
|
addSessionCanceledPendingListener,
|
||||||
addSessionCanceledRejectedListener,
|
addSessionCanceledRejectedListener,
|
||||||
} from './listeners/sessionCanceled';
|
} from './listeners/sessionCanceled';
|
||||||
import {
|
|
||||||
addReceivedGalleryImagesFulfilledListener,
|
|
||||||
addReceivedGalleryImagesRejectedListener,
|
|
||||||
} from './listeners/receivedGalleryImages';
|
|
||||||
import {
|
|
||||||
addReceivedUploadImagesPageFulfilledListener,
|
|
||||||
addReceivedUploadImagesPageRejectedListener,
|
|
||||||
} from './listeners/receivedUploadImages';
|
|
||||||
import {
|
import {
|
||||||
addImageUpdatedFulfilledListener,
|
addImageUpdatedFulfilledListener,
|
||||||
addImageUpdatedRejectedListener,
|
addImageUpdatedRejectedListener,
|
||||||
} from './listeners/imageUpdated';
|
} from './listeners/imageUpdated';
|
||||||
|
import {
|
||||||
|
addReceivedPageOfImagesFulfilledListener,
|
||||||
|
addReceivedPageOfImagesRejectedListener,
|
||||||
|
} from './listeners/receivedPageOfImages';
|
||||||
|
import { addStagingAreaImageSavedListener } from './listeners/stagingAreaImageSaved';
|
||||||
|
|
||||||
export const listenerMiddleware = createListenerMiddleware();
|
export const listenerMiddleware = createListenerMiddleware();
|
||||||
|
|
||||||
@ -127,6 +124,7 @@ addCanvasSavedToGalleryListener();
|
|||||||
addCanvasDownloadedAsImageListener();
|
addCanvasDownloadedAsImageListener();
|
||||||
addCanvasCopiedToClipboardListener();
|
addCanvasCopiedToClipboardListener();
|
||||||
addCanvasMergedListener();
|
addCanvasMergedListener();
|
||||||
|
addStagingAreaImageSavedListener();
|
||||||
|
|
||||||
// socketio
|
// socketio
|
||||||
addGeneratorProgressListener();
|
addGeneratorProgressListener();
|
||||||
@ -154,8 +152,6 @@ addSessionCanceledPendingListener();
|
|||||||
addSessionCanceledFulfilledListener();
|
addSessionCanceledFulfilledListener();
|
||||||
addSessionCanceledRejectedListener();
|
addSessionCanceledRejectedListener();
|
||||||
|
|
||||||
// Gallery pages
|
// Images
|
||||||
addReceivedGalleryImagesFulfilledListener();
|
addReceivedPageOfImagesFulfilledListener();
|
||||||
addReceivedGalleryImagesRejectedListener();
|
addReceivedPageOfImagesRejectedListener();
|
||||||
addReceivedUploadImagesPageFulfilledListener();
|
|
||||||
addReceivedUploadImagesPageRejectedListener();
|
|
||||||
|
@ -5,7 +5,7 @@ import { imageUploaded } from 'services/thunks/image';
|
|||||||
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
import { getBaseLayerBlob } from 'features/canvas/util/getBaseLayerBlob';
|
||||||
import { addToast } from 'features/system/store/systemSlice';
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
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' });
|
const moduleLog = log.child({ namespace: 'canvasSavedToGalleryListener' });
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ export const addCanvasSavedToGalleryListener = () => {
|
|||||||
action.meta.arg.formData.file.name === filename
|
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 { clamp } from 'lodash-es';
|
||||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
||||||
import {
|
import {
|
||||||
uploadRemoved,
|
imageRemoved,
|
||||||
uploadsAdapter,
|
imagesAdapter,
|
||||||
} from 'features/gallery/store/uploadsSlice';
|
selectImagesEntities,
|
||||||
import {
|
selectImagesIds,
|
||||||
resultRemoved,
|
} from 'features/gallery/store/imagesSlice';
|
||||||
resultsAdapter,
|
|
||||||
} from 'features/gallery/store/resultsSlice';
|
|
||||||
import { isUploadsImageDTO } from 'services/types/guards';
|
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' });
|
const moduleLog = log.child({ namespace: 'addRequestedImageDeletionListener' });
|
||||||
|
|
||||||
@ -33,19 +30,16 @@ export const addRequestedImageDeletionListener = () => {
|
|||||||
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const selectedImage = state.gallery.selectedImage;
|
const selectedImage = state.gallery.selectedImage;
|
||||||
const isUserImage = isUploadsImageDTO(selectedImage);
|
|
||||||
if (selectedImage && selectedImage.image_name === image_name) {
|
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
|
const deletedImageIndex = ids.findIndex(
|
||||||
? state.uploads.entities
|
|
||||||
: state.results.entities;
|
|
||||||
|
|
||||||
const deletedImageIndex = allIds.findIndex(
|
|
||||||
(result) => result.toString() === image_name
|
(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(
|
const newSelectedImageIndex = clamp(
|
||||||
deletedImageIndex,
|
deletedImageIndex,
|
||||||
@ -55,7 +49,7 @@ export const addRequestedImageDeletionListener = () => {
|
|||||||
|
|
||||||
const newSelectedImageId = filteredIds[newSelectedImageIndex];
|
const newSelectedImageId = filteredIds[newSelectedImageIndex];
|
||||||
|
|
||||||
const newSelectedImage = allEntities[newSelectedImageId];
|
const newSelectedImage = entities[newSelectedImageId];
|
||||||
|
|
||||||
if (newSelectedImageId) {
|
if (newSelectedImageId) {
|
||||||
dispatch(imageSelected(newSelectedImage));
|
dispatch(imageSelected(newSelectedImage));
|
||||||
@ -64,11 +58,7 @@ export const addRequestedImageDeletionListener = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUserImage) {
|
dispatch(imageRemoved(image_name));
|
||||||
dispatch(uploadRemoved(image_name));
|
|
||||||
} else {
|
|
||||||
dispatch(resultRemoved(image_name));
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
imageDeleted({ imageName: image_name, imageOrigin: image_origin })
|
imageDeleted({ imageName: image_name, imageOrigin: image_origin })
|
||||||
@ -86,12 +76,7 @@ export const addImageDeletedPendingListener = () => {
|
|||||||
effect: (action, { dispatch, getState }) => {
|
effect: (action, { dispatch, getState }) => {
|
||||||
const { imageName, imageOrigin } = action.meta.arg;
|
const { imageName, imageOrigin } = action.meta.arg;
|
||||||
// Preemptively remove the image from the gallery
|
// Preemptively remove the image from the gallery
|
||||||
if (imageOrigin === 'external') {
|
imagesAdapter.removeOne(getState().images, imageName);
|
||||||
uploadsAdapter.removeOne(getState().uploads, imageName);
|
|
||||||
}
|
|
||||||
if (imageOrigin === 'internal') {
|
|
||||||
resultsAdapter.removeOne(getState().results, imageName);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
import { imageMetadataReceived } from 'services/thunks/image';
|
import { imageMetadataReceived } from 'services/thunks/image';
|
||||||
import { resultUpserted } from 'features/gallery/store/resultsSlice';
|
import { imageUpserted } from 'features/gallery/store/imagesSlice';
|
||||||
import { uploadUpserted } from 'features/gallery/store/uploadsSlice';
|
|
||||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'image' });
|
const moduleLog = log.child({ namespace: 'image' });
|
||||||
|
|
||||||
@ -11,16 +9,13 @@ export const addImageMetadataReceivedFulfilledListener = () => {
|
|||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: imageMetadataReceived.fulfilled,
|
actionCreator: imageMetadataReceived.fulfilled,
|
||||||
effect: (action, { getState, dispatch }) => {
|
effect: (action, { getState, dispatch }) => {
|
||||||
const imageDTO = action.payload;
|
const image = action.payload;
|
||||||
moduleLog.debug({ data: { imageDTO } }, 'Image metadata received');
|
if (image.is_intermediate) {
|
||||||
|
// No further actions needed for intermediate images
|
||||||
if (imageDTO.image_origin === 'internal') {
|
return;
|
||||||
dispatch(resultUpserted(imageDTO));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imageDTO.image_origin === 'external') {
|
|
||||||
dispatch(uploadUpserted(imageDTO));
|
|
||||||
}
|
}
|
||||||
|
moduleLog.debug({ data: { image } }, 'Image metadata received');
|
||||||
|
dispatch(imageUpserted(image));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
import { startAppListening } from '..';
|
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 { imageUploaded } from 'services/thunks/image';
|
||||||
import { addToast } from 'features/system/store/systemSlice';
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
import { resultUpserted } from 'features/gallery/store/resultsSlice';
|
|
||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
|
import { imageUpserted } from 'features/gallery/store/imagesSlice';
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'image' });
|
const moduleLog = log.child({ namespace: 'image' });
|
||||||
|
|
||||||
@ -26,18 +21,8 @@ export const addImageUploadedFulfilledListener = () => {
|
|||||||
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
|
||||||
// Handle uploads
|
dispatch(imageUpserted(image));
|
||||||
if (image.image_category === 'user' && !image.is_intermediate) {
|
dispatch(addToast({ title: 'Image Uploaded', status: 'success' }));
|
||||||
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'));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
import { imageUrlsReceived } from 'services/thunks/image';
|
import { imageUrlsReceived } from 'services/thunks/image';
|
||||||
import { resultsAdapter } from 'features/gallery/store/resultsSlice';
|
import { imagesAdapter } from 'features/gallery/store/imagesSlice';
|
||||||
import { uploadsAdapter } from 'features/gallery/store/uploadsSlice';
|
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'image' });
|
const moduleLog = log.child({ namespace: 'image' });
|
||||||
|
|
||||||
@ -13,27 +12,15 @@ export const addImageUrlsReceivedFulfilledListener = () => {
|
|||||||
const image = action.payload;
|
const image = action.payload;
|
||||||
moduleLog.debug({ data: { image } }, 'Image URLs received');
|
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') {
|
imagesAdapter.updateOne(getState().images, {
|
||||||
resultsAdapter.updateOne(getState().results, {
|
id: image_name,
|
||||||
id: image_name,
|
changes: {
|
||||||
changes: {
|
image_url,
|
||||||
image_url,
|
thumbnail_url,
|
||||||
thumbnail_url,
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (image_origin === 'uploads') {
|
|
||||||
uploadsAdapter.updateOne(getState().uploads, {
|
|
||||||
id: image_name,
|
|
||||||
changes: {
|
|
||||||
image_url,
|
|
||||||
thumbnail_url,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import { initialImageChanged } from 'features/parameters/store/generationSlice';
|
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 { t } from 'i18next';
|
||||||
import { addToast } from 'features/system/store/systemSlice';
|
import { addToast } from 'features/system/store/systemSlice';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
@ -9,7 +7,7 @@ import {
|
|||||||
isImageDTO,
|
isImageDTO,
|
||||||
} from 'features/parameters/store/actions';
|
} from 'features/parameters/store/actions';
|
||||||
import { makeToast } from 'app/components/Toaster';
|
import { makeToast } from 'app/components/Toaster';
|
||||||
import { ImageDTO } from 'services/api';
|
import { selectImagesById } from 'features/gallery/store/imagesSlice';
|
||||||
|
|
||||||
export const addInitialImageSelectedListener = () => {
|
export const addInitialImageSelectedListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
@ -30,16 +28,8 @@ export const addInitialImageSelectedListener = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { image_name, image_origin } = action.payload;
|
const imageName = action.payload;
|
||||||
|
const image = selectImagesById(getState(), imageName);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!image) {
|
if (!image) {
|
||||||
dispatch(
|
dispatch(
|
||||||
|
@ -1,31 +1,31 @@
|
|||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import { startAppListening } from '..';
|
import { startAppListening } from '..';
|
||||||
import { receivedGalleryImages } from 'services/thunks/gallery';
|
|
||||||
import { serializeError } from 'serialize-error';
|
import { serializeError } from 'serialize-error';
|
||||||
|
import { receivedPageOfImages } from 'services/thunks/image';
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'gallery' });
|
const moduleLog = log.child({ namespace: 'gallery' });
|
||||||
|
|
||||||
export const addReceivedGalleryImagesFulfilledListener = () => {
|
export const addReceivedPageOfImagesFulfilledListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: receivedGalleryImages.fulfilled,
|
actionCreator: receivedPageOfImages.fulfilled,
|
||||||
effect: (action, { getState, dispatch }) => {
|
effect: (action, { getState, dispatch }) => {
|
||||||
const page = action.payload;
|
const page = action.payload;
|
||||||
moduleLog.debug(
|
moduleLog.debug(
|
||||||
{ data: { page } },
|
{ data: { page } },
|
||||||
`Received ${page.items.length} gallery images`
|
`Received ${page.items.length} images`
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addReceivedGalleryImagesRejectedListener = () => {
|
export const addReceivedPageOfImagesRejectedListener = () => {
|
||||||
startAppListening({
|
startAppListening({
|
||||||
actionCreator: receivedGalleryImages.rejected,
|
actionCreator: receivedPageOfImages.rejected,
|
||||||
effect: (action, { getState, dispatch }) => {
|
effect: (action, { getState, dispatch }) => {
|
||||||
if (action.payload) {
|
if (action.payload) {
|
||||||
moduleLog.debug(
|
moduleLog.debug(
|
||||||
{ data: { error: serializeError(action.payload.error) } },
|
{ data: { error: serializeError(action.payload) } },
|
||||||
'Problem receiving gallery images'
|
'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 { sessionCanceled } from 'services/thunks/session';
|
||||||
import { isImageOutput } from 'services/types/guards';
|
import { isImageOutput } from 'services/types/guards';
|
||||||
import { progressImageSet } from 'features/system/store/systemSlice';
|
import { progressImageSet } from 'features/system/store/systemSlice';
|
||||||
import { imageSelected } from 'features/gallery/store/gallerySlice';
|
|
||||||
|
|
||||||
const moduleLog = log.child({ namespace: 'socketio' });
|
const moduleLog = log.child({ namespace: 'socketio' });
|
||||||
const nodeDenylist = ['dataURL_image'];
|
const nodeDenylist = ['dataURL_image'];
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
import { startAppListening } from '../..';
|
import { startAppListening } from '../..';
|
||||||
import { log } from 'app/logging/useLogger';
|
import { log } from 'app/logging/useLogger';
|
||||||
import { socketConnected } from 'services/events/actions';
|
import { socketConnected } from 'services/events/actions';
|
||||||
import {
|
import { receivedPageOfImages } from 'services/thunks/image';
|
||||||
receivedGalleryImages,
|
|
||||||
receivedUploadImages,
|
|
||||||
} from 'services/thunks/gallery';
|
|
||||||
import { receivedModels } from 'services/thunks/model';
|
import { receivedModels } from 'services/thunks/model';
|
||||||
import { receivedOpenAPISchema } from 'services/thunks/schema';
|
import { receivedOpenAPISchema } from 'services/thunks/schema';
|
||||||
|
|
||||||
@ -18,17 +15,12 @@ export const addSocketConnectedListener = () => {
|
|||||||
|
|
||||||
moduleLog.debug({ timestamp }, 'Connected');
|
moduleLog.debug({ timestamp }, 'Connected');
|
||||||
|
|
||||||
const { results, uploads, models, nodes, config } = getState();
|
const { models, nodes, config, images } = getState();
|
||||||
|
|
||||||
const { disabledTabs } = config;
|
const { disabledTabs } = config;
|
||||||
|
|
||||||
// These thunks need to be dispatch in middleware; cannot handle in a reducer
|
if (!images.ids.length) {
|
||||||
if (!results.ids.length) {
|
dispatch(receivedPageOfImages());
|
||||||
dispatch(receivedGalleryImages());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!uploads.ids.length) {
|
|
||||||
dispatch(receivedUploadImages());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!models.ids.length) {
|
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 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 imagesReducer from 'features/gallery/store/imagesSlice';
|
||||||
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';
|
||||||
@ -41,12 +40,11 @@ const allReducers = {
|
|||||||
models: modelsReducer,
|
models: modelsReducer,
|
||||||
nodes: nodesReducer,
|
nodes: nodesReducer,
|
||||||
postprocessing: postprocessingReducer,
|
postprocessing: postprocessingReducer,
|
||||||
results: resultsReducer,
|
|
||||||
system: systemReducer,
|
system: systemReducer,
|
||||||
config: configReducer,
|
config: configReducer,
|
||||||
ui: uiReducer,
|
ui: uiReducer,
|
||||||
uploads: uploadsReducer,
|
|
||||||
hotkeys: hotkeysReducer,
|
hotkeys: hotkeysReducer,
|
||||||
|
images: imagesReducer,
|
||||||
// session: sessionReducer,
|
// session: sessionReducer,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -65,8 +63,6 @@ const rememberedKeys: (keyof typeof allReducers)[] = [
|
|||||||
'system',
|
'system',
|
||||||
'ui',
|
'ui',
|
||||||
// 'hotkeys',
|
// 'hotkeys',
|
||||||
// 'results',
|
|
||||||
// 'uploads',
|
|
||||||
// 'config',
|
// 'config',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { ButtonGroup, Flex } from '@chakra-ui/react';
|
import { ButtonGroup, Flex } from '@chakra-ui/react';
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
// import { saveStagingAreaImageToGallery } from 'app/socketio/actions';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIIconButton from 'common/components/IAIIconButton';
|
import IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
@ -26,6 +25,7 @@ import {
|
|||||||
FaPlus,
|
FaPlus,
|
||||||
FaSave,
|
FaSave,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
|
import { stagingAreaImageSaved } from '../store/actions';
|
||||||
|
|
||||||
const selector = createSelector(
|
const selector = createSelector(
|
||||||
[canvasSelector],
|
[canvasSelector],
|
||||||
@ -157,19 +157,15 @@ const IAICanvasStagingAreaToolbar = () => {
|
|||||||
}
|
}
|
||||||
colorScheme="accent"
|
colorScheme="accent"
|
||||||
/>
|
/>
|
||||||
{/* <IAIIconButton
|
<IAIIconButton
|
||||||
tooltip={t('unifiedCanvas.saveToGallery')}
|
tooltip={t('unifiedCanvas.saveToGallery')}
|
||||||
aria-label={t('unifiedCanvas.saveToGallery')}
|
aria-label={t('unifiedCanvas.saveToGallery')}
|
||||||
icon={<FaSave />}
|
icon={<FaSave />}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
dispatch(
|
dispatch(stagingAreaImageSaved(currentStagingAreaImage.image))
|
||||||
saveStagingAreaImageToGallery(
|
|
||||||
currentStagingAreaImage.image.image_url
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
colorScheme="accent"
|
colorScheme="accent"
|
||||||
/> */}
|
/>
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
tooltip={t('unifiedCanvas.discardAll')}
|
tooltip={t('unifiedCanvas.discardAll')}
|
||||||
aria-label={t('unifiedCanvas.discardAll')}
|
aria-label={t('unifiedCanvas.discardAll')}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
import { ImageDTO } from 'services/api';
|
||||||
|
|
||||||
export const canvasSavedToGallery = createAction('canvas/canvasSavedToGallery');
|
export const canvasSavedToGallery = createAction('canvas/canvasSavedToGallery');
|
||||||
|
|
||||||
@ -11,3 +12,7 @@ export const canvasDownloadedAsImage = createAction(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const canvasMerged = createAction('canvas/canvasMerged');
|
export const canvasMerged = createAction('canvas/canvasMerged');
|
||||||
|
|
||||||
|
export const stagingAreaImageSaved = createAction<ImageDTO>(
|
||||||
|
'canvas/stagingAreaImageSaved'
|
||||||
|
);
|
||||||
|
@ -62,7 +62,6 @@ const CurrentImagePreview = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
e.dataTransfer.setData('invokeai/imageName', image.image_name);
|
e.dataTransfer.setData('invokeai/imageName', image.image_name);
|
||||||
e.dataTransfer.setData('invokeai/imageOrigin', image.image_origin);
|
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
},
|
},
|
||||||
[image]
|
[image]
|
||||||
|
@ -147,7 +147,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
|||||||
const handleDragStart = useCallback(
|
const handleDragStart = useCallback(
|
||||||
(e: DragEvent<HTMLDivElement>) => {
|
(e: DragEvent<HTMLDivElement>) => {
|
||||||
e.dataTransfer.setData('invokeai/imageName', image.image_name);
|
e.dataTransfer.setData('invokeai/imageName', image.image_name);
|
||||||
e.dataTransfer.setData('invokeai/imageOrigin', image.image_origin);
|
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
},
|
},
|
||||||
[image]
|
[image]
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
ButtonGroup,
|
Checkbox,
|
||||||
|
CheckboxGroup,
|
||||||
Flex,
|
Flex,
|
||||||
FlexProps,
|
FlexProps,
|
||||||
Grid,
|
Grid,
|
||||||
@ -16,7 +17,6 @@ import IAIPopover from 'common/components/IAIPopover';
|
|||||||
import IAISlider from 'common/components/IAISlider';
|
import IAISlider from 'common/components/IAISlider';
|
||||||
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
|
import { gallerySelector } from 'features/gallery/store/gallerySelectors';
|
||||||
import {
|
import {
|
||||||
setCurrentCategory,
|
|
||||||
setGalleryImageMinimumWidth,
|
setGalleryImageMinimumWidth,
|
||||||
setGalleryImageObjectFit,
|
setGalleryImageObjectFit,
|
||||||
setShouldAutoSwitchToNewImages,
|
setShouldAutoSwitchToNewImages,
|
||||||
@ -36,54 +36,53 @@ import {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
|
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 { MdPhotoLibrary } from 'react-icons/md';
|
||||||
import HoverableImage from './HoverableImage';
|
import HoverableImage from './HoverableImage';
|
||||||
|
|
||||||
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
|
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 { createSelector } from '@reduxjs/toolkit';
|
||||||
import { RootState } from 'app/store/store';
|
import { RootState } from 'app/store/store';
|
||||||
import { Virtuoso, VirtuosoGrid } from 'react-virtuoso';
|
import { Virtuoso, VirtuosoGrid } from 'react-virtuoso';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import GalleryProgressImage from './GalleryProgressImage';
|
import GalleryProgressImage from './GalleryProgressImage';
|
||||||
import { uiSelector } from 'features/ui/store/uiSelectors';
|
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 PROGRESS_IMAGE_PLACEHOLDER = 'PROGRESS_IMAGE_PLACEHOLDER';
|
||||||
|
const IMAGE_CATEGORIES: ImageCategory[] = [
|
||||||
|
'general',
|
||||||
|
'control',
|
||||||
|
'mask',
|
||||||
|
'user',
|
||||||
|
'other',
|
||||||
|
];
|
||||||
|
|
||||||
const categorySelector = createSelector(
|
const categorySelector = createSelector(
|
||||||
[(state: RootState) => state],
|
[(state: RootState) => state],
|
||||||
(state) => {
|
(state) => {
|
||||||
const { results, uploads, system, gallery } = state;
|
const { system, images } = state;
|
||||||
const { currentCategory } = gallery;
|
const tempImages: (ImageDTO | typeof PROGRESS_IMAGE_PLACEHOLDER)[] = [];
|
||||||
|
|
||||||
if (currentCategory === 'results') {
|
if (system.progressImage) {
|
||||||
const tempImages: (ImageDTO | typeof PROGRESS_IMAGE_PLACEHOLDER)[] = [];
|
tempImages.push(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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { categories } = images;
|
||||||
|
|
||||||
|
const allImages = selectImagesAll(state);
|
||||||
|
const filteredImages = allImages.filter((i) =>
|
||||||
|
categories.includes(i.image_category)
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
images: uploadsAdapter.getSelectors().selectAll(uploads),
|
images: tempImages.concat(filteredImages),
|
||||||
isLoading: uploads.isLoading,
|
isLoading: images.isLoading,
|
||||||
areMoreImagesAvailable: uploads.page < uploads.pages - 1,
|
areMoreImagesAvailable: filteredImages.length < images.total,
|
||||||
|
categories: images.categories,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
defaultSelectorOptions
|
defaultSelectorOptions
|
||||||
@ -93,7 +92,6 @@ const mainSelector = createSelector(
|
|||||||
[gallerySelector, uiSelector],
|
[gallerySelector, uiSelector],
|
||||||
(gallery, ui) => {
|
(gallery, ui) => {
|
||||||
const {
|
const {
|
||||||
currentCategory,
|
|
||||||
galleryImageMinimumWidth,
|
galleryImageMinimumWidth,
|
||||||
galleryImageObjectFit,
|
galleryImageObjectFit,
|
||||||
shouldAutoSwitchToNewImages,
|
shouldAutoSwitchToNewImages,
|
||||||
@ -104,7 +102,6 @@ const mainSelector = createSelector(
|
|||||||
const { shouldPinGallery } = ui;
|
const { shouldPinGallery } = ui;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentCategory,
|
|
||||||
shouldPinGallery,
|
shouldPinGallery,
|
||||||
galleryImageMinimumWidth,
|
galleryImageMinimumWidth,
|
||||||
galleryImageObjectFit,
|
galleryImageObjectFit,
|
||||||
@ -120,7 +117,6 @@ const ImageGalleryContent = () => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const resizeObserverRef = useRef<HTMLDivElement>(null);
|
const resizeObserverRef = useRef<HTMLDivElement>(null);
|
||||||
const [shouldShouldIconButtons, setShouldShouldIconButtons] = useState(true);
|
|
||||||
const rootRef = useRef(null);
|
const rootRef = useRef(null);
|
||||||
const [scroller, setScroller] = useState<HTMLElement | null>(null);
|
const [scroller, setScroller] = useState<HTMLElement | null>(null);
|
||||||
const [initialize, osInstance] = useOverlayScrollbars({
|
const [initialize, osInstance] = useOverlayScrollbars({
|
||||||
@ -137,7 +133,6 @@ const ImageGalleryContent = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
currentCategory,
|
|
||||||
shouldPinGallery,
|
shouldPinGallery,
|
||||||
galleryImageMinimumWidth,
|
galleryImageMinimumWidth,
|
||||||
galleryImageObjectFit,
|
galleryImageObjectFit,
|
||||||
@ -146,18 +141,12 @@ const ImageGalleryContent = () => {
|
|||||||
selectedImage,
|
selectedImage,
|
||||||
} = useAppSelector(mainSelector);
|
} = useAppSelector(mainSelector);
|
||||||
|
|
||||||
const { images, areMoreImagesAvailable, isLoading } =
|
const { images, areMoreImagesAvailable, isLoading, categories } =
|
||||||
useAppSelector(categorySelector);
|
useAppSelector(categorySelector);
|
||||||
|
|
||||||
const handleClickLoadMore = () => {
|
const handleLoadMoreImages = useCallback(() => {
|
||||||
if (currentCategory === 'results') {
|
dispatch(receivedPageOfImages());
|
||||||
dispatch(receivedGalleryImages());
|
}, [dispatch]);
|
||||||
}
|
|
||||||
|
|
||||||
if (currentCategory === 'uploads') {
|
|
||||||
dispatch(receivedUploadImages());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeGalleryImageMinimumWidth = (v: number) => {
|
const handleChangeGalleryImageMinimumWidth = (v: number) => {
|
||||||
dispatch(setGalleryImageMinimumWidth(v));
|
dispatch(setGalleryImageMinimumWidth(v));
|
||||||
@ -168,28 +157,6 @@ const ImageGalleryContent = () => {
|
|||||||
dispatch(requestCanvasRescale());
|
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(() => {
|
useEffect(() => {
|
||||||
const { current: root } = rootRef;
|
const { current: root } = rootRef;
|
||||||
if (scroller && root) {
|
if (scroller && root) {
|
||||||
@ -210,12 +177,15 @@ const ImageGalleryContent = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleEndReached = useCallback(() => {
|
const handleEndReached = useCallback(() => {
|
||||||
if (currentCategory === 'results') {
|
handleLoadMoreImages();
|
||||||
dispatch(receivedGalleryImages());
|
}, [handleLoadMoreImages]);
|
||||||
} else if (currentCategory === 'uploads') {
|
|
||||||
dispatch(receivedUploadImages());
|
const handleCategoriesChanged = useCallback(
|
||||||
}
|
(newCategories: ImageCategory[]) => {
|
||||||
}, [dispatch, currentCategory]);
|
dispatch(imageCategoriesChanged(newCategories));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
@ -232,52 +202,28 @@ const ImageGalleryContent = () => {
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
>
|
>
|
||||||
<ButtonGroup
|
<IAIPopover
|
||||||
size="sm"
|
triggerComponent={
|
||||||
isAttached
|
<IAIIconButton
|
||||||
w="max-content"
|
aria-label="Gallery Filter"
|
||||||
justifyContent="stretch"
|
size="sm"
|
||||||
|
icon={<FaFilter />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{shouldShouldIconButtons ? (
|
<Flex sx={{ flexDirection: 'column', gap: 2 }}>
|
||||||
<>
|
<CheckboxGroup
|
||||||
<IAIIconButton
|
value={categories}
|
||||||
aria-label={t('gallery.showGenerations')}
|
onChange={handleCategoriesChanged}
|
||||||
tooltip={t('gallery.showGenerations')}
|
>
|
||||||
isChecked={currentCategory === 'results'}
|
{IMAGE_CATEGORIES.map((c) => (
|
||||||
role="radio"
|
<Checkbox key={c} value={c}>
|
||||||
icon={<FaImage />}
|
{capitalize(c)}
|
||||||
onClick={() => dispatch(setCurrentCategory('results'))}
|
</Checkbox>
|
||||||
/>
|
))}
|
||||||
<IAIIconButton
|
</CheckboxGroup>
|
||||||
aria-label={t('gallery.showUploads')}
|
</Flex>
|
||||||
tooltip={t('gallery.showUploads')}
|
</IAIPopover>
|
||||||
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 gap={2}>
|
<Flex gap={2}>
|
||||||
<IAIPopover
|
<IAIPopover
|
||||||
@ -400,7 +346,7 @@ const ImageGalleryContent = () => {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<IAIButton
|
<IAIButton
|
||||||
onClick={handleClickLoadMore}
|
onClick={handleLoadMoreImages}
|
||||||
isDisabled={!areMoreImagesAvailable}
|
isDisabled={!areMoreImagesAvailable}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
loadingText="Loading"
|
loadingText="Loading"
|
||||||
|
@ -9,6 +9,10 @@ import { gallerySelector } from '../store/gallerySelectors';
|
|||||||
import { RootState } from 'app/store/store';
|
import { RootState } from 'app/store/store';
|
||||||
import { imageSelected } from '../store/gallerySlice';
|
import { imageSelected } from '../store/gallerySlice';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import {
|
||||||
|
selectFilteredImagesAsObject,
|
||||||
|
selectFilteredImagesIds,
|
||||||
|
} from '../store/imagesSlice';
|
||||||
|
|
||||||
const nextPrevButtonTriggerAreaStyles: ChakraProps['sx'] = {
|
const nextPrevButtonTriggerAreaStyles: ChakraProps['sx'] = {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@ -21,9 +25,14 @@ const nextPrevButtonStyles: ChakraProps['sx'] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const nextPrevImageButtonsSelector = createSelector(
|
export const nextPrevImageButtonsSelector = createSelector(
|
||||||
[(state: RootState) => state, gallerySelector],
|
[
|
||||||
(state, gallery) => {
|
(state: RootState) => state,
|
||||||
const { selectedImage, currentCategory } = gallery;
|
gallerySelector,
|
||||||
|
selectFilteredImagesAsObject,
|
||||||
|
selectFilteredImagesIds,
|
||||||
|
],
|
||||||
|
(state, gallery, filteredImagesAsObject, filteredImageIds) => {
|
||||||
|
const { selectedImage } = gallery;
|
||||||
|
|
||||||
if (!selectedImage) {
|
if (!selectedImage) {
|
||||||
return {
|
return {
|
||||||
@ -32,29 +41,29 @@ export const nextPrevImageButtonsSelector = createSelector(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentImageIndex = state[currentCategory].ids.findIndex(
|
const currentImageIndex = filteredImageIds.findIndex(
|
||||||
(i) => i === selectedImage.image_name
|
(i) => i === selectedImage.image_name
|
||||||
);
|
);
|
||||||
|
|
||||||
const nextImageIndex = clamp(
|
const nextImageIndex = clamp(
|
||||||
currentImageIndex + 1,
|
currentImageIndex + 1,
|
||||||
0,
|
0,
|
||||||
state[currentCategory].ids.length - 1
|
filteredImageIds.length - 1
|
||||||
);
|
);
|
||||||
|
|
||||||
const prevImageIndex = clamp(
|
const prevImageIndex = clamp(
|
||||||
currentImageIndex - 1,
|
currentImageIndex - 1,
|
||||||
0,
|
0,
|
||||||
state[currentCategory].ids.length - 1
|
filteredImageIds.length - 1
|
||||||
);
|
);
|
||||||
|
|
||||||
const nextImageId = state[currentCategory].ids[nextImageIndex];
|
const nextImageId = filteredImageIds[nextImageIndex];
|
||||||
const prevImageId = state[currentCategory].ids[prevImageIndex];
|
const prevImageId = filteredImageIds[prevImageIndex];
|
||||||
|
|
||||||
const nextImage = state[currentCategory].entities[nextImageId];
|
const nextImage = filteredImagesAsObject[nextImageId];
|
||||||
const prevImage = state[currentCategory].entities[prevImageId];
|
const prevImage = filteredImagesAsObject[prevImageId];
|
||||||
|
|
||||||
const imagesLength = state[currentCategory].ids.length;
|
const imagesLength = filteredImageIds.length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isOnFirstImage: currentImageIndex === 0,
|
isOnFirstImage: currentImageIndex === 0,
|
||||||
|
@ -1,33 +1,18 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { ResourceOrigin } from 'services/api';
|
import { selectImagesEntities } from '../store/imagesSlice';
|
||||||
import { selectResultsEntities } from '../store/resultsSlice';
|
import { useCallback } from 'react';
|
||||||
import { selectUploadsEntities } from '../store/uploadsSlice';
|
|
||||||
|
|
||||||
const useGetImageByNameSelector = createSelector(
|
const useGetImageByName = () => {
|
||||||
[selectResultsEntities, selectUploadsEntities],
|
const images = useAppSelector(selectImagesEntities);
|
||||||
(allResults, allUploads) => {
|
return useCallback(
|
||||||
return { allResults, allUploads };
|
(name: string | undefined) => {
|
||||||
}
|
if (!name) {
|
||||||
);
|
return;
|
||||||
|
|
||||||
const useGetImageByNameAndOrigin = () => {
|
|
||||||
const { allResults, allUploads } = useAppSelector(useGetImageByNameSelector);
|
|
||||||
return (name: string, origin: ResourceOrigin) => {
|
|
||||||
if (origin === 'internal') {
|
|
||||||
const resultImagesResult = allResults[name];
|
|
||||||
if (resultImagesResult) {
|
|
||||||
return resultImagesResult;
|
|
||||||
}
|
}
|
||||||
}
|
return images[name];
|
||||||
|
},
|
||||||
if (origin === 'external') {
|
[images]
|
||||||
const userImagesResult = allUploads[name];
|
);
|
||||||
if (userImagesResult) {
|
|
||||||
return userImagesResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useGetImageByNameAndOrigin;
|
export default useGetImageByName;
|
||||||
|
@ -4,6 +4,5 @@ import { GalleryState } from './gallerySlice';
|
|||||||
* Gallery slice persist denylist
|
* Gallery slice persist denylist
|
||||||
*/
|
*/
|
||||||
export const galleryPersistDenylist: (keyof GalleryState)[] = [
|
export const galleryPersistDenylist: (keyof GalleryState)[] = [
|
||||||
'currentCategory',
|
|
||||||
'shouldAutoSwitchToNewImages',
|
'shouldAutoSwitchToNewImages',
|
||||||
];
|
];
|
||||||
|
@ -1,12 +1,6 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import {
|
|
||||||
receivedGalleryImages,
|
|
||||||
receivedUploadImages,
|
|
||||||
} from '../../../services/thunks/gallery';
|
|
||||||
import { ImageDTO } from 'services/api';
|
import { ImageDTO } from 'services/api';
|
||||||
import { resultUpserted } from './resultsSlice';
|
|
||||||
import { uploadUpserted } from './uploadsSlice';
|
|
||||||
|
|
||||||
type GalleryImageObjectFitType = 'contain' | 'cover';
|
type GalleryImageObjectFitType = 'contain' | 'cover';
|
||||||
|
|
||||||
@ -16,7 +10,6 @@ export interface GalleryState {
|
|||||||
galleryImageObjectFit: GalleryImageObjectFitType;
|
galleryImageObjectFit: GalleryImageObjectFitType;
|
||||||
shouldAutoSwitchToNewImages: boolean;
|
shouldAutoSwitchToNewImages: boolean;
|
||||||
shouldUseSingleGalleryColumn: boolean;
|
shouldUseSingleGalleryColumn: boolean;
|
||||||
currentCategory: 'results' | 'uploads';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initialGalleryState: GalleryState = {
|
export const initialGalleryState: GalleryState = {
|
||||||
@ -24,7 +17,6 @@ export const initialGalleryState: GalleryState = {
|
|||||||
galleryImageObjectFit: 'cover',
|
galleryImageObjectFit: 'cover',
|
||||||
shouldAutoSwitchToNewImages: true,
|
shouldAutoSwitchToNewImages: true,
|
||||||
shouldUseSingleGalleryColumn: false,
|
shouldUseSingleGalleryColumn: false,
|
||||||
currentCategory: 'results',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gallerySlice = createSlice({
|
export const gallerySlice = createSlice({
|
||||||
@ -48,12 +40,6 @@ export const gallerySlice = createSlice({
|
|||||||
setShouldAutoSwitchToNewImages: (state, action: PayloadAction<boolean>) => {
|
setShouldAutoSwitchToNewImages: (state, action: PayloadAction<boolean>) => {
|
||||||
state.shouldAutoSwitchToNewImages = action.payload;
|
state.shouldAutoSwitchToNewImages = action.payload;
|
||||||
},
|
},
|
||||||
setCurrentCategory: (
|
|
||||||
state,
|
|
||||||
action: PayloadAction<'results' | 'uploads'>
|
|
||||||
) => {
|
|
||||||
state.currentCategory = action.payload;
|
|
||||||
},
|
|
||||||
setShouldUseSingleGalleryColumn: (
|
setShouldUseSingleGalleryColumn: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<boolean>
|
action: PayloadAction<boolean>
|
||||||
@ -61,55 +47,6 @@ export const gallerySlice = createSlice({
|
|||||||
state.shouldUseSingleGalleryColumn = action.payload;
|
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 {
|
export const {
|
||||||
@ -118,7 +55,6 @@ export const {
|
|||||||
setGalleryImageObjectFit,
|
setGalleryImageObjectFit,
|
||||||
setShouldAutoSwitchToNewImages,
|
setShouldAutoSwitchToNewImages,
|
||||||
setShouldUseSingleGalleryColumn,
|
setShouldUseSingleGalleryColumn,
|
||||||
setCurrentCategory,
|
|
||||||
} = gallerySlice.actions;
|
} = gallerySlice.actions;
|
||||||
|
|
||||||
export default gallerySlice.reducer;
|
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 { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder';
|
import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder';
|
||||||
import { useGetUrl } from 'common/util/getUrl';
|
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 { fieldValueChanged } from 'features/nodes/store/nodesSlice';
|
||||||
import {
|
import {
|
||||||
@ -11,7 +11,6 @@ import {
|
|||||||
} from 'features/nodes/types/types';
|
} from 'features/nodes/types/types';
|
||||||
import { DragEvent, memo, useCallback, useState } from 'react';
|
import { DragEvent, memo, useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { ResourceOrigin } from 'services/api';
|
|
||||||
import { FieldComponentProps } from './types';
|
import { FieldComponentProps } from './types';
|
||||||
|
|
||||||
const ImageInputFieldComponent = (
|
const ImageInputFieldComponent = (
|
||||||
@ -19,7 +18,7 @@ const ImageInputFieldComponent = (
|
|||||||
) => {
|
) => {
|
||||||
const { nodeId, field } = props;
|
const { nodeId, field } = props;
|
||||||
|
|
||||||
const getImageByNameAndType = useGetImageByNameAndOrigin();
|
const getImageByName = useGetImageByName();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [url, setUrl] = useState<string | undefined>(field.value?.image_url);
|
const [url, setUrl] = useState<string | undefined>(field.value?.image_url);
|
||||||
const { getUrl } = useGetUrl();
|
const { getUrl } = useGetUrl();
|
||||||
@ -27,15 +26,7 @@ const ImageInputFieldComponent = (
|
|||||||
const handleDrop = useCallback(
|
const handleDrop = useCallback(
|
||||||
(e: DragEvent<HTMLDivElement>) => {
|
(e: DragEvent<HTMLDivElement>) => {
|
||||||
const name = e.dataTransfer.getData('invokeai/imageName');
|
const name = e.dataTransfer.getData('invokeai/imageName');
|
||||||
const type = e.dataTransfer.getData(
|
const image = getImageByName(name);
|
||||||
'invokeai/imageOrigin'
|
|
||||||
) as ResourceOrigin;
|
|
||||||
|
|
||||||
if (!name || !type) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const image = getImageByNameAndType(name, type);
|
|
||||||
|
|
||||||
if (!image) {
|
if (!image) {
|
||||||
return;
|
return;
|
||||||
@ -51,7 +42,7 @@ const ImageInputFieldComponent = (
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[getImageByNameAndType, dispatch, field.name, nodeId]
|
[getImageByName, dispatch, field.name, nodeId]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -26,18 +26,21 @@ const buildBaseNode = (
|
|||||||
| ImageToImageInvocation
|
| ImageToImageInvocation
|
||||||
| InpaintInvocation
|
| InpaintInvocation
|
||||||
| undefined => {
|
| undefined => {
|
||||||
const dimensionsOverride = state.canvas.boundingBoxDimensions;
|
const overrides = {
|
||||||
|
...state.canvas.boundingBoxDimensions,
|
||||||
|
is_intermediate: true,
|
||||||
|
};
|
||||||
|
|
||||||
if (nodeType === 'txt2img') {
|
if (nodeType === 'txt2img') {
|
||||||
return buildTxt2ImgNode(state, dimensionsOverride);
|
return buildTxt2ImgNode(state, overrides);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nodeType === 'img2img') {
|
if (nodeType === 'img2img') {
|
||||||
return buildImg2ImgNode(state, dimensionsOverride);
|
return buildImg2ImgNode(state, overrides);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nodeType === 'inpaint' || nodeType === 'outpaint') {
|
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 { clearInitialImage } from 'features/parameters/store/generationSlice';
|
||||||
import { DragEvent, useCallback } from 'react';
|
import { DragEvent, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ResourceOrigin } from 'services/api';
|
|
||||||
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
|
||||||
import { generationSelector } from 'features/parameters/store/generationSelectors';
|
import { generationSelector } from 'features/parameters/store/generationSelectors';
|
||||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||||
@ -55,11 +54,7 @@ const InitialImagePreview = () => {
|
|||||||
const handleDrop = useCallback(
|
const handleDrop = useCallback(
|
||||||
(e: DragEvent<HTMLDivElement>) => {
|
(e: DragEvent<HTMLDivElement>) => {
|
||||||
const name = e.dataTransfer.getData('invokeai/imageName');
|
const name = e.dataTransfer.getData('invokeai/imageName');
|
||||||
const type = e.dataTransfer.getData(
|
dispatch(initialImageSelected(name));
|
||||||
'invokeai/imageOrigin'
|
|
||||||
) as ResourceOrigin;
|
|
||||||
|
|
||||||
dispatch(initialImageSelected({ image_name: name, image_origin: type }));
|
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
@ -88,7 +88,7 @@ export const useParameters = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(initialImageSelected(image));
|
dispatch(initialImageSelected(image.image_name));
|
||||||
toaster({
|
toaster({
|
||||||
title: t('toast.initialImageSet'),
|
title: t('toast.initialImageSet'),
|
||||||
status: 'info',
|
status: 'info',
|
||||||
|
@ -26,6 +26,6 @@ export const isImageDTO = (image: any): image is ImageDTO => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initialImageSelected = createAction<
|
export const initialImageSelected = createAction<ImageDTO | string | undefined>(
|
||||||
ImageDTO | ImageNameAndOrigin | undefined
|
'generation/initialImageSelected'
|
||||||
>('generation/initialImageSelected');
|
);
|
||||||
|
@ -1,34 +1,3 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
|
||||||
import { RootState } from 'app/store/store';
|
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 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 { createAppAsyncThunk } from 'app/store/storeUtils';
|
||||||
import { InvokeTabName } from 'features/ui/store/tabMap';
|
import { selectImagesAll } from 'features/gallery/store/imagesSlice';
|
||||||
import { ImagesService } from 'services/api';
|
import { ImagesService } from 'services/api';
|
||||||
|
|
||||||
type imageUrlsReceivedArg = Parameters<
|
type imageUrlsReceivedArg = Parameters<
|
||||||
@ -71,3 +71,32 @@ export const imageUpdated = createAppAsyncThunk(
|
|||||||
return response;
|
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 { get, isObject, isString } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
GraphExecutionState,
|
GraphExecutionState,
|
||||||
@ -10,17 +9,9 @@ import {
|
|||||||
CollectInvocationOutput,
|
CollectInvocationOutput,
|
||||||
ImageField,
|
ImageField,
|
||||||
LatentsOutput,
|
LatentsOutput,
|
||||||
ImageDTO,
|
|
||||||
ResourceOrigin,
|
ResourceOrigin,
|
||||||
} from 'services/api';
|
} 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 = (
|
export const isImageOutput = (
|
||||||
output: GraphExecutionState['results'][string]
|
output: GraphExecutionState['results'][string]
|
||||||
): output is ImageOutput => output.type === 'image_output';
|
): output is ImageOutput => output.type === 'image_output';
|
||||||
|
Loading…
Reference in New Issue
Block a user