fix(dictionary): add configurable collapsible section defaults

This commit is contained in:
2026-03-06 23:46:24 -08:00
parent 78cd99a2d0
commit 755c1175b0
7 changed files with 116 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
---
id: TASK-99
title: Add configurable character dictionary collapsible section open states
status: Done
assignee: []
created_date: '2026-03-07 00:00'
updated_date: '2026-03-07 00:00'
labels:
- dictionary
- config
references:
- /home/sudacode/projects/japanese/SubMiner/src/main/character-dictionary-runtime.ts
- /home/sudacode/projects/japanese/SubMiner/src/config/resolve/integrations.ts
- /home/sudacode/projects/japanese/SubMiner/src/config/definitions/defaults-integrations.ts
priority: medium
dependencies: []
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Add per-section config for character dictionary collapsible glossary sections so Description, Character Information, and Voiced by can each default open or closed independently. Default all sections closed.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Config supports `anilist.characterDictionary.collapsibleSections.description`.
- [x] #2 Config supports `anilist.characterDictionary.collapsibleSections.characterInformation`.
- [x] #3 Config supports `anilist.characterDictionary.collapsibleSections.voicedBy`.
- [x] #4 Default config keeps all generated character dictionary collapsible sections closed.
- [x] #5 Regression coverage verifies config parsing/warnings and generated glossary `details.open` behavior.
<!-- AC:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Added per-section open-state config under `anilist.characterDictionary.collapsibleSections` for `description`, `characterInformation`, and `voicedBy`, all defaulting to `false`. Wired the glossary generator to read those settings so generated `details.open` matches config, and added regression coverage for defaults, parsing/warnings, registry exposure, and runtime glossary output.
<!-- SECTION:FINAL_SUMMARY:END -->

View File

@@ -92,6 +92,11 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
maxLoaded: 3,
evictionPolicy: 'delete',
profileScope: 'all',
collapsibleSections: {
description: false,
characterInformation: false,
voicedBy: false,
},
},
},
jellyfin: {

View File

@@ -171,6 +171,26 @@ export function buildIntegrationConfigOptionRegistry(
defaultValue: defaultConfig.anilist.characterDictionary.profileScope,
description: 'Yomitan profile scope for dictionary enable/disable updates.',
},
{
path: 'anilist.characterDictionary.collapsibleSections.description',
kind: 'boolean',
defaultValue: defaultConfig.anilist.characterDictionary.collapsibleSections.description,
description: 'Open the Description section by default in character dictionary glossary entries.',
},
{
path: 'anilist.characterDictionary.collapsibleSections.characterInformation',
kind: 'boolean',
defaultValue:
defaultConfig.anilist.characterDictionary.collapsibleSections.characterInformation,
description:
'Open the Character Information section by default in character dictionary glossary entries.',
},
{
path: 'anilist.characterDictionary.collapsibleSections.voicedBy',
kind: 'boolean',
defaultValue: defaultConfig.anilist.characterDictionary.collapsibleSections.voicedBy,
description: 'Open the Voiced by section by default in character dictionary glossary entries.',
},
{
path: 'jellyfin.enabled',
kind: 'boolean',

View File

@@ -124,6 +124,31 @@ export function applyIntegrationConfig(context: ResolveContext): void {
'Expected string.',
);
}
if (isObject(characterDictionary.collapsibleSections)) {
const collapsibleSections = characterDictionary.collapsibleSections;
const keys = ['description', 'characterInformation', 'voicedBy'] as const;
for (const key of keys) {
const value = asBoolean(collapsibleSections[key]);
if (value !== undefined) {
resolved.anilist.characterDictionary.collapsibleSections[key] = value;
} else if (collapsibleSections[key] !== undefined) {
warn(
`anilist.characterDictionary.collapsibleSections.${key}`,
collapsibleSections[key],
resolved.anilist.characterDictionary.collapsibleSections[key],
'Expected boolean.',
);
}
}
} else if (characterDictionary.collapsibleSections !== undefined) {
warn(
'anilist.characterDictionary.collapsibleSections',
characterDictionary.collapsibleSections,
resolved.anilist.characterDictionary.collapsibleSections,
'Expected object.',
);
}
} else if (src.anilist.characterDictionary !== undefined) {
warn(
'anilist.characterDictionary',

View File

@@ -72,6 +72,11 @@ test('anilist character dictionary fields are parsed, clamped, and enum-validate
maxLoaded: 99,
evictionPolicy: 'purge' as never,
profileScope: 'global' as never,
collapsibleSections: {
description: true,
characterInformation: 'invalid' as never,
voicedBy: true,
} as never,
},
},
});
@@ -83,10 +88,19 @@ test('anilist character dictionary fields are parsed, clamped, and enum-validate
assert.equal(context.resolved.anilist.characterDictionary.maxLoaded, 20);
assert.equal(context.resolved.anilist.characterDictionary.evictionPolicy, 'delete');
assert.equal(context.resolved.anilist.characterDictionary.profileScope, 'all');
assert.equal(context.resolved.anilist.characterDictionary.collapsibleSections.description, true);
assert.equal(
context.resolved.anilist.characterDictionary.collapsibleSections.characterInformation,
false,
);
assert.equal(context.resolved.anilist.characterDictionary.collapsibleSections.voicedBy, true);
const warnedPaths = warnings.map((warning) => warning.path);
assert.ok(warnedPaths.includes('anilist.characterDictionary.refreshTtlHours'));
assert.ok(warnedPaths.includes('anilist.characterDictionary.maxLoaded'));
assert.ok(warnedPaths.includes('anilist.characterDictionary.evictionPolicy'));
assert.ok(warnedPaths.includes('anilist.characterDictionary.profileScope'));
assert.ok(
warnedPaths.includes('anilist.characterDictionary.collapsibleSections.characterInformation'),
);
});

View File

@@ -1147,6 +1147,8 @@ const characterDictionaryRuntime = createCharacterDictionaryRuntimeService({
getCurrentMediaTitle: () => appState.currentMediaTitle,
resolveMediaPathForJimaku: (mediaPath) => mediaRuntime.resolveMediaPathForJimaku(mediaPath),
guessAnilistMediaInfo: (mediaPath, mediaTitle) => guessAnilistMediaInfo(mediaPath, mediaTitle),
getCollapsibleSectionOpenState: (section) =>
getResolvedConfig().anilist.characterDictionary.collapsibleSections[section],
now: () => Date.now(),
logInfo: (message) => logger.info(message),
logWarn: (message) => logger.warn(message),

View File

@@ -398,6 +398,16 @@ export interface JimakuConfig {
export type AnilistCharacterDictionaryEvictionPolicy = 'disable' | 'delete';
export type AnilistCharacterDictionaryProfileScope = 'all' | 'active';
export type AnilistCharacterDictionaryCollapsibleSectionKey =
| 'description'
| 'characterInformation'
| 'voicedBy';
export interface AnilistCharacterDictionaryCollapsibleSectionsConfig {
description?: boolean;
characterInformation?: boolean;
voicedBy?: boolean;
}
export interface AnilistCharacterDictionaryConfig {
enabled?: boolean;
@@ -405,6 +415,7 @@ export interface AnilistCharacterDictionaryConfig {
maxLoaded?: number;
evictionPolicy?: AnilistCharacterDictionaryEvictionPolicy;
profileScope?: AnilistCharacterDictionaryProfileScope;
collapsibleSections?: AnilistCharacterDictionaryCollapsibleSectionsConfig;
}
export interface AnilistConfig {
@@ -604,6 +615,7 @@ export interface ResolvedConfig {
maxLoaded: number;
evictionPolicy: AnilistCharacterDictionaryEvictionPolicy;
profileScope: AnilistCharacterDictionaryProfileScope;
collapsibleSections: Required<AnilistCharacterDictionaryCollapsibleSectionsConfig>;
};
};
jellyfin: {