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
This commit is contained in:
parent
0b5d54e7c6
commit
19bba07a8b
@ -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);
|
||||
|
@ -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": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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');
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,24 +180,37 @@ class DOMDataBinder {
|
||||
}
|
||||
|
||||
_getElementValue(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;
|
||||
case 'TEXTAREA':
|
||||
case 'SELECT':
|
||||
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':
|
||||
{
|
||||
let {type} = element;
|
||||
if (type === 'password') { type = 'text'; }
|
||||
return type;
|
||||
}
|
||||
case 'TEXTAREA':
|
||||
return 'textarea';
|
||||
case 'SELECT':
|
||||
return 'select';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Utilities
|
||||
|
||||
static convertToNumber(value, constraints) {
|
||||
|
@ -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;
|
||||
|
@ -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">
|
||||
|
@ -454,7 +454,8 @@ function createProfileOptionsUpdatedTestData1() {
|
||||
checkForDuplicates: true,
|
||||
fieldTemplates: null,
|
||||
suspendNewCards: false,
|
||||
noteGuiMode: 'browse'
|
||||
noteGuiMode: 'browse',
|
||||
apiKey: ''
|
||||
},
|
||||
sentenceParsing: {
|
||||
scanExtent: 200,
|
||||
|
Loading…
Reference in New Issue
Block a user