diff options
author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2021-01-30 12:33:29 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-01-30 12:33:29 -0500 |
commit | d0b8b605db93c51b5ce2501a482c57432b45bfa6 (patch) | |
tree | c42bf4f59d7945c8b9c42cd0677e3827d326a71a | |
parent | af6e9a8153c24d0400592005b31d56fecff67068 (diff) |
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
-rw-r--r-- | ext/bg/js/anki-note-builder.js | 6 | ||||
-rw-r--r-- | ext/bg/js/anki.js | 53 | ||||
-rw-r--r-- | ext/bg/js/backend.js | 71 | ||||
-rw-r--r-- | ext/bg/js/clipboard-reader.js | 4 | ||||
-rw-r--r-- | ext/bg/js/settings/anki-templates-controller.js | 32 | ||||
-rw-r--r-- | ext/mixed/css/display.css | 8 | ||||
-rw-r--r-- | ext/mixed/display-templates.html | 5 | ||||
-rw-r--r-- | ext/mixed/js/display-generator.js | 17 | ||||
-rw-r--r-- | ext/mixed/js/display.js | 106 |
9 files changed, 230 insertions, 72 deletions
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 @@ <template id="footer-notification-tag-details-template" data-remove-whitespace-text="true"> <div class="tag-details"></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> <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> diff --git a/ext/mixed/js/display-generator.js b/ext/mixed/js/display-generator.js index d7cc5bd4..70cbcf13 100644 --- a/ext/mixed/js/display-generator.js +++ b/ext/mixed/js/display-generator.js @@ -192,6 +192,23 @@ class DisplayGenerator { 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() { return this._templates.instantiate('profile-list-item'); } diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 15724fe4..c1044872 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -110,8 +110,10 @@ class Display extends EventDispatcher { this._frameResizeStartOffset = null; this._frameResizeEventListeners = new EventListenerCollection(); this._tagNotification = null; - this._tagNotificationContainer = document.querySelector('#content-footer'); + this._footerNotificationContainer = document.querySelector('#content-footer'); this._displayAudio = new DisplayAudio(this); + this._ankiNoteNotification = null; + this._ankiNoteNotificationEventListeners = null; this._hotkeyHandler.registerActions([ ['close', () => { this._onHotkeyClose(); }], @@ -525,6 +527,7 @@ class Display extends EventDispatcher { this._mediaLoader.unloadAll(); this._displayAudio.cleanupEntries(); this._hideTagNotification(false); + this._hideAnkiNoteErrors(false); this._definitions = []; this._definitionNodes = []; @@ -791,7 +794,7 @@ class Display extends EventDispatcher { if (this._tagNotification === null) { const node = this._displayGenerator.createEmptyFooterNotification(); 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); @@ -1170,38 +1173,85 @@ class Display extends EventDispatcher { } 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 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); try { const {anki: {suspendNewCards}} = this._options; const noteContext = this._getNoteContext(); - const note = await this._createNote(definition, mode, noteContext, true); - const noteId = await api.addAnkiNote(note); - if (noteId) { - if (suspendNewCards) { - try { - await api.suspendAnkiCardsForNote(noteId); - } catch (e) { - // NOP + const note = await this._createNote(definition, mode, noteContext, true, errors); + + let noteId = null; + let addNoteOkay = false; + try { + noteId = await api.addAnkiNote(note); + addNoteOkay = true; + } 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) { - this.onError(e); - return false; + errors.push(e); } finally { 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() { @@ -1372,7 +1422,7 @@ class Display extends EventDispatcher { const notePromises = []; for (const definition of definitions) { 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); } } @@ -1400,7 +1450,7 @@ class Display extends EventDispatcher { return results; } - async _createNote(definition, mode, context, injectMedia) { + async _createNote(definition, mode, context, injectMedia, errors) { const options = this._options; const templates = this._ankiFieldTemplates; const { @@ -1412,7 +1462,16 @@ class Display extends EventDispatcher { const {deck: deckName, model: modelName} = modeOptions; 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({ definition, @@ -1428,7 +1487,8 @@ class Display extends EventDispatcher { resultOutputMode, glossaryLayoutMode, compactTags, - injectedMedia + injectedMedia, + errors }); } |