mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): gallery image hover button to open in viewer
This commit is contained in:
committed by
Kent Keirsey
parent
04232876e8
commit
3a42285a3f
@@ -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}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user