diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js
index eae5fbe4..e1399f66 100644
--- a/ext/bg/js/anki-note-builder.js
+++ b/ext/bg/js/anki-note-builder.js
@@ -117,7 +117,11 @@ class AnkiNoteBuilder {
try {
return await this._renderTemplate(templates, data, marker);
} 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}`;
}
});
diff --git a/ext/bg/js/anki.js b/ext/bg/js/anki.js
index 68d9fc43..251e0e0c 100644
--- a/ext/bg/js/anki.js
+++ b/ext/bg/js/anki.js
@@ -162,22 +162,49 @@ class AnkiConnect {
}
async _invoke(action, params) {
- const response = await fetch(this._server, {
- method: 'POST',
- mode: 'cors',
- cache: 'default',
- credentials: 'omit',
- redirect: 'follow',
- referrerPolicy: 'no-referrer',
- body: JSON.stringify({action, params, version: this._localVersion})
- });
- const result = await response.json();
+ let response;
+ try {
+ response = await fetch(this._server, {
+ method: 'POST',
+ mode: 'cors',
+ cache: 'default',
+ credentials: 'omit',
+ redirect: 'follow',
+ referrerPolicy: 'no-referrer',
+ 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)) {
- const error = result.error;
- if (typeof error !== 'undefined') {
- throw new Error(`AnkiConnect error: ${error}`);
+ const apiError = result.error;
+ if (typeof apiError !== 'undefined') {
+ const error = new Error(`Anki error: ${apiError}`);
+ error.data = {action, params, status: response.status, apiError};
+ throw error;
}
}
+
return result;
}
diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
index 9a8844c5..b0648ac5 100644
--- a/ext/bg/js/backend.js
+++ b/ext/bg/js/backend.js
@@ -1539,13 +1539,14 @@ class Backend {
let clipboardImageFileName = null;
let clipboardText = null;
let audioFileName = null;
+ const errors = [];
try {
if (screenshotDetails !== null) {
screenshotFileName = await this._injectAnkNoteScreenshot(ankiConnect, timestamp, definitionDetails, screenshotDetails);
}
} catch (e) {
- // NOP
+ errors.push(serializeError(e));
}
try {
@@ -1553,7 +1554,7 @@ class Backend {
clipboardImageFileName = await this._injectAnkNoteClipboardImage(ankiConnect, timestamp, definitionDetails);
}
} catch (e) {
- // NOP
+ errors.push(serializeError(e));
}
try {
@@ -1561,7 +1562,7 @@ class Backend {
clipboardText = await this._clipboardReader.getText();
}
} catch (e) {
- // NOP
+ errors.push(serializeError(e));
}
try {
@@ -1569,34 +1570,50 @@ class Backend {
audioFileName = await this._injectAnkNoteAudio(ankiConnect, timestamp, definitionDetails, audioDetails);
}
} 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) {
const {type, expression, reading} = definitionDetails;
- if (type === 'kanji') {
- throw new Error('Cannot inject audio for kanji');
- }
- if (!reading && !expression) {
- throw new Error('Invalid reading and expression');
+ if (
+ type === 'kanji' ||
+ typeof expression !== 'string' ||
+ typeof reading !== 'string' ||
+ (expression.length === 0 && reading.length === 0)
+ ) {
+ return null;
}
const {sources, customSourceUrl, customSourceType} = details;
- const data = await this._downloadDefinitionAudio(
- sources,
- expression,
- reading,
- {
- textToSpeechVoice: null,
- customSourceUrl,
- customSourceType,
- binary: true,
- disableCache: true
- }
- );
+ let data;
+ try {
+ data = await this._downloadDefinitionAudio(
+ sources,
+ expression,
+ reading,
+ {
+ textToSpeechVoice: null,
+ customSourceUrl,
+ customSourceType,
+ binary: true,
+ disableCache: true
+ }
+ );
+ } catch (e) {
+ // No audio
+ return null;
+ }
let fileName = this._generateAnkiNoteMediaFileName('yomichan_audio', '.mp3', timestamp, definitionDetails);
fileName = fileName.replace(/\]/g, '');
@@ -1611,7 +1628,9 @@ class Backend {
const {mediaType, data} = this._getDataUrlInfo(dataUrl);
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);
await ankiConnect.storeMediaFile(fileName, data);
@@ -1622,12 +1641,14 @@ class Backend {
async _injectAnkNoteClipboardImage(ankiConnect, timestamp, definitionDetails) {
const dataUrl = await this._clipboardReader.getImage();
if (dataUrl === null) {
- throw new Error('No clipboard image');
+ return null;
}
const {mediaType, data} = this._getDataUrlInfo(dataUrl);
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);
await ankiConnect.storeMediaFile(fileName, data);
diff --git a/ext/bg/js/clipboard-reader.js b/ext/bg/js/clipboard-reader.js
index 8065cb16..ae432246 100644
--- a/ext/bg/js/clipboard-reader.js
+++ b/ext/bg/js/clipboard-reader.js
@@ -73,7 +73,7 @@ class ClipboardReader {
const document = this._document;
if (document === null) {
- throw new Error('Not supported');
+ throw new Error('Clipboard reading not supported in this context');
}
let target = this._pasteTarget;
@@ -118,7 +118,7 @@ class ClipboardReader {
const document = this._document;
if (document === null) {
- throw new Error('Not supported');
+ throw new Error('Clipboard reading not supported in this context');
}
let target = this._imagePasteTarget;
diff --git a/ext/bg/js/settings/anki-templates-controller.js b/ext/bg/js/settings/anki-templates-controller.js
index 125d8e16..31bd1e92 100644
--- a/ext/bg/js/settings/anki-templates-controller.js
+++ b/ext/bg/js/settings/anki-templates-controller.js
@@ -165,7 +165,7 @@ class AnkiTemplatesController {
async _validate(infoNode, field, mode, showSuccessResult, invalidateInput) {
const text = this._renderTextInput.value || '';
- const exceptions = [];
+ const errors = [];
let result = `No definition found for ${text}`;
try {
const optionsContext = this._settingsController.getOptionsContext();
@@ -193,20 +193,36 @@ class AnkiTemplatesController {
resultOutputMode,
glossaryLayoutMode,
compactTags,
- errors: exceptions
+ errors
});
result = note.fields.field;
}
} catch (e) {
- exceptions.push(e);
+ errors.push(e);
}
- const hasException = exceptions.length > 0;
- infoNode.hidden = !(showSuccessResult || hasException);
- infoNode.textContent = hasException ? exceptions.map((e) => `${e}`).join('\n') : (showSuccessResult ? result : '');
- infoNode.classList.toggle('text-danger', hasException);
+ const errorToMessageString = (e) => {
+ if (isObject(e)) {
+ let v = e.data;
+ 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) {
- this._fieldTemplatesTextarea.dataset.invalid = `${hasException}`;
+ this._fieldTemplatesTextarea.dataset.invalid = `${hasError}`;
}
}
}
diff --git a/ext/mixed/css/display.css b/ext/mixed/css/display.css
index 4a1e2324..e13d8f91 100644
--- a/ext/mixed/css/display.css
+++ b/ext/mixed/css/display.css
@@ -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 */
:root:not([data-enable-search-tags=true]) .tag[data-category=search] {
display: none;
diff --git a/ext/mixed/display-templates.html b/ext/mixed/display-templates.html
index 6b744271..0eb92282 100644
--- a/ext/mixed/display-templates.html
+++ b/ext/mixed/display-templates.html
@@ -145,6 +145,11 @@
+