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 (