mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-15 10:35:21 -05:00
feat(ui): reworked layout (wip)
This commit is contained in:
@@ -14,12 +14,14 @@ import {
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useGallerySearchTerm } from 'features/gallery/components/ImageGrid/useGallerySearchTerm';
|
||||
import { selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import { galleryViewChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import CurrentImageButtons from 'features/gallery/components/ImageViewer/CurrentImageButtons';
|
||||
import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview';
|
||||
import { selectIsMiniViewerOpen, selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import { galleryViewChanged, isMiniViewerOpenToggled, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiMagnifyingGlassBold } from 'react-icons/pi';
|
||||
import { PiEyeBold, PiEyeClosedBold, PiMagnifyingGlassBold } from 'react-icons/pi';
|
||||
import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||
|
||||
import GalleryImageGrid from './ImageGrid/GalleryImageGrid';
|
||||
@@ -38,7 +40,7 @@ const SELECTED_STYLES: ChakraProps['sx'] = {
|
||||
color: 'invokeBlue.300',
|
||||
};
|
||||
|
||||
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
|
||||
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0, width: '100%' };
|
||||
|
||||
const selectGalleryView = createSelector(selectGallerySlice, (gallery) => gallery.galleryView);
|
||||
const selectSearchTerm = createSelector(selectGallerySlice, (gallery) => gallery.searchTerm);
|
||||
@@ -50,7 +52,11 @@ export const Gallery = () => {
|
||||
const initialSearchTerm = useAppSelector(selectSearchTerm);
|
||||
const searchDisclosure = useDisclosure({ defaultIsOpen: initialSearchTerm.length > 0 });
|
||||
const [searchTerm, onChangeSearchTerm, onResetSearchTerm] = useGallerySearchTerm();
|
||||
const isMiniViewerOpen = useAppSelector(selectIsMiniViewerOpen);
|
||||
|
||||
const toggleMiniViewer = useCallback(() => {
|
||||
dispatch(isMiniViewerOpenToggled());
|
||||
}, [dispatch]);
|
||||
const handleClickImages = useCallback(() => {
|
||||
dispatch(galleryViewChanged('images'));
|
||||
}, [dispatch]);
|
||||
@@ -68,7 +74,7 @@ export const Gallery = () => {
|
||||
const boardName = useBoardName(selectedBoardId);
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column" alignItems="center" justifyContent="space-between" h="full" w="full" pt={1}>
|
||||
<Flex flexDirection="column" alignItems="center" justifyContent="space-between" h="full" w="full" pt={1} minH={0}>
|
||||
<Tabs index={galleryView === 'images' ? 0 : 1} variant="enclosed" display="flex" flexDir="column" w="full">
|
||||
<TabList gap={2} fontSize="sm" borderColor="base.800" alignItems="center" w="full">
|
||||
<Text fontSize="sm" fontWeight="semibold" noOfLines={1} px="2" wordBreak="break-all">
|
||||
@@ -81,28 +87,54 @@ export const Gallery = () => {
|
||||
<Tab sx={BASE_STYLES} _selected={SELECTED_STYLES} onClick={handleClickAssets} data-testid="assets-tab">
|
||||
{t('gallery.assets')}
|
||||
</Tab>
|
||||
<IconButton
|
||||
onClick={handleClickSearch}
|
||||
tooltip={searchDisclosure.isOpen ? `${t('gallery.exitSearch')}` : `${t('gallery.displaySearch')}`}
|
||||
aria-label={t('gallery.displaySearch')}
|
||||
icon={<PiMagnifyingGlassBold />}
|
||||
colorScheme={searchDisclosure.isOpen ? 'invokeBlue' : 'base'}
|
||||
variant="link"
|
||||
/>
|
||||
<Flex h="full">
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={toggleMiniViewer}
|
||||
tooltip={t('gallery.toggleMiniViewer')}
|
||||
aria-label={t('gallery.toggleMiniViewer')}
|
||||
icon={isMiniViewerOpen ? <PiEyeBold /> : <PiEyeClosedBold />}
|
||||
colorScheme={isMiniViewerOpen ? 'invokeBlue' : 'base'}
|
||||
/>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={handleClickSearch}
|
||||
tooltip={searchDisclosure.isOpen ? `${t('gallery.exitSearch')}` : `${t('gallery.displaySearch')}`}
|
||||
aria-label={t('gallery.displaySearch')}
|
||||
icon={<PiMagnifyingGlassBold />}
|
||||
/>
|
||||
</Flex>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
|
||||
<Box w="full">
|
||||
<Collapse in={searchDisclosure.isOpen} style={COLLAPSE_STYLES}>
|
||||
<Box w="full" pt={2}>
|
||||
<GallerySearch
|
||||
searchTerm={searchTerm}
|
||||
onChangeSearchTerm={onChangeSearchTerm}
|
||||
onResetSearchTerm={onResetSearchTerm}
|
||||
/>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
<Collapse in={searchDisclosure.isOpen} style={COLLAPSE_STYLES}>
|
||||
<Box w="full" pt={2}>
|
||||
<GallerySearch
|
||||
searchTerm={searchTerm}
|
||||
onChangeSearchTerm={onChangeSearchTerm}
|
||||
onResetSearchTerm={onResetSearchTerm}
|
||||
/>
|
||||
</Box>
|
||||
</Collapse>
|
||||
<Collapse in={isMiniViewerOpen} style={COLLAPSE_STYLES}>
|
||||
<Box position="relative" w="full" mt={2} aspectRatio="1/1">
|
||||
<CurrentImagePreview />
|
||||
<Flex
|
||||
position="absolute"
|
||||
top={2}
|
||||
gap={2}
|
||||
justifyContent="space-between"
|
||||
left="50%"
|
||||
transform="translateX(-50%)"
|
||||
>
|
||||
<CurrentImageButtons />
|
||||
</Flex>
|
||||
</Box>
|
||||
</Collapse>
|
||||
<GalleryImageGrid />
|
||||
<GalleryPagination />
|
||||
</Flex>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Button, Collapse, Divider, Flex, IconButton, useDisclosure } from '@invoke-ai/ui-library';
|
||||
import { Box, Button, Collapse, Divider, Flex, IconButton, Spacer, useDisclosure } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
|
||||
@@ -60,36 +60,31 @@ const GalleryPanelContent = () => {
|
||||
|
||||
return (
|
||||
<Flex ref={ref} position="relative" flexDirection="column" h="full" w="full" tabIndex={-1}>
|
||||
<Flex alignItems="center" gap={0}>
|
||||
<GalleryHeader />
|
||||
<Flex alignItems="center" justifyContent="space-between" w="full">
|
||||
<Button
|
||||
<GalleryHeader />
|
||||
<Flex alignItems="center" w="full">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={handleToggleBoardPanel}
|
||||
rightIcon={boardsListPanel.isCollapsed ? <PiCaretDownBold /> : <PiCaretUpBold />}
|
||||
>
|
||||
{boardsListPanel.isCollapsed ? t('boards.viewBoards') : t('boards.hideBoards')}
|
||||
</Button>
|
||||
<Spacer />
|
||||
<Flex h="full">
|
||||
<GallerySettingsPopover />
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={handleToggleBoardPanel}
|
||||
rightIcon={boardsListPanel.isCollapsed ? <PiCaretDownBold /> : <PiCaretUpBold />}
|
||||
>
|
||||
{boardsListPanel.isCollapsed ? t('boards.viewBoards') : t('boards.hideBoards')}
|
||||
</Button>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<GallerySettingsPopover />
|
||||
<Flex>
|
||||
<IconButton
|
||||
w="full"
|
||||
h="full"
|
||||
onClick={handleClickBoardSearch}
|
||||
tooltip={
|
||||
boardSearchDisclosure.isOpen
|
||||
? `${t('gallery.exitBoardSearch')}`
|
||||
: `${t('gallery.displayBoardSearch')}`
|
||||
}
|
||||
aria-label={t('gallery.displayBoardSearch')}
|
||||
icon={<PiMagnifyingGlassBold />}
|
||||
colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'}
|
||||
variant="link"
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
onClick={handleClickBoardSearch}
|
||||
tooltip={
|
||||
boardSearchDisclosure.isOpen ? `${t('gallery.exitBoardSearch')}` : `${t('gallery.displayBoardSearch')}`
|
||||
}
|
||||
aria-label={t('gallery.displayBoardSearch')}
|
||||
icon={<PiMagnifyingGlassBold />}
|
||||
colorScheme={boardSearchDisclosure.isOpen ? 'invokeBlue' : 'base'}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
|
||||
@@ -17,7 +17,13 @@ const GallerySettingsPopover = () => {
|
||||
return (
|
||||
<Popover isLazy>
|
||||
<PopoverTrigger>
|
||||
<IconButton aria-label={t('gallery.gallerySettings')} icon={<RiSettings4Fill />} variant="link" h="full" />
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
aria-label={t('gallery.gallerySettings')}
|
||||
icon={<RiSettings4Fill />}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverBody>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { imageSelected, imageToCompareChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiEyeBold } from 'react-icons/pi';
|
||||
@@ -11,13 +11,12 @@ export const ImageMenuItemOpenInViewer = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const imageViewer = useImageViewer();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(imageToCompareChanged(null));
|
||||
dispatch(imageSelected(imageDTO));
|
||||
imageViewer.onOpen();
|
||||
}, [dispatch, imageDTO, imageViewer]);
|
||||
dispatch(setActiveTab('gallery'));
|
||||
}, [dispatch, imageDTO]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiEyeBold />} onClick={onClick}>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
|
||||
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { sentImageToCanvas } from 'features/gallery/store/actions';
|
||||
import { toast } from 'features/toast/toast';
|
||||
@@ -21,7 +20,6 @@ export const ImageMenuItemSendToCanvas = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const imageDTO = useImageDTOContext();
|
||||
const bboxRect = useAppSelector(selectBboxRect);
|
||||
const imageViewer = useImageViewer();
|
||||
|
||||
const handleSendToCanvas = useCallback(() => {
|
||||
const imageObject = imageDTOToImageObject(imageDTO);
|
||||
@@ -32,13 +30,12 @@ export const ImageMenuItemSendToCanvas = memo(() => {
|
||||
dispatch(sentImageToCanvas());
|
||||
dispatch(rasterLayerAdded({ overrides, isSelected: true }));
|
||||
dispatch(setActiveTab('generation'));
|
||||
imageViewer.onClose();
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
status: 'success',
|
||||
});
|
||||
}, [bboxRect.x, bboxRect.y, dispatch, imageDTO, imageViewer, t]);
|
||||
}, [bboxRect.x, bboxRect.y, dispatch, imageDTO, t]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<PiShareFatBold />} onClickCapture={handleSendToCanvas} id="send-to-canvas">
|
||||
|
||||
@@ -13,11 +13,8 @@ import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid
|
||||
import { useMultiselect } from 'features/gallery/hooks/useMultiselect';
|
||||
import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView';
|
||||
import { selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import {
|
||||
imageToCompareChanged,
|
||||
isImageViewerOpenChanged,
|
||||
selectGallerySlice,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import type { MouseEvent, MouseEventHandler } from 'react';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -116,7 +113,7 @@ const GalleryImage = ({ index, imageDTO }: HoverableImageProps) => {
|
||||
}, []);
|
||||
|
||||
const onDoubleClick = useCallback(() => {
|
||||
dispatch(isImageViewerOpenChanged(true));
|
||||
dispatch(setActiveTab('gallery'));
|
||||
dispatch(imageToCompareChanged(null));
|
||||
}, [dispatch]);
|
||||
|
||||
@@ -150,7 +147,7 @@ const GalleryImage = ({ index, imageDTO }: HoverableImageProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box w="full" h="full" p={1.5} className={GALLERY_IMAGE_CLASS_NAME} data-testid={dataTestId} sx={boxSx}>
|
||||
<Box w="full" h="full" className={GALLERY_IMAGE_CLASS_NAME} data-testid={dataTestId} sx={boxSx}>
|
||||
<Flex
|
||||
ref={imageContainerRef}
|
||||
userSelect="none"
|
||||
@@ -183,13 +180,12 @@ const GalleryImage = ({ index, imageDTO }: HoverableImageProps) => {
|
||||
color="base.50"
|
||||
fontSize="sm"
|
||||
fontWeight="semibold"
|
||||
bottom={0}
|
||||
left={0}
|
||||
bottom={1}
|
||||
left={1}
|
||||
opacity={0.7}
|
||||
px={2}
|
||||
lineHeight={1.25}
|
||||
borderTopEndRadius="base"
|
||||
borderBottomStartRadius="base"
|
||||
sx={badgeSx}
|
||||
pointerEvents="none"
|
||||
>{`${imageDTO.width}x${imageDTO.height}`}</Text>
|
||||
@@ -199,8 +195,8 @@ const GalleryImage = ({ index, imageDTO }: HoverableImageProps) => {
|
||||
icon={starIcon}
|
||||
tooltip={starTooltip}
|
||||
position="absolute"
|
||||
top={1}
|
||||
insetInlineEnd={1}
|
||||
top={2}
|
||||
insetInlineEnd={2}
|
||||
/>
|
||||
|
||||
{isHovered && <DeleteIcon onClick={handleDelete} />}
|
||||
@@ -227,8 +223,8 @@ const DeleteIcon = ({ onClick }: { onClick: MouseEventHandler }) => {
|
||||
icon={<PiTrashSimpleFill size="16px" />}
|
||||
tooltip={t('gallery.deleteImage_one')}
|
||||
position="absolute"
|
||||
bottom={1}
|
||||
insetInlineEnd={1}
|
||||
bottom={2}
|
||||
insetInlineEnd={2}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -78,24 +78,52 @@ const Content = () => {
|
||||
// Managing refs for dynamically rendered components is a bit tedious:
|
||||
// - https://react.dev/learn/manipulating-the-dom-with-refs#how-to-manage-a-list-of-refs-using-a-ref-callback
|
||||
// As a easy workaround, we can just grab the first gallery image element directly.
|
||||
const galleryImageEl = document.querySelector(`.${GALLERY_IMAGE_CLASS_NAME}`);
|
||||
if (!galleryImageEl) {
|
||||
const imageEl = document.querySelector(`.${GALLERY_IMAGE_CLASS_NAME}`);
|
||||
if (!imageEl) {
|
||||
// No images in gallery?
|
||||
return;
|
||||
}
|
||||
|
||||
const galleryImageRect = galleryImageEl.getBoundingClientRect();
|
||||
const gridEl = document.querySelector(`.${GALLERY_GRID_CLASS_NAME}`);
|
||||
|
||||
if (!gridEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageRect = imageEl.getBoundingClientRect();
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
|
||||
if (!galleryImageRect.width || !galleryImageRect.height || !containerRect.width || !containerRect.height) {
|
||||
// We need to account for the gap between images
|
||||
const gridElStyle = window.getComputedStyle(gridEl);
|
||||
const gap = parseFloat(gridElStyle.gap);
|
||||
|
||||
if (!imageRect.width || !imageRect.height || !containerRect.width || !containerRect.height) {
|
||||
// Gallery is too small to fit images or not rendered yet
|
||||
return;
|
||||
}
|
||||
|
||||
// Floating-point precision requires we round to get the correct number of images per row
|
||||
const imagesPerRow = Math.round(containerRect.width / galleryImageRect.width);
|
||||
// However, when calculating the number of images per column, we want to floor the value to not overflow the container
|
||||
const imagesPerColumn = Math.floor(containerRect.height / galleryImageRect.height);
|
||||
let imagesPerColumn = 0;
|
||||
let spaceUsed = 0;
|
||||
|
||||
while (spaceUsed + imageRect.height <= containerRect.height) {
|
||||
imagesPerColumn++; // Increment the number of images
|
||||
spaceUsed += imageRect.height; // Add image size to the used space
|
||||
if (spaceUsed + gap <= containerRect.height) {
|
||||
spaceUsed += gap; // Add gap size to the used space after each image except after the last image
|
||||
}
|
||||
}
|
||||
|
||||
let imagesPerRow = 0;
|
||||
spaceUsed = 0;
|
||||
|
||||
while (spaceUsed + imageRect.width <= containerRect.width) {
|
||||
imagesPerRow++; // Increment the number of images
|
||||
spaceUsed += imageRect.width; // Add image size to the used space
|
||||
if (spaceUsed + gap <= containerRect.width) {
|
||||
spaceUsed += gap; // Add gap size to the used space after each image except after the last image
|
||||
}
|
||||
}
|
||||
|
||||
// Always load at least 1 row of images
|
||||
const limit = Math.max(imagesPerRow, imagesPerRow * imagesPerColumn);
|
||||
dispatch(limitChanged(limit));
|
||||
@@ -139,6 +167,7 @@ const Content = () => {
|
||||
<Grid
|
||||
className={GALLERY_GRID_CLASS_NAME}
|
||||
gridTemplateColumns={`repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, 1fr))`}
|
||||
gap={1}
|
||||
>
|
||||
{imageDTOs.map((imageDTO, index) => (
|
||||
<GalleryImage key={imageDTO.image_name} imageDTO={imageDTO} index={index} />
|
||||
|
||||
@@ -4,13 +4,13 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { $activeScopes } from 'common/hooks/interactionScopes';
|
||||
import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
|
||||
import { selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { $isGalleryPanelOpen } from 'features/ui/store/uiSlice';
|
||||
import { $isRightPanelOpen } from 'features/ui/store/uiSlice';
|
||||
import { computed } from 'nanostores';
|
||||
import { useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const $isSelectAllEnabled = computed([$activeScopes, $isGalleryPanelOpen], (activeScopes, isGalleryPanelOpen) => {
|
||||
const $isSelectAllEnabled = computed([$activeScopes, $isRightPanelOpen], (activeScopes, isGalleryPanelOpen) => {
|
||||
return activeScopes.has('gallery') && !activeScopes.has('workflows') && isGalleryPanelOpen;
|
||||
});
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ export const ImageViewer = memo(() => {
|
||||
<Flex
|
||||
ref={ref}
|
||||
tabIndex={-1}
|
||||
layerStyle="body"
|
||||
layerStyle="first"
|
||||
p={2}
|
||||
borderRadius="base"
|
||||
position="absolute"
|
||||
flexDirection="column"
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { IconSwitch } from 'common/components/IconSwitch';
|
||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiEyeBold, PiPencilBold } from 'react-icons/pi';
|
||||
|
||||
const TooltipEdit = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<Text fontWeight="semibold">{t('common.edit')}</Text>
|
||||
<Text fontWeight="normal">{t('common.editDesc')}</Text>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
TooltipEdit.displayName = 'TooltipEdit';
|
||||
|
||||
const TooltipView = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex flexDir="column">
|
||||
<Text fontWeight="semibold">{t('common.view')}</Text>
|
||||
<Text fontWeight="normal">{t('common.viewDesc')}</Text>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
TooltipView.displayName = 'TooltipView';
|
||||
|
||||
export const ViewerToggle = memo(() => {
|
||||
const imageViewer = useImageViewer();
|
||||
useHotkeys('z', imageViewer.onToggle, [imageViewer]);
|
||||
useHotkeys('esc', imageViewer.onClose, [imageViewer]);
|
||||
const onChange = useCallback(
|
||||
(isChecked: boolean) => {
|
||||
if (isChecked) {
|
||||
imageViewer.onClose();
|
||||
} else {
|
||||
imageViewer.onOpen();
|
||||
}
|
||||
},
|
||||
[imageViewer]
|
||||
);
|
||||
|
||||
return (
|
||||
<IconSwitch
|
||||
isChecked={!imageViewer.isOpen}
|
||||
onChange={onChange}
|
||||
iconUnchecked={<PiEyeBold />}
|
||||
tooltipUnchecked={<TooltipView />}
|
||||
iconChecked={<PiPencilBold />}
|
||||
tooltipChecked={<TooltipEdit />}
|
||||
ariaLabel="Toggle viewer"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
ViewerToggle.displayName = 'ViewerToggle';
|
||||
@@ -1,23 +1,11 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
|
||||
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { memo } from 'react';
|
||||
|
||||
import CurrentImageButtons from './CurrentImageButtons';
|
||||
import { ViewerToggle } from './ViewerToggleMenu';
|
||||
|
||||
const selectShowToggle = createSelector(selectActiveTab, (tab) => {
|
||||
if (tab === 'upscaling' || tab === 'workflows') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
export const ViewerToolbar = memo(() => {
|
||||
const showToggle = useAppSelector(selectShowToggle);
|
||||
return (
|
||||
<Flex w="full" gap={2}>
|
||||
<Flex flex={1} justifyContent="center">
|
||||
@@ -30,9 +18,7 @@ export const ViewerToolbar = memo(() => {
|
||||
<CurrentImageButtons />
|
||||
</Flex>
|
||||
<Flex flex={1} justifyContent="center">
|
||||
<Flex gap={2} marginInlineStart="auto">
|
||||
{showToggle && <ViewerToggle />}
|
||||
</Flex>
|
||||
<Flex gap={2} marginInlineStart="auto" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectHasImageToCompare, selectIsImageViewerOpen } from 'features/gallery/store/gallerySelectors';
|
||||
import {
|
||||
imageToCompareChanged,
|
||||
isImageViewerOpenChanged,
|
||||
selectGallerySlice,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
|
||||
import { selectUiSlice } from 'features/ui/store/uiSlice';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const selectIsOpen = createSelector(selectUiSlice, selectWorkflowSlice, selectGallerySlice, (ui, workflow, gallery) => {
|
||||
const tab = ui.activeTab;
|
||||
const workflowsMode = workflow.mode;
|
||||
if (tab === 'models' || tab === 'queue') {
|
||||
return false;
|
||||
}
|
||||
if (tab === 'workflows' && workflowsMode === 'edit') {
|
||||
return false;
|
||||
}
|
||||
if (tab === 'workflows' && workflowsMode === 'view') {
|
||||
return true;
|
||||
}
|
||||
if (tab === 'upscaling') {
|
||||
return true;
|
||||
}
|
||||
return gallery.isImageViewerOpen;
|
||||
});
|
||||
|
||||
export const useIsImageViewerOpen = () => {
|
||||
const isOpen = useAppSelector(selectIsOpen);
|
||||
return isOpen;
|
||||
};
|
||||
|
||||
const selectIsForcedOpen = createSelector(selectUiSlice, selectWorkflowSlice, (ui, workflow) => {
|
||||
return ui.activeTab === 'upscaling' || (ui.activeTab === 'workflows' && workflow.mode === 'view');
|
||||
});
|
||||
|
||||
export const useImageViewer = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isComparing = useAppSelector(selectHasImageToCompare);
|
||||
const isNaturallyOpen = useAppSelector(selectIsImageViewerOpen);
|
||||
const isForcedOpen = useAppSelector(selectIsForcedOpen);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
if (isForcedOpen) {
|
||||
return;
|
||||
}
|
||||
if (isComparing && isNaturallyOpen) {
|
||||
dispatch(imageToCompareChanged(null));
|
||||
} else {
|
||||
dispatch(isImageViewerOpenChanged(false));
|
||||
}
|
||||
}, [dispatch, isComparing, isForcedOpen, isNaturallyOpen]);
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
dispatch(isImageViewerOpenChanged(true));
|
||||
}, [dispatch]);
|
||||
|
||||
const onToggle = useCallback(() => {
|
||||
if (isForcedOpen) {
|
||||
return;
|
||||
}
|
||||
if (isComparing && isNaturallyOpen) {
|
||||
dispatch(imageToCompareChanged(null));
|
||||
} else {
|
||||
dispatch(isImageViewerOpenChanged(!isNaturallyOpen));
|
||||
}
|
||||
}, [dispatch, isComparing, isForcedOpen, isNaturallyOpen]);
|
||||
|
||||
return { isOpen: isNaturallyOpen || isForcedOpen, onOpen, onClose, onToggle, isComparing };
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation';
|
||||
import { useGalleryPagination } from 'features/gallery/hooks/useGalleryPagination';
|
||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { $isGalleryPanelOpen } from 'features/ui/store/uiSlice';
|
||||
import { $isRightPanelOpen } from 'features/ui/store/uiSlice';
|
||||
import { computed } from 'nanostores';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useListImagesQuery } from 'services/api/endpoints/images';
|
||||
@@ -15,7 +15,7 @@ const $leftRightHotkeysEnabled = computed($activeScopes, (activeScopes) => {
|
||||
return !activeScopes.has('canvas') || activeScopes.has('imageViewer');
|
||||
});
|
||||
|
||||
const $upDownHotkeysEnabled = computed([$activeScopes, $isGalleryPanelOpen], (activeScopes, isGalleryPanelOpen) => {
|
||||
const $upDownHotkeysEnabled = computed([$activeScopes, $isRightPanelOpen], (activeScopes, isGalleryPanelOpen) => {
|
||||
// The up and down hotkeys can be used when the gallery is focused and the canvas is not focused, and the gallery panel is open.
|
||||
return !activeScopes.has('canvas') && isGalleryPanelOpen;
|
||||
});
|
||||
|
||||
@@ -57,4 +57,4 @@ export const selectImageToCompare = createSelector(selectGallerySlice, (gallery)
|
||||
export const selectHasImageToCompare = createSelector(selectImageToCompare, (imageToCompare) =>
|
||||
Boolean(imageToCompare)
|
||||
);
|
||||
export const selectIsImageViewerOpen = createSelector(selectGallerySlice, (gallery) => gallery.isImageViewerOpen);
|
||||
export const selectIsMiniViewerOpen = createSelector(selectGallerySlice, (gallery) => gallery.isMiniViewerOpen);
|
||||
|
||||
@@ -21,11 +21,11 @@ const initialGalleryState: GalleryState = {
|
||||
starredFirst: true,
|
||||
orderDir: 'DESC',
|
||||
searchTerm: '',
|
||||
isImageViewerOpen: true,
|
||||
imageToCompare: null,
|
||||
comparisonMode: 'slider',
|
||||
comparisonFit: 'fill',
|
||||
shouldShowArchivedBoards: false,
|
||||
isMiniViewerOpen: false,
|
||||
};
|
||||
|
||||
export const gallerySlice = createSlice({
|
||||
@@ -40,9 +40,6 @@ export const gallerySlice = createSlice({
|
||||
},
|
||||
imageToCompareChanged: (state, action: PayloadAction<ImageDTO | null>) => {
|
||||
state.imageToCompare = action.payload;
|
||||
if (action.payload) {
|
||||
state.isImageViewerOpen = true;
|
||||
}
|
||||
},
|
||||
comparisonModeChanged: (state, action: PayloadAction<ComparisonMode>) => {
|
||||
state.comparisonMode = action.payload;
|
||||
@@ -91,8 +88,8 @@ export const gallerySlice = createSlice({
|
||||
alwaysShowImageSizeBadgeChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.alwaysShowImageSizeBadge = action.payload;
|
||||
},
|
||||
isImageViewerOpenChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.isImageViewerOpen = action.payload;
|
||||
isMiniViewerOpenToggled: (state) => {
|
||||
state.isMiniViewerOpen = !state.isMiniViewerOpen;
|
||||
},
|
||||
comparedImagesSwapped: (state) => {
|
||||
if (state.imageToCompare) {
|
||||
@@ -138,7 +135,6 @@ export const {
|
||||
selectionChanged,
|
||||
boardSearchTextChanged,
|
||||
alwaysShowImageSizeBadgeChanged,
|
||||
isImageViewerOpenChanged,
|
||||
imageToCompareChanged,
|
||||
comparisonModeChanged,
|
||||
comparedImagesSwapped,
|
||||
@@ -150,6 +146,7 @@ export const {
|
||||
starredFirstChanged,
|
||||
shouldShowArchivedBoardsChanged,
|
||||
searchTermChanged,
|
||||
isMiniViewerOpenToggled,
|
||||
} = gallerySlice.actions;
|
||||
|
||||
export const selectGallerySlice = (state: RootState) => state.gallery;
|
||||
@@ -166,13 +163,5 @@ export const galleryPersistConfig: PersistConfig<GalleryState> = {
|
||||
name: gallerySlice.name,
|
||||
initialState: initialGalleryState,
|
||||
migrate: migrateGalleryState,
|
||||
persistDenylist: [
|
||||
'selection',
|
||||
'selectedBoardId',
|
||||
'galleryView',
|
||||
'offset',
|
||||
'limit',
|
||||
'isImageViewerOpen',
|
||||
'imageToCompare',
|
||||
],
|
||||
persistDenylist: ['selection', 'selectedBoardId', 'galleryView', 'offset', 'limit', 'imageToCompare'],
|
||||
};
|
||||
|
||||
@@ -27,6 +27,6 @@ export type GalleryState = {
|
||||
imageToCompare: ImageDTO | null;
|
||||
comparisonMode: ComparisonMode;
|
||||
comparisonFit: ComparisonFit;
|
||||
isImageViewerOpen: boolean;
|
||||
shouldShowArchivedBoards: boolean;
|
||||
isMiniViewerOpen: boolean;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user