From 81ccbc5c6a160eb6ada68d5bf1a98bacf0fbb865 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 17 Jul 2023 18:04:29 +1000 Subject: [PATCH 1/3] feat(ui): improve context menu feel - faster animation - do not handle context menu events inside context menu (fixes issue where the context menu appears to not fire) --- .../ImageContextMenu/ImageContextMenu.tsx | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx index 44fa964596..dae5e44dcd 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx @@ -4,16 +4,42 @@ import { stateSelector } from 'app/store/store'; import { useAppSelector } from 'app/store/storeHooks'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu'; -import { memo, useMemo } from 'react'; +import { MouseEvent, memo, useCallback, useMemo } from 'react'; import { ImageDTO } from 'services/api/types'; import MultipleSelectionMenuItems from './MultipleSelectionMenuItems'; import SingleSelectionMenuItems from './SingleSelectionMenuItems'; +import { MotionProps } from 'framer-motion'; type Props = { imageDTO: ImageDTO; children: ContextMenuProps['children']; }; +const motionProps: MotionProps = { + variants: { + enter: { + visibility: 'visible', + opacity: 1, + scale: 1, + transition: { + duration: 0.07, + ease: [0.4, 0, 0.2, 1], + }, + }, + exit: { + transitionEnd: { + visibility: 'hidden', + }, + opacity: 0, + scale: 0.8, + transition: { + duration: 0.07, + easings: 'easeOut', + }, + }, + }, +}; + const ImageContextMenu = ({ imageDTO, children }: Props) => { const selector = useMemo( () => @@ -31,11 +57,20 @@ const ImageContextMenu = ({ imageDTO, children }: Props) => { const { selectionCount } = useAppSelector(selector); + const handleContextMenu = useCallback((e: MouseEvent) => { + e.preventDefault(); + }, []); + return ( menuProps={{ size: 'sm', isLazy: true }} + menuButtonProps={{ bg: 'transparent', _hover: { bg: 'transparent' } }} renderMenu={() => ( - + {selectionCount === 1 ? ( ) : ( From 380aa1d7b5eba7dad15bf350a1d5359dadc5a853 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 17 Jul 2023 19:00:59 +1000 Subject: [PATCH 2/3] feat(ui): fix copy image, add context menu to IAIDndImage - restore copy image functionality* in image context menu, current image buttons - give IAIDndImage the same context menu * copying image to clipboard is not possible on Firefox unless the user enables a setting which is disabled by default. if the browser does not support copying an image, the copy functionality is disabled. --- invokeai/frontend/web/public/locales/en.json | 1 + .../web/src/common/components/IAIDndImage.tsx | 212 +++++++++--------- .../IAICanvasToolbar/IAICanvasToolbar.tsx | 25 ++- .../CurrentImage/CurrentImageButtons.tsx | 77 ++++--- .../ImageContextMenu/ImageContextMenu.tsx | 35 +-- .../SingleSelectionMenuItems.tsx | 15 +- .../UnifiedCanvasCopyToClipboard.tsx | 18 +- .../ui/hooks/useCopyImageToClipboard.ts | 52 +++++ 8 files changed, 271 insertions(+), 164 deletions(-) create mode 100644 invokeai/frontend/web/src/features/ui/hooks/useCopyImageToClipboard.ts diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index a8f52742d4..d573175fe8 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -572,6 +572,7 @@ "uploadFailedInvalidUploadDesc": "Must be single PNG or JPEG image", "downloadImageStarted": "Image Download Started", "imageCopied": "Image Copied", + "problemCopyingImage": "Unable to Copy Image", "imageLinkCopied": "Image Link Copied", "problemCopyingImageLink": "Unable to Copy Image Link", "imageNotLoaded": "No Image Loaded", diff --git a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx index 991398f5a0..c024622d2e 100644 --- a/invokeai/frontend/web/src/common/components/IAIDndImage.tsx +++ b/invokeai/frontend/web/src/common/components/IAIDndImage.tsx @@ -21,6 +21,7 @@ import { ImageDTO } from 'services/api/types'; import { mode } from 'theme/util/mode'; import IAIDraggable from './IAIDraggable'; import IAIDroppable from './IAIDroppable'; +import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu'; type IAIDndImageProps = { imageDTO: ImageDTO | undefined; @@ -96,119 +97,124 @@ const IAIDndImage = (props: IAIDndImageProps) => { }; return ( - - {imageDTO && ( + + {(ref) => ( - } - width={imageDTO.width} - height={imageDTO.height} - onError={onError} - draggable={false} - sx={{ - objectFit: 'contain', - maxW: 'full', - maxH: 'full', - borderRadius: 'base', - shadow: isSelected ? 'selected.light' : undefined, - _dark: { shadow: isSelected ? 'selected.dark' : undefined }, - ...imageSx, - }} - /> - {withMetadataOverlay && } - - )} - {!imageDTO && !isUploadDisabled && ( - <> - - - + } + width={imageDTO.width} + height={imageDTO.height} + onError={onError} + draggable={false} + sx={{ + objectFit: 'contain', + maxW: 'full', + maxH: 'full', + borderRadius: 'base', + shadow: isSelected ? 'selected.light' : undefined, + _dark: { shadow: isSelected ? 'selected.dark' : undefined }, + ...imageSx, + }} + /> + {withMetadataOverlay && } + + )} + {!imageDTO && !isUploadDisabled && ( + <> + + + + + + )} + {!imageDTO && isUploadDisabled && noContentFallback} + {!isDropDisabled && ( + + )} + {imageDTO && !isDragDisabled && ( + + )} + {onClickReset && withResetIcon && imageDTO && ( + - - + )} + )} - {!imageDTO && isUploadDisabled && noContentFallback} - {!isDropDisabled && ( - - )} - {imageDTO && !isDragDisabled && ( - - )} - {onClickReset && withResetIcon && imageDTO && ( - - )} - + ); }; diff --git a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx index 04d3e81379..e90c771c63 100644 --- a/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx +++ b/invokeai/frontend/web/src/features/canvas/components/IAICanvasToolbar/IAICanvasToolbar.tsx @@ -48,6 +48,7 @@ import IAICanvasRedoButton from './IAICanvasRedoButton'; import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover'; import IAICanvasToolChooserOptions from './IAICanvasToolChooserOptions'; import IAICanvasUndoButton from './IAICanvasUndoButton'; +import { useCopyImageToClipboard } from 'features/ui/hooks/useCopyImageToClipboard'; export const selector = createSelector( [systemSelector, canvasSelector, isStagingSelector], @@ -79,6 +80,7 @@ const IAICanvasToolbar = () => { const canvasBaseLayer = getCanvasBaseLayer(); const { t } = useTranslation(); + const { isClipboardAPIAvailable } = useCopyImageToClipboard(); const { openUploader } = useImageUploader(); @@ -136,10 +138,10 @@ const IAICanvasToolbar = () => { handleCopyImageToClipboard(); }, { - enabled: () => !isStaging, + enabled: () => !isStaging && isClipboardAPIAvailable, preventDefault: true, }, - [canvasBaseLayer, isProcessing] + [canvasBaseLayer, isProcessing, isClipboardAPIAvailable] ); useHotkeys( @@ -189,6 +191,9 @@ const IAICanvasToolbar = () => { }; const handleCopyImageToClipboard = () => { + if (!isClipboardAPIAvailable) { + return; + } dispatch(canvasCopiedToClipboard()); }; @@ -256,13 +261,15 @@ const IAICanvasToolbar = () => { onClick={handleSaveToGallery} isDisabled={isStaging} /> - } - onClick={handleCopyImageToClipboard} - isDisabled={isStaging} - /> + {isClipboardAPIAvailable && ( + } + onClick={handleCopyImageToClipboard} + isDisabled={isStaging} + /> + )} { const toaster = useAppToaster(); const { t } = useTranslation(); + const { isClipboardAPIAvailable, copyImageToClipboard } = + useCopyImageToClipboard(); + const { recallBothPrompts, recallSeed, recallAllParameters } = useRecallParameters(); @@ -128,7 +132,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { 500 ); - const { currentData: image, isFetching } = useGetImageDTOQuery( + const { currentData: imageDTO, isFetching } = useGetImageDTOQuery( lastSelectedImage ?? skipToken ); @@ -142,15 +146,15 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { const handleCopyImageLink = useCallback(() => { const getImageUrl = () => { - if (!image) { + if (!imageDTO) { return; } - if (image.image_url.startsWith('http')) { - return image.image_url; + if (imageDTO.image_url.startsWith('http')) { + return imageDTO.image_url; } - return window.location.toString() + image.image_url; + return window.location.toString() + imageDTO.image_url; }; const url = getImageUrl(); @@ -174,7 +178,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { isClosable: true, }); }); - }, [toaster, t, image]); + }, [toaster, t, imageDTO]); const handleClickUseAllParameters = useCallback(() => { recallAllParameters(metadata); @@ -192,31 +196,31 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { recallSeed(metadata?.seed); }, [metadata?.seed, recallSeed]); - useHotkeys('s', handleUseSeed, [image]); + useHotkeys('s', handleUseSeed, [imageDTO]); const handleUsePrompt = useCallback(() => { recallBothPrompts(metadata?.positive_prompt, metadata?.negative_prompt); }, [metadata?.negative_prompt, metadata?.positive_prompt, recallBothPrompts]); - useHotkeys('p', handleUsePrompt, [image]); + useHotkeys('p', handleUsePrompt, [imageDTO]); const handleSendToImageToImage = useCallback(() => { dispatch(sentImageToImg2Img()); - dispatch(initialImageSelected(image)); - }, [dispatch, image]); + dispatch(initialImageSelected(imageDTO)); + }, [dispatch, imageDTO]); - useHotkeys('shift+i', handleSendToImageToImage, [image]); + useHotkeys('shift+i', handleSendToImageToImage, [imageDTO]); const handleClickUpscale = useCallback(() => { // selectedImage && dispatch(runESRGAN(selectedImage)); }, []); const handleDelete = useCallback(() => { - if (!image) { + if (!imageDTO) { return; } - dispatch(imageToDeleteSelected(image)); - }, [dispatch, image]); + dispatch(imageToDeleteSelected(imageDTO)); + }, [dispatch, imageDTO]); useHotkeys( 'Shift+U', @@ -236,7 +240,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { }, [ isUpscalingEnabled, - image, + imageDTO, isESRGANAvailable, shouldDisableToolbarButtons, isConnected, @@ -268,7 +272,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { [ isFaceRestoreEnabled, - image, + imageDTO, isGFPGANAvailable, shouldDisableToolbarButtons, isConnected, @@ -283,10 +287,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { ); const handleSendToCanvas = useCallback(() => { - if (!image) return; + if (!imageDTO) return; dispatch(sentImageToCanvas()); - dispatch(setInitialCanvasImage(image)); + dispatch(setInitialCanvasImage(imageDTO)); dispatch(requestCanvasRescale()); if (activeTabName !== 'unifiedCanvas') { @@ -299,12 +303,12 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { duration: 2500, isClosable: true, }); - }, [image, dispatch, activeTabName, toaster, t]); + }, [imageDTO, dispatch, activeTabName, toaster, t]); useHotkeys( 'i', () => { - if (image) { + if (imageDTO) { handleClickShowImageDetails(); } else { toaster({ @@ -315,13 +319,20 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { }); } }, - [image, shouldShowImageDetails, toaster] + [imageDTO, shouldShowImageDetails, toaster] ); const handleClickProgressImagesToggle = useCallback(() => { dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer)); }, [dispatch, shouldShowProgressInViewer]); + const handleCopyImage = useCallback(() => { + if (!imageDTO) { + return; + } + copyImageToClipboard(imageDTO.image_url); + }, [copyImageToClipboard, imageDTO]); + return ( <> { } /> } @@ -369,13 +380,15 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { )} - {/* } - > - {t('parameters.copyImage')} - */} + {isClipboardAPIAvailable && ( + } + > + {t('parameters.copyImage')} + + )} { {t('parameters.copyImageToLink')} - + } size="sm" w="100%"> {t('parameters.downloadImage')} @@ -443,7 +456,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { { ['children']; }; @@ -64,20 +64,25 @@ const ImageContextMenu = ({ imageDTO, children }: Props) => { return ( menuProps={{ size: 'sm', isLazy: true }} - menuButtonProps={{ bg: 'transparent', _hover: { bg: 'transparent' } }} - renderMenu={() => ( - - {selectionCount === 1 ? ( - - ) : ( - - )} - - )} + menuButtonProps={{ + bg: 'transparent', + _hover: { bg: 'transparent' }, + }} + renderMenu={() => + imageDTO ? ( + + {selectionCount === 1 ? ( + + ) : ( + + )} + + ) : null + } > {children} diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index fda984e2c3..3dea6fea34 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -14,10 +14,11 @@ import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletio import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; import { initialImageSelected } from 'features/parameters/store/actions'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; +import { useCopyImageToClipboard } from 'features/ui/hooks/useCopyImageToClipboard'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { memo, useCallback, useContext, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { FaFolder, FaShare, FaTrash } from 'react-icons/fa'; +import { FaCopy, FaFolder, FaShare, FaTrash } from 'react-icons/fa'; import { IoArrowUndoCircleOutline } from 'react-icons/io5'; import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages'; import { useGetImageMetadataQuery } from 'services/api/endpoints/images'; @@ -61,6 +62,9 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { const { currentData } = useGetImageMetadataQuery(imageDTO.image_name); + const { isClipboardAPIAvailable, copyImageToClipboard } = + useCopyImageToClipboard(); + const metadata = currentData?.metadata; const handleDelete = useCallback(() => { @@ -130,11 +134,20 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { dispatch(imagesAddedToBatch([imageDTO.image_name])); }, [dispatch, imageDTO.image_name]); + const handleCopyImage = useCallback(() => { + copyImageToClipboard(imageDTO.image_url); + }, [copyImageToClipboard, imageDTO.image_url]); + return ( <> } onClickCapture={handleOpenInNewTab}> {t('common.openInNewTab')} + {isClipboardAPIAvailable && ( + } onClickCapture={handleCopyImage}> + {t('parameters.copyImage')} + + )} } onClickCapture={handleRecallPrompt} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolbar/UnifiedCanvasCopyToClipboard.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolbar/UnifiedCanvasCopyToClipboard.tsx index 5e23a4c0d6..a68794a930 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolbar/UnifiedCanvasCopyToClipboard.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasBeta/UnifiedCanvasToolbar/UnifiedCanvasCopyToClipboard.tsx @@ -4,6 +4,8 @@ import IAIIconButton from 'common/components/IAIIconButton'; import { canvasCopiedToClipboard } from 'features/canvas/store/actions'; import { isStagingSelector } from 'features/canvas/store/canvasSelectors'; import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider'; +import { useCopyImageToClipboard } from 'features/ui/hooks/useCopyImageToClipboard'; +import { useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { FaCopy } from 'react-icons/fa'; @@ -11,6 +13,7 @@ import { FaCopy } from 'react-icons/fa'; export default function UnifiedCanvasCopyToClipboard() { const isStaging = useAppSelector(isStagingSelector); const canvasBaseLayer = getCanvasBaseLayer(); + const { isClipboardAPIAvailable } = useCopyImageToClipboard(); const isProcessing = useAppSelector( (state: RootState) => state.system.isProcessing @@ -25,15 +28,22 @@ export default function UnifiedCanvasCopyToClipboard() { handleCopyImageToClipboard(); }, { - enabled: () => !isStaging, + enabled: () => !isStaging && isClipboardAPIAvailable, preventDefault: true, }, - [canvasBaseLayer, isProcessing] + [canvasBaseLayer, isProcessing, isClipboardAPIAvailable] ); - const handleCopyImageToClipboard = () => { + const handleCopyImageToClipboard = useCallback(() => { + if (!isClipboardAPIAvailable) { + return; + } dispatch(canvasCopiedToClipboard()); - }; + }, [dispatch, isClipboardAPIAvailable]); + + if (!isClipboardAPIAvailable) { + return null; + } return ( { + const toaster = useAppToaster(); + const { t } = useTranslation(); + + const isClipboardAPIAvailable = useMemo(() => { + return Boolean(navigator.clipboard) && Boolean(window.ClipboardItem); + }, []); + + const copyImageToClipboard = useCallback( + async (image_url: string) => { + if (!isClipboardAPIAvailable) { + toaster({ + title: t('toast.problemCopyingImage'), + description: "Your browser doesn't support the Clipboard API.", + status: 'error', + duration: 2500, + isClosable: true, + }); + } + try { + const response = await fetch(image_url); + const blob = await response.blob(); + await navigator.clipboard.write([ + new ClipboardItem({ + [blob.type]: blob, + }), + ]); + toaster({ + title: t('toast.imageCopied'), + status: 'success', + duration: 2500, + isClosable: true, + }); + } catch (err) { + toaster({ + title: t('toast.problemCopyingImage'), + description: String(err), + status: 'error', + duration: 2500, + isClosable: true, + }); + } + }, + [isClipboardAPIAvailable, t, toaster] + ); + + return { isClipboardAPIAvailable, copyImageToClipboard }; +}; From c82ae746100685aa0eba4c96d9275435a123b800 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 17 Jul 2023 19:17:11 +1000 Subject: [PATCH 3/3] feat(ui): consolidate imagecontextmenu and send to menu Both support the same actions: - Open in new tab - Copy image (if supported by browser) - Use prompt - Use seed - Use all - Send to img2img - Send to canvas - Change board - Download image - Delete --- .../CurrentImage/CurrentImageButtons.tsx | 84 ++++++------------- .../ImageContextMenu/ImageContextMenu.tsx | 29 +------ .../SingleSelectionMenuItems.tsx | 38 ++++++--- .../frontend/web/src/theme/components/menu.ts | 30 +++++++ 4 files changed, 84 insertions(+), 97 deletions(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx index 0ef484b4be..c0c82d5456 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImage/CurrentImageButtons.tsx @@ -1,7 +1,16 @@ import { createSelector } from '@reduxjs/toolkit'; import { isEqual } from 'lodash-es'; -import { ButtonGroup, Flex, FlexProps, Link } from '@chakra-ui/react'; +import { + ButtonGroup, + Flex, + FlexProps, + Link, + Menu, + MenuButton, + MenuItem, + MenuList, +} from '@chakra-ui/react'; // import { runESRGAN, runFacetool } from 'app/socketio/actions'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIButton from 'common/components/IAIButton'; @@ -49,6 +58,8 @@ import { } from 'services/api/endpoints/images'; import { useDebounce } from 'use-debounce'; import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions'; +import { menuListMotionProps } from 'theme/components/menu'; +import SingleSelectionMenuItems from '../ImageContextMenu/SingleSelectionMenuItems'; const currentImageButtonsSelector = createSelector( [stateSelector, activeTabNameSelector], @@ -345,65 +356,18 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { {...props} > - } - /> - } - > - - } - id="send-to-img2img" - > - {t('parameters.sendToImg2Img')} - - {isCanvasEnabled && ( - } - id="send-to-canvas" - > - {t('parameters.sendToUnifiedCanvas')} - - )} - - {isClipboardAPIAvailable && ( - } - > - {t('parameters.copyImage')} - - )} - } - > - {t('parameters.copyImageToLink')} - - - - } size="sm" w="100%"> - {t('parameters.downloadImage')} - - - - + + } + /> + + {imageDTO && } + + diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx index 8d8e7233c2..a903b36caf 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageContextMenu.tsx @@ -6,40 +6,15 @@ import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { ContextMenu, ContextMenuProps } from 'chakra-ui-contextmenu'; import { MouseEvent, memo, useCallback, useMemo } from 'react'; import { ImageDTO } from 'services/api/types'; +import { menuListMotionProps } from 'theme/components/menu'; import MultipleSelectionMenuItems from './MultipleSelectionMenuItems'; import SingleSelectionMenuItems from './SingleSelectionMenuItems'; -import { MotionProps } from 'framer-motion'; type Props = { imageDTO: ImageDTO | undefined; children: ContextMenuProps['children']; }; -const motionProps: MotionProps = { - variants: { - enter: { - visibility: 'visible', - opacity: 1, - scale: 1, - transition: { - duration: 0.07, - ease: [0.4, 0, 0.2, 1], - }, - }, - exit: { - transitionEnd: { - visibility: 'hidden', - }, - opacity: 0, - scale: 0.8, - transition: { - duration: 0.07, - easings: 'easeOut', - }, - }, - }, -}; - const ImageContextMenu = ({ imageDTO, children }: Props) => { const selector = useMemo( () => @@ -72,7 +47,7 @@ const ImageContextMenu = ({ imageDTO, children }: Props) => { imageDTO ? ( {selectionCount === 1 ? ( diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx index 3dea6fea34..95872495df 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx @@ -1,5 +1,4 @@ -import { ExternalLinkIcon } from '@chakra-ui/icons'; -import { MenuItem } from '@chakra-ui/react'; +import { Link, MenuItem } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppToaster } from 'app/components/Toaster'; import { stateSelector } from 'app/store/store'; @@ -18,8 +17,17 @@ import { useCopyImageToClipboard } from 'features/ui/hooks/useCopyImageToClipboa import { setActiveTab } from 'features/ui/store/uiSlice'; import { memo, useCallback, useContext, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { FaCopy, FaFolder, FaShare, FaTrash } from 'react-icons/fa'; -import { IoArrowUndoCircleOutline } from 'react-icons/io5'; +import { + FaAsterisk, + FaCopy, + FaDownload, + FaExternalLinkAlt, + FaFolder, + FaQuoteRight, + FaSeedling, + FaShare, + FaTrash, +} from 'react-icons/fa'; import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages'; import { useGetImageMetadataQuery } from 'services/api/endpoints/images'; import { ImageDTO } from 'services/api/types'; @@ -140,16 +148,21 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { return ( <> - } onClickCapture={handleOpenInNewTab}> - {t('common.openInNewTab')} - + + } + onClickCapture={handleOpenInNewTab} + > + {t('common.openInNewTab')} + + {isClipboardAPIAvailable && ( } onClickCapture={handleCopyImage}> {t('parameters.copyImage')} )} } + icon={} onClickCapture={handleRecallPrompt} isDisabled={ metadata?.positive_prompt === undefined && @@ -160,14 +173,14 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { } + icon={} onClickCapture={handleRecallSeed} isDisabled={metadata?.seed === undefined} > {t('parameters.useSeed')} } + icon={} onClickCapture={handleUseAllParameters} isDisabled={!metadata} > @@ -206,6 +219,11 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => { Remove from Board )} + + } w="100%"> + {t('parameters.downloadImage')} + + } diff --git a/invokeai/frontend/web/src/theme/components/menu.ts b/invokeai/frontend/web/src/theme/components/menu.ts index 324720a040..563c5ce6a9 100644 --- a/invokeai/frontend/web/src/theme/components/menu.ts +++ b/invokeai/frontend/web/src/theme/components/menu.ts @@ -1,6 +1,7 @@ import { menuAnatomy } from '@chakra-ui/anatomy'; import { createMultiStyleConfigHelpers } from '@chakra-ui/react'; import { mode } from '@chakra-ui/theme-tools'; +import { MotionProps } from 'framer-motion'; const { definePartsStyle, defineMultiStyleConfig } = createMultiStyleConfigHelpers(menuAnatomy.keys); @@ -21,6 +22,7 @@ const invokeAI = definePartsStyle((props) => ({ }, list: { zIndex: 9999, + color: mode('base.900', 'base.150')(props), bg: mode('base.200', 'base.800')(props), shadow: 'dark-lg', border: 'none', @@ -35,6 +37,9 @@ const invokeAI = definePartsStyle((props) => ({ _focus: { bg: mode('base.400', 'base.600')(props), }, + svg: { + opacity: 0.5, + }, }, })); @@ -46,3 +51,28 @@ export const menuTheme = defineMultiStyleConfig({ variant: 'invokeAI', }, }); + +export const menuListMotionProps: MotionProps = { + variants: { + enter: { + visibility: 'visible', + opacity: 1, + scale: 1, + transition: { + duration: 0.07, + ease: [0.4, 0, 0.2, 1], + }, + }, + exit: { + transitionEnd: { + visibility: 'hidden', + }, + opacity: 0, + scale: 0.8, + transition: { + duration: 0.07, + easings: 'easeOut', + }, + }, + }, +};