diff options
| author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2022-05-29 21:24:41 -0400 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-05-29 21:24:41 -0400 | 
| commit | 331a2e62941e04a4d50a21faefed663a92ddc00a (patch) | |
| tree | 1212a1e7cd57ea2331fab2101afdc325cb3a4766 | |
| parent | f3024c50186344aa6a6b09500ea02540463ce5c9 (diff) | |
Add support for guiEditNote to view notes (#2143)
* Add AnkiConnect.guiEditNote
* Update _onApiNoteView to first try guiEditNote
* Add setting
* Update noteView API
* Use setting
* Return which mode was used
* Update DisplayGenerator
* Handle errors in DisplayAnki
* Update docs
* Add isErrorUnsupportedAction function
* Add an allowFallback option to noteView
* Disambiguate
* Simplify now that preferredMode isn't used
* Update settings info
* Implement test buttons
* Update styles
* Update status visibility
* Wrap layout
* Update description
* Update date
| -rw-r--r-- | ext/css/settings.css | 22 | ||||
| -rw-r--r-- | ext/data/schemas/options-schema.json | 8 | ||||
| -rw-r--r-- | ext/display-templates.html | 4 | ||||
| -rw-r--r-- | ext/js/background/backend.js | 18 | ||||
| -rw-r--r-- | ext/js/comm/anki.js | 24 | ||||
| -rw-r--r-- | ext/js/comm/api.js | 4 | ||||
| -rw-r--r-- | ext/js/data/options-util.js | 12 | ||||
| -rw-r--r-- | ext/js/display/display-anki.js | 24 | ||||
| -rw-r--r-- | ext/js/display/display-generator.js | 38 | ||||
| -rw-r--r-- | ext/js/pages/settings/anki-controller.js | 58 | ||||
| -rw-r--r-- | ext/settings.html | 33 | ||||
| -rw-r--r-- | test/test-options-util.js | 5 | 
12 files changed, 222 insertions, 28 deletions
| diff --git a/ext/css/settings.css b/ext/css/settings.css index 19f587e9..62e979e0 100644 --- a/ext/css/settings.css +++ b/ext/css/settings.css @@ -2147,6 +2147,28 @@ button.hotkey-list-item-enabled-button[data-scope-count='0'] {      display: none;  } +.test-anki-note-viewer-container { +    margin-top: 0.85em; +    display: flex; +    flex-flow: row wrap; +    align-items: flex-start; +} +.test-anki-note-viewer-container>:nth-child(n+2) { +    margin-left: 0.5em; +} +.test-anki-note-viewer-button { +    flex: 0 0 auto; +} +.test-anki-note-viewer-results { +    align-self: center; +} +.test-anki-note-viewer-results[data-success=true] { +    color: var(--success-color); +} +.test-anki-note-viewer-results[data-success=false] { +    color: var(--danger-color); +} +  /* Dictionary settings */  .dictionary-list { diff --git a/ext/data/schemas/options-schema.json b/ext/data/schemas/options-schema.json index ef86f8c3..46d8a32a 100644 --- a/ext/data/schemas/options-schema.json +++ b/ext/data/schemas/options-schema.json @@ -844,7 +844,8 @@                                      "checkForDuplicates",                                      "fieldTemplates",                                      "suspendNewCards", -                                    "displayTags" +                                    "displayTags", +                                    "noteGuiMode"                                  ],                                  "properties": {                                      "enable": { @@ -959,6 +960,11 @@                                          "type": "string",                                          "enum": ["never", "always", "non-standard"],                                          "default": "never" +                                    }, +                                    "noteGuiMode": { +                                        "type": "string", +                                        "enum": ["browse", "edit"], +                                        "default": "browse"                                      }                                  }                              }, diff --git a/ext/display-templates.html b/ext/display-templates.html index 8b2e4450..8d147253 100644 --- a/ext/display-templates.html +++ b/ext/display-templates.html @@ -179,6 +179,10 @@      <ul class="anki-note-error-list"></ul>      <div class="anki-note-error-log-container"><a tabindex="0" class="anki-note-error-log-link">Log debug info to console</a></div>  </div></template> +<template id="footer-notification-anki-view-note-error-template" data-remove-whitespace-text="true"> +    Note viewer window could not be opened.<br> +    Check the <a href="/settings.html#!anki" target="_blank" rel="noopener"><em>Anki</em> › <em>Note viewer window</em></a> setting. +</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>      <div class="profile-list-item-name"></div> diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index cdbfde1e..07d6fd98 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -512,8 +512,22 @@ class Backend {          );      } -    async _onApiNoteView({noteId}) { -        return await this._anki.guiBrowseNote(noteId); +    async _onApiNoteView({noteId, mode, allowFallback}) { +        if (mode === 'edit') { +            try { +                await this._anki.guiEditNote(noteId); +                return 'edit'; +            } catch (e) { +                if (!this._anki.isErrorUnsupportedAction(e)) { +                    throw e; +                } else if (!allowFallback) { +                    throw new Error('Mode not supported'); +                } +            } +        } +        // Fallback +        await this._anki.guiBrowseNote(noteId); +        return 'browse';      }      async _onApiSuspendAnkiCardsForNote({noteId}) { diff --git a/ext/js/comm/anki.js b/ext/js/comm/anki.js index 7ffb747b..f5dc62f2 100644 --- a/ext/js/comm/anki.js +++ b/ext/js/comm/anki.js @@ -106,6 +106,15 @@ class AnkiConnect {      }      /** +     * Opens the note editor GUI. +     * @param {number} noteId The ID of the note. +     * @returns {Promise<null>} Nothing is returned. +     */ +    async guiEditNote(noteId) { +        return await this._invoke('guiEditNote', {note: noteId}); +    } + +    /**       * Stores a file with the specified base64-encoded content inside Anki's media folder.       * @param {string} fileName The name of the file.       * @param {string} content The base64-encoded content of the file. @@ -187,6 +196,21 @@ class AnkiConnect {          return actions.includes(action);      } +    /** +     * Checks if a specific error object corresponds to an unsupported action. +     * @param {Error} error An error object generated by an API call. +     * @returns {boolean} Whether or not the error indicates the action is not supported. +     */ +    isErrorUnsupportedAction(error) { +        if (error instanceof Error) { +            const {data} = error; +            if (isObject(data) && data.apiError === 'unsupported action') { +                return true; +            } +        } +        return false; +    } +      // Private      async _checkVersion() { diff --git a/ext/js/comm/api.js b/ext/js/comm/api.js index 75a01dd5..2ffe2d8c 100644 --- a/ext/js/comm/api.js +++ b/ext/js/comm/api.js @@ -60,8 +60,8 @@ class API {          return this._invoke('injectAnkiNoteMedia', {timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails, dictionaryMediaDetails});      } -    noteView(noteId) { -        return this._invoke('noteView', {noteId}); +    noteView(noteId, mode, allowFallback) { +        return this._invoke('noteView', {noteId, mode, allowFallback});      }      suspendAnkiCardsForNote(noteId) { diff --git a/ext/js/data/options-util.js b/ext/js/data/options-util.js index 593fed29..f87bfa4b 100644 --- a/ext/js/data/options-util.js +++ b/ext/js/data/options-util.js @@ -467,7 +467,8 @@ class OptionsUtil {              {async: false, update: this._updateVersion15.bind(this)},              {async: false, update: this._updateVersion16.bind(this)},              {async: false, update: this._updateVersion17.bind(this)}, -            {async: false, update: this._updateVersion18.bind(this)} +            {async: false, update: this._updateVersion18.bind(this)}, +            {async: false, update: this._updateVersion19.bind(this)}          ];          if (typeof targetVersion === 'number' && targetVersion < result.length) {              result.splice(targetVersion); @@ -947,4 +948,13 @@ class OptionsUtil {          }          return options;      } + +    _updateVersion19(options) { +        // Version 19 changes: +        //  Added anki.noteGuiMode. +        for (const profile of options.profiles) { +            profile.options.anki.noteGuiMode = 'browse'; +        } +        return options; +    }  } diff --git a/ext/js/display/display-anki.js b/ext/js/display/display-anki.js index 0af8831a..12133ad0 100644 --- a/ext/js/display/display-anki.js +++ b/ext/js/display/display-anki.js @@ -47,6 +47,7 @@ class DisplayAnki {          this._screenshotFormat = 'png';          this._screenshotQuality = 100;          this._scanLength = 10; +        this._noteGuiMode = 'browse';          this._noteTags = [];          this._modeOptions = new Map();          this._dictionaryEntryTypeModeMap = new Map([ @@ -132,7 +133,7 @@ class DisplayAnki {      _onOptionsUpdated({options}) {          const {              general: {resultOutputMode, glossaryLayoutMode, compactTags}, -            anki: {tags, duplicateScope, duplicateScopeCheckAllModels, suspendNewCards, checkForDuplicates, displayTags, kanji, terms, screenshot: {format, quality}}, +            anki: {tags, duplicateScope, duplicateScopeCheckAllModels, suspendNewCards, checkForDuplicates, displayTags, kanji, terms, noteGuiMode, screenshot: {format, quality}},              scanning: {length: scanLength}          } = options; @@ -147,6 +148,7 @@ class DisplayAnki {          this._screenshotFormat = format;          this._screenshotQuality = quality;          this._scanLength = scanLength; +        this._noteGuiMode = noteGuiMode;          this._noteTags = [...tags];          this._modeOptions.clear();          this._modeOptions.set('kanji', kanji); @@ -418,7 +420,9 @@ class DisplayAnki {          return error;      } -    _showErrorNotification(errors) { +    _showErrorNotification(errors, displayErrors) { +        if (typeof displayErrors === 'undefined') { displayErrors = errors; } +          if (this._errorNotificationEventListeners !== null) {              this._errorNotificationEventListeners.removeAllEventListeners();          } @@ -428,7 +432,7 @@ class DisplayAnki {              this._errorNotificationEventListeners = new EventListenerCollection();          } -        const content = this._display.displayGenerator.createAnkiNoteErrorsNotificationContent(errors); +        const content = this._display.displayGenerator.createAnkiNoteErrorsNotificationContent(displayErrors);          for (const node of content.querySelectorAll('.anki-note-error-log-link')) {              this._errorNotificationEventListeners.addEventListener(node, 'click', () => {                  console.log({ankiNoteErrors: errors}); @@ -634,10 +638,20 @@ class DisplayAnki {          }      } -    _viewNote(node) { +    async _viewNote(node) {          const noteIds = this._getNodeNoteIds(node);          if (noteIds.length === 0) { return; } -        yomichan.api.noteView(noteIds[0]); +        try { +            await yomichan.api.noteView(noteIds[0], this._noteGuiMode, false); +        } catch (e) { +            const displayErrors = ( +                e.message === 'Mode not supported' ? +                [this._display.displayGenerator.instantiateTemplateFragment('footer-notification-anki-view-note-error')] : +                void 0 +            ); +            this._showErrorNotification([e], displayErrors); +            return; +        }      }      _showViewNoteMenu(node) { diff --git a/ext/js/display/display-generator.js b/ext/js/display/display-generator.js index 95080e27..851808f2 100644 --- a/ext/js/display/display-generator.js +++ b/ext/js/display/display-generator.js @@ -220,23 +220,27 @@ class DisplayGenerator {          for (const error of errors) {              const div = document.createElement('li');              div.className = 'anki-note-error-message'; -            let message = isObject(error) && typeof error.message === 'string' ? error.message : `${error}`; -            let link = null; -            if (isObject(error) && isObject(error.data)) { -                const {referenceUrl} = error.data; -                if (typeof referenceUrl === 'string') { -                    message = message.trimEnd(); -                    if (!/[.!?]^/.test()) { message += '.'; } -                    message += ' '; -                    link = document.createElement('a'); -                    link.href = referenceUrl; -                    link.target = '_blank'; -                    link.rel = 'noreferrer noopener'; -                    link.textContent = 'More info'; +            if (error instanceof DocumentFragment || error instanceof Node) { +                div.appendChild(error); +            } else { +                let message = isObject(error) && typeof error.message === 'string' ? error.message : `${error}`; +                let link = null; +                if (isObject(error) && isObject(error.data)) { +                    const {referenceUrl} = error.data; +                    if (typeof referenceUrl === 'string') { +                        message = message.trimEnd(); +                        if (!/[.!?]^/.test()) { message += '.'; } +                        message += ' '; +                        link = document.createElement('a'); +                        link.href = referenceUrl; +                        link.target = '_blank'; +                        link.rel = 'noreferrer noopener'; +                        link.textContent = 'More info'; +                    }                  } +                this._setTextContent(div, message); +                if (link !== null) { div.appendChild(link); }              } -            this._setTextContent(div, message); -            if (link !== null) { div.appendChild(link); }              list.appendChild(div);          } @@ -251,6 +255,10 @@ class DisplayGenerator {          return this._templates.instantiate(name);      } +    instantiateTemplateFragment(name) { +        return this._templates.instantiateFragment(name); +    } +      // Private      _createTermHeadword(headword, headwordIndex, pronunciations) { diff --git a/ext/js/pages/settings/anki-controller.js b/ext/js/pages/settings/anki-controller.js index daf09143..d03fa535 100644 --- a/ext/js/pages/settings/anki-controller.js +++ b/ext/js/pages/settings/anki-controller.js @@ -71,6 +71,12 @@ class AnkiController {              input.addEventListener('change', this._onAnkiCardPrimaryTypeRadioChange.bind(this), false);          } +        const testAnkiNoteViewerButtons = document.querySelectorAll('.test-anki-note-viewer-button'); +        const onTestAnkiNoteViewerButtonClick = this._onTestAnkiNoteViewerButtonClick.bind(this); +        for (const button of testAnkiNoteViewerButtons) { +            button.addEventListener('click', onTestAnkiNoteViewerButtonClick, false); +        } +          document.querySelector('#anki-error-log').addEventListener('click', this._onAnkiErrorLogLinkClick.bind(this));          const options = await this._settingsController.getOptions(); @@ -192,6 +198,10 @@ class AnkiController {          console.log({error: this._ankiError});      } +    _onTestAnkiNoteViewerButtonClick(e) { +        this._testAnkiNoteViewerSafe(e.currentTarget.dataset.mode); +    } +      _setAnkiCardPrimaryType(ankiCardType, ankiCardMenu) {          if (this._ankiCardPrimary === null) { return; }          this._ankiCardPrimary.dataset.ankiCardType = ankiCardType; @@ -336,6 +346,54 @@ class AnkiController {          const stringComparer = this._stringComparer;          array.sort((a, b) => stringComparer.compare(a, b));      } + +    async _testAnkiNoteViewerSafe(mode) { +        this._setAnkiNoteViewerStatus(false, null); +        try { +            await this._testAnkiNoteViewer(mode); +        } catch (e) { +            this._setAnkiNoteViewerStatus(true, e); +            return; +        } +        this._setAnkiNoteViewerStatus(true, null); +    } + +    async _testAnkiNoteViewer(mode) { +        const queries = [ +            '"よむ" deck:current', +            '"よむ"', +            'deck:current', +            '' +        ]; + +        let noteId = null; +        for (const query of queries) { +            const notes = await yomichan.api.findAnkiNotes(query); +            if (notes.length > 0) { +                noteId = notes[0]; +                break; +            } +        } + +        if (noteId === null) { +            throw new Error('Could not find a note to test with'); +        } + +        await yomichan.api.noteView(noteId, mode, false); +    } + +    _setAnkiNoteViewerStatus(visible, error) { +        const node = document.querySelector('#test-anki-note-viewer-results'); +        if (visible) { +            const success = (error === null); +            node.textContent = success ? 'Success!' : error.message; +            node.dataset.success = `${success}`; +        } else { +            node.textContent = ''; +            delete node.dataset.success; +        } +        node.hidden = !visible; +    }  }  class AnkiCardController { diff --git a/ext/settings.html b/ext/settings.html index ac296b8d..1ad0c37d 100644 --- a/ext/settings.html +++ b/ext/settings.html @@ -1767,6 +1767,39 @@          <div class="settings-item advanced-only">              <div class="settings-item-inner">                  <div class="settings-item-left"> +                    <div class="settings-item-label">Note viewer window</div> +                    <div class="settings-item-description"> +                        Clicking the <em>View added note</em> button shows this window. +                        <a tabindex="0" class="more-toggle more-only" data-parent-distance="4">More…</a> +                    </div> +                </div> +                <div class="settings-item-right"> +                    <select data-setting="anki.noteGuiMode"> +                        <option value="browse">Card browser</option> +                        <option value="edit">Note editor</option> +                    </select> +                </div> +            </div> +            <div class="settings-item-children more" hidden> +                <p> +                    AnkiConnect releases after around 2022-05-29 support a new note editor window +                    which can be shown when clicking the <em>View added note</em> button. +                    This can be tested using the buttons below. +                    If an error occurs, Anki and/or AnkiConnect may need to be updated. +                </p> +                <div class="test-anki-note-viewer-container"> +                    <button class="test-anki-note-viewer-button" data-mode="browse">Test <em>Card browser</em></button> +                    <button class="test-anki-note-viewer-button" data-mode="edit">Test <em>Note editor</em></button> +                    <div class="test-anki-note-viewer-results" id="test-anki-note-viewer-results" hidden></div> +                </div> +                <p class="margin-above"> +                    <a tabindex="0" class="more-toggle" data-parent-distance="3">Less…</a> +                </p> +            </div> +        </div> +        <div class="settings-item advanced-only"> +            <div class="settings-item-inner"> +                <div class="settings-item-left">                      <div class="settings-item-label">                          Show card tags                          <a tabindex="0" class="more-toggle more-only" data-parent-distance="4">(?)</a> diff --git a/test/test-options-util.js b/test/test-options-util.js index e706b720..425201ce 100644 --- a/test/test-options-util.js +++ b/test/test-options-util.js @@ -453,7 +453,8 @@ function createProfileOptionsUpdatedTestData1() {              displayTags: 'never',              checkForDuplicates: true,              fieldTemplates: null, -            suspendNewCards: false +            suspendNewCards: false, +            noteGuiMode: 'browse'          },          sentenceParsing: {              scanExtent: 200, @@ -602,7 +603,7 @@ function createOptionsUpdatedTestData1() {              }          ],          profileCurrent: 0, -        version: 18, +        version: 19,          global: {              database: {                  prefixWildcardsSupported: false |