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

View File

@ -1,28 +1,47 @@
import { Box, Flex, Heading } from '@chakra-ui/react'; import { Box, Flex, Heading } from '@chakra-ui/react';
import type { AnimationProps } from 'framer-motion';
import { motion } from 'framer-motion';
import { memo } from 'react'; import { memo } from 'react';
import type { DropzoneState } from 'react-dropzone';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; 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 = { type ImageUploadOverlayProps = {
isDragAccept: boolean; dropzone: DropzoneState;
isDragReject: boolean;
setIsHandlingUpload: (isHandlingUpload: boolean) => void; setIsHandlingUpload: (isHandlingUpload: boolean) => void;
}; };
const ImageUploadOverlay = (props: ImageUploadOverlayProps) => { const ImageUploadOverlay = (props: ImageUploadOverlayProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { const { dropzone, setIsHandlingUpload } = props;
isDragAccept,
isDragReject: _isDragAccept,
setIsHandlingUpload,
} = props;
useHotkeys('esc', () => { useHotkeys(
setIsHandlingUpload(false); 'esc',
}); () => {
setIsHandlingUpload(false);
},
[setIsHandlingUpload]
);
return ( return (
<Box <Box
key="image-upload-overlay"
initial={initial}
animate={animate}
exit={exit}
as={motion.div}
position="absolute" position="absolute"
top={0} top={0}
insetInlineStart={0} insetInlineStart={0}
@ -67,7 +86,7 @@ const ImageUploadOverlay = (props: ImageUploadOverlayProps) => {
color="base.100" color="base.100"
borderColor="base.200" borderColor="base.200"
> >
{isDragAccept ? ( {dropzone.isDragAccept ? (
<Heading size="lg">{t('gallery.dropToUpload')}</Heading> <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 { useAppToaster } from 'app/components/Toaster';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import type { AnimationProps } from 'framer-motion'; import { useCallback, useEffect, useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import type { PropsWithChildren } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
import type { Accept, FileRejection } from 'react-dropzone'; import type { Accept, FileRejection } from 'react-dropzone';
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useUploadImageMutation } from 'services/api/endpoints/images'; import { useUploadImageMutation } from 'services/api/endpoints/images';
import type { PostUploadAction } from 'services/api/types'; import type { PostUploadAction } from 'services/api/types';
import ImageUploadOverlay from './ImageUploadOverlay';
const accept: Accept = { const accept: Accept = {
'image/png': ['.png'], 'image/png': ['.png'],
'image/jpeg': ['.jpg', '.jpeg', '.png'], 'image/jpeg': ['.jpg', '.jpeg', '.png'],
}; };
const dropzoneRootProps = { style: {} };
const selector = createMemoizedSelector( const selector = createMemoizedSelector(
[stateSelector, activeTabNameSelector], [stateSelector, activeTabNameSelector],
({ gallery }, activeTabName) => { ({ gallery }, activeTabName) => {
@ -45,7 +37,7 @@ const selector = createMemoizedSelector(
} }
); );
const ImageUploader = (props: PropsWithChildren) => { export const useFullscreenDropzone = () => {
const { autoAddBoardId, postUploadAction } = useAppSelector(selector); const { autoAddBoardId, postUploadAction } = useAppSelector(selector);
const toaster = useAppToaster(); const toaster = useAppToaster();
const { t } = useTranslation(); const { t } = useTranslation();
@ -105,14 +97,7 @@ const ImageUploader = (props: PropsWithChildren) => {
setIsHandlingUpload(true); setIsHandlingUpload(true);
}, []); }, []);
const { const dropzone = useDropzone({
getRootProps,
getInputProps,
isDragAccept,
isDragReject,
isDragActive,
inputRef,
} = useDropzone({
accept, accept,
noClick: true, noClick: true,
onDrop, onDrop,
@ -124,15 +109,17 @@ const ImageUploader = (props: PropsWithChildren) => {
useEffect(() => { useEffect(() => {
// This is a hack to allow pasting images into the uploader // This is a hack to allow pasting images into the uploader
const handlePaste = async (e: ClipboardEvent) => { const handlePaste = async (e: ClipboardEvent) => {
if (!inputRef.current) { if (!dropzone.inputRef.current) {
return; return;
} }
if (e.clipboardData?.files) { if (e.clipboardData?.files) {
// Set the files on the inputRef // Set the files on the dropzone.inputRef
inputRef.current.files = e.clipboardData.files; dropzone.inputRef.current.files = e.clipboardData.files;
// Dispatch the change event, dropzone catches this and we get to use its own validation // 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 () => { return () => {
document.removeEventListener('paste', handlePaste); document.removeEventListener('paste', handlePaste);
}; };
}, [inputRef]); }, [dropzone.inputRef]);
return ( return { dropzone, isHandlingUpload, setIsHandlingUpload };
<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 },
}; };

View File

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