mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
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:
parent
0db47dd5e7
commit
8cf14287b6
@ -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 />
|
||||||
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -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 },
|
|
||||||
};
|
};
|
@ -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}>
|
||||||
|
Loading…
Reference in New Issue
Block a user