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);