diff options
| -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          });      } |