mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-29 19:21:33 -07:00
Anki maturity-based known-word highlighting (#172)
This commit is contained in:
@@ -2182,6 +2182,7 @@ test('runtime options registry is centralized', () => {
|
||||
assert.deepEqual(ids, [
|
||||
'anki.autoUpdateNewCards',
|
||||
'subtitle.annotation.knownWords.highlightEnabled',
|
||||
'subtitle.annotation.knownWords.maturityEnabled',
|
||||
'subtitle.annotation.nPlusOne',
|
||||
'subtitle.annotation.jlpt',
|
||||
'subtitle.annotation.frequency',
|
||||
|
||||
@@ -60,6 +60,8 @@ export const INTEGRATIONS_DEFAULT_CONFIG: Pick<
|
||||
},
|
||||
knownWords: {
|
||||
highlightEnabled: false,
|
||||
maturityEnabled: false,
|
||||
matureThresholdDays: 21,
|
||||
refreshMinutes: 1440,
|
||||
addMinedWordsImmediately: true,
|
||||
matchMode: 'headword',
|
||||
|
||||
@@ -32,6 +32,12 @@ export const SUBTITLE_DEFAULT_CONFIG: Pick<ResolvedConfig, 'subtitleStyle' | 'su
|
||||
backdropFilter: 'blur(6px)',
|
||||
nPlusOneColor: '#c6a0f6',
|
||||
knownWordColor: '#a6da95',
|
||||
knownWordMaturityColors: {
|
||||
new: '#ee99a0',
|
||||
learning: '#b7bdf8',
|
||||
young: '#91d7e3',
|
||||
mature: '#a6da95',
|
||||
},
|
||||
jlptColors: {
|
||||
N1: '#ed8796',
|
||||
N2: '#f5a97f',
|
||||
|
||||
@@ -295,6 +295,20 @@ export function buildIntegrationConfigOptionRegistry(
|
||||
defaultValue: defaultConfig.ankiConnect.knownWords.highlightEnabled,
|
||||
description: 'Enable fast local highlighting for words already known in Anki.',
|
||||
},
|
||||
{
|
||||
path: 'ankiConnect.knownWords.maturityEnabled',
|
||||
kind: 'boolean',
|
||||
defaultValue: defaultConfig.ankiConnect.knownWords.maturityEnabled,
|
||||
description:
|
||||
'Color known-word highlights by Anki card maturity (new, learning, young, mature) instead of a single color. Requires known-word highlighting.',
|
||||
},
|
||||
{
|
||||
path: 'ankiConnect.knownWords.matureThresholdDays',
|
||||
kind: 'number',
|
||||
defaultValue: defaultConfig.ankiConnect.knownWords.matureThresholdDays,
|
||||
description:
|
||||
'Card interval in days at which a known word counts as mature (Anki convention: 21).',
|
||||
},
|
||||
{
|
||||
path: 'ankiConnect.knownWords.refreshMinutes',
|
||||
kind: 'number',
|
||||
|
||||
@@ -109,6 +109,34 @@ export function buildSubtitleConfigOptionRegistry(
|
||||
defaultValue: defaultConfig.subtitleStyle.nPlusOneColor,
|
||||
description: 'Color used for the single N+1 target token subtitle highlight.',
|
||||
},
|
||||
{
|
||||
path: 'subtitleStyle.knownWordMaturityColors.new',
|
||||
kind: 'string',
|
||||
defaultValue: defaultConfig.subtitleStyle.knownWordMaturityColors.new,
|
||||
description:
|
||||
'Color for known words whose Anki cards are new (never reviewed), when maturity highlighting is enabled.',
|
||||
},
|
||||
{
|
||||
path: 'subtitleStyle.knownWordMaturityColors.learning',
|
||||
kind: 'string',
|
||||
defaultValue: defaultConfig.subtitleStyle.knownWordMaturityColors.learning,
|
||||
description:
|
||||
'Color for known words whose Anki cards are in (re)learning, when maturity highlighting is enabled.',
|
||||
},
|
||||
{
|
||||
path: 'subtitleStyle.knownWordMaturityColors.young',
|
||||
kind: 'string',
|
||||
defaultValue: defaultConfig.subtitleStyle.knownWordMaturityColors.young,
|
||||
description:
|
||||
'Color for known words whose Anki cards are in review below the mature threshold, when maturity highlighting is enabled.',
|
||||
},
|
||||
{
|
||||
path: 'subtitleStyle.knownWordMaturityColors.mature',
|
||||
kind: 'string',
|
||||
defaultValue: defaultConfig.subtitleStyle.knownWordMaturityColors.mature,
|
||||
description:
|
||||
'Color for known words whose Anki cards are at or above the mature interval threshold, when maturity highlighting is enabled.',
|
||||
},
|
||||
{
|
||||
path: 'subtitleStyle.frequencyDictionary.enabled',
|
||||
kind: 'boolean',
|
||||
|
||||
@@ -35,6 +35,22 @@ export function buildRuntimeOptionRegistry(
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'subtitle.annotation.knownWords.maturityEnabled',
|
||||
path: 'ankiConnect.knownWords.maturityEnabled',
|
||||
label: 'Known Word Maturity Colors',
|
||||
scope: 'subtitle',
|
||||
valueType: 'boolean',
|
||||
allowedValues: [true, false],
|
||||
defaultValue: defaultConfig.ankiConnect.knownWords.maturityEnabled,
|
||||
requiresRestart: false,
|
||||
formatValueForOsd: (value) => (value === true ? 'On' : 'Off'),
|
||||
toAnkiPatch: (value) => ({
|
||||
knownWords: {
|
||||
maturityEnabled: value === true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'subtitle.annotation.nPlusOne',
|
||||
path: 'ankiConnect.nPlusOne.enabled',
|
||||
|
||||
@@ -328,3 +328,54 @@ test('warns and falls back for invalid proxy settings', () => {
|
||||
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.proxy.port'));
|
||||
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.proxy.upstreamUrl'));
|
||||
});
|
||||
|
||||
test('resolves knownWords maturity settings from config', () => {
|
||||
const { context, warnings } = makeContext({
|
||||
knownWords: { highlightEnabled: true, maturityEnabled: true, matureThresholdDays: 30 },
|
||||
});
|
||||
|
||||
applyAnkiConnectResolution(context);
|
||||
|
||||
assert.equal(context.resolved.ankiConnect.knownWords.maturityEnabled, true);
|
||||
assert.equal(context.resolved.ankiConnect.knownWords.matureThresholdDays, 30);
|
||||
assert.deepEqual(warnings, []);
|
||||
});
|
||||
|
||||
test('warns and falls back for invalid knownWords maturity settings', () => {
|
||||
const { context, warnings } = makeContext({
|
||||
knownWords: { maturityEnabled: 'yes', matureThresholdDays: 0 },
|
||||
});
|
||||
|
||||
applyAnkiConnectResolution(context);
|
||||
|
||||
assert.equal(
|
||||
context.resolved.ankiConnect.knownWords.maturityEnabled,
|
||||
DEFAULT_CONFIG.ankiConnect.knownWords.maturityEnabled,
|
||||
);
|
||||
assert.equal(
|
||||
context.resolved.ankiConnect.knownWords.matureThresholdDays,
|
||||
DEFAULT_CONFIG.ankiConnect.knownWords.matureThresholdDays,
|
||||
);
|
||||
assert.ok(warnings.some((warning) => warning.path === 'ankiConnect.knownWords.maturityEnabled'));
|
||||
assert.ok(
|
||||
warnings.some((warning) => warning.path === 'ankiConnect.knownWords.matureThresholdDays'),
|
||||
);
|
||||
});
|
||||
|
||||
test('omitted knownWords maturity settings fall back to defaults', () => {
|
||||
const { context, warnings } = makeContext({
|
||||
knownWords: { highlightEnabled: true },
|
||||
});
|
||||
|
||||
applyAnkiConnectResolution(context);
|
||||
|
||||
assert.equal(
|
||||
context.resolved.ankiConnect.knownWords.maturityEnabled,
|
||||
DEFAULT_CONFIG.ankiConnect.knownWords.maturityEnabled,
|
||||
);
|
||||
assert.equal(
|
||||
context.resolved.ankiConnect.knownWords.matureThresholdDays,
|
||||
DEFAULT_CONFIG.ankiConnect.knownWords.matureThresholdDays,
|
||||
);
|
||||
assert.deepEqual(warnings, []);
|
||||
});
|
||||
|
||||
@@ -49,6 +49,46 @@ export function applyAnkiKnownWordsResolution(
|
||||
}
|
||||
}
|
||||
|
||||
const knownWordsMaturityEnabled = asBoolean(knownWordsConfig.maturityEnabled);
|
||||
if (knownWordsMaturityEnabled !== undefined) {
|
||||
context.resolved.ankiConnect.knownWords.maturityEnabled = knownWordsMaturityEnabled;
|
||||
} else if (hasOwn(knownWordsConfig, 'maturityEnabled')) {
|
||||
context.warn(
|
||||
'ankiConnect.knownWords.maturityEnabled',
|
||||
knownWordsConfig.maturityEnabled,
|
||||
context.resolved.ankiConnect.knownWords.maturityEnabled,
|
||||
'Expected boolean.',
|
||||
);
|
||||
context.resolved.ankiConnect.knownWords.maturityEnabled =
|
||||
DEFAULT_CONFIG.ankiConnect.knownWords.maturityEnabled;
|
||||
} else {
|
||||
context.resolved.ankiConnect.knownWords.maturityEnabled =
|
||||
DEFAULT_CONFIG.ankiConnect.knownWords.maturityEnabled;
|
||||
}
|
||||
|
||||
const knownWordsMatureThresholdDays = asNumber(knownWordsConfig.matureThresholdDays);
|
||||
const hasValidMatureThresholdDays =
|
||||
knownWordsMatureThresholdDays !== undefined &&
|
||||
Number.isInteger(knownWordsMatureThresholdDays) &&
|
||||
knownWordsMatureThresholdDays >= 1;
|
||||
if (hasOwn(knownWordsConfig, 'matureThresholdDays')) {
|
||||
if (hasValidMatureThresholdDays) {
|
||||
context.resolved.ankiConnect.knownWords.matureThresholdDays = knownWordsMatureThresholdDays;
|
||||
} else {
|
||||
context.warn(
|
||||
'ankiConnect.knownWords.matureThresholdDays',
|
||||
knownWordsConfig.matureThresholdDays,
|
||||
DEFAULT_CONFIG.ankiConnect.knownWords.matureThresholdDays,
|
||||
'Expected an integer of at least 1.',
|
||||
);
|
||||
context.resolved.ankiConnect.knownWords.matureThresholdDays =
|
||||
DEFAULT_CONFIG.ankiConnect.knownWords.matureThresholdDays;
|
||||
}
|
||||
} else {
|
||||
context.resolved.ankiConnect.knownWords.matureThresholdDays =
|
||||
DEFAULT_CONFIG.ankiConnect.knownWords.matureThresholdDays;
|
||||
}
|
||||
|
||||
const knownWordsRefreshMinutes = asNumber(knownWordsConfig.refreshMinutes);
|
||||
const hasValidKnownWordsRefreshMinutes =
|
||||
knownWordsRefreshMinutes !== undefined &&
|
||||
|
||||
@@ -125,6 +125,32 @@ test('settings registry groups annotation display fields by config group', () =>
|
||||
assert.equal(field('subtitleStyle.jlptColors.N1').control, 'color');
|
||||
});
|
||||
|
||||
test('settings registry groups maturity settings under Known Words with color controls', () => {
|
||||
assert.equal(field('ankiConnect.knownWords.maturityEnabled').subsection, 'Known Words');
|
||||
assert.equal(field('ankiConnect.knownWords.matureThresholdDays').subsection, 'Known Words');
|
||||
for (const tier of ['new', 'learning', 'young', 'mature']) {
|
||||
const tierField = field(`subtitleStyle.knownWordMaturityColors.${tier}`);
|
||||
assert.equal(tierField.subsection, 'Known Words');
|
||||
assert.equal(tierField.control, 'color');
|
||||
}
|
||||
});
|
||||
|
||||
test('settings registry orders maturity colors from least mature to mature', () => {
|
||||
const knownWordsPaths = fields
|
||||
.filter((candidate) => candidate.subsection === 'Known Words')
|
||||
.map((candidate) => candidate.configPath);
|
||||
assert.deepEqual(knownWordsPaths, [
|
||||
'ankiConnect.knownWords.highlightEnabled',
|
||||
'ankiConnect.knownWords.maturityEnabled',
|
||||
'subtitleStyle.knownWordColor',
|
||||
'ankiConnect.knownWords.matureThresholdDays',
|
||||
'subtitleStyle.knownWordMaturityColors.new',
|
||||
'subtitleStyle.knownWordMaturityColors.learning',
|
||||
'subtitleStyle.knownWordMaturityColors.young',
|
||||
'subtitleStyle.knownWordMaturityColors.mature',
|
||||
]);
|
||||
});
|
||||
|
||||
test('settings registry routes known words sync, n+1, and frequency config to behavior', () => {
|
||||
assert.equal(field('ankiConnect.knownWords.addMinedWordsImmediately').category, 'behavior');
|
||||
assert.equal(field('ankiConnect.knownWords.addMinedWordsImmediately').section, 'Known Words');
|
||||
|
||||
@@ -163,6 +163,12 @@ const PATH_ORDER = new Map<string, number>(
|
||||
'ankiConnect.proxy.enabled',
|
||||
'ankiConnect.isLapis.enabled',
|
||||
'ankiConnect.isKiku.enabled',
|
||||
'subtitleStyle.knownWordColor',
|
||||
'ankiConnect.knownWords.matureThresholdDays',
|
||||
'subtitleStyle.knownWordMaturityColors.new',
|
||||
'subtitleStyle.knownWordMaturityColors.learning',
|
||||
'subtitleStyle.knownWordMaturityColors.young',
|
||||
'subtitleStyle.knownWordMaturityColors.mature',
|
||||
'subtitleStyle.fontColor',
|
||||
'subtitleStyle.backgroundColor',
|
||||
'subtitleStyle.hoverTokenColor',
|
||||
@@ -353,6 +359,7 @@ function categoryAndSection(path: string): { category: ConfigSettingsCategory; s
|
||||
path.startsWith('ankiConnect.nPlusOne.') ||
|
||||
path.startsWith('subtitleStyle.frequencyDictionary.') ||
|
||||
path.startsWith('subtitleStyle.jlptColors.') ||
|
||||
path.startsWith('subtitleStyle.knownWordMaturityColors.') ||
|
||||
path === 'subtitleStyle.enableJlpt' ||
|
||||
path === 'subtitleStyle.knownWordColor' ||
|
||||
path === 'subtitleStyle.nPlusOneColor' ||
|
||||
@@ -516,6 +523,7 @@ function controlForPath(path: string, value: unknown): ConfigSettingsControl {
|
||||
return 'key-code';
|
||||
}
|
||||
if (path.startsWith('subtitleStyle.jlptColors.')) return 'color';
|
||||
if (path.startsWith('subtitleStyle.knownWordMaturityColors.')) return 'color';
|
||||
if (path === 'subtitleStyle.frequencyDictionary.bandedColors') return 'color-list';
|
||||
if (OPTION_BY_PATH.get(path)?.enumValues?.length) return 'select';
|
||||
if (JSON_OBJECT_FIELDS.has(path)) return 'json';
|
||||
@@ -533,6 +541,9 @@ function controlForPath(path: string, value: unknown): ConfigSettingsControl {
|
||||
|
||||
function subsectionForPath(path: string): string | undefined {
|
||||
if (path === 'ankiConnect.knownWords.highlightEnabled') return 'Known Words';
|
||||
if (path === 'ankiConnect.knownWords.maturityEnabled') return 'Known Words';
|
||||
if (path === 'ankiConnect.knownWords.matureThresholdDays') return 'Known Words';
|
||||
if (path.startsWith('subtitleStyle.knownWordMaturityColors.')) return 'Known Words';
|
||||
if (path === 'ankiConnect.nPlusOne.enabled') return 'N+1';
|
||||
if (path === 'subtitleStyle.knownWordColor') return 'Known Words';
|
||||
if (path === 'subtitleStyle.nPlusOneColor') return 'N+1';
|
||||
|
||||
Reference in New Issue
Block a user