fix(dictionary): keep Hachidori lookup highlight on in SubMiner builds

Hachidori's overlay-mode first-install options turn off "Highlight the
word on the page" because GSM-style hosts screenshot the see-through
viewport. SubMiner captures media from mpv, and without the highlight
the subtitle sidebar gave no sign of which word was looked up or where
the match ended. The staged copy now seeds the highlight on; the fork
source is unchanged.
This commit is contained in:
2026-09-22 21:06:19 -07:00
parent c39dbb5a8d
commit 2ba820f41a
3 changed files with 34 additions and 12 deletions
+1 -1
View File
@@ -50,7 +50,7 @@ The dictionary backend is selected once at startup by `dictionaryBackend`. Yomit
`setup-state.json` records one backend's status at a time plus `completedDictionaryBackends`, the backends that finished setup before. The app projects the file onto its active backend on startup and stamps that backend into the file. The launcher gates playback on the stamped backend when an app is already running, since a config edit takes effect only after restart. `setup-state.json` records one backend's status at a time plus `completedDictionaryBackends`, the backends that finished setup before. The app projects the file onto its active backend on startup and stamps that backend into the file. The launcher gates playback on the stamped backend when an app is already running, since a config edit takes effect only after restart.
`vendor/hachidori/` is a submodule of `ksyasuda/hachidori`, tracking the `subminer` branch and pinned to a tested commit. Its nested HoshiDicts submodule and WASM binaries remain upstream versions. Initialize sources with `git submodule update --init --recursive`; merge upstream updates in the fork, test them, then update SubMiner's submodule commit. `SOURCE.json` records the upstream base and artifact checksums; the submodule commit identifies the integrated version. `build:hachidori` verifies recorded artifact checksums and stages the extension for development and packaging. It enables overlay mode, disables custom JavaScript, and removes the unsupported `userScripts` permission only in that staged copy; the fork keeps upstream browser defaults. Before loading the extension, its session clears service worker registrations so Electron uses the current bundled code; dictionary databases and settings remain intact. First-run setup uses Hachidori sharing messages to link or unlink external dictionary hosts and checks their live inventory. Linked dictionaries and dictionary edits use the host, while Anki configuration, pronunciation sources, custom buttons, and mining stay local to SubMiner. The parser bridge adapts its runtime messages to the existing subtitle scanner and dictionary automation. Scanning retains term-entry frequencies, and only tokens without ranks need further frequency lookups through the existing term-entry API. This requires a matching definition entry and does not preserve the frequency source's reading provenance. SubMiner consumes native `hachidori-popup-shown` and `hachidori-popup-hidden` attention events for mouse handling, keyboard focus, and the subtitle sidebar. Attention also covers a left press anywhere on the overlay that may start a selection, and the host element only exists after the first lookup, so once a Hachidori event has been seen popup auto-pause requires an unhidden popup pane in the host's shadow root and rechecks after each successful lookup. The fork retains host attributes, hover and successful-lookup notifications, and commands that need private reader state. The Anki proxy strips local duplicate/overwrite metadata before forwarding requests and enriches only confirmed writes. `vendor/hachidori/` is a submodule of `ksyasuda/hachidori`, tracking the `subminer` branch and pinned to a tested commit. Its nested HoshiDicts submodule and WASM binaries remain upstream versions. Initialize sources with `git submodule update --init --recursive`; merge upstream updates in the fork, test them, then update SubMiner's submodule commit. `SOURCE.json` records the upstream base and artifact checksums; the submodule commit identifies the integrated version. `build:hachidori` verifies recorded artifact checksums and stages the extension for development and packaging. It enables overlay mode, disables custom JavaScript, keeps the lookup highlight on in the overlay first-install options (SubMiner captures media from mpv, not the overlay viewport), and removes the unsupported `userScripts` permission only in that staged copy; the fork keeps upstream browser defaults. Before loading the extension, its session clears service worker registrations so Electron uses the current bundled code; dictionary databases and settings remain intact. First-run setup uses Hachidori sharing messages to link or unlink external dictionary hosts and checks their live inventory. Linked dictionaries and dictionary edits use the host, while Anki configuration, pronunciation sources, custom buttons, and mining stay local to SubMiner. The parser bridge adapts its runtime messages to the existing subtitle scanner and dictionary automation. Scanning retains term-entry frequencies, and only tokens without ranks need further frequency lookups through the existing term-entry API. This requires a matching definition entry and does not preserve the frequency source's reading provenance. SubMiner consumes native `hachidori-popup-shown` and `hachidori-popup-hidden` attention events for mouse handling, keyboard focus, and the subtitle sidebar. Attention also covers a left press anywhere on the overlay that may start a selection, and the host element only exists after the first lookup, so once a Hachidori event has been seen popup auto-pause requires an unhidden popup pane in the host's shadow root and rechecks after each successful lookup. The fork retains host attributes, hover and successful-lookup notifications, and commands that need private reader state. The Anki proxy strips local duplicate/overwrite metadata before forwarding requests and enriches only confirmed writes.
- Small units, explicit boundaries - Small units, explicit boundaries
- Composition over monoliths - Composition over monoliths
+24 -10
View File
@@ -31,17 +31,31 @@ fs.rmSync(output, { recursive: true, force: true });
fs.mkdirSync(output, { recursive: true }); fs.mkdirSync(output, { recursive: true });
fs.cpSync(extension, output, { recursive: true }); fs.cpSync(extension, output, { recursive: true });
// Host configuration belongs in the staged copy, leaving the fork usable in Chrome. // Host configuration belongs in the staged copy, leaving the fork usable in Chrome.
const overlayPath = path.join(output, 'overlay-mode.js'); const hostConfiguration = {
let overlay = fs.readFileSync(overlayPath, 'utf8'); 'overlay-mode.js': [
for (const [original, replacement] of [ ['export const OVERLAY_MODE = false;', 'export const OVERLAY_MODE = true;'],
['export const OVERLAY_MODE = false;', 'export const OVERLAY_MODE = true;'], ['customJavaScript: !IS_FIREFOX,', 'customJavaScript: false,'],
['customJavaScript: !IS_FIREFOX,', 'customJavaScript: false,'], ],
]) { // Overlay hosts seed the lookup highlight off because their Anki screenshot is
if (!overlay.includes(original)) // the see-through viewport. SubMiner captures media from mpv, and without the
throw new Error(`Hachidori host configuration changed upstream: ${original}`); // highlight the sidebar shows nothing for the word being looked up.
overlay = overlay.replace(original, replacement); 'setup-state.js': [
[
'lookupMode: "hover",\n sourceHighlightEnabled: false,',
'lookupMode: "hover",\n sourceHighlightEnabled: true,',
],
],
};
for (const [file, replacements] of Object.entries(hostConfiguration)) {
const filePath = path.join(output, file);
let text = fs.readFileSync(filePath, 'utf8');
for (const [original, replacement] of replacements) {
if (!text.includes(original))
throw new Error(`Hachidori host configuration changed upstream: ${original}`);
text = text.replace(original, replacement);
}
fs.writeFileSync(filePath, text);
} }
fs.writeFileSync(overlayPath, overlay);
manifest.permissions = manifest.permissions.filter((permission) => permission !== 'userScripts'); manifest.permissions = manifest.permissions.filter((permission) => permission !== 'userScripts');
fs.writeFileSync(path.join(output, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n'); fs.writeFileSync(path.join(output, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
for (const file of ['LICENSE', 'SOURCE.json', 'README.md']) { for (const file of ['LICENSE', 'SOURCE.json', 'README.md']) {
+9 -1
View File
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
import test from 'node:test'; import test from 'node:test';
test('Hachidori staging configures Electron without changing the fork source', async () => { test('Hachidori staging configures Electron without changing the fork source', async () => {
const files = ['overlay-mode.js', 'manifest.json']; const files = ['overlay-mode.js', 'setup-state.js', 'manifest.json'];
const source = (file: string) => const source = (file: string) =>
new URL(`../vendor/hachidori/extension/${file}`, import.meta.url); new URL(`../vendor/hachidori/extension/${file}`, import.meta.url);
const before = files.map((file) => readFileSync(source(file), 'utf8')); const before = files.map((file) => readFileSync(source(file), 'utf8'));
@@ -24,6 +24,14 @@ test('Hachidori staging configures Electron without changing the fork source', a
const original = await import(source('overlay-mode.js').href); const original = await import(source('overlay-mode.js').href);
assert.equal(original.OVERLAY_MODE, false); assert.equal(original.OVERLAY_MODE, false);
assert.equal(original.HOST_CAPABILITIES.customJavaScript, true); assert.equal(original.HOST_CAPABILITIES.customJavaScript, true);
// SubMiner mines media from mpv, not the overlay viewport, so the lookup
// highlight stays on for a first install.
const stagedSetup = await import(
new URL('../build/hachidori/setup-state.js', import.meta.url).href
);
assert.equal(stagedSetup.OVERLAY_MODE_OPTIONS.sourceHighlightEnabled, true);
const originalSetup = await import(source('setup-state.js').href);
assert.equal(originalSetup.OVERLAY_MODE_OPTIONS.sourceHighlightEnabled, false);
const manifest = JSON.parse( const manifest = JSON.parse(
readFileSync(new URL('../build/hachidori/manifest.json', import.meta.url), 'utf8'), readFileSync(new URL('../build/hachidori/manifest.json', import.meta.url), 'utf8'),
); );