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:
psychedelicious 2023-05-28 19:05:34 +10:00 committed by Kent Keirsey
parent 6cc00ef4b7
commit 89aa06e014
38 changed files with 395 additions and 740 deletions

View File

@ -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,
};

View File

@ -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) => {

View File

@ -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();

View File

@ -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));
},
});
};

View File

@ -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);
},
});
};

View File

@ -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));
},
});
};

View File

@ -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' }));
},
});
};

View File

@ -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,
},
});
},
});
};

View File

@ -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(

View File

@ -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'
);
}
},

View File

@ -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'
);
}
},
});
};

View File

@ -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'];

View File

@ -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) {

View File

@ -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' }));
}
},
});
};

View File

@ -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',
];

View File

@ -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')}

View File

@ -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'
);

View File

@ -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]

View File

@ -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]

View File

@ -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"

View File

@ -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,

View File

@ -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;

View File

@ -4,6 +4,5 @@ import { GalleryState } from './gallerySlice';
* Gallery slice persist denylist
*/
export const galleryPersistDenylist: (keyof GalleryState)[] = [
'currentCategory',
'shouldAutoSwitchToNewImages',
];

View File

@ -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;

View 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);
}
);

View File

@ -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)[] = [];

View File

@ -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;

View File

@ -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)[] = [];

View File

@ -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;

View File

@ -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 (

View File

@ -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);
}
};

View File

@ -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]
);

View File

@ -88,7 +88,7 @@ export const useParameters = () => {
return;
}
dispatch(initialImageSelected(image));
dispatch(initialImageSelected(image.image_name));
toaster({
title: t('toast.initialImageSet'),
status: 'info',

View File

@ -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'
);

View File

@ -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);
}
}
);

View File

@ -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;
}
);

View File

@ -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;
}
);

View File

@ -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';