From e7aafdfdbfbb2877d59fcc2525d6e5e939bf2e16 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 7 Feb 2025 09:57:55 +1100 Subject: [PATCH] feat(ui): migrate all clipboard stuff to useClipboard --- invokeai/frontend/web/public/locales/en.json | 5 ++- .../components/AppErrorBoundaryFallback.tsx | 13 +++--- .../common/hooks/useCopyImageToClipboard.ts | 33 +++++--------- .../features/controlLayers/hooks/copyHooks.ts | 44 +++++++++---------- .../ImageContextMenu/ImageMenuItemCopy.tsx | 6 +-- .../ImageMetadataViewer/DataViewer.tsx | 11 +++-- .../WorkflowListMenu/ShareWorkflowModal.tsx | 14 +++--- .../components/AboutModal/AboutModal.tsx | 6 ++- .../system/util/copyBlobToClipboard.ts | 10 ----- .../features/toast/ErrorToastDescription.tsx | 5 ++- 10 files changed, 67 insertions(+), 80 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/system/util/copyBlobToClipboard.ts diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 0ce49fa670..a0a6b23ab5 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1266,7 +1266,10 @@ "workflowLoaded": "Workflow Loaded", "problemRetrievingWorkflow": "Problem Retrieving Workflow", "workflowDeleted": "Workflow Deleted", - "problemDeletingWorkflow": "Problem Deleting Workflow" + "problemDeletingWorkflow": "Problem Deleting Workflow", + "unableToCopy": "Unable to Copy", + "unableToCopyDesc": "Your browser does not support clipboard access. Firefox users may be able to fix this by following ", + "unableToCopyDesc_theseSteps": "these steps" }, "popovers": { "clipSkip": { diff --git a/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx b/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx index b20a8148e2..f061ba15f9 100644 --- a/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx +++ b/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx @@ -1,6 +1,7 @@ import { Button, Flex, Heading, Image, Link, Text } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; +import { useClipboard } from 'common/hooks/useClipboard'; import { selectConfigSlice } from 'features/system/store/configSlice'; import { toast } from 'features/toast/toast'; import newGithubIssueUrl from 'new-github-issue-url'; @@ -20,15 +21,17 @@ const selectIsLocal = createSelector(selectConfigSlice, (config) => config.isLoc const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => { const { t } = useTranslation(); const isLocal = useAppSelector(selectIsLocal); + const clipboard = useClipboard(); const handleCopy = useCallback(() => { const text = JSON.stringify(serializeError(error), null, 2); - navigator.clipboard.writeText(`\`\`\`\n${text}\n\`\`\``); - toast({ - id: 'ERROR_COPIED', - title: t('toast.errorCopied'), + clipboard.writeText(`\`\`\`\n${text}\n\`\`\``, () => { + toast({ + id: 'ERROR_COPIED', + title: t('toast.errorCopied'), + }); }); - }, [error, t]); + }, [clipboard, error, t]); const url = useMemo(() => { if (isLocal) { diff --git a/invokeai/frontend/web/src/common/hooks/useCopyImageToClipboard.ts b/invokeai/frontend/web/src/common/hooks/useCopyImageToClipboard.ts index 345ea98e13..e46227b2f5 100644 --- a/invokeai/frontend/web/src/common/hooks/useCopyImageToClipboard.ts +++ b/invokeai/frontend/web/src/common/hooks/useCopyImageToClipboard.ts @@ -1,26 +1,15 @@ +import { useClipboard } from 'common/hooks/useClipboard'; import { convertImageUrlToBlob } from 'common/util/convertImageUrlToBlob'; -import { copyBlobToClipboard } from 'features/system/util/copyBlobToClipboard'; import { toast } from 'features/toast/toast'; -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; export const useCopyImageToClipboard = () => { const { t } = useTranslation(); - - const isClipboardAPIAvailable = useMemo(() => { - return Boolean(navigator.clipboard) && Boolean(window.ClipboardItem); - }, []); + const clipboard = useClipboard(); const copyImageToClipboard = useCallback( async (image_url: string) => { - if (!isClipboardAPIAvailable) { - toast({ - id: 'PROBLEM_COPYING_IMAGE', - title: t('toast.problemCopyingImage'), - description: "Your browser doesn't support the Clipboard API.", - status: 'error', - }); - } try { const blob = await convertImageUrlToBlob(image_url); @@ -28,12 +17,12 @@ export const useCopyImageToClipboard = () => { throw new Error('Unable to create Blob'); } - copyBlobToClipboard(blob); - - toast({ - id: 'IMAGE_COPIED', - title: t('toast.imageCopied'), - status: 'success', + clipboard.writeImage(blob, () => { + toast({ + id: 'IMAGE_COPIED', + title: t('toast.imageCopied'), + status: 'success', + }); }); } catch (err) { toast({ @@ -44,8 +33,8 @@ export const useCopyImageToClipboard = () => { }); } }, - [isClipboardAPIAvailable, t] + [clipboard, t] ); - return { isClipboardAPIAvailable, copyImageToClipboard }; + return copyImageToClipboard; }; diff --git a/invokeai/frontend/web/src/features/controlLayers/hooks/copyHooks.ts b/invokeai/frontend/web/src/features/controlLayers/hooks/copyHooks.ts index 35552b5937..1780a6e357 100644 --- a/invokeai/frontend/web/src/features/controlLayers/hooks/copyHooks.ts +++ b/invokeai/frontend/web/src/features/controlLayers/hooks/copyHooks.ts @@ -1,12 +1,11 @@ import { logger } from 'app/logging/logger'; -import { withResultAsync } from 'common/util/result'; +import { useClipboard } from 'common/hooks/useClipboard'; import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; 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 { startCase } from 'lodash-es'; import { useCallback } from 'react'; @@ -17,6 +16,7 @@ const log = logger('canvas'); export const useCopyLayerToClipboard = () => { const { t } = useTranslation(); + const clipboard = useClipboard(); const copyLayerToCipboard = useCallback( async ( adapter: @@ -30,27 +30,25 @@ export const useCopyLayerToClipboard = () => { return; } - const result = await withResultAsync(async () => { + try { const canvas = adapter.getCanvas(); const blob = await canvasToBlob(canvas); - copyBlobToClipboard(blob); - }); - - if (result.isOk()) { - log.trace('Layer copied to clipboard'); - toast({ - status: 'info', - title: t('toast.layerCopiedToClipboard'), + clipboard.writeImage(blob, () => { + log.trace('Layer copied to clipboard'); + toast({ + status: 'info', + title: t('toast.layerCopiedToClipboard'), + }); }); - } else { - log.error({ error: serializeError(result.error) }, 'Problem copying layer to clipboard'); + } catch (error) { + log.error({ error: serializeError(error) }, 'Problem copying layer to clipboard'); toast({ status: 'error', title: t('toast.problemCopyingLayer'), }); } }, - [t] + [clipboard, t] ); return copyLayerToCipboard; @@ -58,6 +56,7 @@ export const useCopyLayerToClipboard = () => { export const useCopyCanvasToClipboard = (region: 'canvas' | 'bbox') => { const { t } = useTranslation(); + const clipboard = useClipboard(); const canvasManager = useCanvasManager(); const copyCanvasToClipboard = useCallback(async () => { const rect = @@ -74,20 +73,19 @@ export const useCopyCanvasToClipboard = (region: 'canvas' | 'bbox') => { return; } - const result = await withResultAsync(async () => { + try { const rasterAdapters = canvasManager.compositor.getVisibleAdaptersOfType('raster_layer'); const canvasElement = canvasManager.compositor.getCompositeCanvas(rasterAdapters, rect); const blob = await canvasToBlob(canvasElement); - copyBlobToClipboard(blob); - }); - - if (result.isOk()) { - toast({ title: t('controlLayers.regionCopiedToClipboard', { region: startCase(region) }) }); - } else { - log.error({ error: serializeError(result.error) }, 'Failed to save canvas to gallery'); + clipboard.writeImage(blob, () => { + log.trace('Region copied to clipboard'); + toast({ title: t('controlLayers.regionCopiedToClipboard', { region: startCase(region) }) }); + }); + } catch (error) { + log.error({ error: serializeError(error) }, 'Failed to save canvas to gallery'); toast({ title: t('controlLayers.copyRegionError', { region: startCase(region) }), status: 'error' }); } - }, [canvasManager.compositor, canvasManager.stateApi, region, t]); + }, [canvasManager.compositor, canvasManager.stateApi, clipboard, region, t]); return copyCanvasToClipboard; }; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemCopy.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemCopy.tsx index b51b80d3a7..06c24b3db4 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemCopy.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemCopy.tsx @@ -8,16 +8,12 @@ import { PiCopyBold } from 'react-icons/pi'; export const ImageMenuItemCopy = memo(() => { const { t } = useTranslation(); const imageDTO = useImageDTOContext(); - const { isClipboardAPIAvailable, copyImageToClipboard } = useCopyImageToClipboard(); + const copyImageToClipboard = useCopyImageToClipboard(); const onClick = useCallback(() => { copyImageToClipboard(imageDTO.image_url); }, [copyImageToClipboard, imageDTO.image_url]); - if (!isClipboardAPIAvailable) { - return null; - } - return ( } diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/DataViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/DataViewer.tsx index 2cbf93b899..03b2348ec0 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/DataViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageMetadataViewer/DataViewer.tsx @@ -1,5 +1,6 @@ import { Box, Flex, IconButton, Tooltip, useShiftModifier } from '@invoke-ai/ui-library'; import { getOverlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; +import { useClipboard } from 'common/hooks/useClipboard'; import { Formatter } from 'fracturedjsonjs'; import { isString } from 'lodash-es'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; @@ -25,9 +26,10 @@ const DataViewer = (props: Props) => { const { label, data, fileName, withDownload = true, withCopy = true, extraCopyActions } = props; const dataString = useMemo(() => (isString(data) ? data : formatter.Serialize(data)) ?? '', [data]); const shift = useShiftModifier(); + const clipboard = useClipboard(); const handleCopy = useCallback(() => { - navigator.clipboard.writeText(dataString); - }, [dataString]); + clipboard.writeText(dataString); + }, [clipboard, dataString]); const handleDownload = useCallback(() => { const blob = new Blob([dataString]); @@ -94,9 +96,10 @@ type ExtraCopyActionProps = { }; const ExtraCopyAction = ({ label, data, getData }: ExtraCopyActionProps) => { const { t } = useTranslation(); + const clipboard = useClipboard(); const handleCopy = useCallback(() => { - navigator.clipboard.writeText(JSON.stringify(getData(data), null, 2)); - }, [data, getData]); + clipboard.writeText(JSON.stringify(getData(data), null, 2)); + }, [clipboard, data, getData]); return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/ShareWorkflowModal.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/ShareWorkflowModal.tsx index 8fb7e759e7..34a3eabd6c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/ShareWorkflowModal.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/ShareWorkflowModal.tsx @@ -15,6 +15,7 @@ import { import { useStore } from '@nanostores/react'; import { $projectUrl } from 'app/store/nanostores/projectId'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { useClipboard } from 'common/hooks/useClipboard'; import { toast } from 'features/toast/toast'; import { atom } from 'nanostores'; import { useCallback, useMemo } from 'react'; @@ -38,7 +39,7 @@ export const ShareWorkflowModal = () => { const workflowToShare = useStore($workflowToShare); const projectUrl = useStore($projectUrl); const { t } = useTranslation(); - + const clipboard = useClipboard(); const workflowLink = useMemo(() => { if (!workflowToShare || !projectUrl) { return null; @@ -50,13 +51,14 @@ export const ShareWorkflowModal = () => { if (!workflowLink) { return; } - navigator.clipboard.writeText(workflowLink); - toast({ - status: 'success', - title: t('toast.linkCopied'), + clipboard.writeText(workflowLink, () => { + toast({ + status: 'success', + title: t('toast.linkCopied'), + }); }); $workflowToShare.set(null); - }, [workflowLink, t]); + }, [workflowLink, clipboard, t]); return ( diff --git a/invokeai/frontend/web/src/features/system/components/AboutModal/AboutModal.tsx b/invokeai/frontend/web/src/features/system/components/AboutModal/AboutModal.tsx index 4eb6182ecc..88aca4533b 100644 --- a/invokeai/frontend/web/src/features/system/components/AboutModal/AboutModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/AboutModal/AboutModal.tsx @@ -19,6 +19,7 @@ import { useDisclosure, } from '@invoke-ai/ui-library'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; +import { useClipboard } from 'common/hooks/useClipboard'; import { discordLink, githubLink, websiteLink } from 'features/system/store/constants'; import { map } from 'lodash-es'; import InvokeLogoYellow from 'public/assets/images/invoke-tag-lrg.svg'; @@ -36,6 +37,7 @@ type AboutModalProps = { const AboutModal = ({ children }: AboutModalProps) => { const { isOpen, onOpen, onClose } = useDisclosure(); const { t } = useTranslation(); + const clipboard = useClipboard(); const { depsArray, depsObject } = useGetAppDepsQuery(undefined, { selectFromResult: ({ data }) => ({ depsObject: data, @@ -45,8 +47,8 @@ const AboutModal = ({ children }: AboutModalProps) => { const { data: appVersion } = useGetAppVersionQuery(); const handleCopy = useCallback(() => { - navigator.clipboard.writeText(JSON.stringify(depsObject, null, 2)); - }, [depsObject]); + clipboard.writeText(JSON.stringify(depsObject, null, 2)); + }, [clipboard, depsObject]); return ( <> diff --git a/invokeai/frontend/web/src/features/system/util/copyBlobToClipboard.ts b/invokeai/frontend/web/src/features/system/util/copyBlobToClipboard.ts deleted file mode 100644 index ccdb9e1af0..0000000000 --- a/invokeai/frontend/web/src/features/system/util/copyBlobToClipboard.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Copies a blob to the clipboard by calling navigator.clipboard.write(). - */ -export const copyBlobToClipboard = (blob: Promise | Blob, type = 'image/png') => { - navigator.clipboard.write([ - new ClipboardItem({ - [type]: blob, - }), - ]); -}; diff --git a/invokeai/frontend/web/src/features/toast/ErrorToastDescription.tsx b/invokeai/frontend/web/src/features/toast/ErrorToastDescription.tsx index 23f7ee64bd..30a9ab8a9b 100644 --- a/invokeai/frontend/web/src/features/toast/ErrorToastDescription.tsx +++ b/invokeai/frontend/web/src/features/toast/ErrorToastDescription.tsx @@ -1,4 +1,5 @@ import { Flex, IconButton, Text } from '@invoke-ai/ui-library'; +import { useClipboard } from 'common/hooks/useClipboard'; import { ExternalLink } from 'features/gallery/components/ImageViewer/NoContentForViewer'; import { useCallback, useMemo } from 'react'; import { Trans, useTranslation } from 'react-i18next'; @@ -18,7 +19,7 @@ export const ErrorToastTitle = ({ errorType }: Props) => { export default function ErrorToastDescription({ errorType, isLocal, sessionId, errorMessage }: Props) { const { t } = useTranslation(); - + const clipboard = useClipboard(); const description = useMemo(() => { if (errorType === 'OutOfMemoryError') { if (isLocal) { @@ -38,7 +39,7 @@ export default function ErrorToastDescription({ errorType, isLocal, sessionId, e } }, [errorMessage, errorType, isLocal, t]); - const copySessionId = useCallback(() => navigator.clipboard.writeText(sessionId), [sessionId]); + const copySessionId = useCallback(() => clipboard.writeText(sessionId), [clipboard, sessionId]); return (