summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2022-05-30 12:03:24 -0400
committerGitHub <noreply@github.com>2022-05-30 12:03:24 -0400
commit19bba07a8bccb51a9db85c13fd921d825defe753 (patch)
tree4354e2d3396f5957a005256a85f60d239ab30c0d
parent0b5d54e7c66c17383e23855a1c3d4dbb1ea817fc (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.css35
-rw-r--r--ext/data/schemas/options-schema.json7
-rw-r--r--ext/js/background/backend.js3
-rw-r--r--ext/js/comm/anki-connect.js13
-rw-r--r--ext/js/data/options-util.js2
-rw-r--r--ext/js/dom/dom-data-binder.js61
-rw-r--r--ext/js/pages/settings/anki-controller.js29
-rw-r--r--ext/settings.html9
-rw-r--r--test/test-options-util.js3
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,