feat(ui): simplify App.tsx layout

There was an extra div, needed for the fullscreen file upload dropzone, that made styling the main app containers a bit awkward.

Refactor the uploader a bit to simplify this - no longer need so many app-level wrappers. Much cleaner.
This commit is contained in:
psychedelicious 2024-01-04 15:15:53 +11:00 committed by Kent Keirsey
parent 0db47dd5e7
commit 8cf14287b6
4 changed files with 71 additions and 80 deletions

View File

@ -1,11 +1,12 @@
import { Flex, Grid } from '@chakra-ui/react';
import { Box } from '@chakra-ui/react';
import { useSocketIO } from 'app/hooks/useSocketIO';
import { useLogger } from 'app/logging/useLogger';
import { appStarted } from 'app/store/middleware/listenerMiddleware/listeners/appStarted';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import type { PartialAppConfig } from 'app/types/invokeai';
import ImageUploader from 'common/components/ImageUploader';
import ImageUploadOverlay from 'common/components/ImageUploadOverlay';
import { useClearStorage } from 'common/hooks/useClearStorage';
import { useFullscreenDropzone } from 'common/hooks/useFullscreenDropzone';
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
import { useGlobalModifiersInit } from 'common/hooks/useGlobalModifiers';
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
@ -14,6 +15,7 @@ import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicP
import { configChanged } from 'features/system/store/configSlice';
import { languageSelector } from 'features/system/store/systemSelectors';
import InvokeTabs from 'features/ui/components/InvokeTabs';
import { AnimatePresence } from 'framer-motion';
import i18n from 'i18n';
import { size } from 'lodash-es';
import { memo, useCallback, useEffect } from 'react';
@ -44,6 +46,9 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => {
useGlobalModifiersInit();
useGlobalHotkeys();
const { dropzone, isHandlingUpload, setIsHandlingUpload } =
useFullscreenDropzone();
const handleReset = useCallback(() => {
clearStorage();
location.reload();
@ -70,13 +75,25 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => {
onReset={handleReset}
FallbackComponent={AppErrorBoundaryFallback}
>
<Grid w="100vw" h="100vh" position="relative" overflow="hidden">
<ImageUploader>
<Flex gap={4} p={4} w="full" h="full">
<InvokeTabs />
</Flex>
</ImageUploader>
</Grid>
<Box
id="invoke-app-wrapper"
w="100vw"
h="100vh"
position="relative"
overflow="hidden"
{...dropzone.getRootProps()}
>
<input {...dropzone.getInputProps()} />
<InvokeTabs />
<AnimatePresence>
{dropzone.isDragActive && isHandlingUpload && (
<ImageUploadOverlay
dropzone={dropzone}
setIsHandlingUpload={setIsHandlingUpload}
/>
)}
</AnimatePresence>
</Box>
<DeleteImageModal />
<ChangeBoardModal />
<DynamicPromptsModal />

View File

@ -1,28 +1,47 @@
import { Box, Flex, Heading } from '@chakra-ui/react';
import type { AnimationProps } from 'framer-motion';
import { motion } from 'framer-motion';
import { memo } from 'react';
import type { DropzoneState } from 'react-dropzone';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
const initial: AnimationProps['initial'] = {
opacity: 0,
};
const animate: AnimationProps['animate'] = {
opacity: 1,
transition: { duration: 0.1 },
};
const exit: AnimationProps['exit'] = {
opacity: 0,
transition: { duration: 0.1 },
};
type ImageUploadOverlayProps = {
isDragAccept: boolean;
isDragReject: boolean;
dropzone: DropzoneState;
setIsHandlingUpload: (isHandlingUpload: boolean) => void;
};
const ImageUploadOverlay = (props: ImageUploadOverlayProps) => {
const { t } = useTranslation();
const {
isDragAccept,
isDragReject: _isDragAccept,
setIsHandlingUpload,
} = props;
const { dropzone, setIsHandlingUpload } = props;
useHotkeys('esc', () => {
setIsHandlingUpload(false);
});
useHotkeys(
'esc',
() => {
setIsHandlingUpload(false);
},
[setIsHandlingUpload]
);
return (
<Box
key="image-upload-overlay"
initial={initial}
animate={animate}
exit={exit}
as={motion.div}
position="absolute"
top={0}
insetInlineStart={0}
@ -67,7 +86,7 @@ const ImageUploadOverlay = (props: ImageUploadOverlayProps) => {
color="base.100"
borderColor="base.200"
>
{isDragAccept ? (
{dropzone.isDragAccept ? (
<Heading size="lg">{t('gallery.dropToUpload')}</Heading>
) : (
<>

View File

@ -1,28 +1,20 @@
import { Box } from '@chakra-ui/react';
import { useAppToaster } from 'app/components/Toaster';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import type { PropsWithChildren } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import type { Accept, FileRejection } from 'react-dropzone';
import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next';
import { useUploadImageMutation } from 'services/api/endpoints/images';
import type { PostUploadAction } from 'services/api/types';
import ImageUploadOverlay from './ImageUploadOverlay';
const accept: Accept = {
'image/png': ['.png'],
'image/jpeg': ['.jpg', '.jpeg', '.png'],
};
const dropzoneRootProps = { style: {} };
const selector = createMemoizedSelector(
[stateSelector, activeTabNameSelector],
({ gallery }, activeTabName) => {
@ -45,7 +37,7 @@ const selector = createMemoizedSelector(
}
);
const ImageUploader = (props: PropsWithChildren) => {
export const useFullscreenDropzone = () => {
const { autoAddBoardId, postUploadAction } = useAppSelector(selector);
const toaster = useAppToaster();
const { t } = useTranslation();
@ -105,14 +97,7 @@ const ImageUploader = (props: PropsWithChildren) => {
setIsHandlingUpload(true);
}, []);
const {
getRootProps,
getInputProps,
isDragAccept,
isDragReject,
isDragActive,
inputRef,
} = useDropzone({
const dropzone = useDropzone({
accept,
noClick: true,
onDrop,
@ -124,15 +109,17 @@ const ImageUploader = (props: PropsWithChildren) => {
useEffect(() => {
// This is a hack to allow pasting images into the uploader
const handlePaste = async (e: ClipboardEvent) => {
if (!inputRef.current) {
if (!dropzone.inputRef.current) {
return;
}
if (e.clipboardData?.files) {
// Set the files on the inputRef
inputRef.current.files = e.clipboardData.files;
// Set the files on the dropzone.inputRef
dropzone.inputRef.current.files = e.clipboardData.files;
// Dispatch the change event, dropzone catches this and we get to use its own validation
inputRef.current?.dispatchEvent(new Event('change', { bubbles: true }));
dropzone.inputRef.current?.dispatchEvent(
new Event('change', { bubbles: true })
);
}
};
@ -142,42 +129,7 @@ const ImageUploader = (props: PropsWithChildren) => {
return () => {
document.removeEventListener('paste', handlePaste);
};
}, [inputRef]);
}, [dropzone.inputRef]);
return (
<Box {...getRootProps(dropzoneRootProps)}>
<input {...getInputProps()} />
{props.children}
<AnimatePresence>
{isDragActive && isHandlingUpload && (
<motion.div
key="image-upload-overlay"
initial={initial}
animate={animate}
exit={exit}
>
<ImageUploadOverlay
isDragAccept={isDragAccept}
isDragReject={isDragReject}
setIsHandlingUpload={setIsHandlingUpload}
/>
</motion.div>
)}
</AnimatePresence>
</Box>
);
};
export default memo(ImageUploader);
const initial: AnimationProps['initial'] = {
opacity: 0,
};
const animate: AnimationProps['animate'] = {
opacity: 1,
transition: { duration: 0.1 },
};
const exit: AnimationProps['exit'] = {
opacity: 0,
transition: { duration: 0.1 },
return { dropzone, isHandlingUpload, setIsHandlingUpload };
};

View File

@ -243,12 +243,15 @@ const InvokeTabs = () => {
return (
<InvTabs
id="invoke-app-tabs"
variant="appTabs"
defaultIndex={activeTabIndex}
index={activeTabIndex}
onChange={handleTabChange}
flexGrow={1}
w="full"
h="full"
gap={4}
p={4}
isLazy
>
<Flex flexDir="column" alignItems="center" pt={4}>