diff --git a/changes/hachidori-backend.md b/changes/hachidori-backend.md index 81f431f7..f9c8d17f 100644 --- a/changes/hachidori-backend.md +++ b/changes/hachidori-backend.md @@ -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. diff --git a/docs-site/usage.md b/docs-site/usage.md index fb6f1b3e..1c5c379a 100644 --- a/docs-site/usage.md +++ b/docs-site/usage.md @@ -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. diff --git a/scripts/check-dictionary-backends.cjs b/scripts/check-dictionary-backends.cjs index 65465d9b..fc189b7f 100644 --- a/scripts/check-dictionary-backends.cjs +++ b/scripts/check-dictionary-backends.cjs @@ -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,16 +78,32 @@ function finish(exitCode) { 'PASS actual --hachidori startup, visible settings, isolated backend session, native engine ready', ); app.emit('second-instance', {}, [process.execPath, root, '--yomitan'], root); - let yomi; - for (let i = 0; i < 200; i++) { - yomi = BrowserWindow.getAllWindows().find( - (w) => w.getTitle().includes('Yomitan') && w.isVisible(), + 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', ); - if (yomi) break; - await new Promise((r) => setTimeout(r, 100)); + 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( + (w) => w.getTitle().includes('Yomitan') && w.isVisible(), + ); + if (yomi) break; + await new Promise((r) => setTimeout(r, 100)); + } + assert.ok(yomi, 'inactive --yomitan settings opens'); + assert.equal(yomi.webContents.session, session.defaultSession); } - 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; diff --git a/src/main.ts b/src/main.ts index e6704ec9..cab89449 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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; }