fix(dictionary): sync Hachidori deck to ankiConnect.deck for polling

- Always set the first Hachidori Anki template deck to ankiConnect.deck so polling mode enriches Hachidori cards; other custom template settings stay intact
- Treat right-clicks retargeted to the Hachidori popup shadow host as overlay interactions so they no longer raise mpv or toggle pause
- Update anki-integration docs and changelog fragment
This commit is contained in:
2026-09-22 21:43:53 -07:00
parent 2ba820f41a
commit 9a5fb02cbd
6 changed files with 45 additions and 11 deletions
@@ -43,7 +43,7 @@ async function harness(
if (type !== 'hd_options_write' || target !== 'hoshidicts-worker') throw Error('Unexpected request');
if (race) {
race = false;
options.anki.templates[0].deck = options.anki.deck = 'User edit';
options.anki.templates[0].tags = options.anki.tags = ['User edit'];
options.revision++;
}
if (request.baseRevision !== options.revision) throw Error('conflict');
@@ -101,7 +101,9 @@ test('fresh Hachidori settings inherit deck, tags, a unique model and configured
assert.equal(await h.run('writes'), 1);
});
test('preserves custom templates, tags, intentional blank fields and additional templates', async () => {
// SubMiner's new-card polling only watches ankiConnect.deck, so the first
// template's deck follows it the way Yomitan's term card deck does.
test('moves the first template to the SubMiner deck and preserves the rest of custom templates', async () => {
const h = await harness({
templates: [
{
@@ -118,9 +120,13 @@ test('preserves custom templates, tags, intentional blank fields and additional
{ id: 'second', name: 'Second', deck: 'Other', model: 'Other', tags: [] },
],
});
const before = await h.run('options.anki.templates');
const before = (await h.run('options.anki.templates')) as Array<Record<string, unknown>>;
await h.sync();
assert.deepEqual(await h.run('options.anki.templates'), before);
assert.deepEqual(await h.run('options.anki.templates'), [
{ ...before[0], deck: 'Mining' },
...before.slice(1),
]);
assert.equal(await h.run('options.anki.deck'), 'Mining');
});
test('leaves an ambiguous model unset and retries discovery after Anki reconnects', async () => {
@@ -154,7 +160,7 @@ test('re-reads concurrent settings edits before retrying its revisioned write',
const h = await harness();
await h.run('race = true');
await h.sync();
assert.equal(await h.run('options.anki.deck'), 'User edit');
assert.deepEqual(await h.run('options.anki.tags'), ['User edit']);
assert.equal(await h.run('options.anki.model'), 'Japanese');
});
@@ -46,9 +46,9 @@ export const HACHIDORI_ANKI_SETTINGS_SCRIPT = String.raw`
const first = anki.templates[0];
if (!first) return { updated: false, matched: false, reason: 'no-templates' };
let template = { ...first, fields: { ...first.fields } };
const pristine = !first.model && first.fieldTemplates === null
&& Object.values(first.fields).every(value => !value);
if (deck && (!first.deck || (pristine && first.deck === 'Default'))) template.deck = deck;
// SubMiner's new-card polling only watches its configured deck, so the
// first template follows it like Yomitan's term card deck.
if (deck) template.deck = deck;
if (hints?.tags && JSON.stringify(first.tags) === JSON.stringify(['hachidori'])) {
template.tags = [...hints.tags];
}