mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
update IAIDndImage to use children for icons, add UI for shift+delete to delete images from gallery
This commit is contained in:
parent
767a612746
commit
a512fdc0f6
@ -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>
|
||||
|
@ -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);
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -78,11 +78,8 @@ const ImageInputFieldComponent = (
|
||||
imageDTO={imageDTO}
|
||||
droppableData={droppableData}
|
||||
draggableData={draggableData}
|
||||
onClickReset={handleReset}
|
||||
withResetIcon
|
||||
thumbnail
|
||||
useThumbailFallback
|
||||
postUploadAction={postUploadAction}
|
||||
useThumbailFallback
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user