Skip to content

Commit 8771de9

Browse files
feat(ui): migrate fullscreen drop zone to pdnd
1 parent 122946e commit 8771de9

File tree

2 files changed

+160
-0
lines changed

2 files changed

+160
-0
lines changed

invokeai/frontend/web/src/app/components/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
NewGallerySessionDialog,
1818
} from 'features/controlLayers/components/NewSessionConfirmationAlertDialog';
1919
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
20+
import { FullscreenDropzone } from 'features/dnd/FullscreenDropzone';
2021
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
2122
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
2223
import { ImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
@@ -106,6 +107,7 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => {
106107
<NewGallerySessionDialog />
107108
<NewCanvasSessionDialog />
108109
<ImageContextMenu />
110+
<FullscreenDropzone />
109111
</ErrorBoundary>
110112
);
111113
};
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
2+
import { dropTargetForExternal, monitorForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
3+
import { containsFiles, getFiles } from '@atlaskit/pragmatic-drag-and-drop/external/file';
4+
import { preventUnhandled } from '@atlaskit/pragmatic-drag-and-drop/prevent-unhandled';
5+
import type { SystemStyleObject } from '@invoke-ai/ui-library';
6+
import { Box, Flex, Heading } from '@invoke-ai/ui-library';
7+
import { getStore } from 'app/store/nanostores/store';
8+
import { useAppSelector } from 'app/store/storeHooks';
9+
import { DndDropOverlay } from 'features/dnd/DndDropOverlay';
10+
import type { DndTargetState } from 'features/dnd/types';
11+
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
12+
import { selectMaxImageUploadCount } from 'features/system/store/configSlice';
13+
import { toast } from 'features/toast/toast';
14+
import { memo, useEffect, useRef, useState } from 'react';
15+
import { useTranslation } from 'react-i18next';
16+
import { type UploadImageArg, uploadImages } from 'services/api/endpoints/images';
17+
import { useBoardName } from 'services/api/hooks/useBoardName';
18+
import { z } from 'zod';
19+
20+
const ACCEPTED_IMAGE_TYPES = ['image/png', 'image/jpg', 'image/jpeg'];
21+
const ACCEPTED_FILE_EXTENSIONS = ['.png', '.jpg', '.jpeg'];
22+
23+
// const MAX_IMAGE_SIZE = 4; //In MegaBytes
24+
// const sizeInMB = (sizeInBytes: number, decimalsNum = 2) => {
25+
// const result = sizeInBytes / (1024 * 1024);
26+
// return +result.toFixed(decimalsNum);
27+
// };
28+
29+
const zUploadFile = z
30+
.custom<File>()
31+
// .refine(
32+
// (file) => {
33+
// return sizeInMB(file.size) <= MAX_IMAGE_SIZE;
34+
// },
35+
// () => ({ message: `The maximum image size is ${MAX_IMAGE_SIZE}MB` })
36+
// )
37+
.refine(
38+
(file) => {
39+
return ACCEPTED_IMAGE_TYPES.includes(file.type);
40+
},
41+
(file) => ({ message: `File type ${file.type} is not supported` })
42+
)
43+
.refine(
44+
(file) => {
45+
return ACCEPTED_FILE_EXTENSIONS.some((ext) => file.name.endsWith(ext));
46+
},
47+
(file) => ({ message: `File extension .${file.name.split('.').at(-1)} is not supported` })
48+
);
49+
50+
const getFilesSchema = (max?: number) => {
51+
if (max === undefined) {
52+
return z.array(zUploadFile);
53+
}
54+
return z.array(zUploadFile).max(max);
55+
};
56+
57+
const sx = {
58+
position: 'absolute',
59+
top: 2,
60+
right: 2,
61+
bottom: 2,
62+
left: 2,
63+
'&[data-dnd-state="idle"]': {
64+
pointerEvents: 'none',
65+
},
66+
} satisfies SystemStyleObject;
67+
68+
export const FullscreenDropzone = memo(() => {
69+
const { t } = useTranslation();
70+
const ref = useRef<HTMLDivElement>(null);
71+
const maxImageUploadCount = useAppSelector(selectMaxImageUploadCount);
72+
const [dndState, setDndState] = useState<DndTargetState>('idle');
73+
74+
useEffect(() => {
75+
const element = ref.current;
76+
if (!element) {
77+
return;
78+
}
79+
const { getState } = getStore();
80+
const uploadFilesSchema = getFilesSchema(maxImageUploadCount);
81+
82+
return combine(
83+
dropTargetForExternal({
84+
element,
85+
canDrop: containsFiles,
86+
onDrop: ({ source }) => {
87+
const files = getFiles({ source });
88+
const parseResult = uploadFilesSchema.safeParse(files);
89+
90+
if (!parseResult.success) {
91+
const description =
92+
maxImageUploadCount === undefined
93+
? t('toast.uploadFailedInvalidUploadDesc')
94+
: t('toast.uploadFailedInvalidUploadDesc_withCount', { count: maxImageUploadCount });
95+
96+
toast({
97+
id: 'UPLOAD_FAILED',
98+
title: t('toast.uploadFailed'),
99+
description,
100+
status: 'error',
101+
});
102+
return;
103+
}
104+
const autoAddBoardId = selectAutoAddBoardId(getState());
105+
106+
const uploadArgs: UploadImageArg[] = files.map((file) => ({
107+
file,
108+
image_category: 'user',
109+
is_intermediate: false,
110+
board_id: autoAddBoardId === 'none' ? undefined : autoAddBoardId,
111+
}));
112+
113+
uploadImages(uploadArgs);
114+
},
115+
onDragEnter: () => {
116+
setDndState('over');
117+
},
118+
onDragLeave: () => {
119+
setDndState('idle');
120+
},
121+
}),
122+
monitorForExternal({
123+
canMonitor: containsFiles,
124+
onDragStart: () => {
125+
setDndState('potential');
126+
preventUnhandled.start();
127+
},
128+
onDrop: () => {
129+
setDndState('idle');
130+
preventUnhandled.stop();
131+
},
132+
})
133+
);
134+
}, [maxImageUploadCount, t]);
135+
136+
return (
137+
<Box ref={ref} data-dnd-state={dndState} sx={sx}>
138+
<DndDropOverlay dndState={dndState} label={<DropLabel />} />
139+
</Box>
140+
);
141+
});
142+
143+
FullscreenDropzone.displayName = 'FullscreenDropzone';
144+
145+
const DropLabel = memo(() => {
146+
const { t } = useTranslation();
147+
const boardId = useAppSelector(selectAutoAddBoardId);
148+
const boardName = useBoardName(boardId);
149+
150+
return (
151+
<Flex flexDir="column" gap={4} color="base.100" alignItems="center">
152+
<Heading size="lg">{t('gallery.dropToUpload')}</Heading>
153+
<Heading size="md">{t('toast.imagesWillBeAddedTo', { boardName })}</Heading>
154+
</Flex>
155+
);
156+
});
157+
158+
DropLabel.displayName = 'DropLabel';

0 commit comments

Comments
 (0)