feat(anki): add configurable word card type for Kiku/Lapis

- Add `ankiConnect.lapisKiku.wordCardKind` setting to choose the card-type flag SubMiner marks on mined word cards: word-and-sentence (default), click, sentence, audio, or none
- Support IsClickCard, which SubMiner previously never set
- Clear other card-type flags whenever one is set, so a note can't claim two card types at once
- Extract card-kind flag resolution into new card-kinds.ts module
- Hot-reload the new setting alongside existing Kiku/Lapis config
- Update docs-site and config.example.jsonc
This commit is contained in:
2026-07-30 00:45:38 -07:00
parent 1995200e76
commit d37a3eb299
31 changed files with 752 additions and 218 deletions
@@ -1,5 +1,7 @@
import type { MediaGenerator } from '../../../media-generator.js';
import type { AnkiConnectConfig } from '../../../types.js';
import { applyCardKindFlagFields } from '../../../anki-integration/card-kinds.js';
import { resolveWordCardKind } from '../../../anki-integration/note-field-utils.js';
import { createLogger } from '../../../logger.js';
import type { RetimedSecondarySubtitleInput } from '../secondary-subtitle-sidecar.js';
@@ -94,20 +96,22 @@ export function shouldUseStatsLapisKikuCardFields(ankiConfig: AnkiConnectConfig)
return ankiConfig.isLapis?.enabled === true || ankiConfig.isKiku?.enabled === true;
}
export function applyStatsWordAndSentenceCardFields(
export function applyStatsWordCardFields(
fields: Record<string, string>,
noteInfo: StatsServerNoteInfo | null,
ankiConfig: AnkiConnectConfig,
): void {
if (!shouldUseStatsLapisKikuCardFields(ankiConfig) || !noteInfo) return;
const wordAndSentenceFlag = resolveStatsNoteFieldName(noteInfo, 'IsWordAndSentenceCard');
if (!wordAndSentenceFlag) return;
if (!noteInfo) return;
const cardKind = resolveWordCardKind(noteInfo, {
lapisEnabled: ankiConfig.isLapis?.enabled === true,
kikuEnabled: ankiConfig.isKiku?.enabled === true,
wordCardKind: ankiConfig.lapisKiku?.wordCardKind,
});
if (!cardKind) return;
fields[wordAndSentenceFlag] = 'x';
for (const flagName of ['IsSentenceCard', 'IsAudioCard']) {
const resolved = resolveStatsNoteFieldName(noteInfo, flagName);
if (resolved && resolved !== wordAndSentenceFlag) fields[resolved] = '';
}
applyCardKindFlagFields(fields, cardKind, (preferredName) =>
resolveStatsNoteFieldName(noteInfo, preferredName),
);
}
export function getStatsDirectMiningAudioFieldNames(