mirror of
https://github.com/ankitects/anki.git
synced 2025-09-21 15:32:23 -04:00
Go back to using QClipboard
This commit is contained in:
parent
0bdeab974f
commit
dd673851db
3 changed files with 73 additions and 42 deletions
|
@ -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;
|
||||||
|
}
|
|
@ -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,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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(data);
|
||||||
const image = await getImageData(item);
|
if (!image) {
|
||||||
if (!image) {
|
return null;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const ext = await getPreferredImageExtension();
|
|
||||||
path = await addPastedImage(image, ext, true);
|
|
||||||
}
|
}
|
||||||
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) {
|
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,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue