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 {
ChakraProps,
Flex,
Icon,
Image,
useColorMode,
useColorModeValue,
} from '@chakra-ui/react';
import IAIIconButton from 'common/components/IAIIconButton';
import { ChakraProps, Flex, Icon, Image, useColorMode } from '@chakra-ui/react';
import {
IAILoadingImageFallback,
IAINoContentFallback,
} from 'common/components/IAIImageFallback';
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import {
TypesafeDraggableData,
TypesafeDroppableData,
} from 'features/dnd/types';
import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
import {
MouseEvent,
@ -26,22 +14,22 @@ import {
useCallback,
useState,
} 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 { mode } from 'theme/util/mode';
import IAIDraggable from './IAIDraggable';
import IAIDroppable from './IAIDroppable';
import SelectionOverlay from './SelectionOverlay';
import {
TypesafeDraggableData,
TypesafeDroppableData,
} from 'features/dnd/types';
type IAIDndImageProps = {
imageDTO: ImageDTO | undefined;
onError?: (event: SyntheticEvent<HTMLImageElement>) => void;
onLoad?: (event: SyntheticEvent<HTMLImageElement>) => void;
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
onClickReset?: (event: MouseEvent<HTMLButtonElement>) => void;
withResetIcon?: boolean;
resetIcon?: ReactElement;
resetTooltip?: string;
withMetadataOverlay?: boolean;
isDragDisabled?: boolean;
isDropDisabled?: boolean;
@ -58,15 +46,16 @@ type IAIDndImageProps = {
noContentFallback?: ReactElement;
useThumbailFallback?: boolean;
withHoverOverlay?: boolean;
children?: JSX.Element;
onMouseOver?: () => void;
onMouseOut?: () => void;
};
const IAIDndImage = (props: IAIDndImageProps) => {
const {
imageDTO,
onClickReset,
onError,
onClick,
withResetIcon = false,
withMetadataOverlay = false,
isDropDisabled = false,
isDragDisabled = false,
@ -80,32 +69,30 @@ const IAIDndImage = (props: IAIDndImageProps) => {
dropLabel,
isSelected = false,
thumbnail = false,
resetTooltip = 'Reset',
resetIcon = <FaUndo />,
noContentFallback = <IAINoContentFallback icon={FaImage} />,
useThumbailFallback,
withHoverOverlay = false,
children,
onMouseOver,
onMouseOut,
} = props;
const { colorMode } = useColorMode();
const [isHovered, setIsHovered] = useState(false);
const handleMouseOver = useCallback(() => {
if (onMouseOver) onMouseOver();
setIsHovered(true);
}, []);
}, [onMouseOver]);
const handleMouseOut = useCallback(() => {
if (onMouseOut) onMouseOut();
setIsHovered(false);
}, []);
}, [onMouseOut]);
const { getUploadButtonProps, getUploadInputProps } = useImageUploadButton({
postUploadAction,
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
? {}
: {
@ -212,30 +199,6 @@ const IAIDndImage = (props: IAIDndImageProps) => {
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 && (
<IAIDroppable
data={droppableData}
@ -243,6 +206,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
dropLabel={dropLabel}
/>
)}
{children}
</Flex>
)}
</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 { skipToken } from '@reduxjs/toolkit/dist/query';
import {
@ -16,6 +22,8 @@ import {
ControlNetConfig,
controlNetImageChanged,
} from '../store/controlNetSlice';
import { FaUndo } from 'react-icons/fa';
import IAIDndImageIcon from '../../../common/components/IAIDndImageIcon';
type Props = {
controlNet: ControlNetConfig;
@ -93,6 +101,11 @@ const ControlNetImagePreview = (props: Props) => {
[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 =
controlImage &&
processedControlImage &&
@ -119,11 +132,15 @@ const ControlNetImagePreview = (props: Props) => {
droppableData={droppableData}
imageDTO={controlImage}
isDropDisabled={shouldShowProcessedImage || !isEnabled}
onClickReset={handleResetControlImage}
postUploadAction={postUploadAction}
resetTooltip="Reset Control Image"
withResetIcon={Boolean(controlImage)}
/>
>
<IAIDndImageIcon
onClick={handleResetControlImage}
icon={controlImage ? <FaUndo /> : undefined}
tooltip="Reset Control Image"
/>
</IAIDndImage>
<Box
sx={{
position: 'absolute',
@ -143,10 +160,13 @@ const ControlNetImagePreview = (props: Props) => {
imageDTO={processedControlImage}
isUploadDisabled={true}
isDropDisabled={!isEnabled}
onClickReset={handleResetControlImage}
resetTooltip="Reset Control Image"
withResetIcon={Boolean(controlImage)}
/>
>
<IAIDndImageIcon
onClick={handleResetControlImage}
icon={controlImage ? <FaUndo /> : undefined}
tooltip="Reset Control Image"
/>
</IAIDndImage>
</Box>
{pendingControlImages.includes(controlNetId) && (
<Flex

View File

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

View File

@ -36,7 +36,7 @@ import {
import { ImageDTO } from 'services/api/types';
import { useDebounce } from 'use-debounce';
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
import { BsBookmarkStar, BsFillBookmarkStarFill } from 'react-icons/bs';
import { MdStar, MdStarBorder } from 'react-icons/md';
type SingleSelectionMenuItemsProps = {
imageDTO: ImageDTO;
@ -211,15 +211,12 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
Change Board
</MenuItem>
{imageDTO.pinned ? (
<MenuItem icon={<BsBookmarkStar />} onClickCapture={handleUnpinImage}>
Unpin Image
<MenuItem icon={<MdStar />} onClickCapture={handleUnpinImage}>
Unstar Image
</MenuItem>
) : (
<MenuItem
icon={<BsFillBookmarkStarFill />}
onClickCapture={handlePinImage}
>
Pin Image
<MenuItem icon={<MdStarBorder />} onClickCapture={handlePinImage}>
Star Image
</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 IAIDndImage from 'common/components/IAIDndImage';
import IAIFillSkeleton from 'common/components/IAIFillSkeleton';
@ -9,12 +9,14 @@ import {
TypesafeDraggableData,
} from 'features/dnd/types';
import { useMultiselect } from 'features/gallery/hooks/useMultiselect.ts';
import { MouseEvent, memo, useCallback, useMemo } from 'react';
import { BsBookmarkStar, BsFillBookmarkStarFill } from 'react-icons/bs';
import { MouseEvent, memo, useCallback, useMemo, useState } from 'react';
import { FaTrash } from 'react-icons/fa';
import { MdStar, MdStarBorder } from 'react-icons/md';
import {
useChangeImagePinnedMutation,
useGetImageDTOQuery,
} from 'services/api/endpoints/images';
import IAIDndImageIcon from '../../../../common/components/IAIDndImageIcon';
interface HoverableImageProps {
imageName: string;
@ -70,6 +72,33 @@ const GalleryImage = (props: HoverableImageProps) => {
}
}, [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) {
return <IAIFillSkeleton />;
}
@ -91,18 +120,34 @@ const GalleryImage = (props: HoverableImageProps) => {
draggableData={draggableData}
isSelected={isSelected}
minSize={0}
onClickReset={togglePinnedState}
imageSx={{ w: 'full', h: 'full' }}
isDropDisabled={true}
isUploadDisabled={true}
thumbnail={true}
withHoverOverlay
resetIcon={
imageDTO.pinned ? <BsFillBookmarkStarFill /> : <BsBookmarkStar />
}
resetTooltip="Pin image"
withResetIcon={true}
/>
onMouseOver={() => setIsHovered(true)}
onMouseOut={() => setIsHovered(false)}
>
<>
<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>
</Box>
);

View File

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

View File

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