feat(ui): gallery image hover button to open in viewer

This commit is contained in:
psychedelicious
2024-09-16 21:05:50 +10:00
committed by Kent Keirsey
parent 04232876e8
commit 3a42285a3f
5 changed files with 72 additions and 34 deletions

View File

@@ -10,7 +10,9 @@ const sx: SystemStyleObject = {
transitionDuration: 'normal',
fill: 'base.100',
_hover: { fill: 'base.50' },
filter: 'drop-shadow(0px 0px 0.1rem var(--invoke-colors-base-800))',
filter: `drop-shadow(0px 0px 0.1rem var(--invoke-colors-base-900))
drop-shadow(0px 0px 0.3rem var(--invoke-colors-base-900))
drop-shadow(0px 0px 0.3rem var(--invoke-colors-base-900))`,
},
};
@@ -27,7 +29,6 @@ const IAIDndImageIcon = (props: Props) => {
onClick={onClick}
aria-label={tooltip}
icon={icon}
size="sm"
variant="link"
sx={sx}
data-testid={tooltip}

View File

@@ -1,22 +1,17 @@
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 { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowsOutBold } from 'react-icons/pi';
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.open();
}, [dispatch, imageDTO, imageViewer]);
imageViewer.openImageInViewer(imageDTO);
}, [imageDTO, imageViewer]);
return (
<MenuItem icon={<PiArrowsOutBold />} onClick={onClick}>

View File

@@ -15,10 +15,10 @@ import { useMultiselect } from 'features/gallery/hooks/useMultiselect';
import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView';
import { selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
import type { MouseEvent, MouseEventHandler } from 'react';
import type { MouseEvent } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PiStarBold, PiStarFill, PiTrashSimpleFill } from 'react-icons/pi';
import { PiArrowsOutBold, PiStarBold, PiStarFill, PiTrashSimpleFill } from 'react-icons/pi';
import { useStarImagesMutation, useUnstarImagesMutation } from 'services/api/endpoints/images';
import type { ImageDTO } from 'services/api/types';
@@ -46,7 +46,17 @@ const selectAlwaysShouldImageSizeBadge = createSelector(
(gallery) => gallery.alwaysShowImageSizeBadge
);
const GalleryImage = ({ index, imageDTO }: HoverableImageProps) => {
export const GalleryImage = memo(({ index, imageDTO }: HoverableImageProps) => {
if (!imageDTO) {
return <IAIFillSkeleton />;
}
return <GalleryImageContent index={index} imageDTO={imageDTO} />;
});
GalleryImage.displayName = 'GalleryImage';
const GalleryImageContent = memo(({ index, imageDTO }: HoverableImageProps) => {
const dispatch = useAppDispatch();
const selectedBoardId = useAppSelector(selectSelectedBoardId);
const selectIsSelectedForCompare = useMemo(
@@ -61,17 +71,6 @@ const GalleryImage = ({ index, imageDTO }: HoverableImageProps) => {
const imageContainerRef = useScrollIntoView(isSelected, index, areMultiplesSelected);
const handleDelete = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
if (!imageDTO) {
return;
}
dispatch(imagesToDeleteSelected([imageDTO]));
},
[dispatch, imageDTO]
);
const draggableData = useMemo<TypesafeDraggableData | undefined>(() => {
if (areMultiplesSelected) {
const data: GallerySelectionDraggableData = {
@@ -124,10 +123,10 @@ const GalleryImage = ({ index, imageDTO }: HoverableImageProps) => {
const starIcon = useMemo(() => {
if (imageDTO.starred) {
return customStarUi ? customStarUi.on.icon : <PiStarFill size="20" />;
return customStarUi ? customStarUi.on.icon : <PiStarFill />;
}
if (!imageDTO.starred && isHovered) {
return customStarUi ? customStarUi.off.icon : <PiStarBold size="20" />;
return customStarUi ? customStarUi.off.icon : <PiStarBold />;
}
}, [imageDTO.starred, isHovered, customStarUi]);
@@ -199,20 +198,31 @@ const GalleryImage = ({ index, imageDTO }: HoverableImageProps) => {
top={2}
insetInlineEnd={2}
/>
{isHovered && <DeleteIcon onClick={handleDelete} />}
{isHovered && <DeleteIcon imageDTO={imageDTO} />}
{isHovered && <OpenInViewerIconButton imageDTO={imageDTO} />}
</>
</IAIDndImage>
</Flex>
</Box>
);
};
});
export default memo(GalleryImage);
GalleryImageContent.displayName = 'GalleryImageContent';
const DeleteIcon = ({ onClick }: { onClick: MouseEventHandler }) => {
const DeleteIcon = ({ imageDTO }: { imageDTO: ImageDTO }) => {
const shift = useShiftModifier();
const { t } = useTranslation();
const dispatch = useAppDispatch();
const onClick = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
if (!imageDTO) {
return;
}
dispatch(imagesToDeleteSelected([imageDTO]));
},
[dispatch, imageDTO]
);
if (!shift) {
return null;
@@ -221,7 +231,7 @@ const DeleteIcon = ({ onClick }: { onClick: MouseEventHandler }) => {
return (
<IAIDndImageIcon
onClick={onClick}
icon={<PiTrashSimpleFill size="16px" />}
icon={<PiTrashSimpleFill />}
tooltip={t('gallery.deleteImage_one')}
position="absolute"
bottom={2}
@@ -229,3 +239,23 @@ const DeleteIcon = ({ onClick }: { onClick: MouseEventHandler }) => {
/>
);
};
const OpenInViewerIconButton = ({ imageDTO }: { imageDTO: ImageDTO }) => {
const imageViewer = useImageViewer();
const { t } = useTranslation();
const onClick = useCallback(() => {
imageViewer.openImageInViewer(imageDTO);
}, [imageDTO, imageViewer]);
return (
<IAIDndImageIcon
onClick={onClick}
icon={<PiArrowsOutBold />}
tooltip={t('gallery.openInViewer')}
position="absolute"
insetBlockStart={2}
insetInlineStart={2}
/>
);
};

View File

@@ -14,7 +14,7 @@ import { PiImageBold, PiWarningCircleBold } from 'react-icons/pi';
import { useListImagesQuery } from 'services/api/endpoints/images';
import { GALLERY_GRID_CLASS_NAME } from './constants';
import GalleryImage, { GALLERY_IMAGE_CLASS_NAME } from './GalleryImage';
import { GALLERY_IMAGE_CLASS_NAME, GalleryImage } from './GalleryImage';
const GalleryImageGrid = () => {
useGalleryHotkeys();

View File

@@ -1,14 +1,26 @@
import { useAppDispatch } from 'app/store/storeHooks';
import { buildUseBoolean } from 'common/hooks/useBoolean';
import { useMemo } from 'react';
import { imageSelected, imageToCompareChanged } from 'features/gallery/store/gallerySlice';
import { useCallback, useMemo } from 'react';
import type { ImageDTO } from 'services/api/types';
const [useImageViewerState, $imageViewerState] = buildUseBoolean(true);
export const useImageViewer = () => {
const dispatch = useAppDispatch();
const imageViewerState = useImageViewerState();
const isOpen = useMemo(() => imageViewerState.isTrue, [imageViewerState]);
const open = useMemo(() => imageViewerState.setTrue, [imageViewerState]);
const close = useMemo(() => imageViewerState.setFalse, [imageViewerState]);
const toggle = useMemo(() => imageViewerState.toggle, [imageViewerState]);
const openImageInViewer = useCallback(
(imageDTO: ImageDTO) => {
dispatch(imageToCompareChanged(null));
dispatch(imageSelected(imageDTO));
open();
},
[dispatch, open]
);
return { isOpen, open, close, toggle, $state: $imageViewerState };
return { isOpen, open, close, toggle, $state: $imageViewerState, openImageInViewer };
};