mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): improve drag and drop ux
This commit is contained in:
parent
b1e1e3efc7
commit
fa4d88e163
@ -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} />}
|
||||||
|
@ -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',
|
||||||
}}
|
}}
|
||||||
|
@ -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;
|
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 && (
|
||||||
|
@ -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 }}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 (
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user