feat(anki): add runtime toggle for media timing review

- Add session-only runtime option with live config hot-reload support
- Document the runtime palette toggle
This commit is contained in:
2026-08-30 01:56:16 -07:00
parent 475f81d96d
commit f36e7f6182
14 changed files with 57 additions and 6 deletions
+1
View File
@@ -2181,6 +2181,7 @@ test('runtime options registry is centralized', () => {
const ids = RUNTIME_OPTION_REGISTRY.map((entry) => entry.id);
assert.deepEqual(ids, [
'anki.autoUpdateNewCards',
'anki.mediaReviewTiming',
'subtitle.annotation.knownWords.highlightEnabled',
'subtitle.annotation.knownWords.maturityEnabled',
'subtitle.annotation.nPlusOne',
@@ -202,6 +202,7 @@ export function buildIntegrationConfigOptionRegistry(
defaultValue: defaultConfig.ankiConnect.media.reviewTiming,
description:
'Review and preview subtitle media timing before SubMiner creates or enriches a mined card.',
runtime: runtimeOptionById.get('anki.mediaReviewTiming'),
},
{
path: 'ankiConnect.media.generateImage',
+14
View File
@@ -19,6 +19,20 @@ export function buildRuntimeOptionRegistry(
behavior: { autoUpdateNewCards: value === true },
}),
},
{
id: 'anki.mediaReviewTiming',
path: 'ankiConnect.media.reviewTiming',
label: 'Review Media Timing',
scope: 'ankiConnect',
valueType: 'boolean',
allowedValues: [true, false],
defaultValue: defaultConfig.ankiConnect.media.reviewTiming,
requiresRestart: false,
formatValueForOsd: (value) => (value === true ? 'On' : 'Off'),
toAnkiPatch: (value) => ({
media: { reviewTiming: value === true },
}),
},
{
id: 'subtitle.annotation.knownWords.highlightEnabled',
path: 'ankiConnect.knownWords.highlightEnabled',
+1
View File
@@ -352,6 +352,7 @@ test('settings registry marks safe live config paths as hot-reloadable', () => {
'ankiConnect.deck',
'ankiConnect.media.normalizeAudio',
'ankiConnect.media.mirrorMpvVolume',
'ankiConnect.media.reviewTiming',
'ankiConnect.knownWords.highlightEnabled',
'ankiConnect.knownWords.refreshMinutes',
'ankiConnect.knownWords.addMinedWordsImmediately',
@@ -33,6 +33,7 @@ test('classifyConfigHotReloadDiff treats safe nested config paths as hot-reloada
next.ankiConnect.deck = 'Mining';
next.ankiConnect.media.normalizeAudio = !prev.ankiConnect.media.normalizeAudio;
next.ankiConnect.media.mirrorMpvVolume = !prev.ankiConnect.media.mirrorMpvVolume;
next.ankiConnect.media.reviewTiming = !prev.ankiConnect.media.reviewTiming;
next.ankiConnect.behavior.autoUpdateNewCards = !prev.ankiConnect.behavior.autoUpdateNewCards;
next.ankiConnect.knownWords.highlightEnabled = !prev.ankiConnect.knownWords.highlightEnabled;
next.ankiConnect.knownWords.refreshMinutes = prev.ankiConnect.knownWords.refreshMinutes + 5;
@@ -69,6 +70,7 @@ test('classifyConfigHotReloadDiff treats safe nested config paths as hot-reloada
'ankiConnect.deck',
'ankiConnect.media.normalizeAudio',
'ankiConnect.media.mirrorMpvVolume',
'ankiConnect.media.reviewTiming',
'ankiConnect.behavior.autoUpdateNewCards',
'ankiConnect.knownWords.highlightEnabled',
'ankiConnect.knownWords.refreshMinutes',
+1
View File
@@ -70,6 +70,7 @@ const HOT_RELOAD_EXACT_OR_PREFIX_PATHS = [
'ankiConnect.deck',
'ankiConnect.media.normalizeAudio',
'ankiConnect.media.mirrorMpvVolume',
'ankiConnect.media.reviewTiming',
'ankiConnect.behavior.autoUpdateNewCards',
'ankiConnect.knownWords.highlightEnabled',
'ankiConnect.knownWords.refreshMinutes',
@@ -156,6 +156,7 @@ test('createConfigHotReloadAppliedHandler applies only changed Anki media option
const config = deepCloneConfig(DEFAULT_CONFIG);
config.ankiConnect.media.normalizeAudio = false;
config.ankiConnect.media.mirrorMpvVolume = false;
config.ankiConnect.media.reviewTiming = true;
const ankiPatches: unknown[] = [];
const applyHotReload = createConfigHotReloadAppliedHandler({
@@ -181,10 +182,18 @@ test('createConfigHotReloadAppliedHandler applies only changed Anki media option
},
config,
);
applyHotReload(
{
hotReloadFields: ['ankiConnect.media.reviewTiming'],
restartRequiredFields: [],
},
config,
);
assert.deepEqual(ankiPatches, [
{ media: { normalizeAudio: false } },
{ media: { mirrorMpvVolume: false } },
{ media: { reviewTiming: true } },
]);
});
@@ -100,6 +100,9 @@ function buildAnkiRuntimeConfigPatch(
if (diff.hotReloadFields.includes('ankiConnect.media.mirrorMpvVolume')) {
mediaPatch.mirrorMpvVolume = config.ankiConnect.media.mirrorMpvVolume;
}
if (diff.hotReloadFields.includes('ankiConnect.media.reviewTiming')) {
mediaPatch.reviewTiming = config.ankiConnect.media.reviewTiming;
}
if (Object.keys(mediaPatch).length > 0) {
patch.media = mediaPatch;
}
+16
View File
@@ -82,3 +82,19 @@ test('RuntimeOptionsManager keeps known-word and n+1 annotation toggles separate
assert.equal(effective.nPlusOne?.enabled, true);
assert.deepEqual(patches, []);
});
test('RuntimeOptionsManager applies media timing review to the live Anki config', () => {
const baseConfig = structuredClone(DEFAULT_CONFIG.ankiConnect);
const patches: unknown[] = [];
const manager = new RuntimeOptionsManager(() => structuredClone(baseConfig), {
applyAnkiPatch: (patch) => {
patches.push(patch);
},
onOptionsChanged: () => undefined,
});
assert.equal(manager.getOptionValue('anki.mediaReviewTiming'), false);
assert.equal(manager.setOptionValue('anki.mediaReviewTiming', true).ok, true);
assert.equal(manager.getEffectiveAnkiConnectConfig().media?.reviewTiming, true);
assert.deepEqual(patches, [{ media: { reviewTiming: true } }]);
});
+1
View File
@@ -53,6 +53,7 @@ const SESSION_ACTION_IDS: SessionActionId[] = [
const RUNTIME_OPTION_IDS: RuntimeOptionId[] = [
'anki.autoUpdateNewCards',
'anki.mediaReviewTiming',
'subtitle.annotation.knownWords.highlightEnabled',
'subtitle.annotation.knownWords.maturityEnabled',
'subtitle.annotation.nPlusOne',
+1
View File
@@ -1,5 +1,6 @@
export type RuntimeOptionId =
| 'anki.autoUpdateNewCards'
| 'anki.mediaReviewTiming'
| 'subtitle.annotation.knownWords.highlightEnabled'
| 'subtitle.annotation.knownWords.maturityEnabled'
| 'subtitle.annotation.nPlusOne'