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

View File

@ -2,9 +2,10 @@ import { Badge, Flex } from '@chakra-ui/react';
import { Image } from 'app/types/invokeai'; import { Image } from 'app/types/invokeai';
import { isNumber, isString } from 'lodash-es'; import { isNumber, isString } from 'lodash-es';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { ImageDTO } from 'services/api';
type ImageMetadataOverlayProps = { type ImageMetadataOverlayProps = {
image: Image; image: ImageDTO;
}; };
const ImageMetadataOverlay = ({ image }: ImageMetadataOverlayProps) => { const ImageMetadataOverlay = ({ image }: ImageMetadataOverlayProps) => {
@ -17,11 +18,11 @@ const ImageMetadataOverlay = ({ image }: ImageMetadataOverlayProps) => {
}, [image.metadata]); }, [image.metadata]);
const model = useMemo(() => { const model = useMemo(() => {
if (!isString(image.metadata?.invokeai?.node?.model)) { if (!isString(image.metadata?.model)) {
return; return;
} }
return image.metadata?.invokeai?.node?.model; return image.metadata?.model;
}, [image.metadata]); }, [image.metadata]);
return ( 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) { if (!image) {
return; return;
} }
e.dataTransfer.setData('invokeai/imageName', image.name); e.dataTransfer.setData('invokeai/imageName', image.image_name);
e.dataTransfer.setData('invokeai/imageType', image.type); e.dataTransfer.setData('invokeai/imageType', image.image_type);
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move';
}, },
[image] [image]
@ -108,7 +108,7 @@ const CurrentImagePreview = () => {
image && ( image && (
<> <>
<Image <Image
src={getUrl(image.url)} src={getUrl(image.image_url)}
fallbackStrategy="beforeLoadOrError" fallbackStrategy="beforeLoadOrError"
fallback={<ImageFallbackSpinner />} fallback={<ImageFallbackSpinner />}
onDragStart={handleDragStart} onDragStart={handleDragStart}

View File

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

View File

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

View File

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

View File

@ -1,16 +1,15 @@
import type { PayloadAction } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import { Image } from 'app/types/invokeai';
import { imageReceived, thumbnailReceived } from 'services/thunks/image';
import { import {
receivedResultImagesPage, receivedResultImagesPage,
receivedUploadImagesPage, receivedUploadImagesPage,
} from '../../../services/thunks/gallery'; } from '../../../services/thunks/gallery';
import { ImageDTO } from 'services/api';
type GalleryImageObjectFitType = 'contain' | 'cover'; type GalleryImageObjectFitType = 'contain' | 'cover';
export interface GalleryState { export interface GalleryState {
selectedImage?: Image; selectedImage?: ImageDTO;
galleryImageMinimumWidth: number; galleryImageMinimumWidth: number;
galleryImageObjectFit: GalleryImageObjectFitType; galleryImageObjectFit: GalleryImageObjectFitType;
shouldAutoSwitchToNewImages: boolean; shouldAutoSwitchToNewImages: boolean;
@ -30,7 +29,7 @@ export const gallerySlice = createSlice({
name: 'gallery', name: 'gallery',
initialState: initialGalleryState, initialState: initialGalleryState,
reducers: { reducers: {
imageSelected: (state, action: PayloadAction<Image | undefined>) => { imageSelected: (state, action: PayloadAction<ImageDTO | undefined>) => {
state.selectedImage = action.payload; state.selectedImage = action.payload;
// TODO: if the user selects an image, disable the auto switch? // TODO: if the user selects an image, disable the auto switch?
// state.shouldAutoSwitchToNewImages = false; // state.shouldAutoSwitchToNewImages = false;
@ -61,37 +60,18 @@ export const gallerySlice = createSlice({
}, },
}, },
extraReducers(builder) { 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) => { builder.addCase(receivedResultImagesPage.fulfilled, (state, action) => {
// rehydrate selectedImage URL when results list comes in // rehydrate selectedImage URL when results list comes in
// solves case when outdated URL is in local storage // solves case when outdated URL is in local storage
const selectedImage = state.selectedImage; const selectedImage = state.selectedImage;
if (selectedImage) { if (selectedImage) {
const selectedImageInResults = action.payload.items.find( const selectedImageInResults = action.payload.items.find(
(image) => image.image_name === selectedImage.name (image) => image.image_name === selectedImage.image_name
); );
if (selectedImageInResults) { if (selectedImageInResults) {
selectedImage.url = selectedImageInResults.image_url; selectedImage.image_url = selectedImageInResults.image_url;
selectedImage.thumbnail_url = selectedImageInResults.thumbnail_url;
state.selectedImage = selectedImage; state.selectedImage = selectedImage;
} }
} }
@ -102,10 +82,12 @@ export const gallerySlice = createSlice({
const selectedImage = state.selectedImage; const selectedImage = state.selectedImage;
if (selectedImage) { if (selectedImage) {
const selectedImageInResults = action.payload.items.find( const selectedImageInResults = action.payload.items.find(
(image) => image.image_name === selectedImage.name (image) => image.image_name === selectedImage.image_name
); );
if (selectedImageInResults) { if (selectedImageInResults) {
selectedImage.url = selectedImageInResults.image_url; selectedImage.image_url = selectedImageInResults.image_url;
selectedImage.thumbnail_url = selectedImageInResults.thumbnail_url;
state.selectedImage = selectedImage; state.selectedImage = selectedImage;
} }
} }

View File

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

View File

@ -6,12 +6,18 @@ import {
receivedUploadImagesPage, receivedUploadImagesPage,
IMAGES_PER_PAGE, IMAGES_PER_PAGE,
} from 'services/thunks/gallery'; } from 'services/thunks/gallery';
import { imageDeleted } from 'services/thunks/image'; import { imageDeleted, imageUrlsReceived } from 'services/thunks/image';
import { deserializeImageResponse } from 'services/util/deserializeImageResponse'; import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
import { ImageDTO } from 'services/api';
import { dateComparator } from 'common/util/dateComparator';
export const uploadsAdapter = createEntityAdapter<Image>({ export type UploadsImageDTO = Omit<ImageDTO, 'image_type'> & {
selectId: (image) => image.name, image_type: 'uploads';
sortComparer: (a, b) => b.metadata.created - a.metadata.created, };
export const uploadsAdapter = createEntityAdapter<UploadsImageDTO>({
selectId: (image) => image.image_category,
sortComparer: (a, b) => dateComparator(b.created_at, a.created_at),
}); });
type AdditionalUploadsState = { type AdditionalUploadsState = {
@ -49,11 +55,12 @@ const uploadsSlice = createSlice({
* Received Upload Images Page - FULFILLED * Received Upload Images Page - FULFILLED
*/ */
builder.addCase(receivedUploadImagesPage.fulfilled, (state, action) => { 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.page = page;
state.pages = pages; state.pages = pages;
@ -61,6 +68,24 @@ const uploadsSlice = createSlice({
state.isLoading = false; 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 * Delete Image - pending
* Pre-emptively remove the image from the gallery * Pre-emptively remove the image from the gallery

View File

@ -3,4 +3,4 @@ import { UIState } from './uiTypes';
/** /**
* UI slice persist denylist * 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( export const receivedResultImagesPage = createAppAsyncThunk(
'results/receivedResultImagesPage', 'results/receivedResultImagesPage',
async (_arg, { getState }) => { async (_arg, { getState }) => {
const response = await ImagesService.listImages({ const response = await ImagesService.listImagesWithMetadata({
imageType: 'results', imageType: 'results',
imageCategory: 'image',
page: getState().results.nextPage, page: getState().results.nextPage,
perPage: IMAGES_PER_PAGE, perPage: IMAGES_PER_PAGE,
}); });
@ -24,8 +25,9 @@ export const receivedResultImagesPage = createAppAsyncThunk(
export const receivedUploadImagesPage = createAppAsyncThunk( export const receivedUploadImagesPage = createAppAsyncThunk(
'uploads/receivedUploadImagesPage', 'uploads/receivedUploadImagesPage',
async (_arg, { getState }) => { async (_arg, { getState }) => {
const response = await ImagesService.listImages({ const response = await ImagesService.listImagesWithMetadata({
imageType: 'uploads', imageType: 'uploads',
imageCategory: 'image',
page: getState().uploads.nextPage, page: getState().uploads.nextPage,
perPage: IMAGES_PER_PAGE, perPage: IMAGES_PER_PAGE,
}); });

View File

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