diff options
| author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2022-05-30 12:03:24 -0400 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-05-30 12:03:24 -0400 | 
| commit | 19bba07a8bccb51a9db85c13fd921d825defe753 (patch) | |
| tree | 4354e2d3396f5957a005256a85f60d239ab30c0d | |
| parent | 0b5d54e7c66c17383e23855a1c3d4dbb1ea817fc (diff) | |
Add support for Anki API key (#2169)
* Update material.css to support password fields
* Support password
* Add "apiKey" setting
* Use apiKey
* Update options if API key changes
* Update tests
| -rw-r--r-- | ext/css/material.css | 35 | ||||
| -rw-r--r-- | ext/data/schemas/options-schema.json | 7 | ||||
| -rw-r--r-- | ext/js/background/backend.js | 3 | ||||
| -rw-r--r-- | ext/js/comm/anki-connect.js | 13 | ||||
| -rw-r--r-- | ext/js/data/options-util.js | 2 | ||||
| -rw-r--r-- | ext/js/dom/dom-data-binder.js | 61 | ||||
| -rw-r--r-- | ext/js/pages/settings/anki-controller.js | 29 | ||||
| -rw-r--r-- | ext/settings.html | 9 | ||||
| -rw-r--r-- | test/test-options-util.js | 3 | 
9 files changed, 120 insertions, 42 deletions
| diff --git a/ext/css/material.css b/ext/css/material.css index 4d8eda51..4d319349 100644 --- a/ext/css/material.css +++ b/ext/css/material.css @@ -723,7 +723,8 @@ select::-ms-expand {  /* Material design inputs */  input[type=text], -input[type=number] { +input[type=number], +input[type=password] {      width: var(--input-width);      height: var(--input-height);      line-height: var(--line-height); @@ -745,7 +746,8 @@ input[type=number]::-webkit-outer-spin-button {      -webkit-appearance: none;      margin: 0;  } -input[type=text] { +input[type=text], +input[type=password] {      width: var(--input-width-large);  }  textarea { @@ -763,23 +765,27 @@ select:invalid,  textarea:invalid,  input[type=text]:invalid,  input[type=number]:invalid, +input[type=password]:invalid,  select[data-invalid=true],  textarea[data-invalid=true],  input[type=text][data-invalid=true], -input[type=number][data-invalid=true] { +input[type=number][data-invalid=true], +input[type=password][data-invalid=true] {      border: var(--thin-border-size) solid var(--danger-color);  }  select,  textarea,  input[type=text], -input[type=number] { +input[type=number], +input[type=password] {      box-shadow: none;      transition: box-shadow calc(var(--animation-duration) / 2) linear;  }  select:focus,  textarea:focus,  input[type=text]:focus, -input[type=number]:focus { +input[type=number]:focus, +input[type=password]:focus {      box-shadow: 0 0 0 calc(2em / var(--font-size-no-units)) var(--input-outline-color);      outline: none;  } @@ -787,15 +793,18 @@ select:invalid:focus,  textarea:invalid:focus,  input[type=text]:invalid:focus,  input[type=number]:invalid:focus, +input[type=password]:invalid:focus,  select[data-invalid=true]:focus,  textarea[data-invalid=true]:focus,  input[type=text][data-invalid=true]:focus, -input[type=number][data-invalid=true]:focus { +input[type=number][data-invalid=true]:focus, +input[type=password][data-invalid=true]:focus {      box-shadow: 0 0 0 calc(2em / var(--font-size-no-units)) var(--danger-color);      outline: none;  }  input[type=text].code, -input[type=number].code { +input[type=number].code, +input[type=password].code {      font-family: 'Courier New', Courier, monospace;  } @@ -807,6 +816,7 @@ input[type=number].code {  }  .input-group>input[type=text],  .input-group>input[type=number], +.input-group>input[type=password],  .input-group>button.input-button {      flex: 1 1 auto;      border-top-right-radius: 0; @@ -815,6 +825,7 @@ input[type=number].code {      z-index: 1;  }  .input-suffix, +.button.input-suffix,  button.input-suffix {      display: flex;      flex-flow: row nowrap; @@ -828,11 +839,13 @@ button.input-suffix {      position: relative;  }  .input-suffix:not(:first-child), +.button.input-suffix:not(:first-child),  button.input-suffix:not(:first-child) {      border-top-left-radius: 0;      border-bottom-left-radius: 0;  }  .input-suffix:not(:last-child), +.button.input-suffix:not(:last-child),  button.input-suffix:not(:last-child) {      border-top-right-radius: 0;      border-bottom-right-radius: 0; @@ -842,8 +855,10 @@ button.input-suffix:not(:last-child) {  }  input[type=text]:invalid~.input-suffix:not(button),  input[type=number]:invalid~.input-suffix:not(button), +input[type=password]:invalid~.input-suffix:not(button),  input[type=text][data-invalid=true]~.input-suffix:not(button), -input[type=number][data-invalid=true]~.input-suffix:not(button) { +input[type=number][data-invalid=true]~.input-suffix:not(button), +input[type=password][data-invalid=true]~.input-suffix:not(button) {      border-color: var(--danger-color);      border-width: var(--thin-border-size);      border-style: solid; @@ -1079,8 +1094,10 @@ button.input-suffix.input-suffix-icon-button>.icon {  }  input[type=text]:invalid~button.input-suffix,  input[type=number]:invalid~button.input-suffix, +input[type=password]:invalid~button.input-suffix,  input[type=text][data-invalid=true]~button.input-suffix, -input[type=number][data-invalid=true]~button.input-suffix { +input[type=number][data-invalid=true]~button.input-suffix, +input[type=password][data-invalid=true]~button.input-suffix {      --button-border-color: var(--danger-color);      --button-hover-border-color: var(--danger-color);      --button-active-border-color: var(--danger-color); diff --git a/ext/data/schemas/options-schema.json b/ext/data/schemas/options-schema.json index 46d8a32a..215ca32c 100644 --- a/ext/data/schemas/options-schema.json +++ b/ext/data/schemas/options-schema.json @@ -845,7 +845,8 @@                                      "fieldTemplates",                                      "suspendNewCards",                                      "displayTags", -                                    "noteGuiMode" +                                    "noteGuiMode", +                                    "apiKey"                                  ],                                  "properties": {                                      "enable": { @@ -965,6 +966,10 @@                                          "type": "string",                                          "enum": ["browse", "edit"],                                          "default": "browse" +                                    }, +                                    "apiKey": { +                                        "type": "string", +                                        "default": ""                                      }                                  }                              }, diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index 07d6fd98..75ff7bee 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -998,8 +998,11 @@ class Backend {          const enabled = options.general.enable; +        let {apiKey} = options.anki; +        if (apiKey === '') { apiKey = null; }          this._anki.server = options.anki.server;          this._anki.enabled = options.anki.enable && enabled; +        this._anki.apiKey = apiKey;          this._mecab.setEnabled(options.parsing.enableMecabParser && enabled); diff --git a/ext/js/comm/anki-connect.js b/ext/js/comm/anki-connect.js index f5dc62f2..f0aff8fa 100644 --- a/ext/js/comm/anki-connect.js +++ b/ext/js/comm/anki-connect.js @@ -26,6 +26,7 @@ class AnkiConnect {          this._localVersion = 2;          this._remoteVersion = 0;          this._versionCheckPromise = null; +        this._apiKey = null;      }      get server() { @@ -44,6 +45,14 @@ class AnkiConnect {          this._enabled = value;      } +    get apiKey() { +        return this._apiKey; +    } + +    set apiKey(value) { +        this._apiKey = value; +    } +      async isConnected() {          try {              await this._invoke('version'); @@ -230,6 +239,8 @@ class AnkiConnect {      }      async _invoke(action, params) { +        const body = {action, params, version: this._localVersion}; +        if (this._apiKey !== null) { body.key = this._apiKey; }          let response;          try {              response = await fetch(this._server, { @@ -242,7 +253,7 @@ class AnkiConnect {                  },                  redirect: 'follow',                  referrerPolicy: 'no-referrer', -                body: JSON.stringify({action, params, version: this._localVersion}) +                body: JSON.stringify(body)              });          } catch (e) {              const error = new Error('Anki connection failure'); diff --git a/ext/js/data/options-util.js b/ext/js/data/options-util.js index f87bfa4b..f19094df 100644 --- a/ext/js/data/options-util.js +++ b/ext/js/data/options-util.js @@ -952,8 +952,10 @@ class OptionsUtil {      _updateVersion19(options) {          // Version 19 changes:          //  Added anki.noteGuiMode. +        //  Added anki.apiKey.          for (const profile of options.profiles) {              profile.options.anki.noteGuiMode = 'browse'; +            profile.options.anki.apiKey = '';          }          return options;      } diff --git a/ext/js/dom/dom-data-binder.js b/ext/js/dom/dom-data-binder.js index 4f35ba33..185dc439 100644 --- a/ext/js/dom/dom-data-binder.js +++ b/ext/js/dom/dom-data-binder.js @@ -126,10 +126,9 @@ class DOMDataBinder {      _createObserver(element) {          const metadata = this._createElementMetadata(element); -        const nodeName = element.nodeName.toUpperCase();          const observer = {              element, -            type: (nodeName === 'INPUT' ? element.type : null), +            type: this._getNormalizedElementType(element),              value: null,              hasValue: false,              onChange: null, @@ -157,28 +156,21 @@ class DOMDataBinder {      _isObserverStale(element, observer) {          const {type, metadata} = observer; -        const nodeName = element.nodeName.toUpperCase();          return !( -            type === (nodeName === 'INPUT' ? element.type : null) && +            type === this._getNormalizedElementType(element) &&              this._compareElementMetadata(metadata, this._createElementMetadata(element))          );      }      _setElementValue(element, value) { -        switch (element.nodeName.toUpperCase()) { -            case 'INPUT': -                switch (element.type) { -                    case 'checkbox': -                        element.checked = value; -                        break; -                    case 'text': -                    case 'number': -                        element.value = value; -                        break; -                } +        switch (this._getNormalizedElementType(element)) { +            case 'checkbox': +                element.checked = value;                  break; -            case 'TEXTAREA': -            case 'SELECT': +            case 'text': +            case 'number': +            case 'textarea': +            case 'select':                  element.value = value;                  break;          } @@ -188,22 +180,35 @@ class DOMDataBinder {      }      _getElementValue(element) { +        switch (this._getNormalizedElementType(element)) { +            case 'checkbox': +                return !!element.checked; +            case 'text': +                return `${element.value}`; +            case 'number': +                return DOMDataBinder.convertToNumber(element.value, element); +            case 'textarea': +            case 'select': +                return element.value; +        } +        return null; +    } + +    _getNormalizedElementType(element) {          switch (element.nodeName.toUpperCase()) {              case 'INPUT': -                switch (element.type) { -                    case 'checkbox': -                        return !!element.checked; -                    case 'text': -                        return `${element.value}`; -                    case 'number': -                        return DOMDataBinder.convertToNumber(element.value, element); -                } -                break; +            { +                let {type} = element; +                if (type === 'password') { type = 'text'; } +                return type; +            }              case 'TEXTAREA': +                return 'textarea';              case 'SELECT': -                return element.value; +                return 'select'; +            default: +                return null;          } -        return null;      }      // Utilities diff --git a/ext/js/pages/settings/anki-controller.js b/ext/js/pages/settings/anki-controller.js index d03fa535..cfbac0ea 100644 --- a/ext/js/pages/settings/anki-controller.js +++ b/ext/js/pages/settings/anki-controller.js @@ -61,6 +61,7 @@ class AnkiController {          this._ankiErrorInvalidResponseInfo = document.querySelector('#anki-error-invalid-response-info');          this._ankiEnableCheckbox = document.querySelector('[data-setting="anki.enable"]');          this._ankiCardPrimary = document.querySelector('#anki-card-primary'); +        const ankiApiKeyInput = document.querySelector('#anki-api-key-input');          const ankiCardPrimaryTypeRadios = document.querySelectorAll('input[type=radio][name=anki-card-primary-type]');          this._setupFieldMenus(); @@ -79,9 +80,17 @@ class AnkiController {          document.querySelector('#anki-error-log').addEventListener('click', this._onAnkiErrorLogLinkClick.bind(this)); -        const options = await this._settingsController.getOptions(); +        ankiApiKeyInput.addEventListener('focus', this._onApiKeyInputFocus.bind(this)); +        ankiApiKeyInput.addEventListener('blur', this._onApiKeyInputBlur.bind(this)); + +        const onAnkiSettingChanged = () => { this._updateOptions(); }; +        const nodes = [ankiApiKeyInput, ...document.querySelectorAll('[data-setting="anki.enable"]')]; +        for (const node of nodes) { +            node.addEventListener('settingChanged', onAnkiSettingChanged); +        } + +        await this._updateOptions();          this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); -        this._onOptionsChanged({options});      }      getFieldMarkers(type) { @@ -164,9 +173,17 @@ class AnkiController {      // Private +    async _updateOptions() { +        const options = await this._settingsController.getOptions(); +        this._onOptionsChanged({options}); +    } +      async _onOptionsChanged({options: {anki}}) { +        let {apiKey} = anki; +        if (apiKey === '') { apiKey = null; }          this._ankiConnect.server = anki.server;          this._ankiConnect.enabled = anki.enable; +        this._ankiConnect.apiKey = apiKey;          this._selectorObserver.disconnect();          this._selectorObserver.observe(document.documentElement, true); @@ -202,6 +219,14 @@ class AnkiController {          this._testAnkiNoteViewerSafe(e.currentTarget.dataset.mode);      } +    _onApiKeyInputFocus(e) { +        e.currentTarget.type = 'text'; +    } + +    _onApiKeyInputBlur(e) { +        e.currentTarget.type = 'password'; +    } +      _setAnkiCardPrimaryType(ankiCardType, ankiCardMenu) {          if (this._ankiCardPrimary === null) { return; }          this._ankiCardPrimary.dataset.ankiCardType = ankiCardType; diff --git a/ext/settings.html b/ext/settings.html index 95ab8a03..3284117f 100644 --- a/ext/settings.html +++ b/ext/settings.html @@ -1653,6 +1653,15 @@                  >              </div>          </div></div> +        <div class="settings-item advanced-only"><div class="settings-item-inner settings-item-inner-wrappable"> +            <div class="settings-item-left"> +                <div class="settings-item-label">API key</div> +                <div class="settings-item-description">Pass a secret value to AnkiConnect API calls.</div> +            </div> +            <div class="settings-item-right"> +                <input type="password" placeholder="Disabled" spellcheck="false" autocomplete="off" data-setting="anki.apiKey" id="anki-api-key-input"> +            </div> +        </div></div>          <div class="settings-item advanced-only">              <div class="settings-item-inner">                  <div class="settings-item-left"> diff --git a/test/test-options-util.js b/test/test-options-util.js index 425201ce..c4f9a3a9 100644 --- a/test/test-options-util.js +++ b/test/test-options-util.js @@ -454,7 +454,8 @@ function createProfileOptionsUpdatedTestData1() {              checkForDuplicates: true,              fieldTemplates: null,              suspendNewCards: false, -            noteGuiMode: 'browse' +            noteGuiMode: 'browse', +            apiKey: ''          },          sentenceParsing: {              scanExtent: 200, |