feat(ui): rework comparison activation, add hotkeys

This commit is contained in:
psychedelicious 2024-05-31 20:26:34 +10:00
parent 3501636018
commit 0e5336d8fa
16 changed files with 153 additions and 161 deletions

View File

@ -386,7 +386,8 @@
"sideBySide": "Side-by-Side", "sideBySide": "Side-by-Side",
"swapImages": "Swap Images", "swapImages": "Swap Images",
"compareOptions": "Comparison Options", "compareOptions": "Comparison Options",
"sliderFitLabel": "Stretch second image to fit" "sliderFitLabel": "Stretch second image to fit",
"exitCompare": "Exit Compare"
}, },
"hotkeys": { "hotkeys": {
"searchHotkeys": "Search Hotkeys", "searchHotkeys": "Search Hotkeys",

View File

@ -1,6 +1,6 @@
import { enqueueRequested } from 'app/store/actions'; import { enqueueRequested } from 'app/store/actions';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { viewerModeChanged } from 'features/gallery/store/gallerySlice'; import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig'; import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildGenerationTabGraph } from 'features/nodes/util/graph/generation/buildGenerationTabGraph'; import { buildGenerationTabGraph } from 'features/nodes/util/graph/generation/buildGenerationTabGraph';
import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/generation/buildGenerationTabSDXLGraph'; import { buildGenerationTabSDXLGraph } from 'features/nodes/util/graph/generation/buildGenerationTabSDXLGraph';
@ -34,7 +34,7 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
try { try {
await req.unwrap(); await req.unwrap();
if (shouldShowProgressInViewer) { if (shouldShowProgressInViewer) {
dispatch(viewerModeChanged('view')); dispatch(isImageViewerOpenChanged(true));
} }
} finally { } finally {
req.reset(); req.reset();

View File

@ -1,7 +1,7 @@
import { createAction } from '@reduxjs/toolkit'; import { createAction } from '@reduxjs/toolkit';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors'; import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
import { selectionChanged } from 'features/gallery/store/gallerySlice'; import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
import { imagesApi } from 'services/api/endpoints/images'; import { imagesApi } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types'; import type { ImageDTO } from 'services/api/types';
import { imagesSelectors } from 'services/api/util'; import { imagesSelectors } from 'services/api/util';
@ -11,6 +11,7 @@ export const galleryImageClicked = createAction<{
shiftKey: boolean; shiftKey: boolean;
ctrlKey: boolean; ctrlKey: boolean;
metaKey: boolean; metaKey: boolean;
altKey: boolean;
}>('gallery/imageClicked'); }>('gallery/imageClicked');
/** /**
@ -28,7 +29,7 @@ export const addGalleryImageClickedListener = (startAppListening: AppStartListen
startAppListening({ startAppListening({
actionCreator: galleryImageClicked, actionCreator: galleryImageClicked,
effect: async (action, { dispatch, getState }) => { effect: async (action, { dispatch, getState }) => {
const { imageDTO, shiftKey, ctrlKey, metaKey } = action.payload; const { imageDTO, shiftKey, ctrlKey, metaKey, altKey } = action.payload;
const state = getState(); const state = getState();
const queryArgs = selectListImagesQueryArgs(state); const queryArgs = selectListImagesQueryArgs(state);
const { data: listImagesData } = imagesApi.endpoints.listImages.select(queryArgs)(state); const { data: listImagesData } = imagesApi.endpoints.listImages.select(queryArgs)(state);
@ -41,7 +42,9 @@ export const addGalleryImageClickedListener = (startAppListening: AppStartListen
const imageDTOs = imagesSelectors.selectAll(listImagesData); const imageDTOs = imagesSelectors.selectAll(listImagesData);
const selection = state.gallery.selection; const selection = state.gallery.selection;
if (shiftKey) { if (altKey) {
dispatch(imageToCompareChanged(imageDTO));
} else if (shiftKey) {
const rangeEndImageName = imageDTO.image_name; const rangeEndImageName = imageDTO.image_name;
const lastSelectedImage = selection[selection.length - 1]?.image_name; const lastSelectedImage = selection[selection.length - 1]?.image_name;
const lastClickedIndex = imageDTOs.findIndex((n) => n.image_name === lastSelectedImage); const lastClickedIndex = imageDTOs.findIndex((n) => n.image_name === lastSelectedImage);

View File

@ -7,7 +7,7 @@ import {
boardIdSelected, boardIdSelected,
galleryViewChanged, galleryViewChanged,
imageSelected, imageSelected,
viewerModeChanged, isImageViewerOpenChanged,
} from 'features/gallery/store/gallerySlice'; } from 'features/gallery/store/gallerySlice';
import { IMAGE_CATEGORIES } from 'features/gallery/store/types'; import { IMAGE_CATEGORIES } from 'features/gallery/store/types';
import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState'; import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
@ -108,7 +108,7 @@ export const addInvocationCompleteEventListener = (startAppListening: AppStartLi
} }
dispatch(imageSelected(imageDTO)); dispatch(imageSelected(imageDTO));
dispatch(viewerModeChanged('view')); dispatch(isImageViewerOpenChanged(true));
} }
} }
} }

View File

@ -46,11 +46,7 @@ type SingleSelectionMenuItemsProps = {
const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
const { imageDTO } = props; const { imageDTO } = props;
const optimalDimension = useAppSelector(selectOptimalDimension); const optimalDimension = useAppSelector(selectOptimalDimension);
const maySelectForCompare = useAppSelector( const maySelectForCompare = useAppSelector((s) => s.gallery.imageToCompare?.image_name !== imageDTO.image_name);
(s) =>
s.gallery.imageToCompare?.image_name !== imageDTO.image_name &&
s.gallery.selection.slice(-1)[0]?.image_name !== imageDTO.image_name
);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const isCanvasEnabled = useFeatureStatus('canvas'); const isCanvasEnabled = useFeatureStatus('canvas');

View File

@ -11,7 +11,7 @@ import type { GallerySelectionDraggableData, ImageDraggableData, TypesafeDraggab
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId'; import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
import { useMultiselect } from 'features/gallery/hooks/useMultiselect'; import { useMultiselect } from 'features/gallery/hooks/useMultiselect';
import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView'; import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView';
import { viewerModeChanged } from 'features/gallery/store/gallerySlice'; import { imageToCompareChanged, isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import { memo, useCallback, useMemo, useState } from 'react'; import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -46,9 +46,7 @@ const GalleryImage = (props: HoverableImageProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId); const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
const alwaysShowImageSizeBadge = useAppSelector((s) => s.gallery.alwaysShowImageSizeBadge); const alwaysShowImageSizeBadge = useAppSelector((s) => s.gallery.alwaysShowImageSizeBadge);
const isSelectedForCompare = useAppSelector( const isSelectedForCompare = useAppSelector((s) => s.gallery.imageToCompare?.image_name === imageName);
(s) => s.gallery.imageToCompare?.image_name === imageName && s.gallery.viewerMode === 'compare'
);
const { handleClick, isSelected, areMultiplesSelected } = useMultiselect(imageDTO); const { handleClick, isSelected, areMultiplesSelected } = useMultiselect(imageDTO);
const customStarUi = useStore($customStarUI); const customStarUi = useStore($customStarUI);
@ -107,7 +105,8 @@ const GalleryImage = (props: HoverableImageProps) => {
}, []); }, []);
const onDoubleClick = useCallback(() => { const onDoubleClick = useCallback(() => {
dispatch(viewerModeChanged('view')); dispatch(isImageViewerOpenChanged(true));
dispatch(imageToCompareChanged(null));
}, [dispatch]); }, [dispatch]);
const handleMouseOut = useCallback(() => { const handleMouseOut = useCallback(() => {

View File

@ -12,7 +12,12 @@ import {
Switch, Switch,
} from '@invoke-ai/ui-library'; } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { comparedImagesSwapped, comparisonModeChanged, sliderFitChanged } from 'features/gallery/store/gallerySlice'; import {
comparedImagesSwapped,
comparisonModeChanged,
imageToCompareChanged,
sliderFitChanged,
} from 'features/gallery/store/gallerySlice';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -38,20 +43,12 @@ export const ImageComparisonToolbarButtons = memo(() => {
}, },
[dispatch] [dispatch]
); );
const exitCompare = useCallback(() => {
dispatch(imageToCompareChanged(null));
}, [dispatch]);
return ( return (
<> <>
<ButtonGroup variant="outline">
<Button onClick={setComparisonModeSlider} colorScheme={comparisonMode === 'slider' ? 'invokeBlue' : 'base'}>
{t('gallery.slider')}
</Button>
<Button
onClick={setComparisonModeSideBySide}
colorScheme={comparisonMode === 'side-by-side' ? 'invokeBlue' : 'base'}
>
{t('gallery.sideBySide')}
</Button>
</ButtonGroup>
<Popover isLazy> <Popover isLazy>
<PopoverTrigger> <PopoverTrigger>
<IconButton <IconButton
@ -63,13 +60,25 @@ export const ImageComparisonToolbarButtons = memo(() => {
<PopoverContent> <PopoverContent>
<PopoverBody> <PopoverBody>
<Flex direction="column" gap={2}> <Flex direction="column" gap={2}>
<FormControl> <ButtonGroup variant="outline" size="sm" w="full">
<Button
flex={1}
onClick={setComparisonModeSlider}
colorScheme={comparisonMode === 'slider' ? 'invokeBlue' : 'base'}
>
{t('gallery.slider')}
</Button>
<Button
flex={1}
onClick={setComparisonModeSideBySide}
colorScheme={comparisonMode === 'side-by-side' ? 'invokeBlue' : 'base'}
>
{t('gallery.sideBySide')}
</Button>
</ButtonGroup>
<FormControl isDisabled={comparisonMode !== 'slider'}>
<FormLabel>{t('gallery.sliderFitLabel')}</FormLabel> <FormLabel>{t('gallery.sliderFitLabel')}</FormLabel>
<Switch <Switch isChecked={sliderFit === 'fill'} onChange={onSliderFitChanged} />
isChecked={sliderFit === 'fill'}
isDisabled={comparisonMode !== 'slider'}
onChange={onSliderFitChanged}
/>
</FormControl> </FormControl>
</Flex> </Flex>
</PopoverBody> </PopoverBody>
@ -77,6 +86,7 @@ export const ImageComparisonToolbarButtons = memo(() => {
</Popover> </Popover>
<Button onClick={swapImages}>{t('gallery.swapImages')}</Button> <Button onClick={swapImages}>{t('gallery.swapImages')}</Button>
<Button onClick={exitCompare}>{t('gallery.exitCompare')}</Button>
</> </>
); );
}); });

View File

@ -17,18 +17,19 @@ import { ViewerToggleMenu } from './ViewerToggleMenu';
const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows']; const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows'];
export const ImageViewer = memo(() => { export const ImageViewer = memo(() => {
const { viewerMode, onToggle, openEditor } = useImageViewer(); const { isOpen, onToggle, onClose } = useImageViewer();
const activeTabName = useAppSelector(activeTabNameSelector); const activeTabName = useAppSelector(activeTabNameSelector);
const isComparing = useAppSelector((s) => s.gallery.imageToCompare !== null);
const isViewerEnabled = useMemo(() => VIEWER_ENABLED_TABS.includes(activeTabName), [activeTabName]); const isViewerEnabled = useMemo(() => VIEWER_ENABLED_TABS.includes(activeTabName), [activeTabName]);
const shouldShowViewer = useMemo(() => { const shouldShowViewer = useMemo(() => {
if (!isViewerEnabled) { if (!isViewerEnabled) {
return false; return false;
} }
return viewerMode === 'view' || viewerMode === 'compare'; return isOpen;
}, [viewerMode, isViewerEnabled]); }, [isOpen, isViewerEnabled]);
useHotkeys('z', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]); useHotkeys('z', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]);
useHotkeys('esc', openEditor, { enabled: isViewerEnabled }, [isViewerEnabled, openEditor]); useHotkeys('esc', onClose, { enabled: isViewerEnabled }, [isViewerEnabled, onClose]);
if (!shouldShowViewer) { if (!shouldShowViewer) {
return null; return null;
@ -58,8 +59,8 @@ export const ImageViewer = memo(() => {
</Flex> </Flex>
</Flex> </Flex>
<Flex flex={1} gap={2} justifyContent="center"> <Flex flex={1} gap={2} justifyContent="center">
{viewerMode === 'view' && <CurrentImageButtons />} {!isComparing && <CurrentImageButtons />}
{viewerMode === 'compare' && <ImageComparisonToolbarButtons />} {isComparing && <ImageComparisonToolbarButtons />}
</Flex> </Flex>
<Flex flex={1} justifyContent="center"> <Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineStart="auto"> <Flex gap={2} marginInlineStart="auto">
@ -68,8 +69,8 @@ export const ImageViewer = memo(() => {
</Flex> </Flex>
</Flex> </Flex>
<Box w="full" h="full"> <Box w="full" h="full">
{viewerMode === 'view' && <CurrentImagePreview />} {!isComparing && <CurrentImagePreview />}
{viewerMode === 'compare' && <ImageComparison />} {isComparing && <ImageComparison />}
</Box> </Box>
</Flex> </Flex>
); );

View File

@ -8,60 +8,23 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
Text, Text,
useDisclosure,
} from '@invoke-ai/ui-library'; } from '@invoke-ai/ui-library';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PiCaretDownBold, PiCheckBold, PiEyeBold, PiImagesBold, PiPencilBold } from 'react-icons/pi'; import { PiCaretDownBold, PiCheckBold, PiEyeBold, PiPencilBold } from 'react-icons/pi';
import { useImageViewer } from './useImageViewer'; import { useImageViewer } from './useImageViewer';
export const ViewerToggleMenu = () => { export const ViewerToggleMenu = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onClose, onOpen } = useImageViewer();
const { viewerMode, openEditor, openViewer, openCompare } = useImageViewer();
const icon = useMemo(() => {
if (viewerMode === 'view') {
return <Icon as={PiEyeBold} />;
}
if (viewerMode === 'edit') {
return <Icon as={PiPencilBold} />;
}
if (viewerMode === 'compare') {
return <Icon as={PiImagesBold} />;
}
}, [viewerMode]);
const label = useMemo(() => {
if (viewerMode === 'view') {
return t('common.viewing');
}
if (viewerMode === 'edit') {
return t('common.editing');
}
if (viewerMode === 'compare') {
return t('common.comparing');
}
}, [t, viewerMode]);
const _openEditor = useCallback(() => {
openEditor();
onClose();
}, [onClose, openEditor]);
const _openViewer = useCallback(() => {
openViewer();
onClose();
}, [onClose, openViewer]);
const _openCompare = useCallback(() => {
openCompare();
onClose();
}, [onClose, openCompare]);
return ( return (
<Popover isOpen={isOpen} onClose={onClose} onOpen={onOpen}> <Popover isLazy>
<PopoverTrigger> <PopoverTrigger>
<Button variant="outline" data-testid="toggle-viewer-menu-button"> <Button variant="outline" data-testid="toggle-viewer-menu-button">
<Flex gap={3} w="full" alignItems="center"> <Flex gap={3} w="full" alignItems="center">
{icon} {isOpen ? <Icon as={PiEyeBold} /> : <Icon as={PiPencilBold} />}
<Text fontSize="md">{label}</Text> <Text fontSize="md">{isOpen ? t('common.viewing') : t('common.editing')}</Text>
<Icon as={PiCaretDownBold} /> <Icon as={PiCaretDownBold} />
</Flex> </Flex>
</Button> </Button>
@ -70,9 +33,9 @@ export const ViewerToggleMenu = () => {
<PopoverArrow /> <PopoverArrow />
<PopoverBody> <PopoverBody>
<Flex flexDir="column"> <Flex flexDir="column">
<Button onClick={_openViewer} variant="ghost" h="auto" w="auto" p={2}> <Button onClick={onOpen} variant="ghost" h="auto" w="auto" p={2}>
<Flex gap={2} w="full"> <Flex gap={2} w="full">
<Icon as={PiCheckBold} visibility={viewerMode === 'view' ? 'visible' : 'hidden'} /> <Icon as={PiCheckBold} visibility={isOpen ? 'visible' : 'hidden'} />
<Flex flexDir="column" gap={2} alignItems="flex-start"> <Flex flexDir="column" gap={2} alignItems="flex-start">
<Text fontWeight="semibold" color="base.100"> <Text fontWeight="semibold" color="base.100">
{t('common.viewing')} {t('common.viewing')}
@ -83,9 +46,9 @@ export const ViewerToggleMenu = () => {
</Flex> </Flex>
</Flex> </Flex>
</Button> </Button>
<Button onClick={_openEditor} variant="ghost" h="auto" w="auto" p={2}> <Button onClick={onClose} variant="ghost" h="auto" w="auto" p={2}>
<Flex gap={2} w="full"> <Flex gap={2} w="full">
<Icon as={PiCheckBold} visibility={viewerMode === 'edit' ? 'visible' : 'hidden'} /> <Icon as={PiCheckBold} visibility={isOpen ? 'hidden' : 'visible'} />
<Flex flexDir="column" gap={2} alignItems="flex-start"> <Flex flexDir="column" gap={2} alignItems="flex-start">
<Text fontWeight="semibold" color="base.100"> <Text fontWeight="semibold" color="base.100">
{t('common.editing')} {t('common.editing')}
@ -96,19 +59,6 @@ export const ViewerToggleMenu = () => {
</Flex> </Flex>
</Flex> </Flex>
</Button> </Button>
<Button onClick={_openCompare} variant="ghost" h="auto" w="auto" p={2}>
<Flex gap={2} w="full">
<Icon as={PiCheckBold} visibility={viewerMode === 'compare' ? 'visible' : 'hidden'} />
<Flex flexDir="column" gap={2} alignItems="flex-start">
<Text fontWeight="semibold" color="base.100">
{t('common.comparing')}
</Text>
<Text fontWeight="normal" color="base.300">
{t('common.comparingDesc')}
</Text>
</Flex>
</Flex>
</Button>
</Flex> </Flex>
</PopoverBody> </PopoverBody>
</PopoverContent> </PopoverContent>

View File

@ -1,26 +1,22 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { viewerModeChanged } from 'features/gallery/store/gallerySlice'; import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
import { useCallback } from 'react'; import { useCallback } from 'react';
export const useImageViewer = () => { export const useImageViewer = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const viewerMode = useAppSelector((s) => s.gallery.viewerMode); const isOpen = useAppSelector((s) => s.gallery.isImageViewerOpen);
const openEditor = useCallback(() => { const onClose = useCallback(() => {
dispatch(viewerModeChanged('edit')); dispatch(isImageViewerOpenChanged(false));
}, [dispatch]); }, [dispatch]);
const openViewer = useCallback(() => { const onOpen = useCallback(() => {
dispatch(viewerModeChanged('view')); dispatch(isImageViewerOpenChanged(true));
}, [dispatch]); }, [dispatch]);
const onToggle = useCallback(() => { const onToggle = useCallback(() => {
dispatch(viewerModeChanged(viewerMode === 'view' ? 'edit' : 'view')); dispatch(isImageViewerOpenChanged(!isOpen));
}, [dispatch, viewerMode]); }, [dispatch, isOpen]);
const openCompare = useCallback(() => { return { isOpen, onOpen, onClose, onToggle };
dispatch(viewerModeChanged('compare'));
}, [dispatch]);
return { viewerMode, openEditor, openViewer, openCompare, onToggle };
}; };

View File

@ -27,16 +27,16 @@ export const useGalleryHotkeys = () => {
useGalleryNavigation(); useGalleryNavigation();
useHotkeys( useHotkeys(
'left', ['left', 'alt+left'],
() => { (e) => {
canNavigateGallery && handleLeftImage(); canNavigateGallery && handleLeftImage(e.altKey);
}, },
[handleLeftImage, canNavigateGallery] [handleLeftImage, canNavigateGallery]
); );
useHotkeys( useHotkeys(
'right', ['right', 'alt+right'],
() => { (e) => {
if (!canNavigateGallery) { if (!canNavigateGallery) {
return; return;
} }
@ -45,29 +45,29 @@ export const useGalleryHotkeys = () => {
return; return;
} }
if (!isOnLastImage) { if (!isOnLastImage) {
handleRightImage(); handleRightImage(e.altKey);
} }
}, },
[isOnLastImage, areMoreImagesAvailable, handleLoadMoreImages, isFetching, handleRightImage, canNavigateGallery] [isOnLastImage, areMoreImagesAvailable, handleLoadMoreImages, isFetching, handleRightImage, canNavigateGallery]
); );
useHotkeys( useHotkeys(
'up', ['up', 'alt+up'],
() => { (e) => {
handleUpImage(); handleUpImage(e.altKey);
}, },
{ preventDefault: true }, { preventDefault: true },
[handleUpImage] [handleUpImage]
); );
useHotkeys( useHotkeys(
'down', ['down', 'alt+down'],
() => { (e) => {
if (!areImagesBelowCurrent && areMoreImagesAvailable && !isFetching) { if (!areImagesBelowCurrent && areMoreImagesAvailable && !isFetching) {
handleLoadMoreImages(); handleLoadMoreImages();
return; return;
} }
handleDownImage(); handleDownImage(e.altKey);
}, },
{ preventDefault: true }, { preventDefault: true },
[areImagesBelowCurrent, areMoreImagesAvailable, handleLoadMoreImages, isFetching, handleDownImage] [areImagesBelowCurrent, areMoreImagesAvailable, handleLoadMoreImages, isFetching, handleDownImage]

View File

@ -1,11 +1,11 @@
import { useAltModifier } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId'; import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
import { imageItemContainerTestId } from 'features/gallery/components/ImageGrid/ImageGridItemContainer'; import { imageItemContainerTestId } from 'features/gallery/components/ImageGrid/ImageGridItemContainer';
import { imageListContainerTestId } from 'features/gallery/components/ImageGrid/ImageGridListContainer'; import { imageListContainerTestId } from 'features/gallery/components/ImageGrid/ImageGridListContainer';
import { virtuosoGridRefs } from 'features/gallery/components/ImageGrid/types'; import { virtuosoGridRefs } from 'features/gallery/components/ImageGrid/types';
import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages'; import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; import { imageSelected, imageToCompareChanged } from 'features/gallery/store/gallerySlice';
import { imageSelected } from 'features/gallery/store/gallerySlice';
import { getIsVisible } from 'features/gallery/util/getIsVisible'; import { getIsVisible } from 'features/gallery/util/getIsVisible';
import { getScrollToIndexAlign } from 'features/gallery/util/getScrollToIndexAlign'; import { getScrollToIndexAlign } from 'features/gallery/util/getScrollToIndexAlign';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
@ -106,10 +106,10 @@ const getImageFuncs = {
}; };
type UseGalleryNavigationReturn = { type UseGalleryNavigationReturn = {
handleLeftImage: () => void; handleLeftImage: (alt?: boolean) => void;
handleRightImage: () => void; handleRightImage: (alt?: boolean) => void;
handleUpImage: () => void; handleUpImage: (alt?: boolean) => void;
handleDownImage: () => void; handleDownImage: (alt?: boolean) => void;
isOnFirstImage: boolean; isOnFirstImage: boolean;
isOnLastImage: boolean; isOnLastImage: boolean;
areImagesBelowCurrent: boolean; areImagesBelowCurrent: boolean;
@ -123,7 +123,15 @@ type UseGalleryNavigationReturn = {
*/ */
export const useGalleryNavigation = (): UseGalleryNavigationReturn => { export const useGalleryNavigation = (): UseGalleryNavigationReturn => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const lastSelectedImage = useAppSelector(selectLastSelectedImage); const alt = useAltModifier();
const lastSelectedImage = useAppSelector((s) => {
const lastSelected = s.gallery.selection.slice(-1)[0] ?? null;
if (alt) {
return s.gallery.imageToCompare ?? lastSelected;
} else {
return lastSelected;
}
});
const { const {
queryResult: { data }, queryResult: { data },
} = useGalleryImages(); } = useGalleryImages();
@ -136,7 +144,7 @@ export const useGalleryNavigation = (): UseGalleryNavigationReturn => {
}, [lastSelectedImage, data]); }, [lastSelectedImage, data]);
const handleNavigation = useCallback( const handleNavigation = useCallback(
(direction: 'left' | 'right' | 'up' | 'down') => { (direction: 'left' | 'right' | 'up' | 'down', alt?: boolean) => {
if (!data) { if (!data) {
return; return;
} }
@ -144,10 +152,14 @@ export const useGalleryNavigation = (): UseGalleryNavigationReturn => {
if (!image || index === lastSelectedImageIndex) { if (!image || index === lastSelectedImageIndex) {
return; return;
} }
dispatch(imageSelected(image)); if (alt) {
dispatch(imageToCompareChanged(image));
} else {
dispatch(imageSelected(image));
}
scrollToImage(image.image_name, index); scrollToImage(image.image_name, index);
}, },
[dispatch, lastSelectedImageIndex, data] [data, lastSelectedImageIndex, dispatch]
); );
const isOnFirstImage = useMemo(() => lastSelectedImageIndex === 0, [lastSelectedImageIndex]); const isOnFirstImage = useMemo(() => lastSelectedImageIndex === 0, [lastSelectedImageIndex]);
@ -162,21 +174,33 @@ export const useGalleryNavigation = (): UseGalleryNavigationReturn => {
return lastSelectedImageIndex + imagesPerRow < loadedImagesCount; return lastSelectedImageIndex + imagesPerRow < loadedImagesCount;
}, [lastSelectedImageIndex, loadedImagesCount]); }, [lastSelectedImageIndex, loadedImagesCount]);
const handleLeftImage = useCallback(() => { const handleLeftImage = useCallback(
handleNavigation('left'); (alt?: boolean) => {
}, [handleNavigation]); handleNavigation('left', alt);
},
[handleNavigation]
);
const handleRightImage = useCallback(() => { const handleRightImage = useCallback(
handleNavigation('right'); (alt?: boolean) => {
}, [handleNavigation]); handleNavigation('right', alt);
},
[handleNavigation]
);
const handleUpImage = useCallback(() => { const handleUpImage = useCallback(
handleNavigation('up'); (alt?: boolean) => {
}, [handleNavigation]); handleNavigation('up', alt);
},
[handleNavigation]
);
const handleDownImage = useCallback(() => { const handleDownImage = useCallback(
handleNavigation('down'); (alt?: boolean) => {
}, [handleNavigation]); handleNavigation('down', alt);
},
[handleNavigation]
);
return { return {
handleLeftImage, handleLeftImage,

View File

@ -36,6 +36,7 @@ export const useMultiselect = (imageDTO?: ImageDTO) => {
shiftKey: e.shiftKey, shiftKey: e.shiftKey,
ctrlKey: e.ctrlKey, ctrlKey: e.ctrlKey,
metaKey: e.metaKey, metaKey: e.metaKey,
altKey: e.altKey,
}) })
); );
}, },

View File

@ -6,7 +6,7 @@ import { boardsApi } from 'services/api/endpoints/boards';
import { imagesApi } from 'services/api/endpoints/images'; import { imagesApi } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types'; import type { ImageDTO } from 'services/api/types';
import type { BoardId, ComparisonMode, GalleryState, GalleryView, ViewerMode } from './types'; import type { BoardId, ComparisonMode, GalleryState, GalleryView } from './types';
import { IMAGE_LIMIT, INITIAL_IMAGE_LIMIT } from './types'; import { IMAGE_LIMIT, INITIAL_IMAGE_LIMIT } from './types';
const initialGalleryState: GalleryState = { const initialGalleryState: GalleryState = {
@ -21,7 +21,7 @@ const initialGalleryState: GalleryState = {
boardSearchText: '', boardSearchText: '',
limit: INITIAL_IMAGE_LIMIT, limit: INITIAL_IMAGE_LIMIT,
offset: 0, offset: 0,
viewerMode: 'view', isImageViewerOpen: true,
imageToCompare: null, imageToCompare: null,
comparisonMode: 'slider', comparisonMode: 'slider',
sliderFit: 'fill', sliderFit: 'fill',
@ -40,7 +40,7 @@ export const gallerySlice = createSlice({
imageToCompareChanged: (state, action: PayloadAction<ImageDTO | null>) => { imageToCompareChanged: (state, action: PayloadAction<ImageDTO | null>) => {
state.imageToCompare = action.payload; state.imageToCompare = action.payload;
if (action.payload) { if (action.payload) {
state.viewerMode = 'compare'; state.isImageViewerOpen = true;
} }
}, },
comparisonModeChanged: (state, action: PayloadAction<ComparisonMode>) => { comparisonModeChanged: (state, action: PayloadAction<ComparisonMode>) => {
@ -88,8 +88,12 @@ export const gallerySlice = createSlice({
alwaysShowImageSizeBadgeChanged: (state, action: PayloadAction<boolean>) => { alwaysShowImageSizeBadgeChanged: (state, action: PayloadAction<boolean>) => {
state.alwaysShowImageSizeBadge = action.payload; state.alwaysShowImageSizeBadge = action.payload;
}, },
viewerModeChanged: (state, action: PayloadAction<ViewerMode>) => { isImageViewerOpenChanged: (state, action: PayloadAction<boolean>) => {
state.viewerMode = action.payload; if (state.isImageViewerOpen && state.imageToCompare) {
state.imageToCompare = null;
return;
}
state.isImageViewerOpen = action.payload;
}, },
comparedImagesSwapped: (state) => { comparedImagesSwapped: (state) => {
if (state.imageToCompare) { if (state.imageToCompare) {
@ -138,7 +142,7 @@ export const {
boardSearchTextChanged, boardSearchTextChanged,
moreImagesLoaded, moreImagesLoaded,
alwaysShowImageSizeBadgeChanged, alwaysShowImageSizeBadgeChanged,
viewerModeChanged, isImageViewerOpenChanged,
imageToCompareChanged, imageToCompareChanged,
comparisonModeChanged, comparisonModeChanged,
comparedImagesSwapped, comparedImagesSwapped,
@ -164,5 +168,13 @@ export const galleryPersistConfig: PersistConfig<GalleryState> = {
name: gallerySlice.name, name: gallerySlice.name,
initialState: initialGalleryState, initialState: initialGalleryState,
migrate: migrateGalleryState, migrate: migrateGalleryState,
persistDenylist: ['selection', 'selectedBoardId', 'galleryView', 'offset', 'limit', 'viewerMode', 'imageToCompare'], persistDenylist: [
'selection',
'selectedBoardId',
'galleryView',
'offset',
'limit',
'isImageViewerOpen',
'imageToCompare',
],
}; };

View File

@ -8,7 +8,6 @@ export const IMAGE_LIMIT = 20;
export type GalleryView = 'images' | 'assets'; export type GalleryView = 'images' | 'assets';
export type BoardId = 'none' | (string & Record<never, never>); export type BoardId = 'none' | (string & Record<never, never>);
export type ComparisonMode = 'slider' | 'side-by-side'; export type ComparisonMode = 'slider' | 'side-by-side';
export type ViewerMode = 'edit' | 'view' | 'compare';
export type GalleryState = { export type GalleryState = {
selection: ImageDTO[]; selection: ImageDTO[];
@ -25,5 +24,5 @@ export type GalleryState = {
imageToCompare: ImageDTO | null; imageToCompare: ImageDTO | null;
comparisonMode: ComparisonMode; comparisonMode: ComparisonMode;
sliderFit: 'contain' | 'fill'; sliderFit: 'contain' | 'fill';
viewerMode: ViewerMode; isImageViewerOpen: boolean;
}; };

View File

@ -3,7 +3,7 @@ import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/u
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent'; import { ControlLayersPanelContent } from 'features/controlLayers/components/ControlLayersPanelContent';
import { viewerModeChanged } from 'features/gallery/store/gallerySlice'; import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
import { Prompts } from 'features/parameters/components/Prompts/Prompts'; import { Prompts } from 'features/parameters/components/Prompts/Prompts';
import QueueControls from 'features/queue/components/QueueControls'; import QueueControls from 'features/queue/components/QueueControls';
import { SDXLPrompts } from 'features/sdxl/components/SDXLPrompts/SDXLPrompts'; import { SDXLPrompts } from 'features/sdxl/components/SDXLPrompts/SDXLPrompts';
@ -51,7 +51,7 @@ const ParametersPanelTextToImage = () => {
const onChangeTabs = useCallback( const onChangeTabs = useCallback(
(i: number) => { (i: number) => {
if (i === 1) { if (i === 1) {
dispatch(viewerModeChanged('edit')); dispatch(isImageViewerOpenChanged(false));
} }
}, },
[dispatch] [dispatch]