Add note errors (#1329)

* Update _addAnkiNote to track errors

* Change comparison

* Update anki note adding to show errors

* Fix template

* Show errors when Anki card creation behaves unexpectedly

* Update some errors related to anki media injection

* Update addAnkiNote error handling

* Improve Anki errors

* Simplify error messages related to template rendering
This commit is contained in:
toasted-nutbread 2021-01-30 12:33:29 -05:00 committed by GitHub
parent af6e9a8153
commit d0b8b605db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 230 additions and 72 deletions

View File

@ -117,7 +117,11 @@ class AnkiNoteBuilder {
try { try {
return await this._renderTemplate(templates, data, marker); return await this._renderTemplate(templates, data, marker);
} catch (e) { } catch (e) {
if (errors) { errors.push(e); } if (errors) {
const error = new Error(`Template render error for {${marker}}`);
error.data = {error: e};
errors.push(error);
}
return `{${marker}-render-error}`; return `{${marker}-render-error}`;
} }
}); });

View File

@ -162,22 +162,49 @@ class AnkiConnect {
} }
async _invoke(action, params) { async _invoke(action, params) {
const response = await fetch(this._server, { let response;
method: 'POST', try {
mode: 'cors', response = await fetch(this._server, {
cache: 'default', method: 'POST',
credentials: 'omit', mode: 'cors',
redirect: 'follow', cache: 'default',
referrerPolicy: 'no-referrer', credentials: 'omit',
body: JSON.stringify({action, params, version: this._localVersion}) redirect: 'follow',
}); referrerPolicy: 'no-referrer',
const result = await response.json(); body: JSON.stringify({action, params, version: this._localVersion})
});
} catch (e) {
const error = new Error('Anki connection failure');
error.data = {action, params};
throw error;
}
if (!response.ok) {
const error = new Error(`Anki connection error: ${response.status}`);
error.data = {action, params, status: response.status};
throw error;
}
let responseText = null;
let result;
try {
responseText = await response.text();
result = JSON.parse(responseText);
} catch (e) {
const error = new Error('Invalid Anki response');
error.data = {action, params, status: response.status, responseText};
throw error;
}
if (isObject(result)) { if (isObject(result)) {
const error = result.error; const apiError = result.error;
if (typeof error !== 'undefined') { if (typeof apiError !== 'undefined') {
throw new Error(`AnkiConnect error: ${error}`); const error = new Error(`Anki error: ${apiError}`);
error.data = {action, params, status: response.status, apiError};
throw error;
} }
} }
return result; return result;
} }

View File

@ -1539,13 +1539,14 @@ class Backend {
let clipboardImageFileName = null; let clipboardImageFileName = null;
let clipboardText = null; let clipboardText = null;
let audioFileName = null; let audioFileName = null;
const errors = [];
try { try {
if (screenshotDetails !== null) { if (screenshotDetails !== null) {
screenshotFileName = await this._injectAnkNoteScreenshot(ankiConnect, timestamp, definitionDetails, screenshotDetails); screenshotFileName = await this._injectAnkNoteScreenshot(ankiConnect, timestamp, definitionDetails, screenshotDetails);
} }
} catch (e) { } catch (e) {
// NOP errors.push(serializeError(e));
} }
try { try {
@ -1553,7 +1554,7 @@ class Backend {
clipboardImageFileName = await this._injectAnkNoteClipboardImage(ankiConnect, timestamp, definitionDetails); clipboardImageFileName = await this._injectAnkNoteClipboardImage(ankiConnect, timestamp, definitionDetails);
} }
} catch (e) { } catch (e) {
// NOP errors.push(serializeError(e));
} }
try { try {
@ -1561,7 +1562,7 @@ class Backend {
clipboardText = await this._clipboardReader.getText(); clipboardText = await this._clipboardReader.getText();
} }
} catch (e) { } catch (e) {
// NOP errors.push(serializeError(e));
} }
try { try {
@ -1569,34 +1570,50 @@ class Backend {
audioFileName = await this._injectAnkNoteAudio(ankiConnect, timestamp, definitionDetails, audioDetails); audioFileName = await this._injectAnkNoteAudio(ankiConnect, timestamp, definitionDetails, audioDetails);
} }
} catch (e) { } catch (e) {
// NOP errors.push(serializeError(e));
} }
return {screenshotFileName, clipboardImageFileName, clipboardText, audioFileName}; return {
result: {
screenshotFileName,
clipboardImageFileName,
clipboardText,
audioFileName
},
errors
};
} }
async _injectAnkNoteAudio(ankiConnect, timestamp, definitionDetails, details) { async _injectAnkNoteAudio(ankiConnect, timestamp, definitionDetails, details) {
const {type, expression, reading} = definitionDetails; const {type, expression, reading} = definitionDetails;
if (type === 'kanji') { if (
throw new Error('Cannot inject audio for kanji'); type === 'kanji' ||
} typeof expression !== 'string' ||
if (!reading && !expression) { typeof reading !== 'string' ||
throw new Error('Invalid reading and expression'); (expression.length === 0 && reading.length === 0)
) {
return null;
} }
const {sources, customSourceUrl, customSourceType} = details; const {sources, customSourceUrl, customSourceType} = details;
const data = await this._downloadDefinitionAudio( let data;
sources, try {
expression, data = await this._downloadDefinitionAudio(
reading, sources,
{ expression,
textToSpeechVoice: null, reading,
customSourceUrl, {
customSourceType, textToSpeechVoice: null,
binary: true, customSourceUrl,
disableCache: true customSourceType,
} binary: true,
); disableCache: true
}
);
} catch (e) {
// No audio
return null;
}
let fileName = this._generateAnkiNoteMediaFileName('yomichan_audio', '.mp3', timestamp, definitionDetails); let fileName = this._generateAnkiNoteMediaFileName('yomichan_audio', '.mp3', timestamp, definitionDetails);
fileName = fileName.replace(/\]/g, ''); fileName = fileName.replace(/\]/g, '');
@ -1611,7 +1628,9 @@ class Backend {
const {mediaType, data} = this._getDataUrlInfo(dataUrl); const {mediaType, data} = this._getDataUrlInfo(dataUrl);
const extension = this._mediaUtility.getFileExtensionFromImageMediaType(mediaType); const extension = this._mediaUtility.getFileExtensionFromImageMediaType(mediaType);
if (extension === null) { throw new Error('Unknown image media type'); } if (extension === null) {
throw new Error('Unknown media type for screenshot image');
}
const fileName = this._generateAnkiNoteMediaFileName('yomichan_browser_screenshot', extension, timestamp, definitionDetails); const fileName = this._generateAnkiNoteMediaFileName('yomichan_browser_screenshot', extension, timestamp, definitionDetails);
await ankiConnect.storeMediaFile(fileName, data); await ankiConnect.storeMediaFile(fileName, data);
@ -1622,12 +1641,14 @@ class Backend {
async _injectAnkNoteClipboardImage(ankiConnect, timestamp, definitionDetails) { async _injectAnkNoteClipboardImage(ankiConnect, timestamp, definitionDetails) {
const dataUrl = await this._clipboardReader.getImage(); const dataUrl = await this._clipboardReader.getImage();
if (dataUrl === null) { if (dataUrl === null) {
throw new Error('No clipboard image'); return null;
} }
const {mediaType, data} = this._getDataUrlInfo(dataUrl); const {mediaType, data} = this._getDataUrlInfo(dataUrl);
const extension = this._mediaUtility.getFileExtensionFromImageMediaType(mediaType); const extension = this._mediaUtility.getFileExtensionFromImageMediaType(mediaType);
if (extension === null) { throw new Error('Unknown image media type'); } if (extension === null) {
throw new Error('Unknown media type for clipboard image');
}
const fileName = this._generateAnkiNoteMediaFileName('yomichan_clipboard_image', extension, timestamp, definitionDetails); const fileName = this._generateAnkiNoteMediaFileName('yomichan_clipboard_image', extension, timestamp, definitionDetails);
await ankiConnect.storeMediaFile(fileName, data); await ankiConnect.storeMediaFile(fileName, data);

View File

@ -73,7 +73,7 @@ class ClipboardReader {
const document = this._document; const document = this._document;
if (document === null) { if (document === null) {
throw new Error('Not supported'); throw new Error('Clipboard reading not supported in this context');
} }
let target = this._pasteTarget; let target = this._pasteTarget;
@ -118,7 +118,7 @@ class ClipboardReader {
const document = this._document; const document = this._document;
if (document === null) { if (document === null) {
throw new Error('Not supported'); throw new Error('Clipboard reading not supported in this context');
} }
let target = this._imagePasteTarget; let target = this._imagePasteTarget;

View File

@ -165,7 +165,7 @@ class AnkiTemplatesController {
async _validate(infoNode, field, mode, showSuccessResult, invalidateInput) { async _validate(infoNode, field, mode, showSuccessResult, invalidateInput) {
const text = this._renderTextInput.value || ''; const text = this._renderTextInput.value || '';
const exceptions = []; const errors = [];
let result = `No definition found for ${text}`; let result = `No definition found for ${text}`;
try { try {
const optionsContext = this._settingsController.getOptionsContext(); const optionsContext = this._settingsController.getOptionsContext();
@ -193,20 +193,36 @@ class AnkiTemplatesController {
resultOutputMode, resultOutputMode,
glossaryLayoutMode, glossaryLayoutMode,
compactTags, compactTags,
errors: exceptions errors
}); });
result = note.fields.field; result = note.fields.field;
} }
} catch (e) { } catch (e) {
exceptions.push(e); errors.push(e);
} }
const hasException = exceptions.length > 0; const errorToMessageString = (e) => {
infoNode.hidden = !(showSuccessResult || hasException); if (isObject(e)) {
infoNode.textContent = hasException ? exceptions.map((e) => `${e}`).join('\n') : (showSuccessResult ? result : ''); let v = e.data;
infoNode.classList.toggle('text-danger', hasException); if (isObject(v)) {
v = v.error;
if (isObject(v)) {
e = v;
}
}
v = e.message;
if (typeof v === 'string') { return v; }
}
return `${e}`;
};
const hasError = errors.length > 0;
infoNode.hidden = !(showSuccessResult || hasError);
infoNode.textContent = hasError ? errors.map(errorToMessageString).join('\n') : (showSuccessResult ? result : '');
infoNode.classList.toggle('text-danger', hasError);
if (invalidateInput) { if (invalidateInput) {
this._fieldTemplatesTextarea.dataset.invalid = `${hasException}`; this._fieldTemplatesTextarea.dataset.invalid = `${hasError}`;
} }
} }
} }

View File

@ -1675,6 +1675,14 @@ button.footer-notification-close-button:active {
} }
/* Anki errors */
.anki-note-error-list {
margin: 0;
padding-left: 1.5em;
list-style: disc;
}
/* Conditional styles */ /* Conditional styles */
:root:not([data-enable-search-tags=true]) .tag[data-category=search] { :root:not([data-enable-search-tags=true]) .tag[data-category=search] {
display: none; display: none;

View File

@ -145,6 +145,11 @@
<template id="footer-notification-tag-details-template" data-remove-whitespace-text="true"> <template id="footer-notification-tag-details-template" data-remove-whitespace-text="true">
<div class="tag-details"></div> <div class="tag-details"></div>
<div class="tag-details-disambiguation-list"></div> <div class="tag-details-disambiguation-list"></div>
</template>
<template id="footer-notification-anki-errors-content-template" data-remove-whitespace-text="true"><div class="anki-note-error-info">
<div class="anki-note-error-header"></div>
<ul class="anki-note-error-list"></ul>
<div class="anki-note-error-log-container"><a class="anki-note-error-log-link">Log debug info to console</a></div>
</div></template> </div></template>
<template id="profile-list-item-template"><label class="profile-list-item"> <template id="profile-list-item-template"><label class="profile-list-item">
<div class="profile-list-item-selection"><label class="radio"><input type="radio" class="profile-entry-is-default-radio" name="profile-entry-default-radio"><span class="radio-body"><span class="radio-border"></span><span class="radio-dot"></span></span></label></div> <div class="profile-list-item-selection"><label class="radio"><input type="radio" class="profile-entry-is-default-radio" name="profile-entry-default-radio"><span class="radio-body"><span class="radio-border"></span><span class="radio-dot"></span></span></label></div>

View File

@ -192,6 +192,23 @@ class DisplayGenerator {
return node; return node;
} }
createAnkiNoteErrorsNotificationContent(errors) {
const content = this._templates.instantiate('footer-notification-anki-errors-content');
const header = content.querySelector('.anki-note-error-header');
header.textContent = (errors.length === 1 ? 'An error occurred:' : `${errors.length} errors occurred:`);
const list = content.querySelector('.anki-note-error-list');
for (const error of errors) {
const div = document.createElement('li');
div.className = 'anki-note-error-message';
div.textContent = isObject(error) && typeof error.message === 'string' ? error.message : `${error}`;
list.appendChild(div);
}
return content;
}
createProfileListItem() { createProfileListItem() {
return this._templates.instantiate('profile-list-item'); return this._templates.instantiate('profile-list-item');
} }

View File

@ -110,8 +110,10 @@ class Display extends EventDispatcher {
this._frameResizeStartOffset = null; this._frameResizeStartOffset = null;
this._frameResizeEventListeners = new EventListenerCollection(); this._frameResizeEventListeners = new EventListenerCollection();
this._tagNotification = null; this._tagNotification = null;
this._tagNotificationContainer = document.querySelector('#content-footer'); this._footerNotificationContainer = document.querySelector('#content-footer');
this._displayAudio = new DisplayAudio(this); this._displayAudio = new DisplayAudio(this);
this._ankiNoteNotification = null;
this._ankiNoteNotificationEventListeners = null;
this._hotkeyHandler.registerActions([ this._hotkeyHandler.registerActions([
['close', () => { this._onHotkeyClose(); }], ['close', () => { this._onHotkeyClose(); }],
@ -525,6 +527,7 @@ class Display extends EventDispatcher {
this._mediaLoader.unloadAll(); this._mediaLoader.unloadAll();
this._displayAudio.cleanupEntries(); this._displayAudio.cleanupEntries();
this._hideTagNotification(false); this._hideTagNotification(false);
this._hideAnkiNoteErrors(false);
this._definitions = []; this._definitions = [];
this._definitionNodes = []; this._definitionNodes = [];
@ -791,7 +794,7 @@ class Display extends EventDispatcher {
if (this._tagNotification === null) { if (this._tagNotification === null) {
const node = this._displayGenerator.createEmptyFooterNotification(); const node = this._displayGenerator.createEmptyFooterNotification();
node.classList.add('click-scannable'); node.classList.add('click-scannable');
this._tagNotification = new DisplayNotification(this._tagNotificationContainer, node); this._tagNotification = new DisplayNotification(this._footerNotificationContainer, node);
} }
const content = this._displayGenerator.createTagFooterNotificationDetails(tagNode); const content = this._displayGenerator.createTagFooterNotificationDetails(tagNode);
@ -1170,38 +1173,85 @@ class Display extends EventDispatcher {
} }
async _addAnkiNote(definitionIndex, mode) { async _addAnkiNote(definitionIndex, mode) {
if (definitionIndex < 0 || definitionIndex >= this._definitions.length) { return false; } if (definitionIndex < 0 || definitionIndex >= this._definitions.length) { return; }
const definition = this._definitions[definitionIndex]; const definition = this._definitions[definitionIndex];
const button = this._adderButtonFind(definitionIndex, mode); const button = this._adderButtonFind(definitionIndex, mode);
if (button === null || button.disabled) { return false; } if (button === null || button.disabled) { return; }
this._hideAnkiNoteErrors(true);
const errors = [];
const overrideToken = this._progressIndicatorVisible.setOverride(true); const overrideToken = this._progressIndicatorVisible.setOverride(true);
try { try {
const {anki: {suspendNewCards}} = this._options; const {anki: {suspendNewCards}} = this._options;
const noteContext = this._getNoteContext(); const noteContext = this._getNoteContext();
const note = await this._createNote(definition, mode, noteContext, true); const note = await this._createNote(definition, mode, noteContext, true, errors);
const noteId = await api.addAnkiNote(note);
if (noteId) { let noteId = null;
if (suspendNewCards) { let addNoteOkay = false;
try { try {
await api.suspendAnkiCardsForNote(noteId); noteId = await api.addAnkiNote(note);
} catch (e) { addNoteOkay = true;
// NOP } catch (e) {
errors.length = 0;
errors.push(e);
}
if (addNoteOkay) {
if (noteId === null) {
errors.push(new Error('Note could not be added'));
} else {
if (suspendNewCards) {
try {
await api.suspendAnkiCardsForNote(noteId);
} catch (e) {
errors.push(e);
}
} }
button.disabled = true;
this._viewerButtonShow(definitionIndex, noteId);
} }
button.disabled = true;
this._viewerButtonShow(definitionIndex, noteId);
} else {
throw new Error('Note could not be added');
} }
} catch (e) { } catch (e) {
this.onError(e); errors.push(e);
return false;
} finally { } finally {
this._progressIndicatorVisible.clearOverride(overrideToken); this._progressIndicatorVisible.clearOverride(overrideToken);
} }
return true;
if (errors.length > 0) {
this._showAnkiNoteErrors(errors);
} else {
this._hideAnkiNoteErrors(true);
}
}
_showAnkiNoteErrors(errors) {
if (this._ankiNoteNotificationEventListeners !== null) {
this._ankiNoteNotificationEventListeners.removeAllEventListeners();
}
if (this._ankiNoteNotification === null) {
const node = this._displayGenerator.createEmptyFooterNotification();
this._ankiNoteNotification = new DisplayNotification(this._footerNotificationContainer, node);
this._ankiNoteNotificationEventListeners = new EventListenerCollection();
}
const content = this._displayGenerator.createAnkiNoteErrorsNotificationContent(errors);
for (const node of content.querySelectorAll('.anki-note-error-log-link')) {
this._ankiNoteNotificationEventListeners.addEventListener(node, 'click', () => {
console.log({ankiNoteErrors: errors});
}, false);
}
this._ankiNoteNotification.setContent(content);
this._ankiNoteNotification.open();
}
_hideAnkiNoteErrors(animate) {
if (this._ankiNoteNotification === null) { return; }
this._ankiNoteNotification.close(animate);
this._ankiNoteNotificationEventListeners.removeAllEventListeners();
} }
async _playAudioCurrent() { async _playAudioCurrent() {
@ -1372,7 +1422,7 @@ class Display extends EventDispatcher {
const notePromises = []; const notePromises = [];
for (const definition of definitions) { for (const definition of definitions) {
for (const mode of modes) { for (const mode of modes) {
const notePromise = this._createNote(definition, mode, context, false); const notePromise = this._createNote(definition, mode, context, false, null);
notePromises.push(notePromise); notePromises.push(notePromise);
} }
} }
@ -1400,7 +1450,7 @@ class Display extends EventDispatcher {
return results; return results;
} }
async _createNote(definition, mode, context, injectMedia) { async _createNote(definition, mode, context, injectMedia, errors) {
const options = this._options; const options = this._options;
const templates = this._ankiFieldTemplates; const templates = this._ankiFieldTemplates;
const { const {
@ -1412,7 +1462,16 @@ class Display extends EventDispatcher {
const {deck: deckName, model: modelName} = modeOptions; const {deck: deckName, model: modelName} = modeOptions;
const fields = Object.entries(modeOptions.fields); const fields = Object.entries(modeOptions.fields);
const injectedMedia = (injectMedia ? await this._injectAnkiNoteMedia(definition, mode, options, fields) : null); let injectedMedia = null;
if (injectMedia) {
let errors2;
({result: injectedMedia, errors: errors2} = await this._injectAnkiNoteMedia(definition, mode, options, fields));
if (Array.isArray(errors)) {
for (const error of errors2) {
errors.push(deserializeError(error));
}
}
}
return await this._ankiNoteBuilder.createNote({ return await this._ankiNoteBuilder.createNote({
definition, definition,
@ -1428,7 +1487,8 @@ class Display extends EventDispatcher {
resultOutputMode, resultOutputMode,
glossaryLayoutMode, glossaryLayoutMode,
compactTags, compactTags,
injectedMedia injectedMedia,
errors
}); });
} }