diff --git a/changes/external-yomitan-profile.md b/changes/external-yomitan-profile.md index c39d1db..d845814 100644 --- a/changes/external-yomitan-profile.md +++ b/changes/external-yomitan-profile.md @@ -1,5 +1,6 @@ type: added -area: yomitan +area: config - Added `yomitan.externalProfilePath` to reuse another Electron app's Yomitan profile in read-only mode. - SubMiner now reuses external Yomitan dictionaries/settings without writing back to that profile. +- Fixed default config bootstrap so `config.jsonc` is seeded even when the config directory already exists. diff --git a/docs-site/character-dictionary.md b/docs-site/character-dictionary.md index 51dfdf3..789913d 100644 --- a/docs-site/character-dictionary.md +++ b/docs-site/character-dictionary.md @@ -63,7 +63,7 @@ The first sync for a media title takes a few seconds while character data and po ::: ::: warning -If `yomitan.externalProfilePath` is set, SubMiner switches to read-only external-profile mode. In that mode SubMiner can reuse another app's installed Yomitan dictionaries/settings, but character-dictionary auto-sync does not import or update the merged dictionary. +If `yomitan.externalProfilePath` is set, SubMiner switches to read-only external-profile mode. In that mode SubMiner can reuse another app's installed Yomitan dictionaries/settings, but SubMiner's own character-dictionary features are fully disabled. ::: ## Name Generation diff --git a/docs-site/configuration.md b/docs-site/configuration.md index 2667859..00a4dd0 100644 --- a/docs-site/configuration.md +++ b/docs-site/configuration.md @@ -959,7 +959,7 @@ External-profile mode behavior: - SubMiner reads the external profile's currently active Yomitan profile selection and installed dictionaries. - SubMiner does not open its own Yomitan settings window in this mode. - SubMiner does not import, delete, or update dictionaries/settings in the external profile. -- SubMiner character-dictionary auto-sync is effectively disabled in this mode because it requires Yomitan writes. +- SubMiner character-dictionary features are fully disabled in this mode, including auto-sync, manual generation, and subtitle-side character-dictionary annotations. ### Jellyfin diff --git a/src/main.ts b/src/main.ts index 86ca7de..53024f2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -374,6 +374,10 @@ import { createOverlayVisibilityRuntimeService } from './main/overlay-visibility import { createCharacterDictionaryRuntimeService } from './main/character-dictionary-runtime'; import { createCharacterDictionaryAutoSyncRuntimeService } from './main/runtime/character-dictionary-auto-sync'; import { notifyCharacterDictionaryAutoSyncStatus } from './main/runtime/character-dictionary-auto-sync-notifications'; +import { + getCharacterDictionaryDisabledReason, + isCharacterDictionaryRuntimeEnabled, +} from './main/runtime/character-dictionary-availability'; import { createCurrentMediaTokenizationGate } from './main/runtime/current-media-tokenization-gate'; import { createStartupOsdSequencer } from './main/runtime/startup-osd-sequencer'; import { formatSkippedYomitanWriteAction } from './main/runtime/yomitan-read-only-log'; @@ -1328,7 +1332,9 @@ const characterDictionaryAutoSyncRuntime = createCharacterDictionaryAutoSyncRunt getConfig: () => { const config = getResolvedConfig().anilist.characterDictionary; return { - enabled: config.enabled, + enabled: + config.enabled && + isCharacterDictionaryRuntimeEnabled(getConfiguredExternalYomitanProfilePath()), maxLoaded: config.maxLoaded, profileScope: config.profileScope, }; @@ -2756,6 +2762,9 @@ const { ); }, scheduleCharacterDictionarySync: () => { + if (!isCharacterDictionaryEnabledForCurrentProcess()) { + return; + } characterDictionaryAutoSyncRuntime.scheduleSync(); }, updateCurrentMediaTitle: (title) => { @@ -2833,7 +2842,9 @@ const { 'subtitle.annotation.jlpt', getResolvedConfig().subtitleStyle.enableJlpt, ), - getCharacterDictionaryEnabled: () => getResolvedConfig().anilist.characterDictionary.enabled, + getCharacterDictionaryEnabled: () => + getResolvedConfig().anilist.characterDictionary.enabled && + isCharacterDictionaryEnabledForCurrentProcess(), getNameMatchEnabled: () => getResolvedConfig().subtitleStyle.nameMatchEnabled, getFrequencyDictionaryEnabled: () => getRuntimeBooleanOption( @@ -3035,6 +3046,14 @@ function isYomitanExternalReadOnlyMode(): boolean { return getConfiguredExternalYomitanProfilePath().length > 0; } +function isCharacterDictionaryEnabledForCurrentProcess(): boolean { + return isCharacterDictionaryRuntimeEnabled(getConfiguredExternalYomitanProfilePath()); +} + +function getCharacterDictionaryDisabledReasonForCurrentProcess(): string | null { + return getCharacterDictionaryDisabledReason(getConfiguredExternalYomitanProfilePath()); +} + function logSkippedYomitanWrite(action: string): void { logger.info( `[yomitan] skipping ${action}: yomitan.externalProfilePath is configured; external profile mode is read-only`, @@ -3537,8 +3556,13 @@ const createCliCommandContextHandler = createCliCommandContextFactory({ openJellyfinSetupWindow: () => openJellyfinSetupWindow(), getAnilistQueueStatus: () => anilistStateRuntime.getQueueStatusSnapshot(), processNextAnilistRetryUpdate: () => processNextAnilistRetryUpdate(), - generateCharacterDictionary: (targetPath?: string) => - characterDictionaryRuntime.generateForCurrentMedia(targetPath), + generateCharacterDictionary: async (targetPath?: string) => { + const disabledReason = getCharacterDictionaryDisabledReasonForCurrentProcess(); + if (disabledReason) { + throw new Error(disabledReason); + } + return await characterDictionaryRuntime.generateForCurrentMedia(targetPath); + }, runJellyfinCommand: (argsFromCommand: CliArgs) => runJellyfinCommand(argsFromCommand), openYomitanSettings: () => openYomitanSettings(), cycleSecondarySubMode: () => handleCycleSecondarySubMode(), diff --git a/src/shared/setup-state.test.ts b/src/shared/setup-state.test.ts index 6ef45a5..11ce420 100644 --- a/src/shared/setup-state.test.ts +++ b/src/shared/setup-state.test.ts @@ -65,7 +65,7 @@ test('ensureDefaultConfigBootstrap creates config dir and default jsonc only whe }); }); -test('ensureDefaultConfigBootstrap does not seed default config into an existing config directory', () => { +test('ensureDefaultConfigBootstrap seeds default config into an existing config directory when missing', () => { withTempDir((root) => { const configDir = path.join(root, 'SubMiner'); fs.mkdirSync(configDir, { recursive: true }); @@ -74,10 +74,13 @@ test('ensureDefaultConfigBootstrap does not seed default config into an existing ensureDefaultConfigBootstrap({ configDir, configFilePaths: getDefaultConfigFilePaths(configDir), - generateTemplate: () => 'should-not-write', + generateTemplate: () => '{\n "logging": {}\n}\n', }); - assert.equal(fs.existsSync(path.join(configDir, 'config.jsonc')), false); + assert.equal( + fs.readFileSync(path.join(configDir, 'config.jsonc'), 'utf8'), + '{\n "logging": {}\n}\n', + ); assert.equal(fs.readFileSync(path.join(configDir, 'existing-user-file.txt'), 'utf8'), 'keep\n'); }); }); diff --git a/src/shared/setup-state.ts b/src/shared/setup-state.ts index c82cc99..440a2af 100644 --- a/src/shared/setup-state.ts +++ b/src/shared/setup-state.ts @@ -208,13 +208,8 @@ export function ensureDefaultConfigBootstrap(options: { const existsSync = options.existsSync ?? fs.existsSync; const mkdirSync = options.mkdirSync ?? fs.mkdirSync; const writeFileSync = options.writeFileSync ?? fs.writeFileSync; - const configDirExists = existsSync(options.configDir); - if ( - existsSync(options.configFilePaths.jsoncPath) || - existsSync(options.configFilePaths.jsonPath) || - configDirExists - ) { + if (existsSync(options.configFilePaths.jsoncPath) || existsSync(options.configFilePaths.jsonPath)) { return; }