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:
@@ -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