diff --git a/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx b/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx new file mode 100644 index 0000000000..64d5e1beef --- /dev/null +++ b/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx @@ -0,0 +1,54 @@ +import { Badge, Flex } from '@chakra-ui/react'; +import { Image } from 'app/types/invokeai'; +import { isNumber, isString } from 'lodash-es'; +import { useMemo } from 'react'; + +type ImageMetadataOverlayProps = { + image: Image; +}; + +const ImageMetadataOverlay = ({ image }: ImageMetadataOverlayProps) => { + const dimensions = useMemo(() => { + if (!isNumber(image.metadata?.width) || isNumber(!image.metadata?.height)) { + return; + } + + return `${image.metadata?.width} × ${image.metadata?.height}`; + }, [image.metadata]); + + const model = useMemo(() => { + if (!isString(image.metadata?.invokeai?.node?.model)) { + return; + } + + return image.metadata?.invokeai?.node?.model; + }, [image.metadata]); + + return ( + + {dimensions && ( + + {dimensions} + + )} + {model && ( + + {model} + + )} + + ); +}; + +export default ImageMetadataOverlay; diff --git a/invokeai/frontend/web/src/common/components/ImageToImageOverlay.tsx b/invokeai/frontend/web/src/common/components/ImageToImageOverlay.tsx deleted file mode 100644 index 9d864f5c9c..0000000000 --- a/invokeai/frontend/web/src/common/components/ImageToImageOverlay.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Badge, Box, Flex } from '@chakra-ui/react'; -import { Image } from 'app/types/invokeai'; - -type ImageToImageOverlayProps = { - image: Image; -}; - -const ImageToImageOverlay = ({ image }: ImageToImageOverlayProps) => { - return ( - - - - {image.metadata?.width} × {image.metadata?.height} - - - - ); -}; - -export default ImageToImageOverlay; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx index a0fbd7c5d1..b1dbed5a81 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx @@ -1,4 +1,4 @@ -import { Box, Flex, Image, Skeleton, useBoolean } from '@chakra-ui/react'; +import { Box, Flex, Image } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { useGetUrl } from 'common/util/getUrl'; @@ -11,7 +11,8 @@ import NextPrevImageButtons from './NextPrevImageButtons'; import CurrentImageHidden from './CurrentImageHidden'; import { DragEvent, memo, useCallback } from 'react'; import { systemSelector } from 'features/system/store/systemSelectors'; -import CurrentImageFallback from './CurrentImageFallback'; +import ImageFallbackSpinner from './ImageFallbackSpinner'; +import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay'; export const imagesSelector = createSelector( [uiSelector, gallerySelector, systemSelector], @@ -50,8 +51,6 @@ const CurrentImagePreview = () => { } = useAppSelector(imagesSelector); const { getUrl } = useGetUrl(); - const [isLoaded, { on, off }] = useBoolean(); - const handleDragStart = useCallback( (e: DragEvent) => { if (!image) { @@ -67,11 +66,11 @@ const CurrentImagePreview = () => { return ( {progressImage && shouldShowProgressInViewer ? ( @@ -91,28 +90,23 @@ const CurrentImagePreview = () => { /> ) : ( image && ( - - ) : ( - - ) - } - sx={{ - objectFit: 'contain', - maxWidth: '100%', - maxHeight: '100%', - height: 'auto', - position: 'absolute', - borderRadius: 'base', - }} - /> + <> + } + onDragStart={handleDragStart} + sx={{ + objectFit: 'contain', + maxWidth: '100%', + maxHeight: '100%', + height: 'auto', + position: 'absolute', + borderRadius: 'base', + }} + /> + + ) )} {shouldShowImageDetails && image && 'metadata' in image && ( diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryProgressImage.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryProgressImage.tsx index b812849c44..a2103eb8e2 100644 --- a/invokeai/frontend/web/src/features/gallery/components/GalleryProgressImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/GalleryProgressImage.tsx @@ -1,4 +1,4 @@ -import { Box, Flex, Image } from '@chakra-ui/react'; +import { Flex, Image, Spinner } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; @@ -42,6 +42,7 @@ const GalleryProgressImage = () => { alignItems: 'center', justifyContent: 'center', aspectRatio: '1/1', + position: 'relative', }} > { imageRendering: shouldAntialiasProgressImage ? 'auto' : 'pixelated', }} /> + ); }; diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx index 2e5f166025..35ddefe181 100644 --- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx @@ -278,6 +278,7 @@ const HoverableImage = memo((props: HoverableImageProps) => { h: 'full', transition: 'transform 0.2s ease-out', aspectRatio: '1/1', + cursor: 'pointer', }} > { +const ImageFallbackSpinner = (props: ImageFallbackSpinnerProps) => { const { size = 'xl', ...rest } = props; return ( @@ -21,4 +21,4 @@ const CurrentImageFallback = (props: CurrentImageFallbackProps) => { ); }; -export default CurrentImageFallback; +export default ImageFallbackSpinner; diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index 2326295451..81705086b3 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -1,6 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import { Image } from 'app/types/invokeai'; +import { imageReceived, thumbnailReceived } from 'services/thunks/image'; type GalleryImageObjectFitType = 'contain' | 'cover'; @@ -63,6 +64,29 @@ export const gallerySlice = createSlice({ state.shouldUseSingleGalleryColumn = action.payload; }, }, + 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; + } + }); + }, }); export const { diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx index fbb833a14a..3de1e1cebb 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/ImageToImage/InitialImagePreview.tsx @@ -1,17 +1,18 @@ -import { Flex, Image, Spinner } from '@chakra-ui/react'; +import { Flex, Image } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import SelectImagePlaceholder from 'common/components/SelectImagePlaceholder'; import { useGetUrl } from 'common/util/getUrl'; import { clearInitialImage } from 'features/parameters/store/generationSlice'; import { addToast } from 'features/system/store/systemSlice'; -import { DragEvent, useCallback, useState } from 'react'; +import { DragEvent, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { ImageType } from 'services/api'; -import ImageToImageOverlay from 'common/components/ImageToImageOverlay'; +import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay'; import { generationSelector } from 'features/parameters/store/generationSelectors'; import { initialImageSelected } from 'features/parameters/store/actions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import ImageFallbackSpinner from 'features/gallery/components/ImageFallbackSpinner'; const selector = createSelector( [generationSelector], @@ -30,8 +31,6 @@ const InitialImagePreview = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const [isLoaded, setIsLoaded] = useState(false); - const onError = () => { dispatch( addToast({ @@ -42,13 +41,10 @@ const InitialImagePreview = () => { }) ); dispatch(clearInitialImage()); - setIsLoaded(false); }; const handleDrop = useCallback( (e: DragEvent) => { - setIsLoaded(false); - const name = e.dataTransfer.getData('invokeai/imageName'); const type = e.dataTransfer.getData('invokeai/imageType') as ImageType; @@ -62,48 +58,32 @@ const InitialImagePreview = () => { sx={{ width: 'full', height: 'full', + position: 'relative', alignItems: 'center', justifyContent: 'center', - position: 'relative', }} onDrop={handleDrop} > - - {initialImage?.url && ( - <> - { - setIsLoaded(true); - }} - fallback={ - - - - } - /> - {isLoaded && } - - )} - {!initialImage?.url && } - + {initialImage?.url && ( + <> + } + onError={onError} + sx={{ + objectFit: 'contain', + maxWidth: '100%', + maxHeight: '100%', + height: 'auto', + position: 'absolute', + borderRadius: 'base', + }} + /> + + + )} + {!initialImage?.url && } ); }; diff --git a/invokeai/frontend/web/src/features/system/store/systemSlice.ts b/invokeai/frontend/web/src/features/system/store/systemSlice.ts index 1aeb2a1939..e9cbd21a15 100644 --- a/invokeai/frontend/web/src/features/system/store/systemSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/systemSlice.ts @@ -418,6 +418,7 @@ export const systemSlice = createSlice({ state.currentStep = 0; state.totalSteps = 0; state.statusTranslationKey = 'common.statusConnected'; + state.progressImage = null; state.toastQueue.push( makeToast({ title: t('toast.canceled'), status: 'warning' })