diff --git a/blocksuite/affine/block-image/src/utils.ts b/blocksuite/affine/block-image/src/utils.ts index 702bc0ecfbb6c..29a79bb8e1056 100644 --- a/blocksuite/affine/block-image/src/utils.ts +++ b/blocksuite/affine/block-image/src/utils.ts @@ -4,6 +4,7 @@ import type { ImageBlockModel, ImageBlockProps, } from '@blocksuite/affine-model'; +import { NativeClipboardProvider } from '@blocksuite/affine-shared/services'; import { downloadBlob, humanFileSize, @@ -12,7 +13,6 @@ import { } from '@blocksuite/affine-shared/utils'; import type { BlockStdScope, EditorHost } from '@blocksuite/block-std'; import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx'; -import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; import { Bound, type IVec, Point, Vec } from '@blocksuite/global/utils'; import type { BlockModel } from '@blocksuite/store'; @@ -200,7 +200,7 @@ export async function resetImageSize( }); } -function convertToString(blob: Blob): Promise { +function convertToDataURL(blob: Blob): Promise { return new Promise(resolve => { const reader = new FileReader(); reader.addEventListener('load', _ => resolve(reader.result as string)); @@ -234,25 +234,26 @@ function convertToPng(blob: Blob): Promise { export async function copyImageBlob( block: ImageBlockComponent | ImageEdgelessBlockComponent ) { - const { host, model } = block; + const { host, model, std } = block; let blob = await getImageBlob(model); if (!blob) { console.error('Failed to get image blob'); return; } + let copied = false; + try { - // @ts-expect-error FIXME: BS-2239 - if (window.apis?.clipboard?.copyAsImageFromString) { - const dataURL = await convertToString(blob); - if (!dataURL) - throw new BlockSuiteError( - ErrorCode.DefaultRuntimeError, - 'Cant convert a blob to data URL.' - ); - // @ts-expect-error FIXME: BS-2239 - await window.apis.clipboard?.copyAsImageFromString(dataURL); - } else { + // Copies the image directly without converting it to png in Electron. + const copyAsImage = std.getOptional(NativeClipboardProvider)?.copyAsImage; + if (copyAsImage) { + const dataURL = await convertToDataURL(blob); + if (dataURL) { + copied = await copyAsImage(dataURL); + } + } + + if (!copied) { // DOMException: Type image/jpeg not supported on write. if (blob.type !== 'image/png') { const pngBlob = await convertToPng(blob); diff --git a/blocksuite/affine/shared/src/services/index.ts b/blocksuite/affine/shared/src/services/index.ts index 1c805af7c8732..ae5a0d51f8ddb 100644 --- a/blocksuite/affine/shared/src/services/index.ts +++ b/blocksuite/affine/shared/src/services/index.ts @@ -6,6 +6,7 @@ export * from './editor-setting-service'; export * from './embed-option-service'; export * from './font-loader'; export * from './generate-url-service'; +export * from './native-clipboard-service'; export * from './notification-service'; export * from './page-viewport-service'; export * from './parse-url-service'; diff --git a/blocksuite/affine/shared/src/services/native-clipboard-service.ts b/blocksuite/affine/shared/src/services/native-clipboard-service.ts new file mode 100644 index 0000000000000..d3efeb8dc924c --- /dev/null +++ b/blocksuite/affine/shared/src/services/native-clipboard-service.ts @@ -0,0 +1,26 @@ +import type { ExtensionType } from '@blocksuite/block-std'; +import { createIdentifier } from '@blocksuite/global/di'; + +/** + * Copies the image directly without converting it to png in Electron. + * + * In the web, it can only be converted to png before it can be written to the clipboard, + * or it will throw an exception: `DOMException: Type image/jpeg not supported on write.` + */ +export interface NativeClipboardService { + copyAsImage(dataURL: string): Promise; +} + +export const NativeClipboardProvider = createIdentifier( + 'NativeClipboardService' +); + +export function NativeClipboardExtension( + nativeClipboardProvider: NativeClipboardService +): ExtensionType { + return { + setup: di => { + di.addImpl(NativeClipboardProvider, nativeClipboardProvider); + }, + }; +} diff --git a/blocksuite/blocks/src/root-block/widgets/surface-ref-toolbar/utils.ts b/blocksuite/blocks/src/root-block/widgets/surface-ref-toolbar/utils.ts index 5e768d31dbce2..b081a1feee41f 100644 --- a/blocksuite/blocks/src/root-block/widgets/surface-ref-toolbar/utils.ts +++ b/blocksuite/blocks/src/root-block/widgets/surface-ref-toolbar/utils.ts @@ -40,11 +40,5 @@ export const edgelessToBlob = async ( }; export const writeImageBlobToClipboard = async (blob: Blob) => { - // @ts-expect-error FIXME: BS-2239 - if (window.apis?.clipboard?.copyAsImageFromString) { - // @ts-expect-error FIXME: BS-2239 - await window.apis.clipboard?.copyAsImageFromString(blob); - } else { - await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]); - } + await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]); }; diff --git a/packages/frontend/apps/electron/src/main/clipboard/index.ts b/packages/frontend/apps/electron/src/main/clipboard/index.ts index 2d8d394d45fac..94af992ed948a 100644 --- a/packages/frontend/apps/electron/src/main/clipboard/index.ts +++ b/packages/frontend/apps/electron/src/main/clipboard/index.ts @@ -4,7 +4,10 @@ import { clipboard, nativeImage } from 'electron'; import type { NamespaceHandlers } from '../type'; export const clipboardHandlers = { - copyAsImageFromString: async (_: IpcMainInvokeEvent, dataURL: string) => { - clipboard.writeImage(nativeImage.createFromDataURL(dataURL)); + copyAsImage: async (_: IpcMainInvokeEvent, dataURL: string) => { + const image = nativeImage.createFromDataURL(dataURL); + if (image.isEmpty()) return false; + clipboard.writeImage(image); + return true; }, } satisfies NamespaceHandlers; diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx index 0da7345fdfd93..84f92103e94a0 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx @@ -55,6 +55,7 @@ import { patchEdgelessClipboard, patchEmbedLinkedDocBlockConfig, patchForAttachmentEmbedViews, + patchForClipboardInElectron, patchForMobile, patchForSharedPage, patchGenerateDocUrlExtension, @@ -170,6 +171,9 @@ const usePatchSpecs = (shared: boolean, mode: DocMode) => { if (BUILD_CONFIG.isMobileEdition) { patched = patched.concat(patchForMobile()); } + if (BUILD_CONFIG.isElectron) { + patched = patched.concat(patchForClipboardInElectron(framework)); + } patched = patched.concat( patchDocModeService(docService, docsService, editorService) ); diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx index 7e8992bebf326..65f429c2b1bda 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx @@ -9,6 +9,7 @@ import { } from '@affine/component'; import { AIChatBlockSchema } from '@affine/core/blocksuite/blocks'; import { WorkspaceServerService } from '@affine/core/modules/cloud'; +import { DesktopApiService } from '@affine/core/modules/desktop-api'; import { type DocService, DocsService } from '@affine/core/modules/doc'; import type { EditorService } from '@affine/core/modules/editor'; import { EditorSettingService } from '@affine/core/modules/editor-setting'; @@ -53,6 +54,7 @@ import { EmbedLinkedDocBlockConfigExtension, GenerateDocUrlExtension, MobileSpecsPatches, + NativeClipboardExtension, NotificationExtension, ParseDocUrlExtension, PeekViewExtension, @@ -618,3 +620,10 @@ export function patchForAttachmentEmbedViews( }, }; } + +export function patchForClipboardInElectron(framework: FrameworkProvider) { + const desktopApi = framework.get(DesktopApiService); + return NativeClipboardExtension({ + copyAsImage: desktopApi.handler.clipboard.copyAsImage, + }); +}