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 };
+};