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

View File

@ -10,8 +10,8 @@ const OverlayDragImage = (props: OverlayDragImageProps) => {
return ( return (
<Image <Image
sx={{ sx={{
maxW: 32, maxW: 36,
maxH: 32, maxH: 36,
borderRadius: 'base', borderRadius: 'base',
shadow: 'dark-lg', 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 { AnimatePresence, motion } from 'framer-motion';
import { SyntheticEvent } from 'react'; import { SyntheticEvent } from 'react';
import { memo, useRef } 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 { ImageDTO } from 'services/api';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@ -53,9 +53,8 @@ const IAISelectableImage = (props: IAISelectableImageProps) => {
{image && ( {image && (
<Flex <Flex
sx={{ sx={{
position: 'relative',
w: 'full', w: 'full',
h: 'full', position: 'relative',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}} }}
@ -82,7 +81,7 @@ const IAISelectableImage = (props: IAISelectableImageProps) => {
<IAIIconButton <IAIIconButton
size={resetIconSize} size={resetIconSize}
aria-label="Reset Image" aria-label="Reset Image"
icon={<FaUndo />} icon={<FaTimes />}
onClick={onReset} onClick={onReset}
/> />
</Box> </Box>
@ -184,7 +183,7 @@ const DropOverlay = (props: DropOverlayProps) => {
transitionDuration: '0.15s', transitionDuration: '0.15s',
}} }}
> >
<Text sx={{ fontSize: '2xl', fontWeight: 600, color: 'base.50' }}> <Text sx={{ fontSize: '2xl', fontWeight: 600, color: 'base.200' }}>
Drop Image Drop Image
</Text> </Text>
</Flex> </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 { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { systemSelector } from 'features/system/store/systemSelectors'; import { systemSelector } from 'features/system/store/systemSelectors';
@ -55,10 +55,7 @@ const CurrentImageDisplay = () => {
}} }}
> >
{hasAnImageToDisplay ? ( {hasAnImageToDisplay ? (
<> <CurrentImagePreview />
<CurrentImageButtons />
<CurrentImagePreview />
</>
) : ( ) : (
<Icon <Icon
as={FaImage} as={FaImage}
@ -69,6 +66,11 @@ const CurrentImageDisplay = () => {
/> />
)} )}
</Flex> </Flex>
{hasAnImageToDisplay && (
<Box sx={{ position: 'absolute', top: 0 }}>
<CurrentImageButtons />
</Box>
)}
</Flex> </Flex>
); );
}; };

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
import { Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import InitialImagePreview from './InitialImagePreview'; import InitialImagePreview from './InitialImagePreview';
import InitialImageButtons from 'common/components/InitialImageButtons';
const InitialImageDisplay = () => { const InitialImageDisplay = () => {
return ( 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 { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useGetUrl } from 'common/util/getUrl'; import { useGetUrl } from 'common/util/getUrl';
@ -6,14 +6,10 @@ import {
clearInitialImage, clearInitialImage,
initialImageChanged, initialImageChanged,
} from 'features/parameters/store/generationSlice'; } from 'features/parameters/store/generationSlice';
import { DragEvent, useCallback } from 'react'; import { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import ImageMetadataOverlay from 'common/components/ImageMetadataOverlay';
import { generationSelector } from 'features/parameters/store/generationSelectors'; import { generationSelector } from 'features/parameters/store/generationSelectors';
import { initialImageSelected } from 'features/parameters/store/actions';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; 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 { configSelector } from '../../../../system/store/configSelectors';
import { useAppToaster } from 'app/components/Toaster'; import { useAppToaster } from 'app/components/Toaster';
import IAISelectableImage from 'features/controlNet/components/parameters/IAISelectableImage'; import IAISelectableImage from 'features/controlNet/components/parameters/IAISelectableImage';
@ -76,41 +72,12 @@ const InitialImagePreview = () => {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}} }}
// onDrop={handleDrop}
> >
<IAISelectableImage <IAISelectableImage
image={initialImage} image={initialImage}
onChange={handleChange} onChange={handleChange}
onReset={handleReset} 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> </Flex>
); );
}; };