From 755c1175b098f43aaafac98b59f78efb407f8204 Mon Sep 17 00:00:00 2001 From: sudacode Date: Fri, 6 Mar 2026 23:46:24 -0800 Subject: [PATCH] fix(dictionary): add configurable collapsible section defaults --- ...tionary-collapsible-section-open-states.md | 38 +++++++++++++++++++ .../definitions/defaults-integrations.ts | 5 +++ .../definitions/options-integrations.ts | 20 ++++++++++ src/config/resolve/integrations.ts | 25 ++++++++++++ src/config/resolve/jellyfin.test.ts | 14 +++++++ src/main.ts | 2 + src/types.ts | 12 ++++++ 7 files changed, 116 insertions(+) create mode 100644 backlog/tasks/task-99 - Add-configurable-character-dictionary-collapsible-section-open-states.md diff --git a/backlog/tasks/task-99 - Add-configurable-character-dictionary-collapsible-section-open-states.md b/backlog/tasks/task-99 - Add-configurable-character-dictionary-collapsible-section-open-states.md new file mode 100644 index 0000000..27d0499 --- /dev/null +++ b/backlog/tasks/task-99 - Add-configurable-character-dictionary-collapsible-section-open-states.md @@ -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 + + +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. + + +## Acceptance Criteria + +- [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. + + +## Final Summary + + +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. + diff --git a/src/config/definitions/defaults-integrations.ts b/src/config/definitions/defaults-integrations.ts index 90fa5f6..5765eb2 100644 --- a/src/config/definitions/defaults-integrations.ts +++ b/src/config/definitions/defaults-integrations.ts @@ -92,6 +92,11 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick< maxLoaded: 3, evictionPolicy: 'delete', profileScope: 'all', + collapsibleSections: { + description: false, + characterInformation: false, + voicedBy: false, + }, }, }, jellyfin: { diff --git a/src/config/definitions/options-integrations.ts b/src/config/definitions/options-integrations.ts index a4cfbc6..709d05c 100644 --- a/src/config/definitions/options-integrations.ts +++ b/src/config/definitions/options-integrations.ts @@ -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', diff --git a/src/config/resolve/integrations.ts b/src/config/resolve/integrations.ts index bdff7a0..9a000fe 100644 --- a/src/config/resolve/integrations.ts +++ b/src/config/resolve/integrations.ts @@ -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', diff --git a/src/config/resolve/jellyfin.test.ts b/src/config/resolve/jellyfin.test.ts index 3f68d4d..5f43aed 100644 --- a/src/config/resolve/jellyfin.test.ts +++ b/src/config/resolve/jellyfin.test.ts @@ -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'), + ); }); diff --git a/src/main.ts b/src/main.ts index 502444f..00e5c18 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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), diff --git a/src/types.ts b/src/types.ts index f1a94c3..2231357 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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; }; }; jellyfin: {