mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): wip use new images service
This commit is contained in:
parent
74292eba28
commit
6aebe1614d
@ -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));
|
||||
// }
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -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 (
|
||||
|
12
invokeai/frontend/web/src/common/util/dateComparator.ts
Normal file
12
invokeai/frontend/web/src/common/util/dateComparator.ts
Normal 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;
|
||||
};
|
@ -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}
|
||||
|
@ -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%',
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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}>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -3,4 +3,4 @@ import { UIState } from './uiTypes';
|
||||
/**
|
||||
* UI slice persist denylist
|
||||
*/
|
||||
export const uiPersistDenylist: (keyof UIState)[] = [];
|
||||
export const uiPersistDenylist: (keyof UIState)[] = ['shouldShowImageDetails'];
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user