feat(ui): wip use new images service

This commit is contained in:
psychedelicious 2023-05-22 19:45:08 +10:00 committed by Kent Keirsey
parent 74292eba28
commit 6aebe1614d
13 changed files with 296 additions and 279 deletions

View File

@ -6,12 +6,7 @@ import {
} from 'services/util/deserializeImageField';
import { Image } from 'app/types/invokeai';
import { resultAdded } from 'features/gallery/store/resultsSlice';
import {
imageReceived,
imageRecordReceived,
imageUrlsReceived,
thumbnailReceived,
} from 'services/thunks/image';
import { imageMetadataReceived } from 'services/thunks/image';
import { startAppListening } from '..';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { addImageToStagingArea } from 'features/canvas/store/canvasSlice';
@ -41,75 +36,75 @@ export const addImageResultReceivedListener = () => {
const name = result.image.image_name;
const type = result.image.image_type;
dispatch(imageUrlsReceived({ imageName: name, imageType: type }));
// dispatch(imageUrlsReceived({ imageName: name, imageType: type }));
const [{ payload }] = await take(
(action): action is ReturnType<typeof imageUrlsReceived.fulfilled> =>
imageUrlsReceived.fulfilled.match(action) &&
action.payload.image_name === name
);
// const [{ payload }] = await take(
// (action): action is ReturnType<typeof imageUrlsReceived.fulfilled> =>
// imageUrlsReceived.fulfilled.match(action) &&
// action.payload.image_name === name
// );
console.log(payload);
// console.log(payload);
dispatch(imageRecordReceived({ imageName: name, imageType: type }));
dispatch(imageMetadataReceived({ imageName: name, imageType: type }));
const [x] = await take(
(
action
): action is ReturnType<typeof imageRecordReceived.fulfilled> =>
imageRecordReceived.fulfilled.match(action) &&
action.payload.image_name === name
);
// const [x] = await take(
// (
// action
// ): action is ReturnType<typeof imageMetadataReceived.fulfilled> =>
// imageMetadataReceived.fulfilled.match(action) &&
// action.payload.image_name === name
// );
console.log(x);
// console.log(x);
const state = getState();
// const state = getState();
// if we need to refetch, set URLs to placeholder for now
const { url, thumbnail } = shouldFetchImages
? { url: '', thumbnail: '' }
: buildImageUrls(type, name);
// // if we need to refetch, set URLs to placeholder for now
// const { url, thumbnail } = shouldFetchImages
// ? { url: '', thumbnail: '' }
// : buildImageUrls(type, name);
const timestamp = extractTimestampFromImageName(name);
// const timestamp = extractTimestampFromImageName(name);
const image: Image = {
name,
type,
url,
thumbnail,
metadata: {
created: timestamp,
width: result.width,
height: result.height,
invokeai: {
session_id: graph_execution_state_id,
...(node ? { node } : {}),
},
},
};
// const image: Image = {
// name,
// type,
// url,
// thumbnail,
// metadata: {
// created: timestamp,
// width: result.width,
// height: result.height,
// invokeai: {
// session_id: graph_execution_state_id,
// ...(node ? { node } : {}),
// },
// },
// };
dispatch(resultAdded(image));
// dispatch(resultAdded(image));
if (state.gallery.shouldAutoSwitchToNewImages) {
dispatch(imageSelected(image));
}
// if (state.gallery.shouldAutoSwitchToNewImages) {
// dispatch(imageSelected(image));
// }
if (state.config.shouldFetchImages) {
dispatch(imageReceived({ imageName: name, imageType: type }));
dispatch(
thumbnailReceived({
thumbnailName: name,
thumbnailType: type,
})
);
}
// if (state.config.shouldFetchImages) {
// dispatch(imageReceived({ imageName: name, imageType: type }));
// dispatch(
// thumbnailReceived({
// thumbnailName: name,
// thumbnailType: type,
// })
// );
// }
if (
graph_execution_state_id ===
state.canvas.layerState.stagingArea.sessionId
) {
dispatch(addImageToStagingArea(image));
}
// if (
// graph_execution_state_id ===
// state.canvas.layerState.stagingArea.sessionId
// ) {
// dispatch(addImageToStagingArea(image));
// }
}
},
});

View File

@ -2,9 +2,10 @@ import { Badge, Flex } from '@chakra-ui/react';
import { Image } from 'app/types/invokeai';
import { isNumber, isString } from 'lodash-es';
import { useMemo } from 'react';
import { ImageDTO } from 'services/api';
type ImageMetadataOverlayProps = {
image: Image;
image: ImageDTO;
};
const ImageMetadataOverlay = ({ image }: ImageMetadataOverlayProps) => {
@ -17,11 +18,11 @@ const ImageMetadataOverlay = ({ image }: ImageMetadataOverlayProps) => {
}, [image.metadata]);
const model = useMemo(() => {
if (!isString(image.metadata?.invokeai?.node?.model)) {
if (!isString(image.metadata?.model)) {
return;
}
return image.metadata?.invokeai?.node?.model;
return image.metadata?.model;
}, [image.metadata]);
return (

View File

@ -0,0 +1,12 @@
/**
* Comparator function for sorting dates in ascending order
*/
export const dateComparator = (a: string, b: string) => {
const dateA = new Date(a);
const dateB = new Date(b);
// sort in ascending order
if (dateA > dateB) return 1;
if (dateA < dateB) return -1;
return 0;
};

View File

@ -61,8 +61,8 @@ const CurrentImagePreview = () => {
if (!image) {
return;
}
e.dataTransfer.setData('invokeai/imageName', image.name);
e.dataTransfer.setData('invokeai/imageType', image.type);
e.dataTransfer.setData('invokeai/imageName', image.image_name);
e.dataTransfer.setData('invokeai/imageType', image.image_type);
e.dataTransfer.effectAllowed = 'move';
},
[image]
@ -108,7 +108,7 @@ const CurrentImagePreview = () => {
image && (
<>
<Image
src={getUrl(image.url)}
src={getUrl(image.image_url)}
fallbackStrategy="beforeLoadOrError"
fallback={<ImageFallbackSpinner />}
onDragStart={handleDragStart}

View File

@ -39,6 +39,7 @@ import {
sentImageToImg2Img,
} from '../store/actions';
import { useAppToaster } from 'app/components/Toaster';
import { ImageDTO } from 'services/api';
export const selector = createSelector(
[gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector],
@ -70,14 +71,16 @@ export const selector = createSelector(
);
interface HoverableImageProps {
image: InvokeAI.Image;
image: ImageDTO;
isSelected: boolean;
}
const memoEqualityCheck = (
prev: HoverableImageProps,
next: HoverableImageProps
) => prev.image.name === next.image.name && prev.isSelected === next.isSelected;
) =>
prev.image.image_name === next.image.image_name &&
prev.isSelected === next.isSelected;
/**
* Gallery image component with delete/use all/use seed buttons on hover.
@ -100,7 +103,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
} = useDisclosure();
const { image, isSelected } = props;
const { url, thumbnail, name } = image;
const { image_url, thumbnail_url, image_name } = image;
const { getUrl } = useGetUrl();
const [isHovered, setIsHovered] = useState<boolean>(false);
@ -144,8 +147,8 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const handleDragStart = useCallback(
(e: DragEvent<HTMLDivElement>) => {
e.dataTransfer.setData('invokeai/imageName', image.name);
e.dataTransfer.setData('invokeai/imageType', image.type);
e.dataTransfer.setData('invokeai/imageName', image.image_name);
e.dataTransfer.setData('invokeai/imageType', image.image_type);
e.dataTransfer.effectAllowed = 'move';
},
[image]
@ -153,11 +156,11 @@ const HoverableImage = memo((props: HoverableImageProps) => {
// Recall parameters handlers
const handleRecallPrompt = useCallback(() => {
recallPrompt(image.metadata?.invokeai?.node?.prompt);
recallPrompt(image.metadata?.positive_conditioning);
}, [image, recallPrompt]);
const handleRecallSeed = useCallback(() => {
recallSeed(image.metadata.invokeai?.node?.seed);
recallSeed(image.metadata?.seed);
}, [image, recallSeed]);
const handleSendToImageToImage = useCallback(() => {
@ -200,7 +203,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
};
const handleOpenInNewTab = () => {
window.open(getUrl(image.url), '_blank');
window.open(getUrl(image.image_url), '_blank');
};
return (
@ -223,7 +226,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleRecallPrompt}
isDisabled={image?.metadata?.invokeai?.node?.prompt === undefined}
isDisabled={image?.metadata?.positive_conditioning === undefined}
>
{t('parameters.usePrompt')}
</MenuItem>
@ -231,14 +234,14 @@ const HoverableImage = memo((props: HoverableImageProps) => {
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleRecallSeed}
isDisabled={image?.metadata?.invokeai?.node?.seed === undefined}
isDisabled={image?.metadata?.seed === undefined}
>
{t('parameters.useSeed')}
</MenuItem>
<MenuItem
icon={<IoArrowUndoCircleOutline />}
onClickCapture={handleRecallInitialImage}
isDisabled={image?.metadata?.invokeai?.node?.type !== 'img2img'}
isDisabled={image?.metadata?.type !== 'img2img'}
>
{t('parameters.useInitImg')}
</MenuItem>
@ -247,7 +250,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
onClickCapture={handleUseAllParameters}
isDisabled={
!['txt2img', 'img2img', 'inpaint'].includes(
String(image?.metadata?.invokeai?.node?.type)
String(image?.metadata?.type)
)
}
>
@ -278,7 +281,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
{(ref) => (
<Box
position="relative"
key={name}
key={image_name}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
userSelect="none"
@ -303,7 +306,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
shouldUseSingleGalleryColumn ? 'contain' : galleryImageObjectFit
}
rounded="md"
src={getUrl(thumbnail || url)}
src={getUrl(thumbnail_url || image_url)}
fallback={<FaImage />}
sx={{
width: '100%',

View File

@ -55,6 +55,7 @@ import { Image as ImageType } from 'app/types/invokeai';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import GalleryProgressImage from './GalleryProgressImage';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { ImageDTO } from 'services/api';
const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 290;
const PROGRESS_IMAGE_PLACEHOLDER = 'PROGRESS_IMAGE_PLACEHOLDER';
@ -66,7 +67,7 @@ const categorySelector = createSelector(
const { currentCategory } = gallery;
if (currentCategory === 'results') {
const tempImages: (ImageType | typeof PROGRESS_IMAGE_PLACEHOLDER)[] = [];
const tempImages: (ImageDTO | typeof PROGRESS_IMAGE_PLACEHOLDER)[] = [];
if (system.progressImage) {
tempImages.push(PROGRESS_IMAGE_PLACEHOLDER);
@ -352,7 +353,7 @@ const ImageGalleryContent = () => {
const isSelected =
image === PROGRESS_IMAGE_PLACEHOLDER
? false
: selectedImage?.name === image?.name;
: selectedImage?.image_name === image?.image_name;
return (
<Flex sx={{ pb: 2 }}>
@ -362,7 +363,7 @@ const ImageGalleryContent = () => {
/>
) : (
<HoverableImage
key={`${image.name}-${image.thumbnail}`}
key={`${image.image_name}-${image.thumbnail_url}`}
image={image}
isSelected={isSelected}
/>
@ -385,13 +386,13 @@ const ImageGalleryContent = () => {
const isSelected =
image === PROGRESS_IMAGE_PLACEHOLDER
? false
: selectedImage?.name === image?.name;
: selectedImage?.image_name === image?.image_name;
return image === PROGRESS_IMAGE_PLACEHOLDER ? (
<GalleryProgressImage key={PROGRESS_IMAGE_PLACEHOLDER} />
) : (
<HoverableImage
key={`${image.name}-${image.thumbnail}`}
key={`${image.image_name}-${image.thumbnail_url}`}
image={image}
isSelected={isSelected}
/>

View File

@ -18,7 +18,9 @@ import {
setCfgScale,
setHeight,
setImg2imgStrength,
setNegativePrompt,
setPerlin,
setPrompt,
setScheduler,
setSeamless,
setSeed,
@ -36,6 +38,9 @@ import { useTranslation } from 'react-i18next';
import { FaCopy } from 'react-icons/fa';
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { ImageDTO } from 'services/api';
import { filter } from 'lodash-es';
import { Scheduler } from 'app/constants';
type MetadataItemProps = {
isLink?: boolean;
@ -58,7 +63,6 @@ const MetadataItem = ({
withCopy = false,
}: MetadataItemProps) => {
const { t } = useTranslation();
return (
<Flex gap={2}>
{onClick && (
@ -104,14 +108,14 @@ const MetadataItem = ({
};
type ImageMetadataViewerProps = {
image: InvokeAI.Image;
image: ImageDTO;
};
// TODO: I don't know if this is needed.
const memoEqualityCheck = (
prev: ImageMetadataViewerProps,
next: ImageMetadataViewerProps
) => prev.image.name === next.image.name;
) => prev.image.image_name === next.image.image_name;
// TODO: Show more interesting information in this component.
@ -128,8 +132,9 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
dispatch(setShouldShowImageDetails(false));
});
const sessionId = image.metadata.invokeai?.session_id;
const node = image.metadata.invokeai?.node as Record<string, any>;
const sessionId = image?.session_id;
const metadata = image?.metadata;
const { t } = useTranslation();
const { getUrl } = useGetUrl();
@ -154,110 +159,133 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
>
<Flex gap={2}>
<Text fontWeight="semibold">File:</Text>
<Link href={getUrl(image.url)} isExternal maxW="calc(100% - 3rem)">
{image.url.length > 64
? image.url.substring(0, 64).concat('...')
: image.url}
<Link
href={getUrl(image.image_url)}
isExternal
maxW="calc(100% - 3rem)"
>
{image.image_url.length > 64
? image.image_url.substring(0, 64).concat('...')
: image.image_url}
<ExternalLinkIcon mx="2px" />
</Link>
</Flex>
{node && Object.keys(node).length > 0 ? (
{metadata && Object.keys(metadata).length > 0 ? (
<>
{node.type && (
<MetadataItem label="Invocation type" value={node.type} />
{metadata.type && (
<MetadataItem label="Invocation type" value={metadata.type} />
)}
{node.model && <MetadataItem label="Model" value={node.model} />}
{node.prompt && (
{metadata.width && (
<MetadataItem
label="Width"
value={metadata.width}
onClick={() => dispatch(setWidth(Number(metadata.width)))}
/>
)}
{metadata.height && (
<MetadataItem
label="Height"
value={metadata.height}
onClick={() => dispatch(setHeight(Number(metadata.height)))}
/>
)}
{metadata.model && (
<MetadataItem label="Model" value={metadata.model} />
)}
{metadata.positive_conditioning && (
<MetadataItem
label="Prompt"
labelPosition="top"
value={
typeof node.prompt === 'string'
? node.prompt
: promptToString(node.prompt)
typeof metadata.positive_conditioning === 'string'
? metadata.positive_conditioning
: promptToString(metadata.positive_conditioning)
}
onClick={() => setBothPrompts(node.prompt)}
onClick={() => setPrompt(metadata.positive_conditioning!)}
/>
)}
{node.seed !== undefined && (
{metadata.negative_conditioning && (
<MetadataItem
label="Prompt"
labelPosition="top"
value={
typeof metadata.negative_conditioning === 'string'
? metadata.negative_conditioning
: promptToString(metadata.negative_conditioning)
}
onClick={() => setNegativePrompt(metadata.negative_conditioning!)}
/>
)}
{metadata.seed !== undefined && (
<MetadataItem
label="Seed"
value={node.seed}
onClick={() => dispatch(setSeed(Number(node.seed)))}
value={metadata.seed}
onClick={() => dispatch(setSeed(Number(metadata.seed)))}
/>
)}
{node.threshold !== undefined && (
{/* {metadata.threshold !== undefined && (
<MetadataItem
label="Noise Threshold"
value={node.threshold}
onClick={() => dispatch(setThreshold(Number(node.threshold)))}
value={metadata.threshold}
onClick={() => dispatch(setThreshold(Number(metadata.threshold)))}
/>
)}
{node.perlin !== undefined && (
{metadata.perlin !== undefined && (
<MetadataItem
label="Perlin Noise"
value={node.perlin}
onClick={() => dispatch(setPerlin(Number(node.perlin)))}
value={metadata.perlin}
onClick={() => dispatch(setPerlin(Number(metadata.perlin)))}
/>
)}
{node.scheduler && (
)} */}
{metadata.scheduler && (
<MetadataItem
label="Scheduler"
value={node.scheduler}
onClick={() => dispatch(setScheduler(node.scheduler))}
/>
)}
{node.steps && (
<MetadataItem
label="Steps"
value={node.steps}
onClick={() => dispatch(setSteps(Number(node.steps)))}
/>
)}
{node.cfg_scale !== undefined && (
<MetadataItem
label="CFG scale"
value={node.cfg_scale}
onClick={() => dispatch(setCfgScale(Number(node.cfg_scale)))}
/>
)}
{node.variations && node.variations.length > 0 && (
<MetadataItem
label="Seed-weight pairs"
value={seedWeightsToString(node.variations)}
value={metadata.scheduler}
onClick={() =>
dispatch(setSeedWeights(seedWeightsToString(node.variations)))
dispatch(setScheduler(metadata.scheduler as Scheduler))
}
/>
)}
{node.seamless && (
{metadata.steps && (
<MetadataItem
label="Steps"
value={metadata.steps}
onClick={() => dispatch(setSteps(Number(metadata.steps)))}
/>
)}
{metadata.cfg_scale !== undefined && (
<MetadataItem
label="CFG scale"
value={metadata.cfg_scale}
onClick={() => dispatch(setCfgScale(Number(metadata.cfg_scale)))}
/>
)}
{/* {metadata.variations && metadata.variations.length > 0 && (
<MetadataItem
label="Seed-weight pairs"
value={seedWeightsToString(metadata.variations)}
onClick={() =>
dispatch(
setSeedWeights(seedWeightsToString(metadata.variations))
)
}
/>
)}
{metadata.seamless && (
<MetadataItem
label="Seamless"
value={node.seamless}
onClick={() => dispatch(setSeamless(node.seamless))}
value={metadata.seamless}
onClick={() => dispatch(setSeamless(metadata.seamless))}
/>
)}
{node.hires_fix && (
{metadata.hires_fix && (
<MetadataItem
label="High Resolution Optimization"
value={node.hires_fix}
onClick={() => dispatch(setHiresFix(node.hires_fix))}
value={metadata.hires_fix}
onClick={() => dispatch(setHiresFix(metadata.hires_fix))}
/>
)}
{node.width && (
<MetadataItem
label="Width"
value={node.width}
onClick={() => dispatch(setWidth(Number(node.width)))}
/>
)}
{node.height && (
<MetadataItem
label="Height"
value={node.height}
onClick={() => dispatch(setHeight(Number(node.height)))}
/>
)}
)} */}
{/* {init_image_path && (
<MetadataItem
label="Initial image"
@ -266,22 +294,22 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
onClick={() => dispatch(setInitialImage(init_image_path))}
/>
)} */}
{node.strength && (
{metadata.strength && (
<MetadataItem
label="Image to image strength"
value={node.strength}
value={metadata.strength}
onClick={() =>
dispatch(setImg2imgStrength(Number(node.strength)))
dispatch(setImg2imgStrength(Number(metadata.strength)))
}
/>
)}
{node.fit && (
{/* {metadata.fit && (
<MetadataItem
label="Image to image fit"
value={node.fit}
onClick={() => dispatch(setShouldFitToWidthHeight(node.fit))}
value={metadata.fit}
onClick={() => dispatch(setShouldFitToWidthHeight(metadata.fit))}
/>
)}
)} */}
</>
) : (
<Center width="100%" pt={10}>

View File

@ -1,16 +1,15 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { Image } from 'app/types/invokeai';
import { imageReceived, thumbnailReceived } from 'services/thunks/image';
import {
receivedResultImagesPage,
receivedUploadImagesPage,
} from '../../../services/thunks/gallery';
import { ImageDTO } from 'services/api';
type GalleryImageObjectFitType = 'contain' | 'cover';
export interface GalleryState {
selectedImage?: Image;
selectedImage?: ImageDTO;
galleryImageMinimumWidth: number;
galleryImageObjectFit: GalleryImageObjectFitType;
shouldAutoSwitchToNewImages: boolean;
@ -30,7 +29,7 @@ export const gallerySlice = createSlice({
name: 'gallery',
initialState: initialGalleryState,
reducers: {
imageSelected: (state, action: PayloadAction<Image | undefined>) => {
imageSelected: (state, action: PayloadAction<ImageDTO | undefined>) => {
state.selectedImage = action.payload;
// TODO: if the user selects an image, disable the auto switch?
// state.shouldAutoSwitchToNewImages = false;
@ -61,37 +60,18 @@ export const gallerySlice = createSlice({
},
},
extraReducers(builder) {
builder.addCase(imageReceived.fulfilled, (state, action) => {
// When we get an updated URL for an image, we need to update the selectedImage in gallery,
// which is currently its own object (instead of a reference to an image in results/uploads)
const { imagePath } = action.payload;
const { imageName } = action.meta.arg;
if (state.selectedImage?.name === imageName) {
state.selectedImage.url = imagePath;
}
});
builder.addCase(thumbnailReceived.fulfilled, (state, action) => {
// When we get an updated URL for an image, we need to update the selectedImage in gallery,
// which is currently its own object (instead of a reference to an image in results/uploads)
const { thumbnailPath } = action.payload;
const { thumbnailName } = action.meta.arg;
if (state.selectedImage?.name === thumbnailName) {
state.selectedImage.thumbnail = thumbnailPath;
}
});
builder.addCase(receivedResultImagesPage.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.name
(image) => image.image_name === selectedImage.image_name
);
if (selectedImageInResults) {
selectedImage.url = selectedImageInResults.image_url;
selectedImage.image_url = selectedImageInResults.image_url;
selectedImage.thumbnail_url = selectedImageInResults.thumbnail_url;
state.selectedImage = selectedImage;
}
}
@ -102,10 +82,12 @@ export const gallerySlice = createSlice({
const selectedImage = state.selectedImage;
if (selectedImage) {
const selectedImageInResults = action.payload.items.find(
(image) => image.image_name === selectedImage.name
(image) => image.image_name === selectedImage.image_name
);
if (selectedImageInResults) {
selectedImage.url = selectedImageInResults.image_url;
selectedImage.image_url = selectedImageInResults.image_url;
selectedImage.thumbnail_url = selectedImageInResults.thumbnail_url;
state.selectedImage = selectedImage;
}
}

View File

@ -1,21 +1,24 @@
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
import { Image } from 'app/types/invokeai';
import { RootState } from 'app/store/store';
import {
receivedResultImagesPage,
IMAGES_PER_PAGE,
} from 'services/thunks/gallery';
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
import {
imageDeleted,
imageReceived,
thumbnailReceived,
imageMetadataReceived,
imageUrlsReceived,
} from 'services/thunks/image';
import { ImageDTO } from 'services/api';
import { dateComparator } from 'common/util/dateComparator';
export const resultsAdapter = createEntityAdapter<Image>({
selectId: (image) => image.name,
sortComparer: (a, b) => b.metadata.created - a.metadata.created,
export type ResultsImageDTO = Omit<ImageDTO, 'image_type'> & {
image_type: 'results';
};
export const resultsAdapter = createEntityAdapter<ResultsImageDTO>({
selectId: (image) => image.image_name,
sortComparer: (a, b) => dateComparator(b.created_at, a.created_at),
});
type AdditionalResultsState = {
@ -53,13 +56,12 @@ const resultsSlice = createSlice({
* Received Result Images Page - FULFILLED
*/
builder.addCase(receivedResultImagesPage.fulfilled, (state, action) => {
const { items, page, pages } = action.payload;
const { page, pages } = action.payload;
const resultImages = items.map((image) =>
deserializeImageResponse(image)
);
// We know these will all be of the results type, but it's not represented in the API types
const items = action.payload.items as ResultsImageDTO[];
resultsAdapter.setMany(state, resultImages);
resultsAdapter.setMany(state, items);
state.page = page;
state.pages = pages;
@ -68,33 +70,32 @@ const resultsSlice = createSlice({
});
/**
* Image Received - FULFILLED
* Image Metadata Received - FULFILLED
*/
builder.addCase(imageReceived.fulfilled, (state, action) => {
const { imagePath } = action.payload;
const { imageName } = action.meta.arg;
builder.addCase(imageMetadataReceived.fulfilled, (state, action) => {
const { image_type } = action.payload;
resultsAdapter.updateOne(state, {
id: imageName,
changes: {
url: imagePath,
},
});
if (image_type === 'results') {
resultsAdapter.upsertOne(state, action.payload as ResultsImageDTO);
}
});
/**
* Thumbnail Received - FULFILLED
* Image URLs Received - FULFILLED
*/
builder.addCase(thumbnailReceived.fulfilled, (state, action) => {
const { thumbnailPath } = action.payload;
const { thumbnailName } = action.meta.arg;
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
const { image_name, image_type, image_url, thumbnail_url } =
action.payload;
resultsAdapter.updateOne(state, {
id: thumbnailName,
changes: {
thumbnail: thumbnailPath,
},
});
if (image_type === 'results') {
resultsAdapter.updateOne(state, {
id: image_name,
changes: {
image_url: image_url,
thumbnail_url: thumbnail_url,
},
});
}
});
/**

View File

@ -6,12 +6,18 @@ import {
receivedUploadImagesPage,
IMAGES_PER_PAGE,
} from 'services/thunks/gallery';
import { imageDeleted } from 'services/thunks/image';
import { imageDeleted, imageUrlsReceived } from 'services/thunks/image';
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
import { ImageDTO } from 'services/api';
import { dateComparator } from 'common/util/dateComparator';
export const uploadsAdapter = createEntityAdapter<Image>({
selectId: (image) => image.name,
sortComparer: (a, b) => b.metadata.created - a.metadata.created,
export type UploadsImageDTO = Omit<ImageDTO, 'image_type'> & {
image_type: 'uploads';
};
export const uploadsAdapter = createEntityAdapter<UploadsImageDTO>({
selectId: (image) => image.image_category,
sortComparer: (a, b) => dateComparator(b.created_at, a.created_at),
});
type AdditionalUploadsState = {
@ -49,11 +55,12 @@ const uploadsSlice = createSlice({
* Received Upload Images Page - FULFILLED
*/
builder.addCase(receivedUploadImagesPage.fulfilled, (state, action) => {
const { items, page, pages } = action.payload;
const { page, pages } = action.payload;
const images = items.map((image) => deserializeImageResponse(image));
// We know these will all be of the uploads type, but it's not represented in the API types
const items = action.payload.items as UploadsImageDTO[];
uploadsAdapter.setMany(state, images);
uploadsAdapter.setMany(state, items);
state.page = page;
state.pages = pages;
@ -61,6 +68,24 @@ const uploadsSlice = createSlice({
state.isLoading = false;
});
/**
* Image URLs Received - FULFILLED
*/
builder.addCase(imageUrlsReceived.fulfilled, (state, action) => {
const { image_name, image_type, image_url, thumbnail_url } =
action.payload;
if (image_type === 'uploads') {
uploadsAdapter.updateOne(state, {
id: image_name,
changes: {
image_url: image_url,
thumbnail_url: thumbnail_url,
},
});
}
});
/**
* Delete Image - pending
* Pre-emptively remove the image from the gallery

View File

@ -3,4 +3,4 @@ import { UIState } from './uiTypes';
/**
* UI slice persist denylist
*/
export const uiPersistDenylist: (keyof UIState)[] = [];
export const uiPersistDenylist: (keyof UIState)[] = ['shouldShowImageDetails'];

View File

@ -9,8 +9,9 @@ const galleryLog = log.child({ namespace: 'gallery' });
export const receivedResultImagesPage = createAppAsyncThunk(
'results/receivedResultImagesPage',
async (_arg, { getState }) => {
const response = await ImagesService.listImages({
const response = await ImagesService.listImagesWithMetadata({
imageType: 'results',
imageCategory: 'image',
page: getState().results.nextPage,
perPage: IMAGES_PER_PAGE,
});
@ -24,8 +25,9 @@ export const receivedResultImagesPage = createAppAsyncThunk(
export const receivedUploadImagesPage = createAppAsyncThunk(
'uploads/receivedUploadImagesPage',
async (_arg, { getState }) => {
const response = await ImagesService.listImages({
const response = await ImagesService.listImagesWithMetadata({
imageType: 'uploads',
imageCategory: 'image',
page: getState().uploads.nextPage,
perPage: IMAGES_PER_PAGE,
});

View File

@ -1,3 +1,4 @@
import { AnyAction } from '@reduxjs/toolkit';
import { log } from 'app/logging/useLogger';
import { createAppAsyncThunk } from 'app/store/storeUtils';
import { InvokeTabName } from 'features/ui/store/tabMap';
@ -22,56 +23,22 @@ export const imageUrlsReceived = createAppAsyncThunk(
}
);
type imageRecordReceivedArg = Parameters<
(typeof ImagesService)['getImageUrls']
type imageMetadataReceivedArg = Parameters<
(typeof ImagesService)['getImageMetadata']
>[0];
/**
* `ImagesService.getImageUrls()` thunk
*/
export const imageRecordReceived = createAppAsyncThunk(
'api/imageUrlsReceived',
async (arg: imageRecordReceivedArg) => {
const response = await ImagesService.getImageRecord(arg);
export const imageMetadataReceived = createAppAsyncThunk(
'api/imageMetadataReceived',
async (arg: imageMetadataReceivedArg) => {
const response = await ImagesService.getImageMetadata(arg);
imagesLog.info({ arg, response }, 'Received image record');
return response;
}
);
type ImageReceivedArg = Parameters<(typeof ImagesService)['getImage']>[0];
/**
* `ImagesService.getImage()` thunk
*/
export const imageReceived = createAppAsyncThunk(
'api/imageReceived',
async (arg: ImageReceivedArg) => {
const response = await ImagesService.getImage(arg);
imagesLog.info({ arg, response }, 'Received image');
return response;
}
);
type ThumbnailReceivedArg = Parameters<
(typeof ImagesService)['getThumbnail']
>[0];
/**
* `ImagesService.getThumbnail()` thunk
*/
export const thumbnailReceived = createAppAsyncThunk(
'api/thumbnailReceived',
async (arg: ThumbnailReceivedArg) => {
const response = await ImagesService.getThumbnail(arg);
imagesLog.info({ arg, response }, 'Received thumbnail');
return response;
}
);
type ImageUploadedArg = Parameters<(typeof ImagesService)['uploadImage']>[0] & {
// extra arg to determine post-upload actions - we check for this when the image is uploaded
// to determine if we should set the init image