diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index f0ac55905e..4ed56fe1f1 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1081,6 +1081,7 @@ "importFailed": "Import Failed", "importSuccessful": "Import Successful", "invalidUpload": "Invalid Upload", + "layerCopiedToClipboard": "Layer Copied to Clipboard", "loadedWithWarnings": "Workflow Loaded with Warnings", "maskSavedAssets": "Mask Saved to Assets", "maskSentControlnetAssets": "Mask Sent to ControlNet & Assets", @@ -1101,6 +1102,7 @@ "problemCopyingCanvas": "Problem Copying Canvas", "problemCopyingCanvasDesc": "Unable to export base layer", "problemCopyingImage": "Unable to Copy Image", + "problemCopyingLayer": "Unable to Copy Layer", "problemDownloadingImage": "Unable to Download Image", "problemDownloadingCanvas": "Problem Downloading Canvas", "problemDownloadingCanvasDesc": "Unable to export base layer", @@ -1669,6 +1671,7 @@ "sendToGallery": "Send To Gallery", "sendToGalleryDesc": "Pressing Invoke generates and saves a unique image to your gallery.", "sendToCanvas": "Send To Canvas", + "copyToClipboard": "Copy to Clipboard", "sendToCanvasDesc": "Pressing Invoke stages your work in progress on the canvas.", "viewProgressInViewer": "View progress and outputs in the Image Viewer.", "viewProgressOnCanvas": "View progress and stage outputs on the Canvas.", diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems.tsx index 19c673fd69..4369a9925a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasContextMenu/CanvasContextMenuSelectedEntityMenuItems.tsx @@ -1,5 +1,6 @@ import { MenuGroup } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; +import { CanvasEntityMenuItemsCopy } from 'features/controlLayers/components/common/CanvasEntityMenuItemsCopy'; import { CanvasEntityMenuItemsDelete } from 'features/controlLayers/components/common/CanvasEntityMenuItemsDelete'; import { CanvasEntityMenuItemsFilter } from 'features/controlLayers/components/common/CanvasEntityMenuItemsFilter'; import { CanvasEntityMenuItemsTransform } from 'features/controlLayers/components/common/CanvasEntityMenuItemsTransform'; @@ -20,6 +21,7 @@ const CanvasContextMenuSelectedEntityMenuItemsContent = memo(() => { {isFilterableEntityIdentifier(entityIdentifier) && } {isTransformableEntityIdentifier(entityIdentifier) && } + ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityCopyToClipboard.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityCopyToClipboard.tsx new file mode 100644 index 0000000000..1d4fc22fdd --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityCopyToClipboard.tsx @@ -0,0 +1,34 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy'; +import { useCopyLayerToClipboard } from 'features/controlLayers/hooks/useCopyLayerToClipboard'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiCopyBold } from 'react-icons/pi'; + +export const CanvasEntityCopyToClipboard = memo(() => { + const { t } = useTranslation(); + const entityIdentifier = useEntityIdentifierContext(); + const adapter = useEntityAdapterSafe(entityIdentifier); + const isBusy = useCanvasIsBusy(); + const copyLayerToClipboard = useCopyLayerToClipboard(); + const onClick = useCallback(() => { + copyLayerToClipboard(adapter); + }, [copyLayerToClipboard, adapter]); + + return ( + } + onClick={onClick} + isDisabled={isBusy} + /> + ); +}); + +CanvasEntityCopyToClipboard.displayName = 'CanvasEntityCopyToClipboard'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx index cc89cb02f6..fd4771b895 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityHeaderCommonActions.tsx @@ -6,11 +6,14 @@ import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/co import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { memo } from 'react'; +import { CanvasEntityCopyToClipboard } from './CanvasEntityCopyToClipboard'; + export const CanvasEntityHeaderCommonActions = memo(() => { const entityIdentifier = useEntityIdentifierContext(); return ( + {entityIdentifier.type !== 'reference_image' && } diff --git a/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsCopy.tsx b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsCopy.tsx new file mode 100644 index 0000000000..040c439d87 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/common/CanvasEntityMenuItemsCopy.tsx @@ -0,0 +1,28 @@ +import { MenuItem } from '@invoke-ai/ui-library'; +import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext'; +import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; +import { useCopyLayerToClipboard } from 'features/controlLayers/hooks/useCopyLayerToClipboard'; +import { useIsEntityInteractable } from 'features/controlLayers/hooks/useEntityIsInteractable'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiCopyBold } from 'react-icons/pi'; + +export const CanvasEntityMenuItemsCopy = memo(() => { + const { t } = useTranslation(); + const entityIdentifier = useEntityIdentifierContext(); + const adapter = useEntityAdapterSafe(entityIdentifier); + const isInteractable = useIsEntityInteractable(entityIdentifier); + const copyLayerToClipboard = useCopyLayerToClipboard(); + + const onClick = useCallback(() => { + copyLayerToClipboard(adapter); + }, [copyLayerToClipboard, adapter]); + + return ( + } isDisabled={!isInteractable}> + {t('controlLayers.copyToClipboard')} + + ); +}); + +CanvasEntityMenuItemsCopy.displayName = 'CanvasEntityMenuItemsCopy'; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/useCopyLayerToClipboard.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/useCopyLayerToClipboard.ts new file mode 100644 index 0000000000..9221c3e2cb --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/useCopyLayerToClipboard.ts @@ -0,0 +1,44 @@ +import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterControlLayer'; +import type { CanvasEntityAdapterInpaintMask } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterInpaintMask'; +import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer'; +import type { CanvasEntityAdapterRegionalGuidance } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRegionalGuidance'; +import { canvasToBlob } from 'features/controlLayers/konva/util'; +import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard'; +import { toast } from 'features/toast/toast'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const useCopyLayerToClipboard = () => { + const { t } = useTranslation(); + const copyLayerToCipboard = useCallback( + async ( + adapter: + | CanvasEntityAdapterRasterLayer + | CanvasEntityAdapterControlLayer + | CanvasEntityAdapterInpaintMask + | CanvasEntityAdapterRegionalGuidance + | null + ) => { + if (!adapter) { + return; + } + try { + const canvas = adapter.getCanvas(); + const blob = await canvasToBlob(canvas); + copyBlobToClipboard(blob); + toast({ + status: 'info', + title: t('toast.layerCopiedToClipboard'), + }); + } catch (error) { + toast({ + status: 'error', + title: t('toast.problemCopyingLayer'), + }); + } + }, + [t] + ); + + return copyLayerToCipboard; +}; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index a6990a910a..944d8592e2 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -299,11 +299,11 @@ export type EntityIdentifierPayload< U extends CanvasEntityType = CanvasEntityType, > = T extends void ? { - entityIdentifier: CanvasEntityIdentifier; - } + entityIdentifier: CanvasEntityIdentifier; + } : { - entityIdentifier: CanvasEntityIdentifier; - } & T; + entityIdentifier: CanvasEntityIdentifier; + } & T; export type EntityMovedPayload = EntityIdentifierPayload<{ position: Coordinate }>; export type EntityBrushLineAddedPayload = EntityIdentifierPayload<{ brushLine: CanvasBrushLineState }>;