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
+39
View File
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: GPL-3.0-or-later
import createAvifEncoderModule from "./vendor/avif-encoder.mjs";
import { encodeJpegSequence } from "./avif-sequence.js";
let modulePromise;
function encoderModule() {
modulePromise ??= createAvifEncoderModule({
locateFile: name => new URL(`./vendor/${name}`, import.meta.url).href,
});
return modulePromise;
}
// A dedicated worker receives messages only from its owning Worker. Window
// MessageEvent.origin checks do not form a security boundary in this context.
self.addEventListener("message", async event => { // NOSONAR -- S2819 applies to Window messaging, not this worker.
const request = event.data;
if (request?.type !== "encode" || typeof request.id !== "string") return;
try {
const module = await encoderModule();
const data = await encodeJpegSequence(module, request.frames, {
endMs: request.endMs,
quality: request.videoPreset === "compact" ? 50 : 55,
speed: 8,
onProgress(completed, total) {
self.postMessage({ type: "progress", id: request.id, completed, total,
heapBytes: module.HEAPU8.byteLength });
},
});
// WebAssembly memory only grows; this includes any final muxing allocation.
self.postMessage({ type: "progress", id: request.id,
completed: request.frames.length, total: request.frames.length,
heapBytes: module.HEAPU8.byteLength });
self.postMessage({ type: "result", id: request.id, ok: true, data: data.buffer }, [data.buffer]);
} catch (error) {
self.postMessage({ type: "result", id: request.id, ok: false,
error: error instanceof Error ? error.message : String(error) });
}
});