Disable character-dictionary features in external profile mode

- Gate character-dictionary runtime, auto-sync, annotations, and CLI generation when `yomitan.externalProfilePath` is set
- Return explicit disabled reason for blocked character-dictionary generation in read-only external-profile mode
- Fix default config bootstrap to seed `config.jsonc` when config dir exists but config file is missing
- Update tests, changelog fragment, and docs to reflect the new behavior
This commit is contained in:
2026-03-11 21:02:00 -07:00
parent 8c2529b083
commit 41986885fb
6 changed files with 39 additions and 16 deletions

View File

@@ -375,6 +375,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';
@@ -1329,7 +1333,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,
};
@@ -2757,6 +2763,9 @@ const {
);
},
scheduleCharacterDictionarySync: () => {
if (!isCharacterDictionaryEnabledForCurrentProcess()) {
return;
}
characterDictionaryAutoSyncRuntime.scheduleSync();
},
updateCurrentMediaTitle: (title) => {
@@ -2834,7 +2843,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(
@@ -3036,6 +3047,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`,
@@ -3547,8 +3566,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(),

View File

@@ -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');
});
});

View File

@@ -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;
}