Add inline character portraits and dictionary search workflow (#83)

This commit is contained in:
2026-05-25 03:16:25 -07:00
committed by GitHub
parent 7e6f9672cf
commit 807c0ff3db
54 changed files with 2306 additions and 178 deletions
+39
View File
@@ -64,6 +64,7 @@ test('loads defaults when config is missing', () => {
assert.equal(config.ankiConnect.media.audioPadding, 0);
assert.equal(config.anilist.enabled, false);
assert.equal(config.anilist.characterDictionary.enabled, false);
assert.equal(config.subtitleStyle.nameMatchImagesEnabled, false);
assert.equal(config.anilist.characterDictionary.refreshTtlHours, 168);
assert.equal(config.anilist.characterDictionary.maxLoaded, 3);
assert.equal(config.anilist.characterDictionary.evictionPolicy, 'delete');
@@ -740,6 +741,44 @@ test('parses subtitleStyle.nameMatchEnabled and warns on invalid values', () =>
);
});
test('parses subtitleStyle.nameMatchImagesEnabled and warns on invalid values', () => {
const validDir = makeTempDir();
fs.writeFileSync(
path.join(validDir, 'config.jsonc'),
`{
"subtitleStyle": {
"nameMatchImagesEnabled": true
}
}`,
'utf-8',
);
const validService = new ConfigService(validDir);
assert.equal(validService.getConfig().subtitleStyle.nameMatchImagesEnabled, true);
const invalidDir = makeTempDir();
fs.writeFileSync(
path.join(invalidDir, 'config.jsonc'),
`{
"subtitleStyle": {
"nameMatchImagesEnabled": "yes"
}
}`,
'utf-8',
);
const invalidService = new ConfigService(invalidDir);
assert.equal(
invalidService.getConfig().subtitleStyle.nameMatchImagesEnabled,
DEFAULT_CONFIG.subtitleStyle.nameMatchImagesEnabled,
);
assert.ok(
invalidService
.getWarnings()
.some((warning) => warning.path === 'subtitleStyle.nameMatchImagesEnabled'),
);
});
test('parses anilist.enabled and warns for invalid value', () => {
const dir = makeTempDir();
fs.writeFileSync(
@@ -11,6 +11,7 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle' | 'su
hoverTokenColor: '#f4dbd6',
hoverTokenBackgroundColor: 'transparent',
nameMatchEnabled: false,
nameMatchImagesEnabled: false,
nameMatchColor: '#f5bde6',
fontFamily: 'Hiragino Sans, M PLUS 1, Source Han Sans JP, Noto Sans CJK JP',
fontSize: 35,
@@ -76,6 +76,13 @@ export function buildSubtitleConfigOptionRegistry(
description:
'Enable subtitle token coloring for matches from the SubMiner character dictionary.',
},
{
path: 'subtitleStyle.nameMatchImagesEnabled',
kind: 'boolean',
defaultValue: defaultConfig.subtitleStyle.nameMatchImagesEnabled,
description:
'Show small character portraits beside subtitle tokens matched from the SubMiner character dictionary.',
},
{
path: 'subtitleStyle.nameMatchColor',
kind: 'string',
+20
View File
@@ -190,6 +190,8 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
const fallbackSubtitleStyleHoverTokenBackgroundColor =
resolved.subtitleStyle.hoverTokenBackgroundColor;
const fallbackSubtitleStyleNameMatchEnabled = resolved.subtitleStyle.nameMatchEnabled;
const fallbackSubtitleStyleNameMatchImagesEnabled =
resolved.subtitleStyle.nameMatchImagesEnabled;
const fallbackSubtitleStyleNameMatchColor = resolved.subtitleStyle.nameMatchColor;
const fallbackSubtitleStyleKnownWordColor = resolved.subtitleStyle.knownWordColor;
const fallbackSubtitleStyleNPlusOneColor = resolved.subtitleStyle.nPlusOneColor;
@@ -390,6 +392,24 @@ export function applySubtitleDomainConfig(context: ResolveContext): void {
);
}
const nameMatchImagesEnabled = asBoolean(
(src.subtitleStyle as { nameMatchImagesEnabled?: unknown }).nameMatchImagesEnabled,
);
if (nameMatchImagesEnabled !== undefined) {
resolved.subtitleStyle.nameMatchImagesEnabled = nameMatchImagesEnabled;
} else if (
(src.subtitleStyle as { nameMatchImagesEnabled?: unknown }).nameMatchImagesEnabled !==
undefined
) {
resolved.subtitleStyle.nameMatchImagesEnabled = fallbackSubtitleStyleNameMatchImagesEnabled;
warn(
'subtitleStyle.nameMatchImagesEnabled',
(src.subtitleStyle as { nameMatchImagesEnabled?: unknown }).nameMatchImagesEnabled,
resolved.subtitleStyle.nameMatchImagesEnabled,
'Expected boolean.',
);
}
if (nameMatchColor !== undefined) {
resolved.subtitleStyle.nameMatchColor = nameMatchColor;
} else if ((src.subtitleStyle as { nameMatchColor?: unknown }).nameMatchColor !== undefined) {
+25
View File
@@ -172,6 +172,31 @@ test('subtitleStyle nameMatchEnabled falls back on invalid value', () => {
);
});
test('subtitleStyle nameMatchImagesEnabled accepts boolean and warns on invalid', () => {
const valid = createResolveContext({
subtitleStyle: {
nameMatchImagesEnabled: true,
},
});
applySubtitleDomainConfig(valid.context);
assert.equal(valid.context.resolved.subtitleStyle.nameMatchImagesEnabled, true);
const invalid = createResolveContext({
subtitleStyle: {
nameMatchImagesEnabled: 'yes' as unknown as boolean,
},
});
applySubtitleDomainConfig(invalid.context);
assert.equal(invalid.context.resolved.subtitleStyle.nameMatchImagesEnabled, false);
assert.ok(
invalid.warnings.some(
(warning) =>
warning.path === 'subtitleStyle.nameMatchImagesEnabled' &&
warning.message === 'Expected boolean.',
),
);
});
test('subtitleStyle frequencyDictionary defaults to the teal fourth band color', () => {
const { context } = createResolveContext({});
+1
View File
@@ -173,6 +173,7 @@ test('settings registry exposes css declaration editor for primary and secondary
assert.equal(field('subtitleStyle.WebkitTextStroke').settingsHidden, true);
assert.equal(field('subtitleStyle.knownWordColor').settingsHidden, false);
assert.equal(field('subtitleStyle.nPlusOneColor').settingsHidden, false);
assert.equal(field('subtitleStyle.nameMatchImagesEnabled').settingsHidden, false);
assert.equal(field('subtitleStyle.nameMatchColor').settingsHidden, false);
assert.equal(field('subtitleStyle.jlptColors.N1').settingsHidden, false);
assert.equal(field('subtitleStyle.frequencyDictionary.singleColor').settingsHidden, false);
+6 -1
View File
@@ -345,6 +345,7 @@ function categoryAndSection(path: string): { category: ConfigSettingsCategory; s
path === 'subtitleStyle.knownWordColor' ||
path === 'subtitleStyle.nPlusOneColor' ||
path === 'subtitleStyle.nameMatchEnabled' ||
path === 'subtitleStyle.nameMatchImagesEnabled' ||
path === 'subtitleStyle.nameMatchColor'
) {
return { category: 'appearance', section: 'Annotation Display' };
@@ -524,7 +525,11 @@ function subsectionForPath(path: string): string | undefined {
) {
return 'Frequency Highlighting';
}
if (path === 'subtitleStyle.nameMatchEnabled' || path === 'subtitleStyle.nameMatchColor') {
if (
path === 'subtitleStyle.nameMatchEnabled' ||
path === 'subtitleStyle.nameMatchImagesEnabled' ||
path === 'subtitleStyle.nameMatchColor'
) {
return 'Character Names';
}
if (path === 'anilist.characterDictionary.collapsibleSections.description') {