mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-22 17:16:19 -07:00
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:
+143
@@ -0,0 +1,143 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
import { extensionApi as chrome } from "./browser-api.js";
|
||||
import "./reader-options.js";
|
||||
import { HOST_BROWSER, HOST_CAPABILITIES } from "./overlay-mode.js";
|
||||
|
||||
const { normaliseOptions } = globalThis.HDReaderOptions;
|
||||
const elements = Object.fromEntries([...document.querySelectorAll("[id]")].map(node => [node.id, node]));
|
||||
let options = null;
|
||||
let linkedAddress = null;
|
||||
let revision = -1;
|
||||
let pending = false;
|
||||
let requestCounter = 0;
|
||||
|
||||
function adoptOptions(stored) {
|
||||
const nextRevision = Number.isInteger(stored?.revision) && stored.revision >= 0 ? stored.revision : 0;
|
||||
if (nextRevision < revision) return;
|
||||
revision = nextRevision;
|
||||
options = normaliseOptions(stored);
|
||||
render();
|
||||
}
|
||||
|
||||
function render() {
|
||||
elements["lookup-toggle"].disabled = pending || !options;
|
||||
elements["record-screen"].hidden = HOST_BROWSER === "firefox";
|
||||
elements["record-screen"].disabled = pending || !options || !HOST_CAPABILITIES.mediaCapture;
|
||||
if (!options) return;
|
||||
elements["lookup-toggle"].setAttribute("aria-checked", String(options.hoverEnabled));
|
||||
elements["lookup-state"].textContent = options.hoverEnabled ? "On" : "Off";
|
||||
const activeHint = options.lookupMode !== "hover"
|
||||
? `Hold ${options.activationKey} to scan` : "Hover over Japanese text to scan";
|
||||
elements["activation-hint"].textContent = options.hoverEnabled ? activeHint : "Lookups paused";
|
||||
}
|
||||
|
||||
function showError(error) {
|
||||
elements["toolbar-error"].textContent = error instanceof Error ? error.message : String(error);
|
||||
elements["toolbar-error"].hidden = false;
|
||||
}
|
||||
|
||||
async function send(type, fields = {}, target = "hachidori-capture") {
|
||||
const reply = await chrome.runtime.sendMessage({ target, type, requestId: `toolbar-${++requestCounter}`, ...fields });
|
||||
if (reply?.options) adoptOptions(reply.options);
|
||||
if (!reply?.ok) throw new Error(reply?.error || "Hachidori did not reply. Please try again.");
|
||||
return reply;
|
||||
}
|
||||
|
||||
async function writeOptions(patch) {
|
||||
await send("hd_options_write", { baseRevision: revision, options: patch }, "hoshidicts-worker");
|
||||
}
|
||||
|
||||
async function run(action) {
|
||||
if (pending) return;
|
||||
pending = true;
|
||||
elements["toolbar-error"].hidden = true;
|
||||
render();
|
||||
try { await action(); }
|
||||
catch (error) { showError(error); }
|
||||
finally { pending = false; render(); }
|
||||
}
|
||||
|
||||
function renderCapture(recording) {
|
||||
if (!HOST_CAPABILITIES.mediaCapture) {
|
||||
elements["record-screen"].classList.toggle("is-recording", false);
|
||||
elements["record-screen"].title = HOST_BROWSER === "firefox"
|
||||
? "Context capture is unavailable in Firefox"
|
||||
: "Context capture is unavailable in this overlay";
|
||||
elements["record-label"].textContent = "Context capture unavailable";
|
||||
return;
|
||||
}
|
||||
elements["record-screen"].classList.toggle("is-recording", recording);
|
||||
elements["record-screen"].title = recording ? "Open capture controls"
|
||||
: "Capture a screen, window, or tab as context for your Japanese cards";
|
||||
elements["record-label"].textContent = recording ? "Recording context" : "Record context for Anki";
|
||||
}
|
||||
|
||||
// A linked install shows where its lookups go and whether that host answers.
|
||||
function adoptSharing(stored) {
|
||||
linkedAddress = typeof stored?.client?.address === "string" ? stored.client.address : null;
|
||||
elements["toolbar-sharing"].hidden = linkedAddress === null;
|
||||
if (linkedAddress === null) elements["toolbar-sharing"].textContent = "";
|
||||
}
|
||||
|
||||
async function refreshSharing() {
|
||||
if (linkedAddress === null) return;
|
||||
try {
|
||||
const status = await send("hd_sharing_status", {}, "hachidori-sharing");
|
||||
elements["toolbar-sharing"].textContent = status.sharing?.client?.connected
|
||||
? "Linked to another Hachidori" : "Linked Hachidori not reachable";
|
||||
} catch (error) { showError(error); }
|
||||
}
|
||||
|
||||
async function refreshCapture() {
|
||||
if (!HOST_CAPABILITIES.mediaCapture) {
|
||||
renderCapture(false);
|
||||
return;
|
||||
}
|
||||
if (!options?.mediaCapture.enabled) {
|
||||
renderCapture(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const status = await send("hd_capture_status");
|
||||
renderCapture(status.state === "recording");
|
||||
} catch (error) { showError(error); }
|
||||
}
|
||||
|
||||
elements["lookup-toggle"].addEventListener("click", () => {
|
||||
void run(() => writeOptions({ hoverEnabled: !options.hoverEnabled }));
|
||||
});
|
||||
elements["record-screen"].addEventListener("click", () => {
|
||||
if (!HOST_CAPABILITIES.mediaCapture) return;
|
||||
void run(async () => {
|
||||
// The shortcut enables the existing capture controls. The recorder still
|
||||
// requires Start capture and an explicit source choice in Chrome's picker.
|
||||
if (!options.mediaCapture.enabled) {
|
||||
await writeOptions({ mediaCapture: { ...options.mediaCapture, enabled: true } });
|
||||
}
|
||||
await send("hd_capture_open");
|
||||
window.close();
|
||||
});
|
||||
});
|
||||
elements["open-settings"].addEventListener("click", () => {
|
||||
void run(async () => { await chrome.runtime.openOptionsPage(); window.close(); });
|
||||
});
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (area !== "local") return;
|
||||
if (changes.options) {
|
||||
adoptOptions(changes.options.newValue);
|
||||
void refreshCapture();
|
||||
}
|
||||
if (changes.sharing) {
|
||||
adoptSharing(changes.sharing.newValue);
|
||||
void refreshSharing();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const stored = await chrome.storage.local.get(["options", "sharing"]);
|
||||
adoptOptions(stored.options);
|
||||
adoptSharing(stored.sharing);
|
||||
await Promise.all([refreshCapture(), refreshSharing()]);
|
||||
} catch (error) { showError(error); }
|
||||
const capturePoll = setInterval(() => { void refreshCapture(); void refreshSharing(); }, 1000);
|
||||
window.addEventListener("pagehide", () => clearInterval(capturePoll), { once: true });
|
||||
Reference in New Issue
Block a user