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')} - - + ) : ( <> { }; export default memo(ImageContextMenu); + +type SingleSelectionMenuItemsProps = { + image: ImageDTO; +}; + +const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { + const { image } = props; + + const selector = useMemo( + () => + createSelector( + [stateSelector], + ({ batch }) => { + const isInBatch = batch.imageNames.includes(image.image_name); + + return { isInBatch }; + }, + defaultSelectorOptions + ), + [image.image_name] + ); + + const { isInBatch } = 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); + + const { currentData } = useGetImageMetadataQuery(image.image_name); + + const metadata = currentData?.metadata; + + const handleDelete = useCallback(() => { + if (!image) { + return; + } + dispatch(imageToDeleteSelected(image)); + }, [dispatch, image]); + + const { recallBothPrompts, recallSeed, recallAllParameters } = + useRecallParameters(); + + const [removeFromBoard] = useRemoveImageFromBoardMutation(); + + // Recall parameters handlers + const handleRecallPrompt = useCallback(() => { + recallBothPrompts(metadata?.positive_prompt, metadata?.negative_prompt); + }, [metadata?.negative_prompt, metadata?.positive_prompt, recallBothPrompts]); + + const handleRecallSeed = useCallback(() => { + recallSeed(metadata?.seed); + }, [metadata?.seed, recallSeed]); + + const handleSendToImageToImage = useCallback(() => { + dispatch(sentImageToImg2Img()); + dispatch(initialImageSelected(image)); + }, [dispatch, image]); + + 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(() => { + console.log(metadata); + recallAllParameters(metadata); + }, [metadata, 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 handleAddToBatch = useCallback(() => { + dispatch(imagesAddedToBatch([image.image_name])); + }, [dispatch, image.image_name]); + + return ( + <> + } onClickCapture={handleOpenInNewTab}> + {t('common.openInNewTab')} + + {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"