update IAIDndImage to use children for icons, add UI for shift+delete to delete images from gallery

This commit is contained in:
Mary Hipp 2023-08-14 14:36:14 -04:00 committed by psychedelicious
parent 767a612746
commit a512fdc0f6
8 changed files with 177 additions and 107 deletions

View File

@ -1,22 +1,10 @@
import { import { ChakraProps, Flex, Icon, Image, useColorMode } from '@chakra-ui/react';
ChakraProps,
Flex,
Icon,
Image,
useColorMode,
useColorModeValue,
} from '@chakra-ui/react';
import IAIIconButton from 'common/components/IAIIconButton';
import { import {
IAILoadingImageFallback, IAILoadingImageFallback,
IAINoContentFallback, IAINoContentFallback,
} from 'common/components/IAIImageFallback'; } from 'common/components/IAIImageFallback';
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay'; import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton'; import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import {
TypesafeDraggableData,
TypesafeDroppableData,
} from 'features/dnd/types';
import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu'; import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
import { import {
MouseEvent, MouseEvent,
@ -26,22 +14,22 @@ import {
useCallback, useCallback,
useState, useState,
} from 'react'; } from 'react';
import { FaImage, FaUndo, FaUpload } from 'react-icons/fa'; import { FaImage, FaUpload } from 'react-icons/fa';
import { ImageDTO, PostUploadAction } from 'services/api/types'; import { ImageDTO, PostUploadAction } from 'services/api/types';
import { mode } from 'theme/util/mode'; import { mode } from 'theme/util/mode';
import IAIDraggable from './IAIDraggable'; import IAIDraggable from './IAIDraggable';
import IAIDroppable from './IAIDroppable'; import IAIDroppable from './IAIDroppable';
import SelectionOverlay from './SelectionOverlay'; import SelectionOverlay from './SelectionOverlay';
import {
TypesafeDraggableData,
TypesafeDroppableData,
} from 'features/dnd/types';
type IAIDndImageProps = { type IAIDndImageProps = {
imageDTO: ImageDTO | undefined; imageDTO: ImageDTO | undefined;
onError?: (event: SyntheticEvent<HTMLImageElement>) => void; onError?: (event: SyntheticEvent<HTMLImageElement>) => void;
onLoad?: (event: SyntheticEvent<HTMLImageElement>) => void; onLoad?: (event: SyntheticEvent<HTMLImageElement>) => void;
onClick?: (event: MouseEvent<HTMLDivElement>) => void; onClick?: (event: MouseEvent<HTMLDivElement>) => void;
onClickReset?: (event: MouseEvent<HTMLButtonElement>) => void;
withResetIcon?: boolean;
resetIcon?: ReactElement;
resetTooltip?: string;
withMetadataOverlay?: boolean; withMetadataOverlay?: boolean;
isDragDisabled?: boolean; isDragDisabled?: boolean;
isDropDisabled?: boolean; isDropDisabled?: boolean;
@ -58,15 +46,16 @@ type IAIDndImageProps = {
noContentFallback?: ReactElement; noContentFallback?: ReactElement;
useThumbailFallback?: boolean; useThumbailFallback?: boolean;
withHoverOverlay?: boolean; withHoverOverlay?: boolean;
children?: JSX.Element;
onMouseOver?: () => void;
onMouseOut?: () => void;
}; };
const IAIDndImage = (props: IAIDndImageProps) => { const IAIDndImage = (props: IAIDndImageProps) => {
const { const {
imageDTO, imageDTO,
onClickReset,
onError, onError,
onClick, onClick,
withResetIcon = false,
withMetadataOverlay = false, withMetadataOverlay = false,
isDropDisabled = false, isDropDisabled = false,
isDragDisabled = false, isDragDisabled = false,
@ -80,32 +69,30 @@ const IAIDndImage = (props: IAIDndImageProps) => {
dropLabel, dropLabel,
isSelected = false, isSelected = false,
thumbnail = false, thumbnail = false,
resetTooltip = 'Reset',
resetIcon = <FaUndo />,
noContentFallback = <IAINoContentFallback icon={FaImage} />, noContentFallback = <IAINoContentFallback icon={FaImage} />,
useThumbailFallback, useThumbailFallback,
withHoverOverlay = false, withHoverOverlay = false,
children,
onMouseOver,
onMouseOut,
} = props; } = props;
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const handleMouseOver = useCallback(() => { const handleMouseOver = useCallback(() => {
if (onMouseOver) onMouseOver();
setIsHovered(true); setIsHovered(true);
}, []); }, [onMouseOver]);
const handleMouseOut = useCallback(() => { const handleMouseOut = useCallback(() => {
if (onMouseOut) onMouseOut();
setIsHovered(false); setIsHovered(false);
}, []); }, [onMouseOut]);
const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({ const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
postUploadAction, postUploadAction,
isDisabled: isUploadDisabled, isDisabled: isUploadDisabled,
}); });
const resetIconShadow = useColorModeValue(
`drop-shadow(0px 0px 0.1rem var(--invokeai-colors-base-600))`,
`drop-shadow(0px 0px 0.1rem var(--invokeai-colors-base-800))`
);
const uploadButtonStyles = isUploadDisabled const uploadButtonStyles = isUploadDisabled
? {} ? {}
: { : {
@ -212,30 +199,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {
onClick={onClick} onClick={onClick}
/> />
)} )}
{onClickReset && withResetIcon && imageDTO && (
<IAIIconButton
onClick={onClickReset}
aria-label={resetTooltip}
tooltip={resetTooltip}
icon={resetIcon}
size="sm"
variant="link"
sx={{
position: 'absolute',
top: 1,
insetInlineEnd: 1,
p: 0,
minW: 0,
svg: {
transitionProperty: 'common',
transitionDuration: 'normal',
fill: 'base.100',
_hover: { fill: 'base.50' },
filter: resetIconShadow,
},
}}
/>
)}
{!isDropDisabled && ( {!isDropDisabled && (
<IAIDroppable <IAIDroppable
data={droppableData} data={droppableData}
@ -243,6 +206,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
dropLabel={dropLabel} dropLabel={dropLabel}
/> />
)} )}
{children}
</Flex> </Flex>
)} )}
</ImageContextMenu> </ImageContextMenu>

View File

@ -0,0 +1,46 @@
import { JSXElementConstructor, ReactElement, memo, MouseEvent } from 'react';
import IAIIconButton from './IAIIconButton';
import { SystemStyleObject, useColorModeValue } from '@chakra-ui/react';
type Props = {
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
tooltip: string;
icon?: ReactElement<any, string | JSXElementConstructor<any>>;
styleOverrides?: SystemStyleObject;
};
const IAIDndImageIcon = (props: Props) => {
const { onClick, tooltip, icon, styleOverrides } = props;
const resetIconShadow = useColorModeValue(
`drop-shadow(0px 0px 0.1rem var(--invokeai-colors-base-600))`,
`drop-shadow(0px 0px 0.1rem var(--invokeai-colors-base-800))`
);
return (
<IAIIconButton
onClick={onClick}
aria-label={tooltip}
tooltip={tooltip}
icon={icon}
size="sm"
variant="link"
sx={{
position: 'absolute',
top: 1,
insetInlineEnd: 1,
p: 0,
minW: 0,
svg: {
transitionProperty: 'common',
transitionDuration: 'normal',
fill: 'base.100',
_hover: { fill: 'base.50' },
filter: resetIconShadow,
},
...styleOverrides,
}}
/>
);
};
export default memo(IAIDndImageIcon);

View File

@ -1,4 +1,10 @@
import { Box, Flex, Spinner, SystemStyleObject } from '@chakra-ui/react'; import {
Box,
Flex,
Spinner,
SystemStyleObject,
useColorModeValue,
} from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { skipToken } from '@reduxjs/toolkit/dist/query'; import { skipToken } from '@reduxjs/toolkit/dist/query';
import { import {
@ -16,6 +22,8 @@ import {
ControlNetConfig, ControlNetConfig,
controlNetImageChanged, controlNetImageChanged,
} from '../store/controlNetSlice'; } from '../store/controlNetSlice';
import { FaUndo } from 'react-icons/fa';
import IAIDndImageIcon from '../../../common/components/IAIDndImageIcon';
type Props = { type Props = {
controlNet: ControlNetConfig; controlNet: ControlNetConfig;
@ -93,6 +101,11 @@ const ControlNetImagePreview = (props: Props) => {
[controlNetId] [controlNetId]
); );
const resetIconShadow = useColorModeValue(
`drop-shadow(0px 0px 0.1rem var(--invokeai-colors-base-600))`,
`drop-shadow(0px 0px 0.1rem var(--invokeai-colors-base-800))`
);
const shouldShowProcessedImage = const shouldShowProcessedImage =
controlImage && controlImage &&
processedControlImage && processedControlImage &&
@ -119,11 +132,15 @@ const ControlNetImagePreview = (props: Props) => {
droppableData={droppableData} droppableData={droppableData}
imageDTO={controlImage} imageDTO={controlImage}
isDropDisabled={shouldShowProcessedImage || !isEnabled} isDropDisabled={shouldShowProcessedImage || !isEnabled}
onClickReset={handleResetControlImage}
postUploadAction={postUploadAction} postUploadAction={postUploadAction}
resetTooltip="Reset Control Image" >
withResetIcon={Boolean(controlImage)} <IAIDndImageIcon
/> onClick={handleResetControlImage}
icon={controlImage ? <FaUndo /> : undefined}
tooltip="Reset Control Image"
/>
</IAIDndImage>
<Box <Box
sx={{ sx={{
position: 'absolute', position: 'absolute',
@ -143,10 +160,13 @@ const ControlNetImagePreview = (props: Props) => {
imageDTO={processedControlImage} imageDTO={processedControlImage}
isUploadDisabled={true} isUploadDisabled={true}
isDropDisabled={!isEnabled} isDropDisabled={!isEnabled}
onClickReset={handleResetControlImage} >
resetTooltip="Reset Control Image" <IAIDndImageIcon
withResetIcon={Boolean(controlImage)} onClick={handleResetControlImage}
/> icon={controlImage ? <FaUndo /> : undefined}
tooltip="Reset Control Image"
/>
</IAIDndImage>
</Box> </Box>
{pendingControlImages.includes(controlNetId) && ( {pendingControlImages.includes(controlNetId) && (
<Flex <Flex

View File

@ -11,7 +11,6 @@ import {
autoAssignBoardOnClickChanged, autoAssignBoardOnClickChanged,
setGalleryImageMinimumWidth, setGalleryImageMinimumWidth,
shouldAutoSwitchChanged, shouldAutoSwitchChanged,
shouldShowDeleteButtonChanged,
} from 'features/gallery/store/gallerySlice'; } from 'features/gallery/store/gallerySlice';
import { ChangeEvent, useCallback } from 'react'; import { ChangeEvent, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -26,14 +25,12 @@ const selector = createSelector(
galleryImageMinimumWidth, galleryImageMinimumWidth,
shouldAutoSwitch, shouldAutoSwitch,
autoAssignBoardOnClick, autoAssignBoardOnClick,
shouldShowDeleteButton,
} = state.gallery; } = state.gallery;
return { return {
galleryImageMinimumWidth, galleryImageMinimumWidth,
shouldAutoSwitch, shouldAutoSwitch,
autoAssignBoardOnClick, autoAssignBoardOnClick,
shouldShowDeleteButton,
}; };
}, },
defaultSelectorOptions defaultSelectorOptions
@ -43,12 +40,8 @@ const GallerySettingsPopover = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const { const { galleryImageMinimumWidth, shouldAutoSwitch, autoAssignBoardOnClick } =
galleryImageMinimumWidth, useAppSelector(selector);
shouldAutoSwitch,
autoAssignBoardOnClick,
shouldShowDeleteButton,
} = useAppSelector(selector);
const handleChangeGalleryImageMinimumWidth = useCallback( const handleChangeGalleryImageMinimumWidth = useCallback(
(v: number) => { (v: number) => {
@ -68,13 +61,6 @@ const GallerySettingsPopover = () => {
[dispatch] [dispatch]
); );
const handleChangeShowDeleteButton = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
dispatch(shouldShowDeleteButtonChanged(e.target.checked));
},
[dispatch]
);
return ( return (
<IAIPopover <IAIPopover
triggerComponent={ triggerComponent={
@ -90,7 +76,7 @@ const GallerySettingsPopover = () => {
<IAISlider <IAISlider
value={galleryImageMinimumWidth} value={galleryImageMinimumWidth}
onChange={handleChangeGalleryImageMinimumWidth} onChange={handleChangeGalleryImageMinimumWidth}
min={32} min={45}
max={256} max={256}
hideTooltip={true} hideTooltip={true}
label={t('gallery.galleryImageSize')} label={t('gallery.galleryImageSize')}
@ -102,11 +88,6 @@ const GallerySettingsPopover = () => {
isChecked={shouldAutoSwitch} isChecked={shouldAutoSwitch}
onChange={handleChangeAutoSwitch} onChange={handleChangeAutoSwitch}
/> />
<IAISwitch
label="Show Delete Button"
isChecked={shouldShowDeleteButton}
onChange={handleChangeShowDeleteButton}
/>
<IAISimpleCheckbox <IAISimpleCheckbox
label={t('gallery.autoAssignBoardOnClick')} label={t('gallery.autoAssignBoardOnClick')}
isChecked={autoAssignBoardOnClick} isChecked={autoAssignBoardOnClick}

View File

@ -36,7 +36,7 @@ import {
import { ImageDTO } from 'services/api/types'; import { ImageDTO } from 'services/api/types';
import { useDebounce } from 'use-debounce'; import { useDebounce } from 'use-debounce';
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions'; import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
import { BsBookmarkStar, BsFillBookmarkStarFill } from 'react-icons/bs'; import { MdStar, MdStarBorder } from 'react-icons/md';
type SingleSelectionMenuItemsProps = { type SingleSelectionMenuItemsProps = {
imageDTO: ImageDTO; imageDTO: ImageDTO;
@ -211,15 +211,12 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
Change Board Change Board
</MenuItem> </MenuItem>
{imageDTO.pinned ? ( {imageDTO.pinned ? (
<MenuItem icon={<BsBookmarkStar />} onClickCapture={handleUnpinImage}> <MenuItem icon={<MdStar />} onClickCapture={handleUnpinImage}>
Unpin Image Unstar Image
</MenuItem> </MenuItem>
) : ( ) : (
<MenuItem <MenuItem icon={<MdStarBorder />} onClickCapture={handlePinImage}>
icon={<BsFillBookmarkStarFill />} Star Image
onClickCapture={handlePinImage}
>
Pin Image
</MenuItem> </MenuItem>
)} )}
<MenuItem <MenuItem

View File

@ -1,4 +1,4 @@
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex, useColorModeValue } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImage from 'common/components/IAIDndImage';
import IAIFillSkeleton from 'common/components/IAIFillSkeleton'; import IAIFillSkeleton from 'common/components/IAIFillSkeleton';
@ -9,12 +9,14 @@ import {
TypesafeDraggableData, TypesafeDraggableData,
} from 'features/dnd/types'; } from 'features/dnd/types';
import { useMultiselect } from 'features/gallery/hooks/useMultiselect.ts'; import { useMultiselect } from 'features/gallery/hooks/useMultiselect.ts';
import { MouseEvent, memo, useCallback, useMemo } from 'react'; import { MouseEvent, memo, useCallback, useMemo, useState } from 'react';
import { BsBookmarkStar, BsFillBookmarkStarFill } from 'react-icons/bs'; import { FaTrash } from 'react-icons/fa';
import { MdStar, MdStarBorder } from 'react-icons/md';
import { import {
useChangeImagePinnedMutation, useChangeImagePinnedMutation,
useGetImageDTOQuery, useGetImageDTOQuery,
} from 'services/api/endpoints/images'; } from 'services/api/endpoints/images';
import IAIDndImageIcon from '../../../../common/components/IAIDndImageIcon';
interface HoverableImageProps { interface HoverableImageProps {
imageName: string; imageName: string;
@ -70,6 +72,33 @@ const GalleryImage = (props: HoverableImageProps) => {
} }
}, [togglePin, imageDTO]); }, [togglePin, imageDTO]);
const [isHovered, setIsHovered] = useState(false);
const pinIcon = useMemo(() => {
if (imageDTO?.pinned) return <MdStar size="20" />;
if (!imageDTO?.pinned && isHovered) return <MdStarBorder size="20" />;
}, [imageDTO?.pinned, isHovered]);
const resetIconShadow = useColorModeValue(
`drop-shadow(0px 0px 0.1rem var(--invokeai-colors-base-600))`,
`drop-shadow(0px 0px 0.1rem var(--invokeai-colors-base-800))`
);
const iconButtonStyles = {
position: 'absolute',
top: 1,
insetInlineEnd: 1,
p: 0,
minW: 0,
svg: {
transitionProperty: 'common',
transitionDuration: 'normal',
fill: 'base.100',
_hover: { fill: 'base.50' },
filter: resetIconShadow,
},
};
if (!imageDTO) { if (!imageDTO) {
return <IAIFillSkeleton />; return <IAIFillSkeleton />;
} }
@ -91,18 +120,34 @@ const GalleryImage = (props: HoverableImageProps) => {
draggableData={draggableData} draggableData={draggableData}
isSelected={isSelected} isSelected={isSelected}
minSize={0} minSize={0}
onClickReset={togglePinnedState}
imageSx={{ w: 'full', h: 'full' }} imageSx={{ w: 'full', h: 'full' }}
isDropDisabled={true} isDropDisabled={true}
isUploadDisabled={true} isUploadDisabled={true}
thumbnail={true} thumbnail={true}
withHoverOverlay withHoverOverlay
resetIcon={ onMouseOver={() => setIsHovered(true)}
imageDTO.pinned ? <BsFillBookmarkStarFill /> : <BsBookmarkStar /> onMouseOut={() => setIsHovered(false)}
} >
resetTooltip="Pin image" <>
withResetIcon={true} <IAIDndImageIcon
/> onClick={togglePinnedState}
icon={pinIcon}
tooltip={imageDTO.pinned ? 'Unstar' : 'Star'}
/>
{isHovered && shouldShowDeleteButton && (
<IAIDndImageIcon
onClick={handleDelete}
icon={<FaTrash />}
tooltip={'Delete'}
styleOverrides={{
bottom: 1,
top: 'auto',
}}
/>
)}
</>
</IAIDndImage>
</Flex> </Flex>
</Box> </Box>
); );

View File

@ -1,5 +1,5 @@
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIButton from 'common/components/IAIButton'; import IAIButton from 'common/components/IAIButton';
import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors'; import { selectListImagesBaseQueryArgs } from 'features/gallery/store/gallerySelectors';
@ -20,6 +20,8 @@ import { useBoardTotal } from 'services/api/hooks/useBoardTotal';
import GalleryImage from './GalleryImage'; import GalleryImage from './GalleryImage';
import ImageGridItemContainer from './ImageGridItemContainer'; import ImageGridItemContainer from './ImageGridItemContainer';
import ImageGridListContainer from './ImageGridListContainer'; import ImageGridListContainer from './ImageGridListContainer';
import { useHotkeys } from 'react-hotkeys-hook';
import { shouldShowDeleteButtonChanged } from '../../store/gallerySlice';
const overlayScrollbarsConfig: UseOverlayScrollbarsParams = { const overlayScrollbarsConfig: UseOverlayScrollbarsParams = {
defer: true, defer: true,
@ -36,6 +38,7 @@ const overlayScrollbarsConfig: UseOverlayScrollbarsParams = {
const GalleryImageGrid = () => { const GalleryImageGrid = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch();
const rootRef = useRef<HTMLDivElement>(null); const rootRef = useRef<HTMLDivElement>(null);
const [scroller, setScroller] = useState<HTMLElement | null>(null); const [scroller, setScroller] = useState<HTMLElement | null>(null);
const [initialize, osInstance] = useOverlayScrollbars( const [initialize, osInstance] = useOverlayScrollbars(
@ -85,6 +88,23 @@ const GalleryImageGrid = () => {
return () => osInstance()?.destroy(); return () => osInstance()?.destroy();
}, [scroller, initialize, osInstance]); }, [scroller, initialize, osInstance]);
useHotkeys(
'shift',
() => {
dispatch(shouldShowDeleteButtonChanged(true));
},
[shouldShowDeleteButtonChanged]
);
useHotkeys(
'shift',
() => {
dispatch(shouldShowDeleteButtonChanged(false));
},
{ keyup: true },
[shouldShowDeleteButtonChanged]
);
if (!currentData) { if (!currentData) {
return ( return (
<Flex <Flex

View File

@ -78,11 +78,8 @@ const ImageInputFieldComponent = (
imageDTO={imageDTO} imageDTO={imageDTO}
droppableData={droppableData} droppableData={droppableData}
draggableData={draggableData} draggableData={draggableData}
onClickReset={handleReset}
withResetIcon
thumbnail
useThumbailFallback
postUploadAction={postUploadAction} postUploadAction={postUploadAction}
useThumbailFallback
/> />
</Flex> </Flex>
); );