Skip to content

Commit

Permalink
Add an option to allow both viewing and adding duplicates (#693)
Browse files Browse the repository at this point in the history
* Detect duplicates when checking if can add note

* Display the stacked add buttons
  • Loading branch information
eloyrobillard authored Mar 18, 2024
1 parent 4fe881d commit 7ee76d7
Show file tree
Hide file tree
Showing 14 changed files with 1,302 additions and 1,538 deletions.
2 changes: 2 additions & 0 deletions ext/css/display.css
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,8 @@ button.action-button:active {
.icon[data-icon=view-note] { background-image: url('/images/view-note.svg'); }
.icon[data-icon=add-term-kanji] { background-image: url('/images/add-term-kanji.svg'); }
.icon[data-icon=add-term-kana] { background-image: url('/images/add-term-kana.svg'); }
.icon[data-icon=add-duplicate-term-kanji] { background-image: url('/images/add-duplicate-term-kanji.svg'); }
.icon[data-icon=add-duplicate-term-kana] { background-image: url('/images/add-duplicate-term-kana.svg'); }
.icon[data-icon=play-audio] { background-image: url('/images/play-audio.svg'); }
.icon[data-icon=source-term] { background-image: url('/images/source-term.svg'); }
.icon[data-icon=entry-current] { background-image: url('/images/entry-current.svg'); }
Expand Down
40 changes: 40 additions & 0 deletions ext/images/add-duplicate-term-kana.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
84 changes: 84 additions & 0 deletions ext/images/add-duplicate-term-kanji.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
87 changes: 77 additions & 10 deletions ext/js/background/backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -544,22 +544,89 @@ export class Backend {
return await this._anki.addNote(note);
}

/**
* @param {import('anki').Note[]} notes
* @returns {Promise<({ canAdd: true; } | { canAdd: false; error: string; })[]>}
*/
async detectDuplicateNotes(notes) {
// `allowDuplicate` is on for all notes by default, so we temporarily set it to false
// to check which notes are duplicates.
const notesNoDuplicatesAllowed = notes.map((note) => ({...note, options: {...note.options, allowDuplicate: false}}));

return await this._anki.canAddNotesWithErrorDetail(notesNoDuplicatesAllowed);
}

/**
* Partitions notes between those that can / cannot be added.
* It further sets the `isDuplicate` strings for notes that have a duplicate.
* @param {import('anki').Note[]} notes
* @returns {Promise<import('backend').CanAddResults>}
*/
async partitionAddibleNotes(notes) {
const canAddResults = await this.detectDuplicateNotes(notes);

/** @type {{ note: import('anki').Note, isDuplicate: boolean }[]} */
const canAddArray = [];

/** @type {import('anki').Note[]} */
const cannotAddArray = [];

for (let i = 0; i < canAddResults.length; i++) {
const result = canAddResults[i];

// If the note is a duplicate, the error is "cannot create note because it is a duplicate".
if (result.canAdd) {
canAddArray.push({note: notes[i], isDuplicate: false});
} else if (result.error.endsWith('duplicate')) {
canAddArray.push({note: notes[i], isDuplicate: true});
} else {
cannotAddArray.push(notes[i]);
}
}

return {canAddArray, cannotAddArray};
}

/** @type {import('api').ApiHandler<'getAnkiNoteInfo'>} */
async _onApiGetAnkiNoteInfo({notes, fetchAdditionalInfo}) {
/** @type {import('anki').NoteInfoWrapper[]} */
const results = [];
const {canAddArray, cannotAddArray} = await this.partitionAddibleNotes(notes);

/** @type {{note: import('anki').Note, info: import('anki').NoteInfoWrapper}[]} */
const cannotAdd = [];
const canAddArray = await this._anki.canAddNotes(notes);
const cannotAdd = cannotAddArray.filter((note) => isNoteDataValid(note)).map((note) => ({note, info: {canAdd: false, valid: false, noteIds: null}}));

/** @type {import('anki').NoteInfoWrapper[]} */
const results = cannotAdd.map(({info}) => info);

/** @type {import('anki').Note[]} */
const duplicateNotes = [];

/** @type {number[]} */
const originalIndices = [];

for (let i = 0; i < canAddArray.length; i++) {
if (canAddArray[i].isDuplicate) {
duplicateNotes.push(canAddArray[i].note);
// Keep original indices to locate duplicate inside `duplicateNoteIds`
originalIndices.push(i);
}
}

const duplicateNoteIds = await this._anki.findNoteIds(duplicateNotes);

for (let i = 0; i < canAddArray.length; ++i) {
const {note, isDuplicate} = canAddArray[i];

for (let i = 0; i < notes.length; ++i) {
const note = notes[i];
let canAdd = canAddArray[i];
const valid = isNoteDataValid(note);
if (!valid) { canAdd = false; }
const info = {canAdd, valid, noteIds: null};

const info = {
canAdd: valid,
valid,
noteIds: isDuplicate ? duplicateNoteIds[originalIndices.indexOf(i)] : null
};

results.push(info);
if (!canAdd && valid) {

if (!valid) {
cannotAdd.push({note, info});
}
}
Expand Down
57 changes: 57 additions & 0 deletions ext/js/comm/anki-connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,17 @@ export class AnkiConnect {
return this._normalizeArray(result, notes.length, 'boolean');
}

/**
* @param {import('anki').Note[]} notes
* @returns {Promise<({ canAdd: true } | { canAdd: false, error: string })[]>}
*/
async canAddNotesWithErrorDetail(notes) {
if (!this._enabled) { return []; }
await this._checkVersion();
const result = await this._invoke('canAddNotesWithErrorDetail', {notes});
return this._normalizeCanAddNotesWithErrorDetailArray(result, notes.length);
}

/**
* @param {import('anki').NoteId[]} noteIds
* @returns {Promise<(?import('anki').NoteInfo)[]>}
Expand Down Expand Up @@ -579,6 +590,52 @@ export class AnkiConnect {
return /** @type {T[]} */ (result);
}

/**
* @param {unknown} result
* @param {number} expectedCount
* @returns {import('anki-connect.js').CanAddResult[]}
* @throws {Error}
*/
_normalizeCanAddNotesWithErrorDetailArray(result, expectedCount) {
if (!Array.isArray(result)) {
throw this._createUnexpectedResultError('array', result, '');
}
if (expectedCount !== result.length) {
throw this._createError(`Unexpected result array size: expected ${expectedCount}, received ${result.length}`, result);
}
/** @type {import('anki-connect.js').CanAddResult[]} */
const result2 = [];
for (let i = 0; i < expectedCount; ++i) {
const item = /** @type {unknown} */ (result[i]);
if (item === null || typeof item !== 'object') {
throw this._createError(`Unexpected result type at index ${i}: expected object, received ${this._getTypeName(item)}`, result);
}

const {canAdd, error} = /** @type {{[key: string]: unknown}} */ (item);
if (typeof canAdd !== 'boolean') {
throw this._createError(`Unexpected result type at index ${i}, field canAdd: expected boolean, received ${this._getTypeName(canAdd)}`, result);
}

if (canAdd && typeof error !== 'undefined') {
throw this._createError(`Unexpected result type at index ${i}, field error: expected undefined, received ${this._getTypeName(error)}`, result);
}
if (!canAdd && typeof error !== 'string') {
throw this._createError(`Unexpected result type at index ${i}, field error: expected string, received ${this._getTypeName(error)}`, result);
}

if (canAdd) {
result2.push({canAdd});
} else if (typeof error === 'string') {
const item2 = {
canAdd,
error
};
result2.push(item2);
}
}
return result2;
}

/**
* @param {unknown} result
* @returns {(?import('anki').NoteInfo)[]}
Expand Down
3 changes: 1 addition & 2 deletions ext/js/data/anki-note-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export class AnkiNoteBuilder {
fields,
tags = [],
requirements = [],
checkForDuplicates = true,
duplicateScope = 'collection',
duplicateScopeCheckAllModels = false,
resultOutputMode = 'split',
Expand Down Expand Up @@ -111,7 +110,7 @@ export class AnkiNoteBuilder {
deckName,
modelName,
options: {
allowDuplicate: !checkForDuplicates,
allowDuplicate: true,
duplicateScope,
duplicateScopeOptions: {
deckName: duplicateScopeDeckName,
Expand Down
Loading

0 comments on commit 7ee76d7

Please sign in to comment.