feat(dictionary): add Hachidori backend support

- Add backend selection, setup gating, Anki integration, and external host support
- Add launcher flags, documentation, packaging, and focused tests
- Open on-demand overlay modals on the first attempt
This commit is contained in:
2026-09-22 00:21:19 -07:00
parent 1508863dbb
commit d9fdc7ef6d
446 changed files with 109060 additions and 244 deletions
+48
View File
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: GPL-3.0-or-later
export function createCaptureFrameEncoder({ WorkerClass = globalThis.Worker,
createBitmap = globalThis.createImageBitmap } = {}) {
const worker = new WorkerClass(new URL("./capture-frame-worker.js", import.meta.url), {
type: "module", name: "hachidori-capture-frames",
});
let pending = null;
let closed = false;
const close = (error = new Error("Frame capture stopped.")) => {
closed = true;
worker.terminate();
pending?.reject(error);
pending = null;
};
worker.addEventListener("error", event => {
close(event.error ?? new Error(event.message || "The frame encoder stopped."));
});
worker.addEventListener("message", ({ data }) => {
const request = pending;
pending = null;
if (!request) return;
if (data.error) request.reject(new Error(data.error));
else request.resolve(data.bytes ? new Uint8Array(data.bytes) : null);
});
return {
async encode(source, dimensions) {
if (closed) throw new Error("Frame capture stopped.");
// Clone raw VideoFrames without copying pixels; the preview fallback
// supplies an ImageBitmap. Only one frame is submitted at a time.
const frame = typeof source.clone === "function" ? source.clone() : await createBitmap(source);
if (closed || pending) {
frame.close();
throw new Error(closed ? "Frame capture stopped." : "A capture frame is already being encoded.");
}
return new Promise((resolve, reject) => {
pending = { resolve, reject };
try { worker.postMessage({ frame, ...dimensions }, [frame]); }
catch (error) {
pending = null;
frame.close();
reject(error);
}
});
},
close,
};
}