mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-22 17:16:19 -07:00
- 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
144 lines
5.5 KiB
JavaScript
144 lines
5.5 KiB
JavaScript
// 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 });
|