From dd673851db6bcd565d51712e22e4590ee88ef9b7 Mon Sep 17 00:00:00 2001 From: Abdo Date: Sat, 28 Jun 2025 16:46:13 +0300 Subject: [PATCH] Go back to using QClipboard --- proto/anki/frontend.proto | 16 ++++ qt/aqt/mediasrv.py | 24 ++++++ .../editor/rich-text-input/data-transfer.ts | 75 ++++++++----------- 3 files changed, 73 insertions(+), 42 deletions(-) diff --git a/proto/anki/frontend.proto b/proto/anki/frontend.proto index 73effc4d6..130f617af 100644 --- a/proto/anki/frontend.proto +++ b/proto/anki/frontend.proto @@ -46,6 +46,10 @@ service FrontendService { // Metadata rpc GetMetaJson(generic.String) returns (generic.Json); rpc SetMetaJson(SetSettingJsonRequest) returns (generic.Empty); + + // Clipboard + rpc ReadClipboard(ReadClipboardRequest) returns (ReadClipboardResponse); + rpc WriteClipboard(WriteClipboardRequest) returns (generic.Empty); } service BackendFrontendService {} @@ -85,3 +89,15 @@ message openFilePickerRequest { string filter_description = 3; repeated string extensions = 4; } + +message ReadClipboardRequest { + repeated string types = 1; +} + +message ReadClipboardResponse { + map data = 1; +} + +message WriteClipboardRequest { + map data = 1; +} \ No newline at end of file diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index 29dd0ebc8..af9b3a769 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -761,6 +761,28 @@ async def record_audio() -> bytes: return generic_pb2.String(val=path if path else "").SerializeToString() +def read_clipboard() -> bytes: + req = frontend_pb2.ReadClipboardRequest() + req.ParseFromString(request.data) + data = {} + clipboard = aqt.mw.app.clipboard() + mime_data = clipboard.mimeData(QClipboard.Mode.Clipboard) + for type in req.types: + data[type] = bytes(mime_data.data(type)) + + return frontend_pb2.ReadClipboardResponse(data=data).SerializeToString() + + +def write_clipboard() -> bytes: + req = frontend_pb2.WriteClipboardRequest() + req.ParseFromString(request.data) + clipboard = aqt.mw.app.clipboard() + mime_data = clipboard.mimeData(QClipboard.Mode.Clipboard) + for type, data in req.data.items(): + mime_data.setData(type, data) + return b"" + + post_handler_list = [ congrats_info, get_deck_configs_for_update, @@ -788,6 +810,8 @@ post_handler_list = [ open_media, show_in_media_folder, record_audio, + read_clipboard, + write_clipboard, ] diff --git a/ts/routes/editor/rich-text-input/data-transfer.ts b/ts/routes/editor/rich-text-input/data-transfer.ts index dd992db67..de4aef0f1 100644 --- a/ts/routes/editor/rich-text-input/data-transfer.ts +++ b/ts/routes/editor/rich-text-input/data-transfer.ts @@ -2,6 +2,7 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { ConfigKey_Bool } from "@generated/anki/config_pb"; +import type { ReadClipboardResponse } from "@generated/anki/frontend_pb"; import { addMediaFile, convertPastedImage, @@ -9,7 +10,9 @@ import { getAbsoluteMediaPath, getConfigBool, openFilePicker, + readClipboard, retrieveUrl as retrieveUrlBackend, + writeClipboard, } from "@generated/backend"; import * as tr from "@generated/ftl"; import { shiftPressed } from "@tslib/keys"; @@ -41,6 +44,19 @@ const audioSuffixes = [ "webm", ]; const mediaSuffixes = [...imageSuffixes, ...audioSuffixes]; +const QIMAGE_FORMATS = [ + "image/jpeg", + "image/png", + "image/gif", + "image/svg+xml", + "image/bmp", + "image/x-portable-bitmap", + "image/x-portable-graymap", + "image/x-portable-pixmap", + "image/x-xbitmap", + "image/x-xpixmap", +]; +const URI_LIST_MIME = "text/uri-list"; let isShiftPressed = false; let lastInternalFieldText = ""; @@ -67,17 +83,12 @@ function escapeHtml(text: string, quote = true): string { return text; } -async function getUrls(data: DataTransfer | ClipboardItem): Promise { - const mime = "text/uri-list"; +async function getUrls(data: DataTransfer | ReadClipboardResponse): Promise { let dataString: string; if (data instanceof DataTransfer) { - dataString = data.getData(mime); + dataString = data.getData(URI_LIST_MIME); } else { - try { - dataString = await (await data.getType(mime)).text(); - } catch (e) { - return []; - } + dataString = new TextDecoder().decode(data.data[URI_LIST_MIME]); } const urls = dataString.split("\n"); return urls[0] ? urls : []; @@ -91,25 +102,12 @@ function getHtml(data: DataTransfer): string { return data.getData("text/html") ?? ""; } -const QIMAGE_FORMATS = [ - "image/jpeg", - "image/png", - "image/gif", - "image/svg+xml", - "image/bmp", - "image/x-portable-bitmap", - "image/x-portable-graymap", - "image/x-portable-pixmap", - "image/x-xbitmap", - "image/x-xpixmap", -]; - -async function getImageData(data: DataTransfer | ClipboardItem): Promise { +async function getImageData(data: DataTransfer | ReadClipboardResponse): Promise { for (const type of QIMAGE_FORMATS) { try { const image = data instanceof DataTransfer ? data.getData(type) - : new Uint8Array(await (await data.getType(type)).arrayBuffer()); + : data.data[type]; if (image) { return image; } else if (data instanceof DataTransfer) { @@ -233,7 +231,7 @@ function isURL(s: string): boolean { } async function processUrls( - data: DataTransfer | ClipboardItem, + data: DataTransfer | ReadClipboardResponse, _extended: Promise, allowedSuffixes: string[] = mediaSuffixes, ): Promise { @@ -419,7 +417,7 @@ export async function extractImagePathFromHtml(html: string): Promise { +export async function extractImagePathFromData(data: DataTransfer | ReadClipboardResponse): Promise { const html = await processUrls(data, Promise.resolve(false), imageSuffixes); if (html) { return await extractImagePathFromHtml(html); @@ -428,26 +426,19 @@ export async function extractImagePathFromData(data: DataTransfer | ClipboardIte } export async function readImageFromClipboard(): Promise { - // TODO: check browser support and available formats - for (const item of await navigator.clipboard.read()) { - let path = await extractImagePathFromData(item); - if (!path) { - const image = await getImageData(item); - if (!image) { - continue; - } - const ext = await getPreferredImageExtension(); - path = await addPastedImage(image, ext, true); + const data = await readClipboard({ types: QIMAGE_FORMATS.concat(URI_LIST_MIME) }); + let path = await extractImagePathFromData(data); + if (!path) { + const image = await getImageData(data); + if (!image) { + return null; } - return (await getAbsoluteMediaPath({ val: path })).val; + const ext = await getPreferredImageExtension(); + path = await addPastedImage(image, ext, true); } - return null; + return (await getAbsoluteMediaPath({ val: path })).val; } export async function writeBlobToClipboard(blob: Blob) { - await navigator.clipboard.write([ - new ClipboardItem({ - [blob.type]: blob, - }), - ]); + await writeClipboard({ data: { [blob.type]: new Uint8Array(await blob.arrayBuffer()) } }); }