fix(dictionary): block --yomitan when an external profile is configured

- Keep read-only external-profile mode while Hachidori is active by checking the configured externalProfilePath
- Hachidori settings remain available
- Smoke check asserts the external-profile block, with a --bundled-yomitan path for the bundled case
- Update usage docs and changelog fragment
This commit is contained in:
2026-09-22 19:03:39 -07:00
parent cacaa2b371
commit f225ac3c89
4 changed files with 31 additions and 13 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ type: added
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` continues to open Yomitan settings. With Hachidori active, an inactive external Yomitan profile does not block the bundled Yomitan settings.
- 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 saves downloadable word audio before sending a note through SubMiner's Anki proxy, so freshly mined animated cards include the word-audio delay.
+1 -1
View File
@@ -333,7 +333,7 @@ For automatic character dictionary sync with a Docker host, set `hachidori.exter
Existing controls such as `startupWarmups.yomitanExtension` and `subtitleStyle.autoPauseVideoOnYomitanPopup` apply to the selected backend. Hachidori has one dictionary configuration, so character-dictionary profile scope applies to that configuration.
First-run setup remembers each backend that finished it, including when setup is reopened for legacy plugin cleanup. Switching to a backend for the first time asks for that backend's dictionaries; switching back to one that already finished does not repeat setup. Until SubMiner restarts, it keeps running the backend it started with, and the launcher gates playback on that running backend and logs a restart reminder. A running Yomitan session continues using its external profile until the restart. While Hachidori is active, `--yomitan` opens the bundled Yomitan settings even if an external Yomitan profile is configured.
First-run setup remembers each backend that finished it, including when setup is reopened for legacy plugin cleanup. Switching to a backend for the first time asks for that backend's dictionaries; switching back to one that already finished does not repeat setup. Until SubMiner restarts, it keeps running the backend it started with, and the launcher gates playback on that running backend and logs a restart reminder. A running Yomitan session continues using its external profile until the restart. When `yomitan.externalProfilePath` is configured, `--yomitan` is disabled to preserve read-only external-profile mode, including while Hachidori is active. Hachidori settings remain available.
Hachidori's own duplicate handling differs from Yomitan's. Choosing **Overwrite** in the Hachidori popup updates the existing note and SubMiner enriches its media, while **Add anyway** creates a new note and runs SubMiner's Kiku/Senren [field grouping](./anki-integration.md#field-grouping-kiku-senren). Mining from the stats dashboard uses the selected backend as well.
+20 -2
View File
@@ -7,6 +7,8 @@ if (process.platform !== 'linux')
throw new Error('This app-entry smoke requires Linux XDG isolation.');
const root = process.cwd();
const backend = process.argv.includes('--backend=yomitan') ? 'yomitan' : 'hachidori';
const externalYomitanProfile =
backend === 'hachidori' && !process.argv.includes('--bundled-yomitan');
const profile = process.env.SUBMINER_DICTIONARY_SMOKE_DATA;
assert(
profile && fs.existsSync(profile),
@@ -19,9 +21,9 @@ fs.writeFileSync(
path.join(profile, 'SubMiner', 'config.json'),
JSON.stringify({
dictionaryBackend: backend,
// An inactive external Yomitan profile must not block bundled settings in Hachidori mode.
// External Yomitan profiles must not block Hachidori settings.
yomitan: {
externalProfilePath: backend === 'hachidori' ? path.join(profile, 'external-yomitan') : '',
externalProfilePath: externalYomitanProfile ? path.join(profile, 'external-yomitan') : '',
},
mpv: { socketPath: path.join(profile, 'missing-mpv.sock') },
ankiConnect: { enabled: false },
@@ -76,6 +78,21 @@ function finish(exitCode) {
'PASS actual --hachidori startup, visible settings, isolated backend session, native engine ready',
);
app.emit('second-instance', {}, [process.execPath, root, '--yomitan'], root);
if (externalYomitanProfile) {
const blockedMessage =
'Yomitan settings unavailable while using read-only external-profile mode.';
for (let i = 0; i < 100; i++) {
if (fs.readFileSync(process.env.SUBMINER_APP_LOG, 'utf8').includes(blockedMessage)) break;
await new Promise((r) => setTimeout(r, 100));
}
assert.ok(
fs.readFileSync(process.env.SUBMINER_APP_LOG, 'utf8').includes(blockedMessage),
'--yomitan is rejected when an external Yomitan profile is configured',
);
assert.equal(session.defaultSession.extensions.getAllExtensions().length, 0);
assert.ok(!BrowserWindow.getAllWindows().some((w) => w.getTitle().includes('Yomitan')));
console.log('PASS external Yomitan settings blocked while Hachidori settings remain available');
} else {
let yomi;
for (let i = 0; i < 200; i++) {
yomi = BrowserWindow.getAllWindows().find(
@@ -86,6 +103,7 @@ function finish(exitCode) {
}
assert.ok(yomi, 'inactive --yomitan settings opens');
assert.equal(yomi.webContents.session, session.defaultSession);
}
assert.equal(window.webContents.session, session.fromPartition('persist:hachidori'));
app.emit('second-instance', {}, [process.execPath, root, '--toggle-visible-overlay'], root);
let overlay;
+1 -1
View File
@@ -5226,7 +5226,7 @@ function initializeOverlayRuntime(): void {
function openYomitanSettings(): boolean {
if (activeDictionaryBackend === 'hachidori') {
if (yomitanProfilePolicy.isExternalReadOnlyMode()) {
if (configService.getConfig().yomitan.externalProfilePath.trim()) {
logger.warn('Yomitan settings unavailable while using read-only external-profile mode.');
return false;
}