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

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