mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-07 03:22:17 -08:00
fix(dictionary): add configurable collapsible section defaults
This commit is contained in:
@@ -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 -->
|
||||||
@@ -92,6 +92,11 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
|||||||
maxLoaded: 3,
|
maxLoaded: 3,
|
||||||
evictionPolicy: 'delete',
|
evictionPolicy: 'delete',
|
||||||
profileScope: 'all',
|
profileScope: 'all',
|
||||||
|
collapsibleSections: {
|
||||||
|
description: false,
|
||||||
|
characterInformation: false,
|
||||||
|
voicedBy: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
|
|||||||
@@ -171,6 +171,26 @@ export function buildIntegrationConfigOptionRegistry(
|
|||||||
defaultValue: defaultConfig.anilist.characterDictionary.profileScope,
|
defaultValue: defaultConfig.anilist.characterDictionary.profileScope,
|
||||||
description: 'Yomitan profile scope for dictionary enable/disable updates.',
|
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',
|
path: 'jellyfin.enabled',
|
||||||
kind: 'boolean',
|
kind: 'boolean',
|
||||||
|
|||||||
@@ -124,6 +124,31 @@ export function applyIntegrationConfig(context: ResolveContext): void {
|
|||||||
'Expected string.',
|
'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) {
|
} else if (src.anilist.characterDictionary !== undefined) {
|
||||||
warn(
|
warn(
|
||||||
'anilist.characterDictionary',
|
'anilist.characterDictionary',
|
||||||
|
|||||||
@@ -72,6 +72,11 @@ test('anilist character dictionary fields are parsed, clamped, and enum-validate
|
|||||||
maxLoaded: 99,
|
maxLoaded: 99,
|
||||||
evictionPolicy: 'purge' as never,
|
evictionPolicy: 'purge' as never,
|
||||||
profileScope: 'global' 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.maxLoaded, 20);
|
||||||
assert.equal(context.resolved.anilist.characterDictionary.evictionPolicy, 'delete');
|
assert.equal(context.resolved.anilist.characterDictionary.evictionPolicy, 'delete');
|
||||||
assert.equal(context.resolved.anilist.characterDictionary.profileScope, 'all');
|
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);
|
const warnedPaths = warnings.map((warning) => warning.path);
|
||||||
assert.ok(warnedPaths.includes('anilist.characterDictionary.refreshTtlHours'));
|
assert.ok(warnedPaths.includes('anilist.characterDictionary.refreshTtlHours'));
|
||||||
assert.ok(warnedPaths.includes('anilist.characterDictionary.maxLoaded'));
|
assert.ok(warnedPaths.includes('anilist.characterDictionary.maxLoaded'));
|
||||||
assert.ok(warnedPaths.includes('anilist.characterDictionary.evictionPolicy'));
|
assert.ok(warnedPaths.includes('anilist.characterDictionary.evictionPolicy'));
|
||||||
assert.ok(warnedPaths.includes('anilist.characterDictionary.profileScope'));
|
assert.ok(warnedPaths.includes('anilist.characterDictionary.profileScope'));
|
||||||
|
assert.ok(
|
||||||
|
warnedPaths.includes('anilist.characterDictionary.collapsibleSections.characterInformation'),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1147,6 +1147,8 @@ const characterDictionaryRuntime = createCharacterDictionaryRuntimeService({
|
|||||||
getCurrentMediaTitle: () => appState.currentMediaTitle,
|
getCurrentMediaTitle: () => appState.currentMediaTitle,
|
||||||
resolveMediaPathForJimaku: (mediaPath) => mediaRuntime.resolveMediaPathForJimaku(mediaPath),
|
resolveMediaPathForJimaku: (mediaPath) => mediaRuntime.resolveMediaPathForJimaku(mediaPath),
|
||||||
guessAnilistMediaInfo: (mediaPath, mediaTitle) => guessAnilistMediaInfo(mediaPath, mediaTitle),
|
guessAnilistMediaInfo: (mediaPath, mediaTitle) => guessAnilistMediaInfo(mediaPath, mediaTitle),
|
||||||
|
getCollapsibleSectionOpenState: (section) =>
|
||||||
|
getResolvedConfig().anilist.characterDictionary.collapsibleSections[section],
|
||||||
now: () => Date.now(),
|
now: () => Date.now(),
|
||||||
logInfo: (message) => logger.info(message),
|
logInfo: (message) => logger.info(message),
|
||||||
logWarn: (message) => logger.warn(message),
|
logWarn: (message) => logger.warn(message),
|
||||||
|
|||||||
12
src/types.ts
12
src/types.ts
@@ -398,6 +398,16 @@ export interface JimakuConfig {
|
|||||||
|
|
||||||
export type AnilistCharacterDictionaryEvictionPolicy = 'disable' | 'delete';
|
export type AnilistCharacterDictionaryEvictionPolicy = 'disable' | 'delete';
|
||||||
export type AnilistCharacterDictionaryProfileScope = 'all' | 'active';
|
export type AnilistCharacterDictionaryProfileScope = 'all' | 'active';
|
||||||
|
export type AnilistCharacterDictionaryCollapsibleSectionKey =
|
||||||
|
| 'description'
|
||||||
|
| 'characterInformation'
|
||||||
|
| 'voicedBy';
|
||||||
|
|
||||||
|
export interface AnilistCharacterDictionaryCollapsibleSectionsConfig {
|
||||||
|
description?: boolean;
|
||||||
|
characterInformation?: boolean;
|
||||||
|
voicedBy?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AnilistCharacterDictionaryConfig {
|
export interface AnilistCharacterDictionaryConfig {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
@@ -405,6 +415,7 @@ export interface AnilistCharacterDictionaryConfig {
|
|||||||
maxLoaded?: number;
|
maxLoaded?: number;
|
||||||
evictionPolicy?: AnilistCharacterDictionaryEvictionPolicy;
|
evictionPolicy?: AnilistCharacterDictionaryEvictionPolicy;
|
||||||
profileScope?: AnilistCharacterDictionaryProfileScope;
|
profileScope?: AnilistCharacterDictionaryProfileScope;
|
||||||
|
collapsibleSections?: AnilistCharacterDictionaryCollapsibleSectionsConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnilistConfig {
|
export interface AnilistConfig {
|
||||||
@@ -604,6 +615,7 @@ export interface ResolvedConfig {
|
|||||||
maxLoaded: number;
|
maxLoaded: number;
|
||||||
evictionPolicy: AnilistCharacterDictionaryEvictionPolicy;
|
evictionPolicy: AnilistCharacterDictionaryEvictionPolicy;
|
||||||
profileScope: AnilistCharacterDictionaryProfileScope;
|
profileScope: AnilistCharacterDictionaryProfileScope;
|
||||||
|
collapsibleSections: Required<AnilistCharacterDictionaryCollapsibleSectionsConfig>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
|
|||||||
Reference in New Issue
Block a user