diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index ac12910bec..0375f0f456 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -2018,6 +2018,7 @@ "replaceCurrent": "Replace Current", "controlLayerEmptyState": "Upload an image, drag an image from the gallery onto this layer, pull the bounding box into this layer, or draw on the canvas to get started.", "referenceImageEmptyState": "Upload an image, drag an image from the gallery onto this layer, or pull the bounding box into this layer to get started.", + "uploadOrDragAnImage": "Drag an image from the gallery or upload an image.", "imageNoise": "Image Noise", "denoiseLimit": "Denoise Limit", "warnings": { diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index a9f274c5cb..db02a6090b 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -8,8 +8,8 @@ import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice'; import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice'; import { canvasPersistConfig, canvasSlice, canvasUndoableConfig } from 'features/controlLayers/store/canvasSlice'; import { - canvasStagingAreaPersistConfig, canvasSessionSlice, + canvasStagingAreaPersistConfig, } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { lorasPersistConfig, lorasSlice } from 'features/controlLayers/store/lorasSlice'; import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice'; diff --git a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx index f4d88db6a0..93b3c50ce0 100644 --- a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx +++ b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx @@ -1,11 +1,11 @@ -import type { IconButtonProps, SystemStyleObject } from '@invoke-ai/ui-library'; -import { IconButton } from '@invoke-ai/ui-library'; +import type { ButtonProps, IconButtonProps, SystemStyleObject } from '@invoke-ai/ui-library'; +import { Button, IconButton } from '@invoke-ai/ui-library'; import { logger } from 'app/logging/logger'; import { useAppSelector } from 'app/store/storeHooks'; import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors'; import { selectIsClientSideUploadEnabled } from 'features/system/store/configSlice'; import { toast } from 'features/toast/toast'; -import { useCallback } from 'react'; +import { memo, useCallback } from 'react'; import type { FileRejection } from 'react-dropzone'; import { useDropzone } from 'react-dropzone'; import { useTranslation } from 'react-i18next'; @@ -163,32 +163,63 @@ const sx = { }, } satisfies SystemStyleObject; -export const UploadImageButton = ({ - isDisabled = false, - onUpload, - isError = false, - ...rest -}: { +export const UploadImageIconButton = memo( + ({ + isDisabled = false, + onUpload, + isError = false, + ...rest + }: { + onUpload?: (imageDTO: ImageDTO) => void; + isError?: boolean; + } & SetOptional) => { + const uploadApi = useImageUploadButton({ isDisabled, allowMultiple: false, onUpload }); + return ( + <> + } + isLoading={uploadApi.request.isLoading} + {...rest} + {...uploadApi.getUploadButtonProps()} + /> + + + ); + } +); +UploadImageIconButton.displayName = 'UploadImageIconButton'; + +type UploadImageButtonProps = { onUpload?: (imageDTO: ImageDTO) => void; isError?: boolean; -} & SetOptional) => { +} & ButtonProps; + +export const UploadImageButton = memo((props: UploadImageButtonProps) => { + const { children, isDisabled = false, onUpload, isError = false, ...rest } = props; const uploadApi = useImageUploadButton({ isDisabled, allowMultiple: false, onUpload }); return ( <> - } + rightIcon={} isLoading={uploadApi.request.isLoading} {...rest} {...uploadApi.getUploadButtonProps()} - /> + > + {children ?? 'Upload'} + ); -}; +}); +UploadImageButton.displayName = 'UploadImageButton'; export const UploadMultipleImageButton = ({ isDisabled = false, diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress.tsx index 26a475469b..4e9dd5ec51 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress.tsx @@ -6,11 +6,11 @@ import { selectIsLocal } from 'features/system/store/configSlice'; import { selectSystemShouldShowInvocationProgressDetail } from 'features/system/store/systemSlice'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -import { $invocationProgressMessage } from 'services/events/stores'; +import { $lastProgressMessage } from 'services/events/stores'; const CanvasAlertsInvocationProgressContentLocal = memo(() => { const { t } = useTranslation(); - const invocationProgressMessage = useStore($invocationProgressMessage); + const invocationProgressMessage = useStore($lastProgressMessage); if (!invocationProgressMessage) { return null; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx index e93587ef18..03f7844950 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx @@ -1,8 +1,21 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library'; -import { Button, ContextMenu, Flex, IconButton, Image, Menu, MenuButton, MenuList, Text } from '@invoke-ai/ui-library'; +import { + Button, + ContextMenu, + Flex, + Heading, + IconButton, + Image, + Menu, + MenuButton, + MenuList, + Text, +} from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; +import { useAppStore } from 'app/store/nanostores/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { FocusRegionWrapper } from 'common/components/FocusRegionWrapper'; +import { useImageUploadButton } from 'common/hooks/useImageUploadButton'; import { CanvasAlertsPreserveMask } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsPreserveMask'; import { CanvasAlertsSelectedEntityStatus } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSelectedEntityStatus'; import { CanvasAlertsSendingToGallery } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsSendingTo'; @@ -32,13 +45,17 @@ import { stagingAreaNextStagedImageSelected, stagingAreaPrevStagedImageSelected, } from 'features/controlLayers/store/canvasStagingAreaSlice'; +import { newCanvasFromImageDndTarget } from 'features/dnd/dnd'; +import { DndDropTarget } from 'features/dnd/DndDropTarget'; +import { newCanvasFromImage } from 'features/imageActions/actions'; import type { ProgressImage } from 'features/nodes/types/common'; -import { memo, useCallback, useEffect } from 'react'; +import { memo, useCallback, useEffect, useMemo } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; -import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi'; +import { Trans, useTranslation } from 'react-i18next'; +import { PiDotsThreeOutlineVerticalFill, PiUploadBold } from 'react-icons/pi'; import type { ImageDTO, S } from 'services/api/types'; import { $lastCanvasProgressImage, $socket } from 'services/events/stores'; -import type { Equals } from 'tsafe'; +import type { Equals, Param0 } from 'tsafe'; import { assert } from 'tsafe'; import { CanvasAlertsInvocationProgress } from './CanvasAlerts/CanvasAlertsInvocationProgress'; @@ -80,45 +97,190 @@ export const CanvasMainPanelContent = memo(() => { CanvasMainPanelContent.displayName = 'CanvasMainPanelContent'; +const generateWithStartingImageDndTargetData = newCanvasFromImageDndTarget.getData({ + type: 'raster_layer', + withResize: true, +}); +const generateWithStartingImageAndInpaintMaskDndTargetData = newCanvasFromImageDndTarget.getData({ + type: 'raster_layer', + withInpaintMask: true, +}); +const generateWithControlImageDndTargetData = newCanvasFromImageDndTarget.getData({ + type: 'control_layer', + withResize: true, +}); + const NoActiveSession = memo(() => { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const newSesh = useCallback(() => { dispatch(canvasSessionStarted({ sessionType: 'advanced' })); }, [dispatch]); + return ( - - No Active Session - - - - Generate with Starting Image - - New Canvas Session - - Dropped image as raster layer - - Bbox resized - - - Generate with Control Image - - New Canvas Session - - Dropped image as control layer - - Bbox resized - - - Edit Image - - New Canvas Session - - Dropped image as raster layer - - Bbox resized - - 1 Inpaint mask layer added + or + + + + ); }); NoActiveSession.displayName = 'NoActiveSession'; +const GenerateWithStartingImage = memo(() => { + const { t } = useTranslation(); + const { getState, dispatch } = useAppStore(); + const useImageUploadButtonOptions = useMemo>( + () => ({ + onUpload: (imageDTO: ImageDTO) => { + newCanvasFromImage({ imageDTO, type: 'raster_layer', withResize: true, getState, dispatch }); + }, + allowMultiple: false, + }), + [dispatch, getState] + ); + const uploadApi = useImageUploadButton(useImageUploadButtonOptions); + const components = useMemo( + () => ({ + UploadButton: ( +