mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-23 05:16:23 -07:00
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:
@@ -4,7 +4,7 @@ area: dictionary
|
||||
- Added a bundled Hachidori dictionary backend alongside the default Yomitan backend. Select it with `dictionaryBackend` and restart SubMiner.
|
||||
- The tray and dictionary-settings shortcut follow the selected backend. `--hachidori` opens Hachidori settings, while `--yomitan` opens Yomitan settings unless a read-only external Yomitan profile is configured. This restriction also applies with Hachidori active, without blocking Hachidori settings.
|
||||
- Hachidori integrates with subtitle scanning, popup controls, lookup tracking, character dictionaries, and Anki media enrichment, with separate dictionaries and settings for each backend. Linked Docker hosts receive character dictionary uploads through `hachidori.externalHostManagementUrl`, retry busy imports, and replace the previous dictionary only after a successful import.
|
||||
- Hachidori auto-populates its first Anki template from SubMiner's deck, tags, and field mappings, detects an unambiguous matching note type, and preserves existing custom templates. Anki discovery retries after an unavailable connection.
|
||||
- Hachidori auto-populates its first Anki template from SubMiner's deck, tags, and field mappings, detects an unambiguous matching note type, and preserves existing custom templates apart from the deck, which follows `ankiConnect.deck` so polling mode enriches Hachidori cards. Anki discovery retries after an unavailable connection.
|
||||
- Hachidori saves downloadable word audio before sending a note through SubMiner's Anki proxy, so freshly mined animated cards include the word-audio delay.
|
||||
- Both bundled dictionary backends reload their background code on startup so extension updates take effect while preserving installed dictionaries and settings. Failed Yomitan connection-setting updates can be retried without manually resetting the managed Anki endpoint.
|
||||
- First-run setup remembers each backend that finished it, including when reopened for legacy plugin cleanup, so switching back does not repeat setup, and the launcher gates playback on the backend the running app started with. Current incomplete or cancelled setup takes precedence over stale completion history. A running Yomitan session keeps using its external profile until it restarts into Hachidori.
|
||||
|
||||
@@ -23,11 +23,11 @@ AnkiConnect and Kiku/Senren settings follow the [configuration validation rules]
|
||||
|
||||
### Reusing SubMiner settings in Hachidori
|
||||
|
||||
When Hachidori is the selected backend, SubMiner uses its Anki settings to populate Hachidori's first Anki template on startup and when opening its settings. It copies the configured deck and tags into untouched defaults, then fills missing word, sentence, pronunciation-audio, and picture mappings with fields that exist in Anki. Pronunciation uses `ankiConnect.fields.wordAudio`, falling back to `fields.audio` when no word-audio field is set.
|
||||
When Hachidori is the selected backend, SubMiner uses its Anki settings to populate Hachidori's first Anki template on startup and when opening its settings. It sets the template's deck to `ankiConnect.deck` when one is configured, copies the configured tags into untouched defaults, then fills missing word, sentence, pronunciation-audio, and picture mappings with fields that exist in Anki. Pronunciation uses `ankiConnect.fields.wordAudio`, falling back to `fields.audio` when no word-audio field is set.
|
||||
|
||||
If the note type is unset, SubMiner looks for a unique match containing its configured word and sentence fields. Enabled Lapis, Kiku, or Senren integration narrows the search; Lapis uses its configured model name. A fresh mapping also receives Hachidori's matching preset for readings, definitions, and other recognized fields. If several note types match, choose one in Hachidori Settings. If Anki is closed, open Hachidori Settings again after starting Anki to retry.
|
||||
|
||||
Existing custom decks, tags, field mappings, advanced templates, and additional templates stay intact. This fills missing settings rather than continually overwriting Hachidori choices. The Anki endpoint continues to follow SubMiner's proxy configuration. Sentence audio, image timing, translation, metadata, and duplicate field grouping remain controlled by SubMiner; pronunciation sources are configured in Hachidori. Linking an external dictionary host does not change this behavior.
|
||||
The deck always follows `ankiConnect.deck`, as it does for Yomitan's mining deck, because polling mode only looks for new cards in that deck. Existing custom tags, field mappings, advanced templates, and additional templates stay intact. Apart from the deck, this fills missing settings rather than continually overwriting Hachidori choices. The Anki endpoint continues to follow SubMiner's proxy configuration. Sentence audio, image timing, translation, metadata, and duplicate field grouping remain controlled by SubMiner; pronunciation sources are configured in Hachidori. Linking an external dictionary host does not change this behavior.
|
||||
|
||||
## Auto-enrichment transport
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import test from 'node:test';
|
||||
|
||||
import { createKeyboardHandlers } from './keyboard.js';
|
||||
import { createRendererState } from '../state.js';
|
||||
import { YOMITAN_POPUP_HOST_SELECTOR } from '../yomitan-popup.js';
|
||||
import type { CompiledSessionBinding } from '../../types';
|
||||
import type { MpvInputBindingsSnapshot } from '../../types/session-bindings';
|
||||
import { DEFAULT_KEYBINDINGS, SPECIAL_COMMANDS } from '../../config/definitions';
|
||||
@@ -442,6 +443,13 @@ function installKeyboardTestGlobals() {
|
||||
target.closest = (selector: string) => (selector.includes('.modal') ? target : null);
|
||||
return target;
|
||||
},
|
||||
// Events inside Hachidori's shadow root reach the document retargeted to its host.
|
||||
createDictionaryPopupHostTarget: () => {
|
||||
const target = new TestElement();
|
||||
target.closest = (selector: string) =>
|
||||
selector === YOMITAN_POPUP_HOST_SELECTOR ? target : null;
|
||||
return target;
|
||||
},
|
||||
setGetSessionBindings: (value: () => Promise<CompiledSessionBinding[]>) => {
|
||||
getSessionBindingsImpl = value;
|
||||
},
|
||||
@@ -683,6 +691,23 @@ test('right-clicking interactive overlay controls does not raise playback window
|
||||
}
|
||||
});
|
||||
|
||||
test('right-clicking inside a dictionary popup host does not raise playback window or toggle pause', async () => {
|
||||
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||
const popupHost = testGlobals.createDictionaryPopupHostTarget();
|
||||
|
||||
try {
|
||||
await handlers.setupMpvInputForwarding();
|
||||
|
||||
testGlobals.dispatchDocumentMouseDown({ button: 2, target: popupHost });
|
||||
await wait(0);
|
||||
|
||||
assert.deepEqual(testGlobals.interactionActivations, []);
|
||||
assert.deepEqual(testGlobals.mpvCommands, []);
|
||||
} finally {
|
||||
testGlobals.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('mpv input forwarding retries a transient keyboard config IPC failure', async () => {
|
||||
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||
let calls = 0;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createMpvInputForwarding } from './mpv-input-forwarding';
|
||||
import {
|
||||
registerDictionaryPopupVisibilityListener,
|
||||
YOMITAN_POPUP_COMMAND_EVENT,
|
||||
YOMITAN_POPUP_HOST_SELECTOR,
|
||||
isYomitanPopupVisible,
|
||||
isYomitanPopupIframe,
|
||||
} from '../yomitan-popup.js';
|
||||
@@ -82,6 +83,8 @@ export function createKeyboardHandlers(
|
||||
if (target.closest('.modal')) return true;
|
||||
if (ctx.dom.subtitleContainer.contains(target)) return true;
|
||||
if (isYomitanPopupIframe(target)) return true;
|
||||
// Hachidori's popup lives in a shadow root, so its events arrive retargeted to the host.
|
||||
if (target.closest(YOMITAN_POPUP_HOST_SELECTOR)) return true;
|
||||
if (target.closest && target.closest('iframe.yomitan-popup, iframe[id^="yomitan-popup"]'))
|
||||
return true;
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user