fix: refresh current subtitle after known-word mining

This commit is contained in:
2026-05-02 20:56:59 -07:00
parent 6607b06437
commit f96467a1d6
6 changed files with 137 additions and 8 deletions
+38
View File
@@ -177,6 +177,44 @@ test('AnkiIntegration.refreshKnownWordCache skips work when highlight mode is di
}
});
test('AnkiIntegration notifies when mined note info updates known words', () => {
const ctx = createIntegrationTestContext({
stateDirPrefix: 'subminer-anki-integration-known-update-',
});
let notifications = 0;
try {
const integrationState = ctx.integration as unknown as {
config: AnkiConnectConfig;
appendKnownWordsFromNoteInfo: (noteInfo: {
noteId: number;
fields: Record<string, { value: string }>;
}) => void;
};
integrationState.config.deck = 'Mining';
integrationState.config.knownWords = {
...integrationState.config.knownWords,
decks: {
Mining: ['Word'],
},
};
ctx.integration.setKnownWordCacheUpdatedCallback(() => {
notifications += 1;
});
integrationState.appendKnownWordsFromNoteInfo({
noteId: 42,
fields: {
Word: { value: '食べる' },
},
});
assert.equal(ctx.integration.isKnownWord('食べる'), true);
assert.equal(notifications, 1);
} finally {
cleanupIntegrationTestContext(ctx);
}
});
test('AnkiIntegration.refreshKnownWordCache deduplicates concurrent refreshes', async () => {
let releaseFindNotes: (() => void) | undefined;
const findNotesPromise = new Promise<void>((resolve) => {
+21 -1
View File
@@ -148,6 +148,7 @@ export class AnkiIntegration {
private runtime: AnkiIntegrationRuntime;
private aiConfig: AiConfig;
private recordCardsMinedCallback: ((count: number, noteIds?: number[]) => void) | null = null;
private knownWordCacheUpdatedCallback: (() => void) | null = null;
private noteIdRedirects = new Map<number, number>();
private trackedDuplicateNoteIds = new Map<number, number[]>();
@@ -552,10 +553,25 @@ export class AnkiIntegration {
return;
}
this.knownWordCache.appendFromNoteInfo({
const changed = this.knownWordCache.appendFromNoteInfo({
noteId: noteInfo.noteId,
fields: noteInfo.fields,
});
if (changed) {
this.notifyKnownWordCacheUpdated();
}
}
private notifyKnownWordCacheUpdated(): void {
if (!this.knownWordCacheUpdatedCallback) {
return;
}
try {
this.knownWordCacheUpdatedCallback();
} catch (error) {
log.warn('Known-word cache update callback failed:', (error as Error).message);
}
}
private getLapisConfig(): {
@@ -1267,6 +1283,10 @@ export class AnkiIntegration {
this.recordCardsMinedCallback = callback;
}
setKnownWordCacheUpdatedCallback(callback: (() => void) | null): void {
this.knownWordCacheUpdatedCallback = callback;
}
resolveCurrentNoteId(noteId: number): number {
let resolved = noteId;
const seen = new Set<number>();
+5 -4
View File
@@ -165,9 +165,9 @@ export class KnownWordCacheManager {
}
}
appendFromNoteInfo(noteInfo: KnownWordCacheNoteInfo): void {
appendFromNoteInfo(noteInfo: KnownWordCacheNoteInfo): boolean {
if (!this.isKnownWordCacheEnabled() || !this.shouldAddMinedWordsImmediately()) {
return;
return false;
}
const currentStateKey = this.getKnownWordCacheStateKey();
@@ -180,13 +180,13 @@ export class KnownWordCacheManager {
const preferredFields = this.getImmediateAppendFields();
if (!preferredFields) {
return;
return false;
}
const nextWords = this.extractNormalizedKnownWordsFromNoteInfo(noteInfo, preferredFields);
const changed = this.replaceNoteSnapshot(noteInfo.noteId, nextWords);
if (!changed) {
return;
return false;
}
if (this.knownWordsLastRefreshedAtMs <= 0) {
@@ -199,6 +199,7 @@ export class KnownWordCacheManager {
`wordCount=${nextWords.length}`,
`scope=${getKnownWordCacheScopeForConfig(this.deps.getConfig())}`,
);
return true;
}
clearKnownWordCacheState(): void {
+11 -3
View File
@@ -1407,9 +1407,8 @@ const subtitleProcessingController = createSubtitleProcessingController(
let subtitlePrefetchService: SubtitlePrefetchService | null = null;
let subtitlePrefetchRefreshTimer: ReturnType<typeof setTimeout> | null = null;
let lastObservedTimePos = 0;
let cancelLinuxMpvFullscreenOverlayRefreshBurst:
| CancelLinuxMpvFullscreenOverlayRefreshBurst
| null = null;
let cancelLinuxMpvFullscreenOverlayRefreshBurst: CancelLinuxMpvFullscreenOverlayRefreshBurst | null =
null;
const SEEK_THRESHOLD_SECONDS = 3;
function clearScheduledSubtitlePrefetchRefresh(): void {
@@ -3439,6 +3438,9 @@ const recordTrackedCardsMined = (count: number, noteIds?: number[]): void => {
ensureImmersionTrackerStarted();
appState.immersionTracker?.recordCardsMined(count, noteIds);
};
const refreshCurrentSubtitleAfterKnownWordUpdate = (): void => {
subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText);
};
let hasAttemptedImmersionTrackerStartup = false;
const ensureImmersionTrackerStarted = (): void => {
if (hasAttemptedImmersionTrackerStartup || appState.immersionTracker) {
@@ -4264,6 +4266,9 @@ function destroyTray(): void {
function initializeOverlayRuntime(): void {
initializeOverlayRuntimeHandler();
appState.ankiIntegration?.setRecordCardsMinedCallback(recordTrackedCardsMined);
appState.ankiIntegration?.setKnownWordCacheUpdatedCallback(
refreshCurrentSubtitleAfterKnownWordUpdate,
);
syncOverlayMpvSubtitleSuppression();
}
@@ -4970,6 +4975,9 @@ const { registerIpcRuntimeHandlers } = composeIpcRuntimeHandlers({
setAnkiIntegration: (integration: AnkiIntegration | null) => {
appState.ankiIntegration = integration;
appState.ankiIntegration?.setRecordCardsMinedCallback(recordTrackedCardsMined);
appState.ankiIntegration?.setKnownWordCacheUpdatedCallback(
refreshCurrentSubtitleAfterKnownWordUpdate,
);
},
getKnownWordCacheStatePath: () => path.join(USER_DATA_PATH, 'known-words-cache.json'),
showDesktopNotification,