Skip to content

Commit be42cb2

Browse files
committed
Add support for canAddNotesWithDetail with fallback to canAddNotes if not supported
* Add support for canAddNotesWithDetail with fallback if not supported * Add changes to playwright <rikaitan.link>MWVlY2YxMWI4M2ZhMzVmMzYyNTZkODYyNDE5ZTUwNTM3MDA3MjllZAo=</rikaitan.link>
1 parent 95b30f0 commit be42cb2

File tree

4 files changed

+94
-16
lines changed

4 files changed

+94
-16
lines changed

ext/js/background/backend.js

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,39 @@ export class Backend {
625625
return newNotes;
626626
}
627627

628+
/**
629+
* @param {import('anki').Note[]} notes
630+
* @param {import('anki').Note[]} notesStrippedNoDuplicates
631+
* @returns {Promise<{ note: import('anki').Note, isDuplicate: boolean }[]>}
632+
*/
633+
async _findDuplicates(notes, notesStrippedNoDuplicates) {
634+
const canAddNotesWithErrors = await this._anki.canAddNotesWithErrorDetail(notesStrippedNoDuplicates);
635+
return canAddNotesWithErrors.map((item, i) => ({
636+
note: notes[i],
637+
isDuplicate: item.error === null ?
638+
false :
639+
item.error.includes('cannot create note because it is a duplicate'),
640+
}));
641+
}
642+
643+
/**
644+
* @param {import('anki').Note[]} notes
645+
* @param {import('anki').Note[]} notesStrippedNoDuplicates
646+
* @param {import('anki').Note[]} notesStrippedDuplicates
647+
* @returns {Promise<{ note: import('anki').Note, isDuplicate: boolean }[]>}
648+
*/
649+
async _findDuplicatesFallback(notes, notesStrippedNoDuplicates, notesStrippedDuplicates) {
650+
const [withDuplicatesAllowed, noDuplicatesAllowed] = await Promise.all([
651+
this._anki.canAddNotes(notesStrippedDuplicates),
652+
this._anki.canAddNotes(notesStrippedNoDuplicates),
653+
]);
654+
655+
return withDuplicatesAllowed.map((item, i) => ({
656+
note: notes[i],
657+
isDuplicate: item !== noDuplicatesAllowed[i],
658+
}));
659+
}
660+
628661
/**
629662
* @param {import('anki').Note[]} notes
630663
* @returns {Promise<import('backend').CanAddResults>}
@@ -638,24 +671,16 @@ export class Backend {
638671
// to check which notes are duplicates.
639672
const notesNoDuplicatesAllowed = strippedNotes.map((note) => ({...note, options: {...note.options, allowDuplicate: false}}));
640673

641-
// If only older AnkiConnect available, use `canAddNotes`.
642-
const [withDuplicatesAllowed, noDuplicatesAllowed] = await Promise.all([
643-
this._anki.canAddNotes(strippedNotes),
644-
this._anki.canAddNotes(notesNoDuplicatesAllowed),
645-
]);
646-
647-
/** @type {{ note: import('anki').Note, isDuplicate: boolean }[]} */
648-
const canAddArray = [];
649-
650-
for (let i = 0; i < withDuplicatesAllowed.length; i++) {
651-
if (withDuplicatesAllowed[i] === noDuplicatesAllowed[i]) {
652-
canAddArray.push({note: notes[i], isDuplicate: false});
653-
} else {
654-
canAddArray.push({note: notes[i], isDuplicate: true});
674+
try {
675+
return await this._findDuplicates(notes, notesNoDuplicatesAllowed);
676+
} catch (e) {
677+
// User has older anki-connect that does not support canAddNotesWithErrorDetail
678+
if (e instanceof ExtensionError && e.message.includes('Anki error: unsupported action')) {
679+
return await this._findDuplicatesFallback(notes, notesNoDuplicatesAllowed, strippedNotes);
655680
}
656-
}
657681

658-
return canAddArray;
682+
throw e;
683+
}
659684
}
660685

661686
/** @type {import('api').ApiHandler<'getAnkiNoteInfo'>} */

ext/js/comm/anki-connect.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,17 @@ export class AnkiConnect {
169169
return this._normalizeArray(result, notes.length, 'boolean');
170170
}
171171

172+
/**
173+
* @param {import('anki').Note[]} notes
174+
* @returns {Promise<import('anki').CanAddNotesDetail[]>}
175+
*/
176+
async canAddNotesWithErrorDetail(notes) {
177+
if (!this._enabled) { return notes.map(() => ({canAdd: false, error: null})); }
178+
await this._checkVersion();
179+
const result = await this._invoke('canAddNotesWithErrorDetail', {notes});
180+
return this._normalizeCanAddNotesWithErrorDetailArray(result, notes.length);
181+
}
182+
172183
/**
173184
* @param {import('anki').NoteId[]} noteIds
174185
* @returns {Promise<(?import('anki').NoteInfo)[]>}
@@ -725,4 +736,40 @@ export class AnkiConnect {
725736
}
726737
return result2;
727738
}
739+
740+
/**
741+
* @param {unknown} result
742+
* @param {number} expectedCount
743+
* @returns {import('anki').CanAddNotesDetail[]}
744+
* @throws {Error}
745+
*/
746+
_normalizeCanAddNotesWithErrorDetailArray(result, expectedCount) {
747+
if (!Array.isArray(result)) {
748+
throw this._createUnexpectedResultError('array', result, '');
749+
}
750+
if (expectedCount !== result.length) {
751+
throw this._createError(`Unexpected result array size: expected ${expectedCount}, received ${result.length}`, result);
752+
}
753+
/** @type {import('anki').CanAddNotesDetail[]} */
754+
const result2 = [];
755+
for (let i = 0; i < expectedCount; ++i) {
756+
const item = /** @type {unknown} */ (result[i]);
757+
if (item === null || typeof item !== 'object') {
758+
throw this._createError(`Unexpected result type at index ${i}: expected object, received ${this._getTypeName(item)}`, result);
759+
}
760+
761+
const {canAdd, error} = /** @type {{[key: string]: unknown}} */ (item);
762+
if (typeof canAdd !== 'boolean') {
763+
throw this._createError(`Unexpected result type at index ${i}, field canAdd: expected boolean, received ${this._getTypeName(canAdd)}`, result);
764+
}
765+
766+
const item2 = {
767+
canAdd: canAdd,
768+
error: typeof error === 'string' ? error : null,
769+
};
770+
771+
result2.push(item2);
772+
}
773+
return result2;
774+
}
728775
}

test/playwright/playwright-util.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ function getResponseBody(action) {
139139
case 'modelNames': return ['Mock Model'];
140140
case 'modelFieldNames': return [...getMockModelFields().keys()];
141141
case 'canAddNotes': return [true, true];
142+
case 'canAddNotesWithErrorDetail': return [{canAdd: true}, {canAdd: true}];
142143
case 'storeMediaFile': return 'mock_audio.mp3';
143144
case 'addNote': return 102312488912;
144145
case 'multi': return [];

types/ext/anki.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,8 @@ export type MessageBody = {
8383
version: number;
8484
key?: string;
8585
};
86+
87+
export type CanAddNotesDetail = {
88+
canAdd: boolean;
89+
error: string | null;
90+
};

0 commit comments

Comments
 (0)