mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
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.
This commit is contained in:
parent
81ccbc5c6a
commit
380aa1d7b5
@ -572,6 +572,7 @@
|
|||||||
"uploadFailedInvalidUploadDesc": "Must be single PNG or JPEG image",
|
"uploadFailedInvalidUploadDesc": "Must be single PNG or JPEG image",
|
||||||
"downloadImageStarted": "Image Download Started",
|
"downloadImageStarted": "Image Download Started",
|
||||||
"imageCopied": "Image Copied",
|
"imageCopied": "Image Copied",
|
||||||
|
"problemCopyingImage": "Unable to Copy Image",
|
||||||
"imageLinkCopied": "Image Link Copied",
|
"imageLinkCopied": "Image Link Copied",
|
||||||
"problemCopyingImageLink": "Unable to Copy Image Link",
|
"problemCopyingImageLink": "Unable to Copy Image Link",
|
||||||
"imageNotLoaded": "No Image Loaded",
|
"imageNotLoaded": "No Image Loaded",
|
||||||
|
@ -21,6 +21,7 @@ import { ImageDTO } from 'services/api/types';
|
|||||||
import { mode } from 'theme/util/mode';
|
import { mode } from 'theme/util/mode';
|
||||||
import IAIDraggable from './IAIDraggable';
|
import IAIDraggable from './IAIDraggable';
|
||||||
import IAIDroppable from './IAIDroppable';
|
import IAIDroppable from './IAIDroppable';
|
||||||
|
import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
|
||||||
|
|
||||||
type IAIDndImageProps = {
|
type IAIDndImageProps = {
|
||||||
imageDTO: ImageDTO | undefined;
|
imageDTO: ImageDTO | undefined;
|
||||||
@ -96,119 +97,124 @@ const IAIDndImage = (props: IAIDndImageProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<ImageContextMenu imageDTO={imageDTO}>
|
||||||
sx={{
|
{(ref) => (
|
||||||
width: 'full',
|
|
||||||
height: 'full',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
position: 'relative',
|
|
||||||
minW: minSize ? minSize : undefined,
|
|
||||||
minH: minSize ? minSize : undefined,
|
|
||||||
userSelect: 'none',
|
|
||||||
cursor: isDragDisabled || !imageDTO ? 'default' : 'pointer',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{imageDTO && (
|
|
||||||
<Flex
|
<Flex
|
||||||
|
ref={ref}
|
||||||
sx={{
|
sx={{
|
||||||
w: 'full',
|
width: 'full',
|
||||||
h: 'full',
|
height: 'full',
|
||||||
position: fitContainer ? 'absolute' : 'relative',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
position: 'relative',
|
||||||
|
minW: minSize ? minSize : undefined,
|
||||||
|
minH: minSize ? minSize : undefined,
|
||||||
|
userSelect: 'none',
|
||||||
|
cursor: isDragDisabled || !imageDTO ? 'default' : 'pointer',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
{imageDTO && (
|
||||||
src={thumbnail ? imageDTO.thumbnail_url : imageDTO.image_url}
|
<Flex
|
||||||
fallbackStrategy="beforeLoadOrError"
|
|
||||||
// If we fall back to thumbnail, it feels much snappier than the skeleton...
|
|
||||||
fallbackSrc={imageDTO.thumbnail_url}
|
|
||||||
// fallback={<IAILoadingImageFallback image={imageDTO} />}
|
|
||||||
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 && <ImageMetadataOverlay image={imageDTO} />}
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
{!imageDTO && !isUploadDisabled && (
|
|
||||||
<>
|
|
||||||
<Flex
|
|
||||||
sx={{
|
|
||||||
minH: minSize,
|
|
||||||
w: 'full',
|
|
||||||
h: 'full',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
borderRadius: 'base',
|
|
||||||
transitionProperty: 'common',
|
|
||||||
transitionDuration: '0.1s',
|
|
||||||
color: mode('base.500', 'base.500')(colorMode),
|
|
||||||
...uploadButtonStyles,
|
|
||||||
}}
|
|
||||||
{...getUploadButtonProps()}
|
|
||||||
>
|
|
||||||
<input {...getUploadInputProps()} />
|
|
||||||
<Icon
|
|
||||||
as={FaUpload}
|
|
||||||
sx={{
|
sx={{
|
||||||
boxSize: 16,
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
position: fitContainer ? 'absolute' : 'relative',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={thumbnail ? imageDTO.thumbnail_url : imageDTO.image_url}
|
||||||
|
fallbackStrategy="beforeLoadOrError"
|
||||||
|
// If we fall back to thumbnail, it feels much snappier than the skeleton...
|
||||||
|
fallbackSrc={imageDTO.thumbnail_url}
|
||||||
|
// fallback={<IAILoadingImageFallback image={imageDTO} />}
|
||||||
|
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 && <ImageMetadataOverlay image={imageDTO} />}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
{!imageDTO && !isUploadDisabled && (
|
||||||
|
<>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
minH: minSize,
|
||||||
|
w: 'full',
|
||||||
|
h: 'full',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: 'base',
|
||||||
|
transitionProperty: 'common',
|
||||||
|
transitionDuration: '0.1s',
|
||||||
|
color: mode('base.500', 'base.500')(colorMode),
|
||||||
|
...uploadButtonStyles,
|
||||||
|
}}
|
||||||
|
{...getUploadButtonProps()}
|
||||||
|
>
|
||||||
|
<input {...getUploadInputProps()} />
|
||||||
|
<Icon
|
||||||
|
as={FaUpload}
|
||||||
|
sx={{
|
||||||
|
boxSize: 16,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!imageDTO && isUploadDisabled && noContentFallback}
|
||||||
|
{!isDropDisabled && (
|
||||||
|
<IAIDroppable
|
||||||
|
data={droppableData}
|
||||||
|
disabled={isDropDisabled}
|
||||||
|
dropLabel={dropLabel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{imageDTO && !isDragDisabled && (
|
||||||
|
<IAIDraggable
|
||||||
|
data={draggableData}
|
||||||
|
disabled={isDragDisabled || !imageDTO}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{onClickReset && withResetIcon && imageDTO && (
|
||||||
|
<IAIIconButton
|
||||||
|
onClick={onClickReset}
|
||||||
|
aria-label={resetTooltip}
|
||||||
|
tooltip={resetTooltip}
|
||||||
|
icon={resetIcon}
|
||||||
|
size="sm"
|
||||||
|
variant="link"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 1,
|
||||||
|
insetInlineEnd: 1,
|
||||||
|
p: 0,
|
||||||
|
minW: 0,
|
||||||
|
svg: {
|
||||||
|
transitionProperty: 'common',
|
||||||
|
transitionDuration: 'normal',
|
||||||
|
fill: 'base.100',
|
||||||
|
_hover: { fill: 'base.50' },
|
||||||
|
filter: resetIconShadow,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
)}
|
||||||
</>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
{!imageDTO && isUploadDisabled && noContentFallback}
|
</ImageContextMenu>
|
||||||
{!isDropDisabled && (
|
|
||||||
<IAIDroppable
|
|
||||||
data={droppableData}
|
|
||||||
disabled={isDropDisabled}
|
|
||||||
dropLabel={dropLabel}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{imageDTO && !isDragDisabled && (
|
|
||||||
<IAIDraggable
|
|
||||||
data={draggableData}
|
|
||||||
disabled={isDragDisabled || !imageDTO}
|
|
||||||
onClick={onClick}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{onClickReset && withResetIcon && imageDTO && (
|
|
||||||
<IAIIconButton
|
|
||||||
onClick={onClickReset}
|
|
||||||
aria-label={resetTooltip}
|
|
||||||
tooltip={resetTooltip}
|
|
||||||
icon={resetIcon}
|
|
||||||
size="sm"
|
|
||||||
variant="link"
|
|
||||||
sx={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 1,
|
|
||||||
insetInlineEnd: 1,
|
|
||||||
p: 0,
|
|
||||||
minW: 0,
|
|
||||||
svg: {
|
|
||||||
transitionProperty: 'common',
|
|
||||||
transitionDuration: 'normal',
|
|
||||||
fill: 'base.100',
|
|
||||||
_hover: { fill: 'base.50' },
|
|
||||||
filter: resetIconShadow,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -48,6 +48,7 @@ import IAICanvasRedoButton from './IAICanvasRedoButton';
|
|||||||
import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover';
|
import IAICanvasSettingsButtonPopover from './IAICanvasSettingsButtonPopover';
|
||||||
import IAICanvasToolChooserOptions from './IAICanvasToolChooserOptions';
|
import IAICanvasToolChooserOptions from './IAICanvasToolChooserOptions';
|
||||||
import IAICanvasUndoButton from './IAICanvasUndoButton';
|
import IAICanvasUndoButton from './IAICanvasUndoButton';
|
||||||
|
import { useCopyImageToClipboard } from 'features/ui/hooks/useCopyImageToClipboard';
|
||||||
|
|
||||||
export const selector = createSelector(
|
export const selector = createSelector(
|
||||||
[systemSelector, canvasSelector, isStagingSelector],
|
[systemSelector, canvasSelector, isStagingSelector],
|
||||||
@ -79,6 +80,7 @@ const IAICanvasToolbar = () => {
|
|||||||
const canvasBaseLayer = getCanvasBaseLayer();
|
const canvasBaseLayer = getCanvasBaseLayer();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { isClipboardAPIAvailable } = useCopyImageToClipboard();
|
||||||
|
|
||||||
const { openUploader } = useImageUploader();
|
const { openUploader } = useImageUploader();
|
||||||
|
|
||||||
@ -136,10 +138,10 @@ const IAICanvasToolbar = () => {
|
|||||||
handleCopyImageToClipboard();
|
handleCopyImageToClipboard();
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: () => !isStaging,
|
enabled: () => !isStaging && isClipboardAPIAvailable,
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
},
|
},
|
||||||
[canvasBaseLayer, isProcessing]
|
[canvasBaseLayer, isProcessing, isClipboardAPIAvailable]
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
@ -189,6 +191,9 @@ const IAICanvasToolbar = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyImageToClipboard = () => {
|
const handleCopyImageToClipboard = () => {
|
||||||
|
if (!isClipboardAPIAvailable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
dispatch(canvasCopiedToClipboard());
|
dispatch(canvasCopiedToClipboard());
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -256,13 +261,15 @@ const IAICanvasToolbar = () => {
|
|||||||
onClick={handleSaveToGallery}
|
onClick={handleSaveToGallery}
|
||||||
isDisabled={isStaging}
|
isDisabled={isStaging}
|
||||||
/>
|
/>
|
||||||
<IAIIconButton
|
{isClipboardAPIAvailable && (
|
||||||
aria-label={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
<IAIIconButton
|
||||||
tooltip={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
aria-label={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
||||||
icon={<FaCopy />}
|
tooltip={`${t('unifiedCanvas.copyToClipboard')} (Cmd/Ctrl+C)`}
|
||||||
onClick={handleCopyImageToClipboard}
|
icon={<FaCopy />}
|
||||||
isDisabled={isStaging}
|
onClick={handleCopyImageToClipboard}
|
||||||
/>
|
isDisabled={isStaging}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
aria-label={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
||||||
tooltip={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
tooltip={`${t('unifiedCanvas.downloadAsImage')} (Shift+D)`}
|
||||||
|
@ -20,6 +20,7 @@ import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/U
|
|||||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
|
import { useCopyImageToClipboard } from 'features/ui/hooks/useCopyImageToClipboard';
|
||||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import {
|
import {
|
||||||
setActiveTab,
|
setActiveTab,
|
||||||
@ -120,6 +121,9 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
const toaster = useAppToaster();
|
const toaster = useAppToaster();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { isClipboardAPIAvailable, copyImageToClipboard } =
|
||||||
|
useCopyImageToClipboard();
|
||||||
|
|
||||||
const { recallBothPrompts, recallSeed, recallAllParameters } =
|
const { recallBothPrompts, recallSeed, recallAllParameters } =
|
||||||
useRecallParameters();
|
useRecallParameters();
|
||||||
|
|
||||||
@ -128,7 +132,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
500
|
500
|
||||||
);
|
);
|
||||||
|
|
||||||
const { currentData: image, isFetching } = useGetImageDTOQuery(
|
const { currentData: imageDTO, isFetching } = useGetImageDTOQuery(
|
||||||
lastSelectedImage ?? skipToken
|
lastSelectedImage ?? skipToken
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -142,15 +146,15 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
|
|
||||||
const handleCopyImageLink = useCallback(() => {
|
const handleCopyImageLink = useCallback(() => {
|
||||||
const getImageUrl = () => {
|
const getImageUrl = () => {
|
||||||
if (!image) {
|
if (!imageDTO) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (image.image_url.startsWith('http')) {
|
if (imageDTO.image_url.startsWith('http')) {
|
||||||
return image.image_url;
|
return imageDTO.image_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
return window.location.toString() + image.image_url;
|
return window.location.toString() + imageDTO.image_url;
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = getImageUrl();
|
const url = getImageUrl();
|
||||||
@ -174,7 +178,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
isClosable: true,
|
isClosable: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [toaster, t, image]);
|
}, [toaster, t, imageDTO]);
|
||||||
|
|
||||||
const handleClickUseAllParameters = useCallback(() => {
|
const handleClickUseAllParameters = useCallback(() => {
|
||||||
recallAllParameters(metadata);
|
recallAllParameters(metadata);
|
||||||
@ -192,31 +196,31 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
recallSeed(metadata?.seed);
|
recallSeed(metadata?.seed);
|
||||||
}, [metadata?.seed, recallSeed]);
|
}, [metadata?.seed, recallSeed]);
|
||||||
|
|
||||||
useHotkeys('s', handleUseSeed, [image]);
|
useHotkeys('s', handleUseSeed, [imageDTO]);
|
||||||
|
|
||||||
const handleUsePrompt = useCallback(() => {
|
const handleUsePrompt = useCallback(() => {
|
||||||
recallBothPrompts(metadata?.positive_prompt, metadata?.negative_prompt);
|
recallBothPrompts(metadata?.positive_prompt, metadata?.negative_prompt);
|
||||||
}, [metadata?.negative_prompt, metadata?.positive_prompt, recallBothPrompts]);
|
}, [metadata?.negative_prompt, metadata?.positive_prompt, recallBothPrompts]);
|
||||||
|
|
||||||
useHotkeys('p', handleUsePrompt, [image]);
|
useHotkeys('p', handleUsePrompt, [imageDTO]);
|
||||||
|
|
||||||
const handleSendToImageToImage = useCallback(() => {
|
const handleSendToImageToImage = useCallback(() => {
|
||||||
dispatch(sentImageToImg2Img());
|
dispatch(sentImageToImg2Img());
|
||||||
dispatch(initialImageSelected(image));
|
dispatch(initialImageSelected(imageDTO));
|
||||||
}, [dispatch, image]);
|
}, [dispatch, imageDTO]);
|
||||||
|
|
||||||
useHotkeys('shift+i', handleSendToImageToImage, [image]);
|
useHotkeys('shift+i', handleSendToImageToImage, [imageDTO]);
|
||||||
|
|
||||||
const handleClickUpscale = useCallback(() => {
|
const handleClickUpscale = useCallback(() => {
|
||||||
// selectedImage && dispatch(runESRGAN(selectedImage));
|
// selectedImage && dispatch(runESRGAN(selectedImage));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
if (!image) {
|
if (!imageDTO) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch(imageToDeleteSelected(image));
|
dispatch(imageToDeleteSelected(imageDTO));
|
||||||
}, [dispatch, image]);
|
}, [dispatch, imageDTO]);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'Shift+U',
|
'Shift+U',
|
||||||
@ -236,7 +240,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
isUpscalingEnabled,
|
isUpscalingEnabled,
|
||||||
image,
|
imageDTO,
|
||||||
isESRGANAvailable,
|
isESRGANAvailable,
|
||||||
shouldDisableToolbarButtons,
|
shouldDisableToolbarButtons,
|
||||||
isConnected,
|
isConnected,
|
||||||
@ -268,7 +272,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
|
|
||||||
[
|
[
|
||||||
isFaceRestoreEnabled,
|
isFaceRestoreEnabled,
|
||||||
image,
|
imageDTO,
|
||||||
isGFPGANAvailable,
|
isGFPGANAvailable,
|
||||||
shouldDisableToolbarButtons,
|
shouldDisableToolbarButtons,
|
||||||
isConnected,
|
isConnected,
|
||||||
@ -283,10 +287,10 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleSendToCanvas = useCallback(() => {
|
const handleSendToCanvas = useCallback(() => {
|
||||||
if (!image) return;
|
if (!imageDTO) return;
|
||||||
dispatch(sentImageToCanvas());
|
dispatch(sentImageToCanvas());
|
||||||
|
|
||||||
dispatch(setInitialCanvasImage(image));
|
dispatch(setInitialCanvasImage(imageDTO));
|
||||||
dispatch(requestCanvasRescale());
|
dispatch(requestCanvasRescale());
|
||||||
|
|
||||||
if (activeTabName !== 'unifiedCanvas') {
|
if (activeTabName !== 'unifiedCanvas') {
|
||||||
@ -299,12 +303,12 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
duration: 2500,
|
duration: 2500,
|
||||||
isClosable: true,
|
isClosable: true,
|
||||||
});
|
});
|
||||||
}, [image, dispatch, activeTabName, toaster, t]);
|
}, [imageDTO, dispatch, activeTabName, toaster, t]);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'i',
|
'i',
|
||||||
() => {
|
() => {
|
||||||
if (image) {
|
if (imageDTO) {
|
||||||
handleClickShowImageDetails();
|
handleClickShowImageDetails();
|
||||||
} else {
|
} else {
|
||||||
toaster({
|
toaster({
|
||||||
@ -315,13 +319,20 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[image, shouldShowImageDetails, toaster]
|
[imageDTO, shouldShowImageDetails, toaster]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClickProgressImagesToggle = useCallback(() => {
|
const handleClickProgressImagesToggle = useCallback(() => {
|
||||||
dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer));
|
dispatch(setShouldShowProgressInViewer(!shouldShowProgressInViewer));
|
||||||
}, [dispatch, shouldShowProgressInViewer]);
|
}, [dispatch, shouldShowProgressInViewer]);
|
||||||
|
|
||||||
|
const handleCopyImage = useCallback(() => {
|
||||||
|
if (!imageDTO) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
copyImageToClipboard(imageDTO.image_url);
|
||||||
|
}, [copyImageToClipboard, imageDTO]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex
|
<Flex
|
||||||
@ -339,7 +350,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label={`${t('parameters.sendTo')}...`}
|
aria-label={`${t('parameters.sendTo')}...`}
|
||||||
tooltip={`${t('parameters.sendTo')}...`}
|
tooltip={`${t('parameters.sendTo')}...`}
|
||||||
isDisabled={!image}
|
isDisabled={!imageDTO}
|
||||||
icon={<FaShareAlt />}
|
icon={<FaShareAlt />}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@ -369,13 +380,15 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
</IAIButton>
|
</IAIButton>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* <IAIButton
|
{isClipboardAPIAvailable && (
|
||||||
size="sm"
|
<IAIButton
|
||||||
onClick={handleCopyImage}
|
size="sm"
|
||||||
leftIcon={<FaCopy />}
|
onClick={handleCopyImage}
|
||||||
>
|
leftIcon={<FaCopy />}
|
||||||
{t('parameters.copyImage')}
|
>
|
||||||
</IAIButton> */}
|
{t('parameters.copyImage')}
|
||||||
|
</IAIButton>
|
||||||
|
)}
|
||||||
<IAIButton
|
<IAIButton
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleCopyImageLink}
|
onClick={handleCopyImageLink}
|
||||||
@ -384,7 +397,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
{t('parameters.copyImageToLink')}
|
{t('parameters.copyImageToLink')}
|
||||||
</IAIButton>
|
</IAIButton>
|
||||||
|
|
||||||
<Link download={true} href={image?.image_url} target="_blank">
|
<Link download={true} href={imageDTO?.image_url} target="_blank">
|
||||||
<IAIButton leftIcon={<FaDownload />} size="sm" w="100%">
|
<IAIButton leftIcon={<FaDownload />} size="sm" w="100%">
|
||||||
{t('parameters.downloadImage')}
|
{t('parameters.downloadImage')}
|
||||||
</IAIButton>
|
</IAIButton>
|
||||||
@ -443,7 +456,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
<IAIButton
|
<IAIButton
|
||||||
isDisabled={
|
isDisabled={
|
||||||
!isGFPGANAvailable ||
|
!isGFPGANAvailable ||
|
||||||
!image ||
|
!imageDTO ||
|
||||||
!(isConnected && !isProcessing) ||
|
!(isConnected && !isProcessing) ||
|
||||||
!facetoolStrength
|
!facetoolStrength
|
||||||
}
|
}
|
||||||
@ -474,7 +487,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
|
|||||||
<IAIButton
|
<IAIButton
|
||||||
isDisabled={
|
isDisabled={
|
||||||
!isESRGANAvailable ||
|
!isESRGANAvailable ||
|
||||||
!image ||
|
!imageDTO ||
|
||||||
!(isConnected && !isProcessing) ||
|
!(isConnected && !isProcessing) ||
|
||||||
!upscalingLevel
|
!upscalingLevel
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import SingleSelectionMenuItems from './SingleSelectionMenuItems';
|
|||||||
import { MotionProps } from 'framer-motion';
|
import { MotionProps } from 'framer-motion';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
imageDTO: ImageDTO;
|
imageDTO: ImageDTO | undefined;
|
||||||
children: ContextMenuProps<HTMLDivElement>['children'];
|
children: ContextMenuProps<HTMLDivElement>['children'];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -64,20 +64,25 @@ const ImageContextMenu = ({ imageDTO, children }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<ContextMenu<HTMLDivElement>
|
<ContextMenu<HTMLDivElement>
|
||||||
menuProps={{ size: 'sm', isLazy: true }}
|
menuProps={{ size: 'sm', isLazy: true }}
|
||||||
menuButtonProps={{ bg: 'transparent', _hover: { bg: 'transparent' } }}
|
menuButtonProps={{
|
||||||
renderMenu={() => (
|
bg: 'transparent',
|
||||||
<MenuList
|
_hover: { bg: 'transparent' },
|
||||||
sx={{ visibility: 'visible !important' }}
|
}}
|
||||||
motionProps={motionProps}
|
renderMenu={() =>
|
||||||
onContextMenu={handleContextMenu}
|
imageDTO ? (
|
||||||
>
|
<MenuList
|
||||||
{selectionCount === 1 ? (
|
sx={{ visibility: 'visible !important' }}
|
||||||
<SingleSelectionMenuItems imageDTO={imageDTO} />
|
motionProps={motionProps}
|
||||||
) : (
|
onContextMenu={handleContextMenu}
|
||||||
<MultipleSelectionMenuItems />
|
>
|
||||||
)}
|
{selectionCount === 1 ? (
|
||||||
</MenuList>
|
<SingleSelectionMenuItems imageDTO={imageDTO} />
|
||||||
)}
|
) : (
|
||||||
|
<MultipleSelectionMenuItems />
|
||||||
|
)}
|
||||||
|
</MenuList>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
@ -14,10 +14,11 @@ import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletio
|
|||||||
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
|
||||||
import { initialImageSelected } from 'features/parameters/store/actions';
|
import { initialImageSelected } from 'features/parameters/store/actions';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
|
import { useCopyImageToClipboard } from 'features/ui/hooks/useCopyImageToClipboard';
|
||||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||||
import { memo, useCallback, useContext, useMemo } from 'react';
|
import { memo, useCallback, useContext, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { IoArrowUndoCircleOutline } from 'react-icons/io5';
|
||||||
import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages';
|
import { useRemoveImageFromBoardMutation } from 'services/api/endpoints/boardImages';
|
||||||
import { useGetImageMetadataQuery } from 'services/api/endpoints/images';
|
import { useGetImageMetadataQuery } from 'services/api/endpoints/images';
|
||||||
@ -61,6 +62,9 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
|
|
||||||
const { currentData } = useGetImageMetadataQuery(imageDTO.image_name);
|
const { currentData } = useGetImageMetadataQuery(imageDTO.image_name);
|
||||||
|
|
||||||
|
const { isClipboardAPIAvailable, copyImageToClipboard } =
|
||||||
|
useCopyImageToClipboard();
|
||||||
|
|
||||||
const metadata = currentData?.metadata;
|
const metadata = currentData?.metadata;
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
@ -130,11 +134,20 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
|||||||
dispatch(imagesAddedToBatch([imageDTO.image_name]));
|
dispatch(imagesAddedToBatch([imageDTO.image_name]));
|
||||||
}, [dispatch, imageDTO.image_name]);
|
}, [dispatch, imageDTO.image_name]);
|
||||||
|
|
||||||
|
const handleCopyImage = useCallback(() => {
|
||||||
|
copyImageToClipboard(imageDTO.image_url);
|
||||||
|
}, [copyImageToClipboard, imageDTO.image_url]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuItem icon={<ExternalLinkIcon />} onClickCapture={handleOpenInNewTab}>
|
<MenuItem icon={<ExternalLinkIcon />} onClickCapture={handleOpenInNewTab}>
|
||||||
{t('common.openInNewTab')}
|
{t('common.openInNewTab')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{isClipboardAPIAvailable && (
|
||||||
|
<MenuItem icon={<FaCopy />} onClickCapture={handleCopyImage}>
|
||||||
|
{t('parameters.copyImage')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={<IoArrowUndoCircleOutline />}
|
icon={<IoArrowUndoCircleOutline />}
|
||||||
onClickCapture={handleRecallPrompt}
|
onClickCapture={handleRecallPrompt}
|
||||||
|
@ -4,6 +4,8 @@ import IAIIconButton from 'common/components/IAIIconButton';
|
|||||||
import { canvasCopiedToClipboard } from 'features/canvas/store/actions';
|
import { canvasCopiedToClipboard } from 'features/canvas/store/actions';
|
||||||
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
|
||||||
import { getCanvasBaseLayer } from 'features/canvas/util/konvaInstanceProvider';
|
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 { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FaCopy } from 'react-icons/fa';
|
import { FaCopy } from 'react-icons/fa';
|
||||||
@ -11,6 +13,7 @@ import { FaCopy } from 'react-icons/fa';
|
|||||||
export default function UnifiedCanvasCopyToClipboard() {
|
export default function UnifiedCanvasCopyToClipboard() {
|
||||||
const isStaging = useAppSelector(isStagingSelector);
|
const isStaging = useAppSelector(isStagingSelector);
|
||||||
const canvasBaseLayer = getCanvasBaseLayer();
|
const canvasBaseLayer = getCanvasBaseLayer();
|
||||||
|
const { isClipboardAPIAvailable } = useCopyImageToClipboard();
|
||||||
|
|
||||||
const isProcessing = useAppSelector(
|
const isProcessing = useAppSelector(
|
||||||
(state: RootState) => state.system.isProcessing
|
(state: RootState) => state.system.isProcessing
|
||||||
@ -25,15 +28,22 @@ export default function UnifiedCanvasCopyToClipboard() {
|
|||||||
handleCopyImageToClipboard();
|
handleCopyImageToClipboard();
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: () => !isStaging,
|
enabled: () => !isStaging && isClipboardAPIAvailable,
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
},
|
},
|
||||||
[canvasBaseLayer, isProcessing]
|
[canvasBaseLayer, isProcessing, isClipboardAPIAvailable]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCopyImageToClipboard = () => {
|
const handleCopyImageToClipboard = useCallback(() => {
|
||||||
|
if (!isClipboardAPIAvailable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
dispatch(canvasCopiedToClipboard());
|
dispatch(canvasCopiedToClipboard());
|
||||||
};
|
}, [dispatch, isClipboardAPIAvailable]);
|
||||||
|
|
||||||
|
if (!isClipboardAPIAvailable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
import { useAppToaster } from 'app/components/Toaster';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export const useCopyImageToClipboard = () => {
|
||||||
|
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 };
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user