mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-01 06:22:44 -08:00
feat(core): add Electron runtime, services, and app composition
This commit is contained in:
214
src/anki-integration/field-grouping-workflow.ts
Normal file
214
src/anki-integration/field-grouping-workflow.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import { KikuDuplicateCardInfo, KikuFieldGroupingChoice } from '../types';
|
||||
|
||||
export interface FieldGroupingWorkflowNoteInfo {
|
||||
noteId: number;
|
||||
fields: Record<string, { value: string }>;
|
||||
}
|
||||
|
||||
export interface FieldGroupingWorkflowDeps {
|
||||
client: {
|
||||
notesInfo(noteIds: number[]): Promise<unknown>;
|
||||
updateNoteFields(noteId: number, fields: Record<string, string>): Promise<void>;
|
||||
deleteNotes(noteIds: number[]): Promise<void>;
|
||||
};
|
||||
getConfig: () => {
|
||||
fields?: {
|
||||
audio?: string;
|
||||
image?: string;
|
||||
};
|
||||
};
|
||||
getEffectiveSentenceCardConfig: () => {
|
||||
sentenceField: string;
|
||||
audioField: string;
|
||||
kikuDeleteDuplicateInAuto: boolean;
|
||||
};
|
||||
getCurrentSubtitleText: () => string | undefined;
|
||||
getFieldGroupingCallback:
|
||||
| (() => Promise<
|
||||
| ((data: {
|
||||
original: KikuDuplicateCardInfo;
|
||||
duplicate: KikuDuplicateCardInfo;
|
||||
}) => Promise<KikuFieldGroupingChoice>)
|
||||
| null
|
||||
>)
|
||||
| (() =>
|
||||
| ((data: {
|
||||
original: KikuDuplicateCardInfo;
|
||||
duplicate: KikuDuplicateCardInfo;
|
||||
}) => Promise<KikuFieldGroupingChoice>)
|
||||
| null);
|
||||
computeFieldGroupingMergedFields: (
|
||||
keepNoteId: number,
|
||||
deleteNoteId: number,
|
||||
keepNoteInfo: FieldGroupingWorkflowNoteInfo,
|
||||
deleteNoteInfo: FieldGroupingWorkflowNoteInfo,
|
||||
includeGeneratedMedia: boolean,
|
||||
) => Promise<Record<string, string>>;
|
||||
extractFields: (fields: Record<string, { value: string }>) => Record<string, string>;
|
||||
hasFieldValue: (noteInfo: FieldGroupingWorkflowNoteInfo, preferredFieldName?: string) => boolean;
|
||||
addConfiguredTagsToNote: (noteId: number) => Promise<void>;
|
||||
removeTrackedNoteId: (noteId: number) => void;
|
||||
showStatusNotification: (message: string) => void;
|
||||
showNotification: (noteId: number, label: string | number) => Promise<void>;
|
||||
showOsdNotification: (message: string) => void;
|
||||
logError: (message: string, ...args: unknown[]) => void;
|
||||
logInfo: (message: string, ...args: unknown[]) => void;
|
||||
truncateSentence: (sentence: string) => string;
|
||||
}
|
||||
|
||||
export class FieldGroupingWorkflow {
|
||||
constructor(private readonly deps: FieldGroupingWorkflowDeps) {}
|
||||
|
||||
async handleAuto(
|
||||
originalNoteId: number,
|
||||
newNoteId: number,
|
||||
newNoteInfo: FieldGroupingWorkflowNoteInfo,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const sentenceCardConfig = this.deps.getEffectiveSentenceCardConfig();
|
||||
await this.performMerge(
|
||||
originalNoteId,
|
||||
newNoteId,
|
||||
newNoteInfo,
|
||||
this.getExpression(newNoteInfo),
|
||||
sentenceCardConfig.kikuDeleteDuplicateInAuto,
|
||||
);
|
||||
} catch (error) {
|
||||
this.deps.logError('Field grouping auto merge failed:', (error as Error).message);
|
||||
this.deps.showOsdNotification(`Field grouping failed: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async handleManual(
|
||||
originalNoteId: number,
|
||||
newNoteId: number,
|
||||
newNoteInfo: FieldGroupingWorkflowNoteInfo,
|
||||
): Promise<boolean> {
|
||||
const callback = await this.resolveFieldGroupingCallback();
|
||||
if (!callback) {
|
||||
this.deps.showOsdNotification('Field grouping UI unavailable');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const originalNotesInfoResult = await this.deps.client.notesInfo([originalNoteId]);
|
||||
const originalNotesInfo = originalNotesInfoResult as FieldGroupingWorkflowNoteInfo[];
|
||||
if (!originalNotesInfo || originalNotesInfo.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const originalNoteInfo = originalNotesInfo[0]!;
|
||||
const expression = this.getExpression(newNoteInfo) || this.getExpression(originalNoteInfo);
|
||||
|
||||
const choice = await callback({
|
||||
original: this.buildDuplicateCardInfo(originalNoteInfo, expression, true),
|
||||
duplicate: this.buildDuplicateCardInfo(newNoteInfo, expression, false),
|
||||
});
|
||||
|
||||
if (choice.cancelled) {
|
||||
this.deps.showOsdNotification('Field grouping cancelled');
|
||||
return false;
|
||||
}
|
||||
|
||||
const keepNoteId = choice.keepNoteId;
|
||||
const deleteNoteId = choice.deleteNoteId;
|
||||
const deleteNoteInfo = deleteNoteId === newNoteId ? newNoteInfo : originalNoteInfo;
|
||||
|
||||
await this.performMerge(
|
||||
keepNoteId,
|
||||
deleteNoteId,
|
||||
deleteNoteInfo,
|
||||
expression,
|
||||
choice.deleteDuplicate,
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.deps.logError('Field grouping manual merge failed:', (error as Error).message);
|
||||
this.deps.showOsdNotification(`Field grouping failed: ${(error as Error).message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async performMerge(
|
||||
keepNoteId: number,
|
||||
deleteNoteId: number,
|
||||
deleteNoteInfo: FieldGroupingWorkflowNoteInfo,
|
||||
expression: string,
|
||||
deleteDuplicate = true,
|
||||
): Promise<void> {
|
||||
const keepNotesInfoResult = await this.deps.client.notesInfo([keepNoteId]);
|
||||
const keepNotesInfo = keepNotesInfoResult as FieldGroupingWorkflowNoteInfo[];
|
||||
if (!keepNotesInfo || keepNotesInfo.length === 0) {
|
||||
this.deps.logInfo('Keep note not found:', keepNoteId);
|
||||
return;
|
||||
}
|
||||
|
||||
const keepNoteInfo = keepNotesInfo[0]!;
|
||||
const mergedFields = await this.deps.computeFieldGroupingMergedFields(
|
||||
keepNoteId,
|
||||
deleteNoteId,
|
||||
keepNoteInfo,
|
||||
deleteNoteInfo,
|
||||
true,
|
||||
);
|
||||
|
||||
if (Object.keys(mergedFields).length > 0) {
|
||||
await this.deps.client.updateNoteFields(keepNoteId, mergedFields);
|
||||
await this.deps.addConfiguredTagsToNote(keepNoteId);
|
||||
}
|
||||
|
||||
if (deleteDuplicate) {
|
||||
await this.deps.client.deleteNotes([deleteNoteId]);
|
||||
this.deps.removeTrackedNoteId(deleteNoteId);
|
||||
}
|
||||
|
||||
this.deps.logInfo('Merged duplicate card:', expression, 'into note:', keepNoteId);
|
||||
this.deps.showStatusNotification(
|
||||
deleteDuplicate
|
||||
? `Merged duplicate: ${expression}`
|
||||
: `Grouped duplicate (kept both): ${expression}`,
|
||||
);
|
||||
await this.deps.showNotification(keepNoteId, expression);
|
||||
}
|
||||
|
||||
private buildDuplicateCardInfo(
|
||||
noteInfo: FieldGroupingWorkflowNoteInfo,
|
||||
fallbackExpression: string,
|
||||
isOriginal: boolean,
|
||||
): KikuDuplicateCardInfo {
|
||||
const sentenceCardConfig = this.deps.getEffectiveSentenceCardConfig();
|
||||
const fields = this.deps.extractFields(noteInfo.fields);
|
||||
return {
|
||||
noteId: noteInfo.noteId,
|
||||
expression: fields.expression || fields.word || fallbackExpression,
|
||||
sentencePreview: this.deps.truncateSentence(
|
||||
fields[(sentenceCardConfig.sentenceField || 'sentence').toLowerCase()] ||
|
||||
(isOriginal ? '' : this.deps.getCurrentSubtitleText() || ''),
|
||||
),
|
||||
hasAudio:
|
||||
this.deps.hasFieldValue(noteInfo, this.deps.getConfig().fields?.audio) ||
|
||||
this.deps.hasFieldValue(noteInfo, sentenceCardConfig.audioField),
|
||||
hasImage: this.deps.hasFieldValue(noteInfo, this.deps.getConfig().fields?.image),
|
||||
isOriginal,
|
||||
};
|
||||
}
|
||||
|
||||
private getExpression(noteInfo: FieldGroupingWorkflowNoteInfo): string {
|
||||
const fields = this.deps.extractFields(noteInfo.fields);
|
||||
return fields.expression || fields.word || '';
|
||||
}
|
||||
|
||||
private async resolveFieldGroupingCallback(): Promise<
|
||||
| ((data: {
|
||||
original: KikuDuplicateCardInfo;
|
||||
duplicate: KikuDuplicateCardInfo;
|
||||
}) => Promise<KikuFieldGroupingChoice>)
|
||||
| null
|
||||
> {
|
||||
const callback = this.deps.getFieldGroupingCallback();
|
||||
if (callback instanceof Promise) {
|
||||
return callback;
|
||||
}
|
||||
return callback;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user