Add vendor dict fallback logic

This commit is contained in:
2026-02-15 22:45:03 -08:00
parent dae1f817e0
commit 01a48f4714
21 changed files with 1194 additions and 19 deletions

View File

@@ -195,6 +195,20 @@ export const DEFAULT_CONFIG: ResolvedConfig = {
N4: "#a6e3a1",
N5: "#8aadf4",
},
frequencyDictionary: {
enabled: false,
sourcePath: "",
topX: 1000,
mode: "single",
singleColor: "#f5a97f",
bandedColors: [
"#ed8796",
"#f5a97f",
"#f9e2af",
"#a6e3a1",
"#8aadf4",
],
},
secondary: {
fontSize: 24,
fontColor: "#ffffff",
@@ -306,6 +320,48 @@ export const CONFIG_OPTION_REGISTRY: ConfigOptionRegistryEntry[] = [
description: "Enable JLPT vocabulary level underlines. "
+ "When disabled, JLPT tagging lookup and underlines are skipped.",
},
{
path: "subtitleStyle.frequencyDictionary.enabled",
kind: "boolean",
defaultValue: DEFAULT_CONFIG.subtitleStyle.frequencyDictionary.enabled,
description:
"Enable frequency-dictionary-based highlighting based on token rank.",
},
{
path: "subtitleStyle.frequencyDictionary.sourcePath",
kind: "string",
defaultValue: DEFAULT_CONFIG.subtitleStyle.frequencyDictionary.sourcePath,
description:
"Optional absolute path to a frequency dictionary directory."
+ " If empty, built-in discovery search paths are used.",
},
{
path: "subtitleStyle.frequencyDictionary.topX",
kind: "number",
defaultValue: DEFAULT_CONFIG.subtitleStyle.frequencyDictionary.topX,
description: "Only color tokens with frequency rank <= topX (default: 1000).",
},
{
path: "subtitleStyle.frequencyDictionary.mode",
kind: "enum",
enumValues: ["single", "banded"],
defaultValue: DEFAULT_CONFIG.subtitleStyle.frequencyDictionary.mode,
description:
"single: use one color for all matching tokens. banded: use color ramp by frequency band.",
},
{
path: "subtitleStyle.frequencyDictionary.singleColor",
kind: "string",
defaultValue: DEFAULT_CONFIG.subtitleStyle.frequencyDictionary.singleColor,
description: "Color used when frequencyDictionary.mode is `single`.",
},
{
path: "subtitleStyle.frequencyDictionary.bandedColors",
kind: "array",
defaultValue: DEFAULT_CONFIG.subtitleStyle.frequencyDictionary.bandedColors,
description:
"Five colors used for rank bands when mode is `banded` (from most common to least within topX).",
},
{
path: "ankiConnect.enabled",
kind: "boolean",

View File

@@ -45,6 +45,21 @@ function asColor(value: unknown): string | undefined {
return hexColorPattern.test(text) ? text : undefined;
}
function asFrequencyBandedColors(
value: unknown,
): [string, string, string, string, string] | undefined {
if (!Array.isArray(value) || value.length !== 5) {
return undefined;
}
const colors = value.map((item) => asColor(item));
if (colors.some((color) => color === undefined)) {
return undefined;
}
return colors as [string, string, string, string, string];
}
export class ConfigService {
private readonly configDir: string;
private readonly configFileJsonc: string;
@@ -468,6 +483,108 @@ export class ConfigService {
"Expected boolean.",
);
}
const frequencyDictionary = isObject(
(src.subtitleStyle as { frequencyDictionary?: unknown })
.frequencyDictionary,
)
? ((src.subtitleStyle as { frequencyDictionary?: unknown })
.frequencyDictionary as Record<string, unknown>)
: {};
const frequencyEnabled = asBoolean(
(frequencyDictionary as { enabled?: unknown }).enabled,
);
if (frequencyEnabled !== undefined) {
resolved.subtitleStyle.frequencyDictionary.enabled = frequencyEnabled;
} else if (
(frequencyDictionary as { enabled?: unknown }).enabled !== undefined
) {
warn(
"subtitleStyle.frequencyDictionary.enabled",
(frequencyDictionary as { enabled?: unknown }).enabled,
resolved.subtitleStyle.frequencyDictionary.enabled,
"Expected boolean.",
);
}
const sourcePath = asString(
(frequencyDictionary as { sourcePath?: unknown }).sourcePath,
);
if (sourcePath !== undefined) {
resolved.subtitleStyle.frequencyDictionary.sourcePath = sourcePath;
} else if (
(frequencyDictionary as { sourcePath?: unknown }).sourcePath !== undefined
) {
warn(
"subtitleStyle.frequencyDictionary.sourcePath",
(frequencyDictionary as { sourcePath?: unknown }).sourcePath,
resolved.subtitleStyle.frequencyDictionary.sourcePath,
"Expected string.",
);
}
const topX = asNumber((frequencyDictionary as { topX?: unknown }).topX);
if (
topX !== undefined &&
Number.isInteger(topX) &&
topX > 0
) {
resolved.subtitleStyle.frequencyDictionary.topX = Math.floor(topX);
} else if ((frequencyDictionary as { topX?: unknown }).topX !== undefined) {
warn(
"subtitleStyle.frequencyDictionary.topX",
(frequencyDictionary as { topX?: unknown }).topX,
resolved.subtitleStyle.frequencyDictionary.topX,
"Expected a positive integer.",
);
}
const frequencyMode = frequencyDictionary.mode;
if (
frequencyMode === "single" ||
frequencyMode === "banded"
) {
resolved.subtitleStyle.frequencyDictionary.mode = frequencyMode;
} else if (frequencyMode !== undefined) {
warn(
"subtitleStyle.frequencyDictionary.mode",
frequencyDictionary.mode,
resolved.subtitleStyle.frequencyDictionary.mode,
"Expected 'single' or 'banded'.",
);
}
const singleColor = asColor(
(frequencyDictionary as { singleColor?: unknown }).singleColor,
);
if (singleColor !== undefined) {
resolved.subtitleStyle.frequencyDictionary.singleColor = singleColor;
} else if (
(frequencyDictionary as { singleColor?: unknown }).singleColor !== undefined
) {
warn(
"subtitleStyle.frequencyDictionary.singleColor",
(frequencyDictionary as { singleColor?: unknown }).singleColor,
resolved.subtitleStyle.frequencyDictionary.singleColor,
"Expected hex color.",
);
}
const bandedColors = asFrequencyBandedColors(
(frequencyDictionary as { bandedColors?: unknown }).bandedColors,
);
if (bandedColors !== undefined) {
resolved.subtitleStyle.frequencyDictionary.bandedColors = bandedColors;
} else if (
(frequencyDictionary as { bandedColors?: unknown }).bandedColors !== undefined
) {
warn(
"subtitleStyle.frequencyDictionary.bandedColors",
(frequencyDictionary as { bandedColors?: unknown }).bandedColors,
resolved.subtitleStyle.frequencyDictionary.bandedColors,
"Expected an array of five hex colors.",
);
}
}
if (isObject(src.ankiConnect)) {