diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts index b5c391afe4..3db8490a6e 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/invocationComplete.ts @@ -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 => - imageUrlsReceived.fulfilled.match(action) && - action.payload.image_name === name - ); + // const [{ payload }] = await take( + // (action): action is ReturnType => + // 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 => - imageRecordReceived.fulfilled.match(action) && - action.payload.image_name === name - ); + // const [x] = await take( + // ( + // action + // ): action is ReturnType => + // 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)); + // } } }, }); diff --git a/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx b/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx index 64d5e1beef..b96bc2ffe2 100644 --- a/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx +++ b/invokeai/frontend/web/src/common/components/ImageMetadataOverlay.tsx @@ -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 ( diff --git a/invokeai/frontend/web/src/common/util/dateComparator.ts b/invokeai/frontend/web/src/common/util/dateComparator.ts new file mode 100644 index 0000000000..ea0dc28b6d --- /dev/null +++ b/invokeai/frontend/web/src/common/util/dateComparator.ts @@ -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; +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx index 879123af2a..4562e3458d 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx @@ -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 && ( <> } onDragStart={handleDragStart} diff --git a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx index 8f3fff4ff3..e1e0f0458c 100644 --- a/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/HoverableImage.tsx @@ -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(false); @@ -144,8 +147,8 @@ const HoverableImage = memo((props: HoverableImageProps) => { const handleDragStart = useCallback( (e: DragEvent) => { - 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) => { } onClickCapture={handleRecallPrompt} - isDisabled={image?.metadata?.invokeai?.node?.prompt === undefined} + isDisabled={image?.metadata?.positive_conditioning === undefined} > {t('parameters.usePrompt')} @@ -231,14 +234,14 @@ const HoverableImage = memo((props: HoverableImageProps) => { } onClickCapture={handleRecallSeed} - isDisabled={image?.metadata?.invokeai?.node?.seed === undefined} + isDisabled={image?.metadata?.seed === undefined} > {t('parameters.useSeed')} } onClickCapture={handleRecallInitialImage} - isDisabled={image?.metadata?.invokeai?.node?.type !== 'img2img'} + isDisabled={image?.metadata?.type !== 'img2img'} > {t('parameters.useInitImg')} @@ -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) => ( { shouldUseSingleGalleryColumn ? 'contain' : galleryImageObjectFit } rounded="md" - src={getUrl(thumbnail || url)} + src={getUrl(thumbnail_url || image_url)} fallback={} sx={{ width: '100%', diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx index 9770ed5887..ce375f9580 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx @@ -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 ( @@ -362,7 +363,7 @@ const ImageGalleryContent = () => { /> ) : ( @@ -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 ? ( ) : ( diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx index c23412a87d..3ec820ade7 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx @@ -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 ( {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; + const sessionId = image?.session_id; + + const metadata = image?.metadata; const { t } = useTranslation(); const { getUrl } = useGetUrl(); @@ -154,110 +159,133 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => { > File: - - {image.url.length > 64 - ? image.url.substring(0, 64).concat('...') - : image.url} + + {image.image_url.length > 64 + ? image.image_url.substring(0, 64).concat('...') + : image.image_url} - {node && Object.keys(node).length > 0 ? ( + {metadata && Object.keys(metadata).length > 0 ? ( <> - {node.type && ( - + {metadata.type && ( + )} - {node.model && } - {node.prompt && ( + {metadata.width && ( + dispatch(setWidth(Number(metadata.width)))} + /> + )} + {metadata.height && ( + dispatch(setHeight(Number(metadata.height)))} + /> + )} + {metadata.model && ( + + )} + {metadata.positive_conditioning && ( setBothPrompts(node.prompt)} + onClick={() => setPrompt(metadata.positive_conditioning!)} /> )} - {node.seed !== undefined && ( + {metadata.negative_conditioning && ( + setNegativePrompt(metadata.negative_conditioning!)} + /> + )} + {metadata.seed !== undefined && ( dispatch(setSeed(Number(node.seed)))} + value={metadata.seed} + onClick={() => dispatch(setSeed(Number(metadata.seed)))} /> )} - {node.threshold !== undefined && ( + {/* {metadata.threshold !== undefined && ( dispatch(setThreshold(Number(node.threshold)))} + value={metadata.threshold} + onClick={() => dispatch(setThreshold(Number(metadata.threshold)))} /> )} - {node.perlin !== undefined && ( + {metadata.perlin !== undefined && ( dispatch(setPerlin(Number(node.perlin)))} + value={metadata.perlin} + onClick={() => dispatch(setPerlin(Number(metadata.perlin)))} /> - )} - {node.scheduler && ( + )} */} + {metadata.scheduler && ( dispatch(setScheduler(node.scheduler))} - /> - )} - {node.steps && ( - dispatch(setSteps(Number(node.steps)))} - /> - )} - {node.cfg_scale !== undefined && ( - dispatch(setCfgScale(Number(node.cfg_scale)))} - /> - )} - {node.variations && node.variations.length > 0 && ( - - dispatch(setSeedWeights(seedWeightsToString(node.variations))) + dispatch(setScheduler(metadata.scheduler as Scheduler)) } /> )} - {node.seamless && ( + {metadata.steps && ( + dispatch(setSteps(Number(metadata.steps)))} + /> + )} + {metadata.cfg_scale !== undefined && ( + dispatch(setCfgScale(Number(metadata.cfg_scale)))} + /> + )} + {/* {metadata.variations && metadata.variations.length > 0 && ( + + dispatch( + setSeedWeights(seedWeightsToString(metadata.variations)) + ) + } + /> + )} + {metadata.seamless && ( dispatch(setSeamless(node.seamless))} + value={metadata.seamless} + onClick={() => dispatch(setSeamless(metadata.seamless))} /> )} - {node.hires_fix && ( + {metadata.hires_fix && ( dispatch(setHiresFix(node.hires_fix))} + value={metadata.hires_fix} + onClick={() => dispatch(setHiresFix(metadata.hires_fix))} /> - )} - {node.width && ( - dispatch(setWidth(Number(node.width)))} - /> - )} - {node.height && ( - dispatch(setHeight(Number(node.height)))} - /> - )} + )} */} + {/* {init_image_path && ( { onClick={() => dispatch(setInitialImage(init_image_path))} /> )} */} - {node.strength && ( + {metadata.strength && ( - dispatch(setImg2imgStrength(Number(node.strength))) + dispatch(setImg2imgStrength(Number(metadata.strength))) } /> )} - {node.fit && ( + {/* {metadata.fit && ( dispatch(setShouldFitToWidthHeight(node.fit))} + value={metadata.fit} + onClick={() => dispatch(setShouldFitToWidthHeight(metadata.fit))} /> - )} + )} */} ) : (
diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index 96c3486b50..9d6f5ece60 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -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) => { + imageSelected: (state, action: PayloadAction) => { 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; } } diff --git a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts index f1286137a9..125f4ff5d5 100644 --- a/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/resultsSlice.ts @@ -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({ - selectId: (image) => image.name, - sortComparer: (a, b) => b.metadata.created - a.metadata.created, +export type ResultsImageDTO = Omit & { + image_type: 'results'; +}; + +export const resultsAdapter = createEntityAdapter({ + 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, + }, + }); + } }); /** diff --git a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts index d0a7821d9d..5910cc087b 100644 --- a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts @@ -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({ - selectId: (image) => image.name, - sortComparer: (a, b) => b.metadata.created - a.metadata.created, +export type UploadsImageDTO = Omit & { + image_type: 'uploads'; +}; + +export const uploadsAdapter = createEntityAdapter({ + 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 diff --git a/invokeai/frontend/web/src/features/ui/store/uiPersistDenylist.ts b/invokeai/frontend/web/src/features/ui/store/uiPersistDenylist.ts index 9f6bd2dd73..b485d71bdd 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiPersistDenylist.ts @@ -3,4 +3,4 @@ import { UIState } from './uiTypes'; /** * UI slice persist denylist */ -export const uiPersistDenylist: (keyof UIState)[] = []; +export const uiPersistDenylist: (keyof UIState)[] = ['shouldShowImageDetails']; diff --git a/invokeai/frontend/web/src/services/thunks/gallery.ts b/invokeai/frontend/web/src/services/thunks/gallery.ts index f908cbddcb..694d44db3e 100644 --- a/invokeai/frontend/web/src/services/thunks/gallery.ts +++ b/invokeai/frontend/web/src/services/thunks/gallery.ts @@ -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, }); diff --git a/invokeai/frontend/web/src/services/thunks/image.ts b/invokeai/frontend/web/src/services/thunks/image.ts index 5528e41bfc..bf6cc40e2e 100644 --- a/invokeai/frontend/web/src/services/thunks/image.ts +++ b/invokeai/frontend/web/src/services/thunks/image.ts @@ -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