From 6bea7bac3668e5ea31ac223a575696775a37bb7e Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Thu, 13 Jul 2023 12:46:54 +1000
Subject: [PATCH] feat(ui): restore recall functionality
- Restore recall functionality to `CurrentImageButtons` and `ImageContextMenu`.
- Debounce metadata requests for `ImageMetadataViewer` and `CurrentImageButtons` by 500ms. It's possible to scroll through these really fast, so we want to debounce the network requests.
- `ImageContextMenu` is lazy-mounted so it does not need to be debounced; it makes the metadata request as soon as you click it.
- Move next/prev image selection logic into hook and add the hotkeys for this to `CurrentImageButtons`. The hotkeys now work when metadata viewer is open.
I will follow up with improved loading state during the debounced calls in the future
---
invokeai/frontend/web/package.json | 1 +
invokeai/frontend/web/src/app/store/store.ts | 21 +-
.../components/CurrentImageButtons.tsx | 47 ++-
.../components/CurrentImagePreview.tsx | 41 ++
.../gallery/components/ImageContextMenu.tsx | 366 +++++++++---------
.../ImageMetadataViewer.tsx | 25 +-
.../components/NextPrevImageButtons.tsx | 120 +-----
.../gallery/hooks/useNextPrevImage.ts | 108 ++++++
.../parameters/hooks/useRecallParameters.ts | 25 +-
invokeai/frontend/web/yarn.lock | 5 +
10 files changed, 420 insertions(+), 339 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/gallery/hooks/useNextPrevImage.ts
diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json
index 921b234aaf..81d6b0c7c7 100644
--- a/invokeai/frontend/web/package.json
+++ b/invokeai/frontend/web/package.json
@@ -108,6 +108,7 @@
"roarr": "^7.15.0",
"serialize-error": "^11.0.0",
"socket.io-client": "^4.7.0",
+ "use-debounce": "^9.0.4",
"use-image": "^1.1.1",
"uuid": "^9.0.0",
"zod": "^3.21.4"
diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts
index 5208933e7b..80688a1585 100644
--- a/invokeai/frontend/web/src/app/store/store.ts
+++ b/invokeai/frontend/web/src/app/store/store.ts
@@ -1,6 +1,7 @@
import {
AnyAction,
ThunkDispatch,
+ autoBatchEnhancer,
combineReducers,
configureStore,
} from '@reduxjs/toolkit';
@@ -79,14 +80,18 @@ const rememberedKeys: (keyof typeof allReducers)[] = [
export const store = configureStore({
reducer: rememberedRootReducer,
- enhancers: [
- rememberEnhancer(window.localStorage, rememberedKeys, {
- persistDebounce: 300,
- serialize,
- unserialize,
- prefix: LOCALSTORAGE_PREFIX,
- }),
- ],
+ enhancers: (existingEnhancers) => {
+ return existingEnhancers
+ .concat(
+ rememberEnhancer(window.localStorage, rememberedKeys, {
+ persistDebounce: 300,
+ serialize,
+ unserialize,
+ prefix: LOCALSTORAGE_PREFIX,
+ })
+ )
+ .concat(autoBatchEnhancer());
+ },
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
immutableCheck: false,
diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx
index efece3ee2f..4e227c4a7d 100644
--- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx
@@ -45,7 +45,11 @@ import {
FaShare,
FaShareAlt,
} from 'react-icons/fa';
-import { useGetImageDTOQuery } from 'services/api/endpoints/images';
+import {
+ useGetImageDTOQuery,
+ useGetImageMetadataQuery,
+} from 'services/api/endpoints/images';
+import { useDebounce } from 'use-debounce';
import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions';
const currentImageButtonsSelector = createSelector(
@@ -128,10 +132,23 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
const { recallBothPrompts, recallSeed, recallAllParameters } =
useRecallParameters();
- const { currentData: image } = useGetImageDTOQuery(
+ const [debouncedMetadataQueryArg, debounceState] = useDebounce(
+ lastSelectedImage,
+ 500
+ );
+
+ const { currentData: image, isFetching } = useGetImageDTOQuery(
lastSelectedImage ?? skipToken
);
+ const { currentData: metadataData } = useGetImageMetadataQuery(
+ debounceState.isPending()
+ ? skipToken
+ : debouncedMetadataQueryArg ?? skipToken
+ );
+
+ const metadata = metadataData?.metadata;
+
// const handleCopyImage = useCallback(async () => {
// if (!image?.url) {
// return;
@@ -193,29 +210,26 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
}, [toaster, t, image]);
const handleClickUseAllParameters = useCallback(() => {
- recallAllParameters(image);
- }, [image, recallAllParameters]);
+ recallAllParameters(metadata);
+ }, [metadata, recallAllParameters]);
useHotkeys(
'a',
() => {
handleClickUseAllParameters;
},
- [image, recallAllParameters]
+ [metadata, recallAllParameters]
);
const handleUseSeed = useCallback(() => {
- recallSeed(image?.metadata?.seed);
- }, [image, recallSeed]);
+ recallSeed(metadata?.seed);
+ }, [metadata?.seed, recallSeed]);
useHotkeys('s', handleUseSeed, [image]);
const handleUsePrompt = useCallback(() => {
- recallBothPrompts(
- image?.metadata?.positive_conditioning,
- image?.metadata?.negative_conditioning
- );
- }, [image, recallBothPrompts]);
+ recallBothPrompts(metadata?.positive_prompt, metadata?.negative_prompt);
+ }, [metadata?.negative_prompt, metadata?.positive_prompt, recallBothPrompts]);
useHotkeys('p', handleUsePrompt, [image]);
@@ -440,7 +454,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
icon={}
tooltip={`${t('parameters.usePrompt')} (P)`}
aria-label={`${t('parameters.usePrompt')} (P)`}
- isDisabled={!image?.metadata?.positive_conditioning}
+ isDisabled={!metadata?.positive_prompt}
onClick={handleUsePrompt}
/>
@@ -448,7 +462,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
icon={}
tooltip={`${t('parameters.useSeed')} (S)`}
aria-label={`${t('parameters.useSeed')} (S)`}
- isDisabled={!image?.metadata?.seed}
+ isDisabled={!metadata?.seed}
onClick={handleUseSeed}
/>
@@ -456,10 +470,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
icon={}
tooltip={`${t('parameters.useAll')} (A)`}
aria-label={`${t('parameters.useAll')} (A)`}
- isDisabled={
- // not sure what this list should be
- !['t2l', 'l2l', 'inpaint'].includes(String(image?.metadata?.type))
- }
+ isDisabled={!metadata}
onClick={handleClickUseAllParameters}
/>
diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx
index ef5228434e..9ef12871bb 100644
--- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx
@@ -11,7 +11,9 @@ import IAIDndImage from 'common/components/IAIDndImage';
import { selectLastSelectedImage } from 'features/gallery/store/gallerySlice';
import { isEqual } from 'lodash-es';
import { memo, useMemo } from 'react';
+import { useHotkeys } from 'react-hotkeys-hook';
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
+import { useNextPrevImage } from '../hooks/useNextPrevImage';
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
import NextPrevImageButtons from './NextPrevImageButtons';
@@ -49,6 +51,45 @@ const CurrentImagePreview = () => {
shouldAntialiasProgressImage,
} = useAppSelector(imagesSelector);
+ const {
+ handlePrevImage,
+ handleNextImage,
+ prevImageId,
+ nextImageId,
+ isOnLastImage,
+ handleLoadMoreImages,
+ areMoreImagesAvailable,
+ isFetching,
+ } = useNextPrevImage();
+
+ useHotkeys(
+ 'left',
+ () => {
+ handlePrevImage();
+ },
+ [prevImageId]
+ );
+
+ useHotkeys(
+ 'right',
+ () => {
+ if (isOnLastImage && areMoreImagesAvailable && !isFetching) {
+ handleLoadMoreImages();
+ return;
+ }
+ if (!isOnLastImage) {
+ handleNextImage();
+ }
+ },
+ [
+ nextImageId,
+ isOnLastImage,
+ areMoreImagesAvailable,
+ handleLoadMoreImages,
+ isFetching,
+ ]
+ );
+
const {
currentData: imageDTO,
isLoading,
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu.tsx
index 64b1d349d8..92da141054 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu.tsx
@@ -6,10 +6,7 @@ import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu';
-import {
- imagesAddedToBatch,
- selectionAddedToBatch,
-} from 'features/batch/store/batchSlice';
+import { imagesAddedToBatch } from 'features/batch/store/batchSlice';
import {
resizeAndScaleCanvas,
setInitialCanvasImage,
@@ -24,6 +21,7 @@ import { useTranslation } from 'react-i18next';
import { FaExpand, FaFolder, FaShare, FaTrash } from 'react-icons/fa';
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages';
+import { useGetImageMetadataQuery } from 'services/api/endpoints/images';
import { ImageDTO } from 'services/api/types';
import { AddImageToBoardContext } from '../../../app/contexts/AddImageToBoardContext';
import { sentImageToCanvas, sentImageToImg2Img } from '../store/actions';
@@ -38,24 +36,17 @@ const ImageContextMenu = ({ image, children }: Props) => {
() =>
createSelector(
[stateSelector],
- ({ gallery, batch }) => {
+ ({ gallery }) => {
const selectionCount = gallery.selection.length;
- const isInBatch = batch.imageNames.includes(image.image_name);
- return { selectionCount, isInBatch };
+ return { selectionCount };
},
defaultSelectorOptions
),
- [image.image_name]
+ []
);
- const { selectionCount, isInBatch } = useAppSelector(selector);
+ const { selectionCount } = useAppSelector(selector);
const dispatch = useAppDispatch();
- const { t } = useTranslation();
-
- const toaster = useAppToaster();
-
- const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
- const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
const { onClickAddToBoard } = useContext(AddImageToBoardContext);
@@ -66,178 +57,17 @@ const ImageContextMenu = ({ image, children }: Props) => {
dispatch(imageToDeleteSelected(image));
}, [dispatch, image]);
- const { recallBothPrompts, recallSeed, recallAllParameters } =
- useRecallParameters();
-
- const [removeFromBoard] = useRemoveImageFromBoardMutation();
-
- // Recall parameters handlers
- const handleRecallPrompt = useCallback(() => {
- recallBothPrompts(
- image.metadata?.positive_conditioning,
- image.metadata?.negative_conditioning
- );
- }, [
- image.metadata?.negative_conditioning,
- image.metadata?.positive_conditioning,
- recallBothPrompts,
- ]);
-
- const handleRecallSeed = useCallback(() => {
- recallSeed(image.metadata?.seed);
- }, [image, recallSeed]);
-
- const handleSendToImageToImage = useCallback(() => {
- dispatch(sentImageToImg2Img());
- dispatch(initialImageSelected(image));
- }, [dispatch, image]);
-
- // const handleRecallInitialImage = useCallback(() => {
- // recallInitialImage(image.metadata.invokeai?.node?.image);
- // }, [image, recallInitialImage]);
-
- const handleSendToCanvas = () => {
- dispatch(sentImageToCanvas());
- dispatch(setInitialCanvasImage(image));
- dispatch(resizeAndScaleCanvas());
- dispatch(setActiveTab('unifiedCanvas'));
-
- toaster({
- title: t('toast.sentToUnifiedCanvas'),
- status: 'success',
- duration: 2500,
- isClosable: true,
- });
- };
-
- const handleUseAllParameters = useCallback(() => {
- recallAllParameters(image);
- }, [image, recallAllParameters]);
-
- const handleLightBox = () => {
- // dispatch(setCurrentImage(image));
- // dispatch(setIsLightboxOpen(true));
- };
-
const handleAddToBoard = useCallback(() => {
onClickAddToBoard(image);
}, [image, onClickAddToBoard]);
- const handleRemoveFromBoard = useCallback(() => {
- if (!image.board_id) {
- return;
- }
- removeFromBoard({ board_id: image.board_id, image_name: image.image_name });
- }, [image.board_id, image.image_name, removeFromBoard]);
-
- const handleOpenInNewTab = () => {
- window.open(image.image_url, '_blank');
- };
-
- const handleAddSelectionToBatch = useCallback(() => {
- dispatch(selectionAddedToBatch());
- }, [dispatch]);
-
- const handleAddToBatch = useCallback(() => {
- dispatch(imagesAddedToBatch([image.image_name]));
- }, [dispatch, image.image_name]);
-
return (
menuProps={{ size: 'sm', isLazy: true }}
renderMenu={() => (
{selectionCount === 1 ? (
- <>
- }
- onClickCapture={handleOpenInNewTab}
- >
- {t('common.openInNewTab')}
-
- {isLightboxEnabled && (
- } onClickCapture={handleLightBox}>
- {t('parameters.openInViewer')}
-
- )}
- }
- onClickCapture={handleRecallPrompt}
- isDisabled={
- image?.metadata?.positive_conditioning === undefined
- }
- >
- {t('parameters.usePrompt')}
-
-
- }
- onClickCapture={handleRecallSeed}
- isDisabled={image?.metadata?.seed === undefined}
- >
- {t('parameters.useSeed')}
-
- {/* }
- onClickCapture={handleRecallInitialImage}
- isDisabled={image?.metadata?.type !== 'img2img'}
- >
- {t('parameters.useInitImg')}
- */}
- }
- onClickCapture={handleUseAllParameters}
- isDisabled={
- // what should these be
- !['t2l', 'l2l', 'inpaint'].includes(
- String(image?.metadata?.type)
- )
- }
- >
- {t('parameters.useAll')}
-
- }
- onClickCapture={handleSendToImageToImage}
- id="send-to-img2img"
- >
- {t('parameters.sendToImg2Img')}
-
- {isCanvasEnabled && (
- }
- onClickCapture={handleSendToCanvas}
- id="send-to-canvas"
- >
- {t('parameters.sendToUnifiedCanvas')}
-
- )}
- {/* }
- isDisabled={isInBatch}
- onClickCapture={handleAddToBatch}
- >
- Add to Batch
- */}
- } onClickCapture={handleAddToBoard}>
- {image.board_id ? 'Change Board' : 'Add to Board'}
-
- {image.board_id && (
- }
- onClickCapture={handleRemoveFromBoard}
- >
- Remove from Board
-
- )}
- }
- onClickCapture={handleDelete}
- >
- {t('gallery.deleteImage')}
-
- >
+
) : (
<>
+ {isLightboxEnabled && (
+ } onClickCapture={handleLightBox}>
+ {t('parameters.openInViewer')}
+
+ )}
+ }
+ onClickCapture={handleRecallPrompt}
+ isDisabled={
+ metadata?.positive_prompt === undefined &&
+ metadata?.negative_prompt === undefined
+ }
+ >
+ {t('parameters.usePrompt')}
+
+
+ }
+ onClickCapture={handleRecallSeed}
+ isDisabled={metadata?.seed === undefined}
+ >
+ {t('parameters.useSeed')}
+
+ }
+ onClickCapture={handleUseAllParameters}
+ isDisabled={!metadata}
+ >
+ {t('parameters.useAll')}
+
+ }
+ onClickCapture={handleSendToImageToImage}
+ id="send-to-img2img"
+ >
+ {t('parameters.sendToImg2Img')}
+
+ {isCanvasEnabled && (
+ }
+ onClickCapture={handleSendToCanvas}
+ id="send-to-canvas"
+ >
+ {t('parameters.sendToUnifiedCanvas')}
+
+ )}
+ }
+ isDisabled={isInBatch}
+ onClickCapture={handleAddToBatch}
+ >
+ Add to Batch
+
+ } onClickCapture={handleAddToBoard}>
+ {image.board_id ? 'Change Board' : 'Add to Board'}
+
+ {image.board_id && (
+ } onClickCapture={handleRemoveFromBoard}>
+ Remove from Board
+
+ )}
+ }
+ onClickCapture={handleDelete}
+ >
+ {t('gallery.deleteImage')}
+
+ >
+ );
+};
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 8a3078be47..83be19658f 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetaDataViewer/ImageMetadataViewer.tsx
@@ -13,6 +13,7 @@ import { skipToken } from '@reduxjs/toolkit/dist/query';
import { memo, useMemo } from 'react';
import { useGetImageMetadataQuery } from 'services/api/endpoints/images';
import { ImageDTO } from 'services/api/types';
+import { useDebounce } from 'use-debounce';
import ImageMetadataActions from './ImageMetadataActions';
import MetadataJSONViewer from './MetadataJSONViewer';
@@ -27,16 +28,26 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
// dispatch(setShouldShowImageDetails(false));
// });
- const { data } = useGetImageMetadataQuery(image?.image_name ?? skipToken);
- const metadata = data?.metadata;
+ const [debouncedMetadataQueryArg, debounceState] = useDebounce(
+ image.image_name,
+ 500
+ );
+
+ const { currentData } = useGetImageMetadataQuery(
+ debounceState.isPending()
+ ? skipToken
+ : debouncedMetadataQueryArg ?? skipToken
+ );
+ const metadata = currentData?.metadata;
+ const graph = currentData?.graph;
const tabData = useMemo(() => {
const _tabData: { label: string; data: object; copyTooltip: string }[] = [];
- if (data?.metadata) {
+ if (metadata) {
_tabData.push({
label: 'Core Metadata',
- data: data?.metadata,
+ data: metadata,
copyTooltip: 'Copy Core Metadata JSON',
});
}
@@ -49,15 +60,15 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
});
}
- if (data?.graph) {
+ if (graph) {
_tabData.push({
label: 'Graph',
- data: data?.graph,
+ data: graph,
copyTooltip: 'Copy Graph JSON',
});
}
return _tabData;
- }, [data?.metadata, data?.graph, image]);
+ }, [metadata, graph, image]);
return (
{
- const { total, isFetching } = state.gallery;
- const lastSelectedImage =
- state.gallery.selection[state.gallery.selection.length - 1];
-
- if (!lastSelectedImage || filteredImages.length === 0) {
- return {
- isOnFirstImage: true,
- isOnLastImage: true,
- };
- }
-
- const currentImageIndex = filteredImages.findIndex(
- (i) => i.image_name === lastSelectedImage
- );
- const nextImageIndex = clamp(
- currentImageIndex + 1,
- 0,
- filteredImages.length - 1
- );
-
- const prevImageIndex = clamp(
- currentImageIndex - 1,
- 0,
- filteredImages.length - 1
- );
-
- const nextImageId = filteredImages[nextImageIndex].image_name;
- const prevImageId = filteredImages[prevImageIndex].image_name;
-
- const nextImage = selectImagesById(state, nextImageId);
- const prevImage = selectImagesById(state, prevImageId);
-
- const imagesLength = filteredImages.length;
-
- return {
- isOnFirstImage: currentImageIndex === 0,
- isOnLastImage:
- !isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
- areMoreImagesAvailable: total > imagesLength,
- isFetching,
- nextImage,
- prevImage,
- nextImageId,
- prevImageId,
- };
- },
- {
- memoizeOptions: {
- resultEqualityCheck: isEqual,
- },
- }
-);
-
const NextPrevImageButtons = () => {
- const dispatch = useAppDispatch();
const { t } = useTranslation();
const {
+ handlePrevImage,
+ handleNextImage,
isOnFirstImage,
isOnLastImage,
- nextImageId,
- prevImageId,
+ handleLoadMoreImages,
areMoreImagesAvailable,
isFetching,
- } = useAppSelector(nextPrevImageButtonsSelector);
+ } = useNextPrevImage();
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
useState(false);
@@ -104,50 +38,6 @@ const NextPrevImageButtons = () => {
setShouldShowNextPrevButtons(false);
}, []);
- const handlePrevImage = useCallback(() => {
- prevImageId && dispatch(imageSelected(prevImageId));
- }, [dispatch, prevImageId]);
-
- const handleNextImage = useCallback(() => {
- nextImageId && dispatch(imageSelected(nextImageId));
- }, [dispatch, nextImageId]);
-
- const handleLoadMoreImages = useCallback(() => {
- dispatch(
- receivedPageOfImages({
- is_intermediate: false,
- })
- );
- }, [dispatch]);
-
- useHotkeys(
- 'left',
- () => {
- handlePrevImage();
- },
- [prevImageId]
- );
-
- useHotkeys(
- 'right',
- () => {
- if (isOnLastImage && areMoreImagesAvailable && !isFetching) {
- handleLoadMoreImages();
- return;
- }
- if (!isOnLastImage) {
- handleNextImage();
- }
- },
- [
- nextImageId,
- isOnLastImage,
- areMoreImagesAvailable,
- handleLoadMoreImages,
- isFetching,
- ]
- );
-
return (
{
+ const { total, isFetching } = state.gallery;
+ const lastSelectedImage =
+ state.gallery.selection[state.gallery.selection.length - 1];
+
+ if (!lastSelectedImage || filteredImages.length === 0) {
+ return {
+ isOnFirstImage: true,
+ isOnLastImage: true,
+ };
+ }
+
+ const currentImageIndex = filteredImages.findIndex(
+ (i) => i.image_name === lastSelectedImage
+ );
+ const nextImageIndex = clamp(
+ currentImageIndex + 1,
+ 0,
+ filteredImages.length - 1
+ );
+
+ const prevImageIndex = clamp(
+ currentImageIndex - 1,
+ 0,
+ filteredImages.length - 1
+ );
+
+ const nextImageId = filteredImages[nextImageIndex].image_name;
+ const prevImageId = filteredImages[prevImageIndex].image_name;
+
+ const nextImage = selectImagesById(state, nextImageId);
+ const prevImage = selectImagesById(state, prevImageId);
+
+ const imagesLength = filteredImages.length;
+
+ return {
+ isOnFirstImage: currentImageIndex === 0,
+ isOnLastImage:
+ !isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
+ areMoreImagesAvailable: total > imagesLength,
+ isFetching,
+ nextImage,
+ prevImage,
+ nextImageId,
+ prevImageId,
+ };
+ },
+ {
+ memoizeOptions: {
+ resultEqualityCheck: isEqual,
+ },
+ }
+);
+
+export const useNextPrevImage = () => {
+ const dispatch = useAppDispatch();
+
+ const {
+ isOnFirstImage,
+ isOnLastImage,
+ nextImageId,
+ prevImageId,
+ areMoreImagesAvailable,
+ isFetching,
+ } = useAppSelector(nextPrevImageButtonsSelector);
+
+ const handlePrevImage = useCallback(() => {
+ prevImageId && dispatch(imageSelected(prevImageId));
+ }, [dispatch, prevImageId]);
+
+ const handleNextImage = useCallback(() => {
+ nextImageId && dispatch(imageSelected(nextImageId));
+ }, [dispatch, nextImageId]);
+
+ const handleLoadMoreImages = useCallback(() => {
+ dispatch(
+ receivedPageOfImages({
+ is_intermediate: false,
+ })
+ );
+ }, [dispatch]);
+
+ return {
+ handlePrevImage,
+ handleNextImage,
+ isOnFirstImage,
+ isOnLastImage,
+ nextImageId,
+ prevImageId,
+ areMoreImagesAvailable,
+ handleLoadMoreImages,
+ isFetching,
+ };
+};
diff --git a/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts b/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts
index bf09ca3ccb..9e4f5aeff0 100644
--- a/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts
+++ b/invokeai/frontend/web/src/features/parameters/hooks/useRecallParameters.ts
@@ -2,6 +2,7 @@ import { useAppToaster } from 'app/components/Toaster';
import { useAppDispatch } from 'app/store/storeHooks';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
+import { UnsafeImageMetadata } from 'services/api/endpoints/images';
import { isImageField } from 'services/api/guards';
import { ImageDTO } from 'services/api/types';
import { initialImageSelected, modelSelected } from '../store/actions';
@@ -269,28 +270,24 @@ export const useRecallParameters = () => {
);
const recallAllParameters = useCallback(
- (image: ImageDTO | undefined) => {
- if (!image || !image.metadata) {
+ (metadata: UnsafeImageMetadata['metadata'] | undefined) => {
+ if (!metadata) {
allParameterNotSetToast();
return;
}
+
const {
cfg_scale,
height,
model,
- positive_conditioning,
- negative_conditioning,
+ positive_prompt,
+ negative_prompt,
scheduler,
seed,
steps,
width,
strength,
- clip,
- extra,
- latents,
- unet,
- vae,
- } = image.metadata;
+ } = metadata;
if (isValidCfgScale(cfg_scale)) {
dispatch(setCfgScale(cfg_scale));
@@ -298,11 +295,11 @@ export const useRecallParameters = () => {
if (isValidMainModel(model)) {
dispatch(modelSelected(model));
}
- if (isValidPositivePrompt(positive_conditioning)) {
- dispatch(setPositivePrompt(positive_conditioning));
+ if (isValidPositivePrompt(positive_prompt)) {
+ dispatch(setPositivePrompt(positive_prompt));
}
- if (isValidNegativePrompt(negative_conditioning)) {
- dispatch(setNegativePrompt(negative_conditioning));
+ if (isValidNegativePrompt(negative_prompt)) {
+ dispatch(setNegativePrompt(negative_prompt));
}
if (isValidScheduler(scheduler)) {
dispatch(setScheduler(scheduler));
diff --git a/invokeai/frontend/web/yarn.lock b/invokeai/frontend/web/yarn.lock
index c1ac8e9c7a..2db168a8ce 100644
--- a/invokeai/frontend/web/yarn.lock
+++ b/invokeai/frontend/web/yarn.lock
@@ -6409,6 +6409,11 @@ use-composed-ref@^1.3.0:
resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda"
integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==
+use-debounce@^9.0.4:
+ version "9.0.4"
+ resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-9.0.4.tgz#51d25d856fbdfeb537553972ce3943b897f1ac85"
+ integrity sha512-6X8H/mikbrt0XE8e+JXRtZ8yYVvKkdYRfmIhWZYsP8rcNs9hk3APV8Ua2mFkKRLcJKVdnX2/Vwrmg2GWKUQEaQ==
+
use-image@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/use-image/-/use-image-1.1.1.tgz#bdd3f2e1718393ffc0e56136f993467103d9d2df"