Go back to using QClipboard

This commit is contained in:
Abdo 2025-06-28 16:46:13 +03:00
parent 0bdeab974f
commit dd673851db
3 changed files with 73 additions and 42 deletions

View file

@ -46,6 +46,10 @@ service FrontendService {
// Metadata // Metadata
rpc GetMetaJson(generic.String) returns (generic.Json); rpc GetMetaJson(generic.String) returns (generic.Json);
rpc SetMetaJson(SetSettingJsonRequest) returns (generic.Empty); rpc SetMetaJson(SetSettingJsonRequest) returns (generic.Empty);
// Clipboard
rpc ReadClipboard(ReadClipboardRequest) returns (ReadClipboardResponse);
rpc WriteClipboard(WriteClipboardRequest) returns (generic.Empty);
} }
service BackendFrontendService {} service BackendFrontendService {}
@ -85,3 +89,15 @@ message openFilePickerRequest {
string filter_description = 3; string filter_description = 3;
repeated string extensions = 4; repeated string extensions = 4;
} }
message ReadClipboardRequest {
repeated string types = 1;
}
message ReadClipboardResponse {
map<string, bytes> data = 1;
}
message WriteClipboardRequest {
map<string, bytes> data = 1;
}

View file

@ -761,6 +761,28 @@ async def record_audio() -> bytes:
return generic_pb2.String(val=path if path else "").SerializeToString() 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 = [ post_handler_list = [
congrats_info, congrats_info,
get_deck_configs_for_update, get_deck_configs_for_update,
@ -788,6 +810,8 @@ post_handler_list = [
open_media, open_media,
show_in_media_folder, show_in_media_folder,
record_audio, record_audio,
read_clipboard,
write_clipboard,
] ]

View file

@ -2,6 +2,7 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { ConfigKey_Bool } from "@generated/anki/config_pb"; import { ConfigKey_Bool } from "@generated/anki/config_pb";
import type { ReadClipboardResponse } from "@generated/anki/frontend_pb";
import { import {
addMediaFile, addMediaFile,
convertPastedImage, convertPastedImage,
@ -9,7 +10,9 @@ import {
getAbsoluteMediaPath, getAbsoluteMediaPath,
getConfigBool, getConfigBool,
openFilePicker, openFilePicker,
readClipboard,
retrieveUrl as retrieveUrlBackend, retrieveUrl as retrieveUrlBackend,
writeClipboard,
} from "@generated/backend"; } from "@generated/backend";
import * as tr from "@generated/ftl"; import * as tr from "@generated/ftl";
import { shiftPressed } from "@tslib/keys"; import { shiftPressed } from "@tslib/keys";
@ -41,6 +44,19 @@ const audioSuffixes = [
"webm", "webm",
]; ];
const mediaSuffixes = [...imageSuffixes, ...audioSuffixes]; 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 isShiftPressed = false;
let lastInternalFieldText = ""; let lastInternalFieldText = "";
@ -67,17 +83,12 @@ function escapeHtml(text: string, quote = true): string {
return text; return text;
} }
async function getUrls(data: DataTransfer | ClipboardItem): Promise<string[]> { async function getUrls(data: DataTransfer | ReadClipboardResponse): Promise<string[]> {
const mime = "text/uri-list";
let dataString: string; let dataString: string;
if (data instanceof DataTransfer) { if (data instanceof DataTransfer) {
dataString = data.getData(mime); dataString = data.getData(URI_LIST_MIME);
} else { } else {
try { dataString = new TextDecoder().decode(data.data[URI_LIST_MIME]);
dataString = await (await data.getType(mime)).text();
} catch (e) {
return [];
}
} }
const urls = dataString.split("\n"); const urls = dataString.split("\n");
return urls[0] ? urls : []; return urls[0] ? urls : [];
@ -91,25 +102,12 @@ function getHtml(data: DataTransfer): string {
return data.getData("text/html") ?? ""; return data.getData("text/html") ?? "";
} }
const QIMAGE_FORMATS = [ async function getImageData(data: DataTransfer | ReadClipboardResponse): Promise<ImageData | null> {
"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<ImageData | null> {
for (const type of QIMAGE_FORMATS) { for (const type of QIMAGE_FORMATS) {
try { try {
const image = data instanceof DataTransfer const image = data instanceof DataTransfer
? data.getData(type) ? data.getData(type)
: new Uint8Array(await (await data.getType(type)).arrayBuffer()); : data.data[type];
if (image) { if (image) {
return image; return image;
} else if (data instanceof DataTransfer) { } else if (data instanceof DataTransfer) {
@ -233,7 +231,7 @@ function isURL(s: string): boolean {
} }
async function processUrls( async function processUrls(
data: DataTransfer | ClipboardItem, data: DataTransfer | ReadClipboardResponse,
_extended: Promise<boolean>, _extended: Promise<boolean>,
allowedSuffixes: string[] = mediaSuffixes, allowedSuffixes: string[] = mediaSuffixes,
): Promise<string | null> { ): Promise<string | null> {
@ -419,7 +417,7 @@ export async function extractImagePathFromHtml(html: string): Promise<string | n
} }
return decodeURI(images[0]); return decodeURI(images[0]);
} }
export async function extractImagePathFromData(data: DataTransfer | ClipboardItem): Promise<string | null> { export async function extractImagePathFromData(data: DataTransfer | ReadClipboardResponse): Promise<string | null> {
const html = await processUrls(data, Promise.resolve(false), imageSuffixes); const html = await processUrls(data, Promise.resolve(false), imageSuffixes);
if (html) { if (html) {
return await extractImagePathFromHtml(html); return await extractImagePathFromHtml(html);
@ -428,26 +426,19 @@ export async function extractImagePathFromData(data: DataTransfer | ClipboardIte
} }
export async function readImageFromClipboard(): Promise<string | null> { export async function readImageFromClipboard(): Promise<string | null> {
// TODO: check browser support and available formats const data = await readClipboard({ types: QIMAGE_FORMATS.concat(URI_LIST_MIME) });
for (const item of await navigator.clipboard.read()) { let path = await extractImagePathFromData(data);
let path = await extractImagePathFromData(item);
if (!path) { if (!path) {
const image = await getImageData(item); const image = await getImageData(data);
if (!image) { if (!image) {
continue; return null;
} }
const ext = await getPreferredImageExtension(); const ext = await getPreferredImageExtension();
path = await addPastedImage(image, ext, true); path = await addPastedImage(image, ext, true);
} }
return (await getAbsoluteMediaPath({ val: path })).val; return (await getAbsoluteMediaPath({ val: path })).val;
} }
return null;
}
export async function writeBlobToClipboard(blob: Blob) { export async function writeBlobToClipboard(blob: Blob) {
await navigator.clipboard.write([ await writeClipboard({ data: { [blob.type]: new Uint8Array(await blob.arrayBuffer()) } });
new ClipboardItem({
[blob.type]: blob,
}),
]);
} }