From af636f08b810ece32ed29474cc36084bae6ec5bd Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 16 Oct 2024 16:41:53 +1000 Subject: [PATCH] feat(ui): add `maxImageUploadCount` config setting --- invokeai/frontend/web/public/locales/en.json | 5 +- .../frontend/web/src/app/components/App.tsx | 9 +- .../frontend/web/src/app/types/invokeai.ts | 1 + .../common/components/ImageUploadOverlay.tsx | 121 ++++++++---------- .../src/common/hooks/useFullscreenDropzone.ts | 22 +++- .../src/common/hooks/useImageUploadButton.tsx | 5 +- .../src/features/system/store/configSlice.ts | 1 + 7 files changed, 87 insertions(+), 77 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 8f43d665f5..878003e03b 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1182,7 +1182,10 @@ "setNodeField": "Set as node field", "somethingWentWrong": "Something Went Wrong", "uploadFailed": "Upload failed", - "uploadFailedInvalidUploadDesc": "Must be PNG or JPEG image(s)", + "imagesWillBeAddedTo": "Uploaded images will be added to board {{boardName}}'s assets.", + "uploadFailedInvalidUploadDesc_withCount_one": "Must be maximum of 1 PNG or JPEG image.", + "uploadFailedInvalidUploadDesc_withCount_other": "Must be maximum of {{count}} PNG or JPEG images.", + "uploadFailedInvalidUploadDesc": "Must be PNG or JPEG images.", "workflowLoaded": "Workflow Loaded", "problemRetrievingWorkflow": "Problem Retrieving Workflow", "workflowDeleted": "Workflow Deleted", diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index 284690928a..0b29182859 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -32,7 +32,6 @@ import { selectLanguage } from 'features/system/store/systemSelectors'; import { AppContent } from 'features/ui/components/AppContent'; import { DeleteWorkflowDialog } from 'features/workflowLibrary/components/DeleteLibraryWorkflowConfirmationAlertDialog'; import { NewWorkflowConfirmationAlertDialog } from 'features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog'; -import { AnimatePresence } from 'framer-motion'; import i18n from 'i18n'; import { size } from 'lodash-es'; import { memo, useCallback, useEffect } from 'react'; @@ -101,11 +100,9 @@ const App = ({ config = DEFAULT_CONFIG, studioInitAction }: Props) => { > - - {dropzone.isDragActive && isHandlingUpload && ( - - )} - + {dropzone.isDragActive && isHandlingUpload && ( + + )} diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index 952dd04e3a..d3aec5f886 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -79,6 +79,7 @@ export type AppConfig = { metadataFetchDebounce?: number; workflowFetchDebounce?: number; isLocal?: boolean; + maxImageUploadCount?: number; sd: { defaultModel?: string; disabledControlNetModels: string[]; diff --git a/invokeai/frontend/web/src/common/components/ImageUploadOverlay.tsx b/invokeai/frontend/web/src/common/components/ImageUploadOverlay.tsx index 24376b1f89..710d91549b 100644 --- a/invokeai/frontend/web/src/common/components/ImageUploadOverlay.tsx +++ b/invokeai/frontend/web/src/common/components/ImageUploadOverlay.tsx @@ -1,22 +1,12 @@ import { Box, Flex, Heading } from '@invoke-ai/ui-library'; -import type { AnimationProps } from 'framer-motion'; -import { motion } from 'framer-motion'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectSelectedBoardId } from 'features/gallery/store/gallerySelectors'; +import { selectMaxImageUploadCount } from 'features/system/store/configSlice'; 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 }, -}; +import { useBoardName } from 'services/api/hooks/useBoardName'; type ImageUploadOverlayProps = { dropzone: DropzoneState; @@ -24,7 +14,6 @@ type ImageUploadOverlayProps = { }; const ImageUploadOverlay = (props: ImageUploadOverlayProps) => { - const { t } = useTranslation(); const { dropzone, setIsHandlingUpload } = props; useHotkeys( @@ -36,67 +25,65 @@ const ImageUploadOverlay = (props: ImageUploadOverlayProps) => { ); return ( - + + - - - {dropzone.isDragAccept ? ( - {t('gallery.dropToUpload')} - ) : ( - <> - {t('toast.invalidUpload')} - {t('toast.uploadFailedInvalidUploadDesc')} - - )} - + {dropzone.isDragAccept && } + {!dropzone.isDragAccept && } ); }; export default memo(ImageUploadOverlay); + +const DragAcceptMessage = () => { + const { t } = useTranslation(); + const selectedBoardId = useAppSelector(selectSelectedBoardId); + const boardName = useBoardName(selectedBoardId); + + return ( + <> + {t('gallery.dropToUpload')} + {t('toast.imagesWillBeAddedTo', { boardName })} + + ); +}; + +const DragRejectMessage = () => { + const { t } = useTranslation(); + const maxImageUploadCount = useAppSelector(selectMaxImageUploadCount); + + if (maxImageUploadCount === undefined) { + return ( + <> + {t('toast.invalidUpload')} + {t('toast.uploadFailedInvalidUploadDesc')} + + ); + } + + return ( + <> + {t('toast.invalidUpload')} + {t('toast.uploadFailedInvalidUploadDesc_withCount', { count: maxImageUploadCount })} + + ); +}; diff --git a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts index f83de60147..4d3b4f1f47 100644 --- a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts +++ b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts @@ -2,6 +2,7 @@ import { logger } from 'app/logging/logger'; import { useAppSelector } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors'; +import { selectMaxImageUploadCount } from 'features/system/store/configSlice'; import { toast } from 'features/toast/toast'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { useCallback, useEffect, useState } from 'react'; @@ -25,6 +26,7 @@ export const useFullscreenDropzone = () => { const [isHandlingUpload, setIsHandlingUpload] = useState(false); const [uploadImage] = useUploadImageMutation(); const activeTabName = useAppSelector(selectActiveTab); + const maxImageUploadCount = useAppSelector(selectMaxImageUploadCount); const getPostUploadAction = useCallback( (isSingleImage: boolean, isLastImage: boolean): PostUploadAction => { @@ -49,12 +51,19 @@ export const useFullscreenDropzone = () => { file: rejection.file.path, })); log.error({ errors }, 'Invalid upload'); + const description = + maxImageUploadCount === undefined + ? t('toast.uploadFailedInvalidUploadDesc') + : t('toast.uploadFailedInvalidUploadDesc_withCount', { count: maxImageUploadCount }); + toast({ id: 'UPLOAD_FAILED', title: t('toast.uploadFailed'), - description: t('toast.uploadFailedInvalidUploadDesc'), + description, status: 'error', }); + + setIsHandlingUpload(false); return; } @@ -73,20 +82,29 @@ export const useFullscreenDropzone = () => { isFirstUploadOfBatch: i === 0, }); } + + setIsHandlingUpload(false); }, - [t, uploadImage, getPostUploadAction, autoAddBoardId] + [t, maxImageUploadCount, uploadImage, getPostUploadAction, autoAddBoardId] ); const onDragOver = useCallback(() => { setIsHandlingUpload(true); }, []); + const onDragLeave = useCallback(() => { + setIsHandlingUpload(false); + }, []); + const dropzone = useDropzone({ accept, noClick: true, onDrop, onDragOver, + onDragLeave, noKeyboard: true, + multiple: maxImageUploadCount === undefined || maxImageUploadCount > 1, + maxFiles: maxImageUploadCount, }); useEffect(() => { diff --git a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx index d4fe3a89fe..b8037c6992 100644 --- a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx +++ b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx @@ -1,5 +1,6 @@ import { useAppSelector } from 'app/store/storeHooks'; import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors'; +import { selectMaxImageUploadCount } from 'features/system/store/configSlice'; import { useCallback } from 'react'; import { useDropzone } from 'react-dropzone'; import { useUploadImageMutation } from 'services/api/endpoints/images'; @@ -37,6 +38,7 @@ export const useImageUploadButton = ({ }: UseImageUploadButtonArgs) => { const autoAddBoardId = useAppSelector(selectAutoAddBoardId); const [uploadImage] = useUploadImageMutation(); + const maxImageUploadCount = useAppSelector(selectMaxImageUploadCount); const onDropAccepted = useCallback( (files: File[]) => { @@ -62,7 +64,8 @@ export const useImageUploadButton = ({ onDropAccepted, disabled: isDisabled, noDrag: true, - multiple: allowMultiple, + multiple: allowMultiple && (maxImageUploadCount === undefined || maxImageUploadCount > 1), + maxFiles: maxImageUploadCount, }); return { getUploadButtonProps, getUploadInputProps, openUploader }; diff --git a/invokeai/frontend/web/src/features/system/store/configSlice.ts b/invokeai/frontend/web/src/features/system/store/configSlice.ts index b4b4ca82d9..2d524d244e 100644 --- a/invokeai/frontend/web/src/features/system/store/configSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/configSlice.ts @@ -218,5 +218,6 @@ export const selectWorkflowFetchDebounce = createConfigSelector((config) => conf export const selectMetadataFetchDebounce = createConfigSelector((config) => config.metadataFetchDebounce ?? 300); export const selectIsModelsTabDisabled = createConfigSelector((config) => config.disabledTabs.includes('models')); +export const selectMaxImageUploadCount = createConfigSelector((config) => config.maxImageUploadCount); export const selectIsLocal = createSelector(selectConfigSlice, (config) => config.isLocal);