feat(ui): improve drag and drop ux

This commit is contained in:
psychedelicious 2023-06-01 19:24:26 +10:00
parent b1e1e3efc7
commit fa4d88e163
10 changed files with 72 additions and 117 deletions

View File

@ -3,6 +3,11 @@ import {
DragEndEvent,
DragOverlay,
DragStartEvent,
KeyboardSensor,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { PropsWithChildren, memo, useCallback, useState } from 'react';
import OverlayDragImage from './OverlayDragImage';
@ -32,8 +37,23 @@ const ImageDndContext = (props: ImageDndContextProps) => {
[draggedImage]
);
const mouseSensor = useSensor(MouseSensor, {
activationConstraint: { distance: 15 },
});
const touchSensor = useSensor(TouchSensor, {
activationConstraint: { distance: 15 },
});
const keyboardSensor = useSensor(KeyboardSensor);
const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor);
return (
<DndContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<DndContext
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
sensors={sensors}
>
{props.children}
<DragOverlay dropAnimation={null}>
{draggedImage && <OverlayDragImage image={draggedImage} />}

View File

@ -10,8 +10,8 @@ const OverlayDragImage = (props: OverlayDragImageProps) => {
return (
<Image
sx={{
maxW: 32,
maxH: 32,
maxW: 36,
maxH: 36,
borderRadius: 'base',
shadow: 'dark-lg',
}}

View File

@ -1,42 +0,0 @@
import { ButtonGroup, Flex, Spacer, Text } from '@chakra-ui/react';
import IAIIconButton from 'common/components/IAIIconButton';
import { useTranslation } from 'react-i18next';
import { FaUndo, FaUpload } from 'react-icons/fa';
import { useAppDispatch } from 'app/store/storeHooks';
import { useCallback } from 'react';
import { clearInitialImage } from 'features/parameters/store/generationSlice';
import useImageUploader from 'common/hooks/useImageUploader';
const InitialImageButtons = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const { openUploader } = useImageUploader();
const handleResetInitialImage = useCallback(() => {
dispatch(clearInitialImage());
}, [dispatch]);
return (
<Flex w="full" alignItems="center">
<Text size="sm" fontWeight={500} color="base.300">
{t('parameters.initialImage')}
</Text>
<Spacer />
<ButtonGroup>
<IAIIconButton
icon={<FaUndo />}
aria-label={t('accessibility.reset')}
onClick={handleResetInitialImage}
/>
<IAIIconButton
icon={<FaUpload />}
onClick={openUploader}
aria-label={t('common.upload')}
/>
</ButtonGroup>
</Flex>
);
};
export default InitialImageButtons;

View File

@ -14,7 +14,7 @@ import { useGetUrl } from 'common/util/getUrl';
import { AnimatePresence, motion } from 'framer-motion';
import { SyntheticEvent } from 'react';
import { memo, useRef } from 'react';
import { FaImage, FaUndo } from 'react-icons/fa';
import { FaImage, FaTimes } from 'react-icons/fa';
import { ImageDTO } from 'services/api';
import { v4 as uuidv4 } from 'uuid';
@ -53,9 +53,8 @@ const IAISelectableImage = (props: IAISelectableImageProps) => {
{image && (
<Flex
sx={{
position: 'relative',
w: 'full',
h: 'full',
position: 'relative',
alignItems: 'center',
justifyContent: 'center',
}}
@ -82,7 +81,7 @@ const IAISelectableImage = (props: IAISelectableImageProps) => {
<IAIIconButton
size={resetIconSize}
aria-label="Reset Image"
icon={<FaUndo />}
icon={<FaTimes />}
onClick={onReset}
/>
</Box>
@ -184,7 +183,7 @@ const DropOverlay = (props: DropOverlayProps) => {
transitionDuration: '0.15s',
}}
>
<Text sx={{ fontSize: '2xl', fontWeight: 600, color: 'base.50' }}>
<Text sx={{ fontSize: '2xl', fontWeight: 600, color: 'base.200' }}>
Drop Image
</Text>
</Flex>

View File

@ -1,4 +1,4 @@
import { Flex, Icon } from '@chakra-ui/react';
import { Box, Flex, Icon } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { systemSelector } from 'features/system/store/systemSelectors';
@ -55,10 +55,7 @@ const CurrentImageDisplay = () => {
}}
>
{hasAnImageToDisplay ? (
<>
<CurrentImageButtons />
<CurrentImagePreview />
</>
<CurrentImagePreview />
) : (
<Icon
as={FaImage}
@ -69,6 +66,11 @@ const CurrentImageDisplay = () => {
/>
)}
</Flex>
{hasAnImageToDisplay && (
<Box sx={{ position: 'absolute', top: 0 }}>
<CurrentImageButtons />
</Box>
)}
</Flex>
);
};

View File

@ -15,6 +15,7 @@ import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
import { configSelector } from '../../system/store/configSelectors';
import { useAppToaster } from 'app/components/Toaster';
import { imageSelected } from '../store/gallerySlice';
import { useDraggable } from '@dnd-kit/core';
export const imagesSelector = createSelector(
[uiSelector, gallerySelector, systemSelector],
@ -46,7 +47,6 @@ const CurrentImagePreview = () => {
const {
shouldShowImageDetails,
image,
shouldHidePreview,
progressImage,
shouldShowProgressInViewer,
shouldAntialiasProgressImage,
@ -56,16 +56,12 @@ const CurrentImagePreview = () => {
const toaster = useAppToaster();
const dispatch = useAppDispatch();
const handleDragStart = useCallback(
(e: DragEvent<HTMLDivElement>) => {
if (!image) {
return;
}
e.dataTransfer.setData('invokeai/imageName', image.image_name);
e.dataTransfer.effectAllowed = 'move';
const { attributes, listeners, setNodeRef } = useDraggable({
id: `currentImage_${image?.image_name}`,
data: {
image,
},
[image]
);
});
const handleError = useCallback(() => {
dispatch(imageSelected());
@ -105,24 +101,32 @@ const CurrentImagePreview = () => {
/>
) : (
image && (
<>
<Flex
sx={{
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
}}
>
<Image
ref={setNodeRef}
{...listeners}
{...attributes}
src={getUrl(image.image_url)}
fallbackStrategy="beforeLoadOrError"
fallback={<ImageFallbackSpinner />}
onDragStart={handleDragStart}
sx={{
objectFit: 'contain',
maxWidth: '100%',
maxHeight: '100%',
height: 'auto',
position: 'absolute',
borderRadius: 'base',
touchAction: 'none',
}}
onError={handleError}
/>
<ImageMetadataOverlay image={image} />
</>
</Flex>
)
)}
{shouldShowImageDetails && image && 'metadata' in image && (

View File

@ -40,7 +40,6 @@ import {
import { useAppToaster } from 'app/components/Toaster';
import { ImageDTO } from 'services/api';
import { useDraggable } from '@dnd-kit/core';
import { CSS } from '@dnd-kit/utilities';
export const selector = createSelector(
[gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector],
@ -120,7 +119,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
useRecallParameters();
const { attributes, listeners, setNodeRef } = useDraggable({
id: image_name,
id: `galleryImage_${image_name}`,
data: {
image,
},
@ -153,14 +152,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
dispatch(imageSelected(image));
}, [image, dispatch]);
const handleDragStart = useCallback(
(e: DragEvent<HTMLDivElement>) => {
e.dataTransfer.setData('invokeai/imageName', image.image_name);
e.dataTransfer.effectAllowed = 'move';
},
[image]
);
// Recall parameters handlers
const handleRecallPrompt = useCallback(() => {
recallBothPrompts(
@ -225,7 +216,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
ref={setNodeRef}
{...listeners}
{...attributes}
sx={{ w: 'full', h: 'full' }}
sx={{ w: 'full', h: 'full', touchAction: 'none' }}
>
<ContextMenu<HTMLDivElement>
menuProps={{ size: 'sm', isLazy: true }}

View File

@ -32,6 +32,16 @@ const ImageInputFieldComponent = (
[dispatch, field.name, nodeId]
);
const handleReset = useCallback(() => {
dispatch(
fieldValueChanged({
nodeId,
fieldName: field.name,
value: undefined,
})
);
}, [dispatch, field.name, nodeId]);
return (
<Flex
sx={{
@ -41,7 +51,12 @@ const ImageInputFieldComponent = (
justifyContent: 'center',
}}
>
<IAISelectableImage image={field.value} onChange={handleChange} />
<IAISelectableImage
image={field.value}
onChange={handleChange}
onReset={handleReset}
resetIconSize="sm"
/>
</Flex>
);
};

View File

@ -1,6 +1,5 @@
import { Flex } from '@chakra-ui/react';
import InitialImagePreview from './InitialImagePreview';
import InitialImageButtons from 'common/components/InitialImageButtons';
const InitialImageDisplay = () => {
return (

View File

@ -1,4 +1,4 @@
import { Flex, Icon, Image } from '@chakra-ui/react';
import { Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useGetUrl } from 'common/util/getUrl';
@ -6,14 +6,10 @@ import {
clearInitialImage,
initialImageChanged,
} from 'features/parameters/store/generationSlice';
import { DragEvent, useCallback } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
import { generationSelector } from 'features/parameters/store/generationSelectors';
import { initialImageSelected } from 'features/parameters/store/actions';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import ImageFallbackSpinner from 'features/gallery/components/ImageFallbackSpinner';
import { FaImage } from 'react-icons/fa';
import { configSelector } from '../../../../system/store/configSelectors';
import { useAppToaster } from 'app/components/Toaster';
import IAISelectableImage from 'features/controlNet/components/parameters/IAISelectableImage';
@ -76,41 +72,12 @@ const InitialImagePreview = () => {
alignItems: 'center',
justifyContent: 'center',
}}
// onDrop={handleDrop}
>
<IAISelectableImage
image={initialImage}
onChange={handleChange}
onReset={handleReset}
/>
{/* {initialImage?.image_url && (
<>
<Image
src={getUrl(initialImage?.image_url)}
fallbackStrategy="beforeLoadOrError"
fallback={<ImageFallbackSpinner />}
onError={handleError}
sx={{
objectFit: 'contain',
maxWidth: '100%',
maxHeight: '100%',
height: 'auto',
position: 'absolute',
borderRadius: 'base',
}}
/>
<ImageMetadataOverlay image={initialImage} />
</>
)}
{!initialImage?.image_url && (
<Icon
as={FaImage}
sx={{
boxSize: 24,
color: 'base.500',
}}
/>
)} */}
</Flex>
);
};