Skip to content

Commit 038a482

Browse files
feat(ui): rework visibility conditions for image viewer
1 parent c325ad3 commit 038a482

File tree

12 files changed

+53
-98
lines changed

12 files changed

+53
-98
lines changed

invokeai/frontend/web/public/locales/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@
380380
"problemDeletingImagesDesc": "One or more images could not be deleted",
381381
"viewerImage": "Viewer Image",
382382
"compareImage": "Compare Image",
383-
"selectForViewer": "Select for Viewer",
383+
"openInViewer": "Open in Viewer",
384384
"selectForCompare": "Select for Compare",
385385
"selectAnImageToCompare": "Select an Image to Compare",
386386
"slider": "Slider",

invokeai/frontend/web/src/features/gallery/components/ImageViewer/CompareToolbar.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
imageToCompareChanged,
88
} from 'features/gallery/store/gallerySlice';
99
import { memo, useCallback } from 'react';
10+
import { useHotkeys } from 'react-hotkeys-hook';
1011
import { useTranslation } from 'react-i18next';
1112
import { PiArrowsOutBold, PiSwapBold, PiXBold } from 'react-icons/pi';
1213

@@ -33,6 +34,7 @@ export const CompareToolbar = memo(() => {
3334
const exitCompare = useCallback(() => {
3435
dispatch(imageToCompareChanged(null));
3536
}, [dispatch]);
37+
useHotkeys('esc', exitCompare, [exitCompare]);
3638

3739
return (
3840
<Flex w="full" gap={2}>

invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparison.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,22 @@
1-
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
21
import { useAppSelector } from 'app/store/storeHooks';
32
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
43
import type { Dimensions } from 'features/canvas/store/canvasTypes';
4+
import { selectComparisonImages } from 'features/gallery/components/ImageViewer/common';
55
import { ImageComparisonHover } from 'features/gallery/components/ImageViewer/ImageComparisonHover';
66
import { ImageComparisonSideBySide } from 'features/gallery/components/ImageViewer/ImageComparisonSideBySide';
77
import { ImageComparisonSlider } from 'features/gallery/components/ImageViewer/ImageComparisonSlider';
8-
import { selectGallerySlice } from 'features/gallery/store/gallerySlice';
98
import { memo } from 'react';
109
import { useTranslation } from 'react-i18next';
1110
import { PiImagesBold } from 'react-icons/pi';
1211

13-
const selector = createMemoizedSelector(selectGallerySlice, (gallerySlice) => {
14-
const firstImage = gallerySlice.selection.slice(-1)[0] ?? null;
15-
const secondImage = gallerySlice.imageToCompare;
16-
return { firstImage, secondImage };
17-
});
18-
1912
type Props = {
2013
containerDims: Dimensions;
2114
};
2215

2316
export const ImageComparison = memo(({ containerDims }: Props) => {
2417
const { t } = useTranslation();
2518
const comparisonMode = useAppSelector((s) => s.gallery.comparisonMode);
26-
const { firstImage, secondImage } = useAppSelector(selector);
19+
const { firstImage, secondImage } = useAppSelector(selectComparisonImages);
2720

2821
if (!firstImage || !secondImage) {
2922
// Should rarely/never happen - we don't render this component unless we have images to compare

invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageComparisonDroppable.tsx

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
import { Flex } from '@invoke-ai/ui-library';
2-
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
32
import { useAppSelector } from 'app/store/storeHooks';
43
import IAIDroppable from 'common/components/IAIDroppable';
54
import type { CurrentImageDropData, SelectForCompareDropData } from 'features/dnd/types';
6-
import { selectGallerySlice } from 'features/gallery/store/gallerySlice';
5+
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
76
import { memo, useMemo } from 'react';
87
import { useTranslation } from 'react-i18next';
98

10-
const selector = createMemoizedSelector(selectGallerySlice, (gallerySlice) => {
11-
const firstImage = gallerySlice.selection.slice(-1)[0] ?? null;
12-
const secondImage = gallerySlice.imageToCompare;
13-
return { firstImage, secondImage };
14-
});
9+
import { selectComparisonImages } from './common';
1510

1611
const setCurrentImageDropData: CurrentImageDropData = {
1712
id: 'current-image',
@@ -20,7 +15,8 @@ const setCurrentImageDropData: CurrentImageDropData = {
2015

2116
export const ImageComparisonDroppable = memo(() => {
2217
const { t } = useTranslation();
23-
const { firstImage, secondImage } = useAppSelector(selector);
18+
const imageViewer = useImageViewer();
19+
const { firstImage, secondImage } = useAppSelector(selectComparisonImages);
2420
const selectForCompareDropData = useMemo<SelectForCompareDropData>(
2521
() => ({
2622
id: 'image-comparison',
@@ -33,14 +29,17 @@ export const ImageComparisonDroppable = memo(() => {
3329
[firstImage?.image_name, secondImage?.image_name]
3430
);
3531

32+
if (!imageViewer.isOpen) {
33+
return (
34+
<Flex position="absolute" top={0} right={0} bottom={0} left={0} gap={2} pointerEvents="none">
35+
<IAIDroppable data={setCurrentImageDropData} dropLabel={t('gallery.openInViewer')} />
36+
</Flex>
37+
);
38+
}
39+
3640
return (
3741
<Flex position="absolute" top={0} right={0} bottom={0} left={0} gap={2} pointerEvents="none">
38-
<Flex position="relative" flex={1}>
39-
<IAIDroppable data={setCurrentImageDropData} dropLabel={t('gallery.selectForViewer')} />
40-
</Flex>
41-
<Flex position="relative" flex={1}>
42-
<IAIDroppable data={selectForCompareDropData} dropLabel={t('gallery.selectForCompare')} />
43-
</Flex>
42+
<IAIDroppable data={selectForCompareDropData} dropLabel={t('gallery.selectForCompare')} />
4443
</Flex>
4544
);
4645
});

invokeai/frontend/web/src/features/gallery/components/ImageViewer/ImageViewer.tsx

Lines changed: 6 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,17 @@
11
import { Box, Flex } from '@invoke-ai/ui-library';
2-
import { useAppSelector } from 'app/store/storeHooks';
32
import { CompareToolbar } from 'features/gallery/components/ImageViewer/CompareToolbar';
43
import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview';
54
import { ImageComparison } from 'features/gallery/components/ImageViewer/ImageComparison';
65
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar';
7-
import type { InvokeTabName } from 'features/ui/store/tabMap';
8-
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
9-
import { memo, useMemo } from 'react';
10-
import { useHotkeys } from 'react-hotkeys-hook';
6+
import { memo } from 'react';
117
import { useMeasure } from 'react-use';
128

139
import { useImageViewer } from './useImageViewer';
1410

15-
const VIEWER_ENABLED_TABS: InvokeTabName[] = ['canvas', 'generation', 'workflows'];
16-
1711
export const ImageViewer = memo(() => {
18-
const { isOpen, onToggle, onClose } = useImageViewer();
19-
const activeTabName = useAppSelector(activeTabNameSelector);
20-
const workflowsMode = useAppSelector((s) => s.workflow.mode);
21-
const isComparing = useAppSelector((s) => s.gallery.imageToCompare !== null);
22-
const isViewerEnabled = useMemo(() => VIEWER_ENABLED_TABS.includes(activeTabName), [activeTabName]);
23-
const shouldShowViewer = useMemo(() => {
24-
if (activeTabName === 'workflows' && workflowsMode === 'view') {
25-
return true;
26-
}
27-
if (!isViewerEnabled) {
28-
return false;
29-
}
30-
return isOpen;
31-
}, [isOpen, isViewerEnabled, workflowsMode, activeTabName]);
12+
const imageViewer = useImageViewer();
3213
const [containerRef, containerDims] = useMeasure<HTMLDivElement>();
3314

34-
useHotkeys('z', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]);
35-
useHotkeys('esc', onClose, { enabled: isViewerEnabled }, [isViewerEnabled, onClose]);
36-
37-
if (!shouldShowViewer) {
38-
return null;
39-
}
40-
4115
return (
4216
<Flex
4317
layerStyle="first"
@@ -53,11 +27,11 @@ export const ImageViewer = memo(() => {
5327
alignItems="center"
5428
justifyContent="center"
5529
>
56-
{isComparing && <CompareToolbar />}
57-
{!isComparing && <ViewerToolbar />}
30+
{imageViewer.isComparing && <CompareToolbar />}
31+
{!imageViewer.isComparing && <ViewerToolbar />}
5832
<Box ref={containerRef} w="full" h="full">
59-
{!isComparing && <CurrentImagePreview />}
60-
{isComparing && <ImageComparison containerDims={containerDims} />}
33+
{!imageViewer.isComparing && <CurrentImagePreview />}
34+
{imageViewer.isComparing && <ImageComparison containerDims={containerDims} />}
6135
</Box>
6236
</Flex>
6337
);

invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToggleMenu.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,35 @@ import {
99
PopoverTrigger,
1010
Text,
1111
} from '@invoke-ai/ui-library';
12+
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
13+
import { useHotkeys } from 'react-hotkeys-hook';
1214
import { useTranslation } from 'react-i18next';
1315
import { PiCaretDownBold, PiCheckBold, PiEyeBold, PiPencilBold } from 'react-icons/pi';
1416

15-
import { useImageViewer } from './useImageViewer';
16-
1717
export const ViewerToggleMenu = () => {
1818
const { t } = useTranslation();
19-
const { isOpen, onClose, onOpen } = useImageViewer();
19+
const imageViewer = useImageViewer();
20+
useHotkeys('z', imageViewer.onToggle, [imageViewer]);
21+
useHotkeys('esc', imageViewer.onClose, [imageViewer]);
2022

2123
return (
2224
<Popover isLazy>
2325
<PopoverTrigger>
24-
<Button variant="outline" data-testid="toggle-viewer-menu-button">
26+
<Button variant="outline" data-testid="toggle-viewer-menu-button" pointerEvents="auto">
2527
<Flex gap={3} w="full" alignItems="center">
26-
{isOpen ? <Icon as={PiEyeBold} /> : <Icon as={PiPencilBold} />}
27-
<Text fontSize="md">{isOpen ? t('common.viewing') : t('common.editing')}</Text>
28+
{imageViewer.isOpen ? <Icon as={PiEyeBold} /> : <Icon as={PiPencilBold} />}
29+
<Text fontSize="md">{imageViewer.isOpen ? t('common.viewing') : t('common.editing')}</Text>
2830
<Icon as={PiCaretDownBold} />
2931
</Flex>
3032
</Button>
3133
</PopoverTrigger>
32-
<PopoverContent p={2}>
34+
<PopoverContent p={2} pointerEvents="auto">
3335
<PopoverArrow />
3436
<PopoverBody>
3537
<Flex flexDir="column">
36-
<Button onClick={onOpen} variant="ghost" h="auto" w="auto" p={2}>
38+
<Button onClick={imageViewer.onOpen} variant="ghost" h="auto" w="auto" p={2}>
3739
<Flex gap={2} w="full">
38-
<Icon as={PiCheckBold} visibility={isOpen ? 'visible' : 'hidden'} />
40+
<Icon as={PiCheckBold} visibility={imageViewer.isOpen ? 'visible' : 'hidden'} />
3941
<Flex flexDir="column" gap={2} alignItems="flex-start">
4042
<Text fontWeight="semibold" color="base.100">
4143
{t('common.viewing')}
@@ -46,9 +48,9 @@ export const ViewerToggleMenu = () => {
4648
</Flex>
4749
</Flex>
4850
</Button>
49-
<Button onClick={onClose} variant="ghost" h="auto" w="auto" p={2}>
51+
<Button onClick={imageViewer.onClose} variant="ghost" h="auto" w="auto" p={2}>
5052
<Flex gap={2} w="full">
51-
<Icon as={PiCheckBold} visibility={isOpen ? 'hidden' : 'visible'} />
53+
<Icon as={PiCheckBold} visibility={imageViewer.isOpen ? 'hidden' : 'visible'} />
5254
<Flex flexDir="column" gap={2} alignItems="flex-start">
5355
<Text fontWeight="semibold" color="base.100">
5456
{t('common.editing')}

invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,13 @@ import { useAppSelector } from 'app/store/storeHooks';
33
import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
44
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
55
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
6-
import { memo, useMemo } from 'react';
6+
import { memo } from 'react';
77

88
import CurrentImageButtons from './CurrentImageButtons';
99
import { ViewerToggleMenu } from './ViewerToggleMenu';
1010

1111
export const ViewerToolbar = memo(() => {
12-
const workflowsMode = useAppSelector((s) => s.workflow.mode);
13-
const activeTabName = useAppSelector(activeTabNameSelector);
14-
const shouldShowToggleMenu = useMemo(() => {
15-
if (activeTabName !== 'workflows') {
16-
return true;
17-
}
18-
return workflowsMode === 'edit';
19-
}, [workflowsMode, activeTabName]);
20-
12+
const tab = useAppSelector(activeTabNameSelector);
2113
return (
2214
<Flex w="full" gap={2}>
2315
<Flex flex={1} justifyContent="center">
@@ -31,7 +23,7 @@ export const ViewerToolbar = memo(() => {
3123
</Flex>
3224
<Flex flex={1} justifyContent="center">
3325
<Flex gap={2} marginInlineStart="auto">
34-
{shouldShowToggleMenu && <ViewerToggleMenu />}
26+
{tab !== 'workflows' && <ViewerToggleMenu />}
3527
</Flex>
3628
</Flex>
3729
</Flex>

invokeai/frontend/web/src/features/gallery/components/ImageViewer/common.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
12
import type { Dimensions } from 'features/canvas/store/canvasTypes';
3+
import { selectGallerySlice } from 'features/gallery/store/gallerySlice';
24
import type { ComparisonFit } from 'features/gallery/store/types';
35
import type { ImageDTO } from 'services/api/types';
46

@@ -55,3 +57,8 @@ export const getSecondImageDims = (
5557

5658
return { width, height };
5759
};
60+
export const selectComparisonImages = createMemoizedSelector(selectGallerySlice, (gallerySlice) => {
61+
const firstImage = gallerySlice.selection.slice(-1)[0] ?? null;
62+
const secondImage = gallerySlice.imageToCompare;
63+
return { firstImage, secondImage };
64+
});

invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,5 @@ export const useImageViewer = () => {
2727
}
2828
}, [dispatch, isComparing, isOpen]);
2929

30-
return { isOpen, onOpen, onClose, onToggle };
30+
return { isOpen, onOpen, onClose, onToggle, isComparing };
3131
};

invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import 'reactflow/dist/style.css';
22

33
import { Flex } from '@invoke-ai/ui-library';
4-
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
54
import { useAppSelector } from 'app/store/storeHooks';
6-
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
75
import QueueControls from 'features/queue/components/QueueControls';
86
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
97
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
@@ -21,14 +19,8 @@ import { WorkflowName } from './WorkflowName';
2119

2220
const panelGroupStyles: CSSProperties = { height: '100%', width: '100%' };
2321

24-
const selector = createMemoizedSelector(selectWorkflowSlice, (workflow) => {
25-
return {
26-
mode: workflow.mode,
27-
};
28-
});
29-
3022
const NodeEditorPanelGroup = () => {
31-
const { mode } = useAppSelector(selector);
23+
const mode = useAppSelector((s) => s.workflow.mode);
3224
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
3325
const panelStorage = usePanelStorage();
3426

0 commit comments

Comments
 (0)