feat(ui): controlnet/image dnd wip

Implement `dnd-kit` for image drag and drop
- vastly simplifies logic bc we can drag and drop non-serializable data (like an `ImageDTO`)
- also much prettier
- also will fix conflicts with file upload via OS drag and drop, bc `dnd-kit` does not use native HTML drag and drop API
- Implemented for Init image, controlnet, and node editor so far

More progress on the ControlNet UI
This commit is contained in:
psychedelicious
2023-06-01 18:27:39 +10:00
parent e2e07696fc
commit 3b9426eb72
42 changed files with 853 additions and 234 deletions

View File

@ -0,0 +1,45 @@
import {
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
} from '@dnd-kit/core';
import { PropsWithChildren, memo, useCallback, useState } from 'react';
import OverlayDragImage from './OverlayDragImage';
import { ImageDTO } from 'services/api';
import { isImageDTO } from 'services/types/guards';
type ImageDndContextProps = PropsWithChildren;
const ImageDndContext = (props: ImageDndContextProps) => {
const [draggedImage, setDraggedImage] = useState<ImageDTO | null>(null);
const handleDragStart = useCallback((event: DragStartEvent) => {
const dragData = event.active.data.current;
if (dragData && 'image' in dragData && isImageDTO(dragData.image)) {
setDraggedImage(dragData.image);
}
}, []);
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const handleDrop = event.over?.data.current?.handleDrop;
if (handleDrop && typeof handleDrop === 'function' && draggedImage) {
handleDrop(draggedImage);
}
setDraggedImage(null);
},
[draggedImage]
);
return (
<DndContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
{props.children}
<DragOverlay dropAnimation={null}>
{draggedImage && <OverlayDragImage image={draggedImage} />}
</DragOverlay>
</DndContext>
);
};
export default memo(ImageDndContext);

View File

@ -0,0 +1,23 @@
import { Image } from '@chakra-ui/react';
import { memo } from 'react';
import { ImageDTO } from 'services/api';
type OverlayDragImageProps = {
image: ImageDTO;
};
const OverlayDragImage = (props: OverlayDragImageProps) => {
return (
<Image
sx={{
maxW: 32,
maxH: 32,
borderRadius: 'base',
shadow: 'dark-lg',
}}
src={props.image.thumbnail_url}
/>
);
};
export default memo(OverlayDragImage);

View File

@ -16,6 +16,7 @@ import { PartialAppConfig } from 'app/types/invokeai';
import '../../i18n';
import { socketMiddleware } from 'services/events/middleware';
import { Middleware } from '@reduxjs/toolkit';
import ImageDndContext from './ImageDnd/ImageDndContext';
const App = lazy(() => import('./App'));
const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider'));
@ -69,11 +70,13 @@ const InvokeAIUI = ({
<Provider store={store}>
<React.Suspense fallback={<Loading />}>
<ThemeLocaleProvider>
<App
config={config}
headerComponent={headerComponent}
setIsReady={setIsReady}
/>
<ImageDndContext>
<App
config={config}
headerComponent={headerComponent}
setIsReady={setIsReady}
/>
</ImageDndContext>
</ThemeLocaleProvider>
</React.Suspense>
</Provider>

View File

@ -2,12 +2,10 @@ import { initialImageChanged } from 'features/parameters/store/generationSlice';
import { t } from 'i18next';
import { addToast } from 'features/system/store/systemSlice';
import { startAppListening } from '..';
import {
initialImageSelected,
isImageDTO,
} from 'features/parameters/store/actions';
import { initialImageSelected } from 'features/parameters/store/actions';
import { makeToast } from 'app/components/Toaster';
import { selectImagesById } from 'features/gallery/store/imagesSlice';
import { isImageDTO } from 'services/types/guards';
export const addInitialImageSelectedListener = () => {
startAppListening({

View File

@ -13,6 +13,7 @@ import galleryReducer from 'features/gallery/store/gallerySlice';
import imagesReducer from 'features/gallery/store/imagesSlice';
import lightboxReducer from 'features/lightbox/store/lightboxSlice';
import generationReducer from 'features/parameters/store/generationSlice';
import controlNetReducer from 'features/controlNet/store/controlNetSlice';
import postprocessingReducer from 'features/parameters/store/postprocessingSlice';
import systemReducer from 'features/system/store/systemSlice';
// import sessionReducer from 'features/system/store/sessionSlice';
@ -45,6 +46,7 @@ const allReducers = {
ui: uiReducer,
hotkeys: hotkeysReducer,
images: imagesReducer,
controlNet: controlNetReducer,
// session: sessionReducer,
};
@ -62,6 +64,7 @@ const rememberedKeys: (keyof typeof allReducers)[] = [
'postprocessing',
'system',
'ui',
'controlNet',
// 'hotkeys',
// 'config',
];