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 */
|
/* Material design inputs */
|
||||||
input[type=text],
|
input[type=text],
|
||||||
input[type=number] {
|
input[type=number],
|
||||||
|
input[type=password] {
|
||||||
width: var(--input-width);
|
width: var(--input-width);
|
||||||
height: var(--input-height);
|
height: var(--input-height);
|
||||||
line-height: var(--line-height);
|
line-height: var(--line-height);
|
||||||
@ -745,7 +746,8 @@ input[type=number]::-webkit-outer-spin-button {
|
|||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
input[type=text] {
|
input[type=text],
|
||||||
|
input[type=password] {
|
||||||
width: var(--input-width-large);
|
width: var(--input-width-large);
|
||||||
}
|
}
|
||||||
textarea {
|
textarea {
|
||||||
@ -763,23 +765,27 @@ select:invalid,
|
|||||||
textarea:invalid,
|
textarea:invalid,
|
||||||
input[type=text]:invalid,
|
input[type=text]:invalid,
|
||||||
input[type=number]:invalid,
|
input[type=number]:invalid,
|
||||||
|
input[type=password]:invalid,
|
||||||
select[data-invalid=true],
|
select[data-invalid=true],
|
||||||
textarea[data-invalid=true],
|
textarea[data-invalid=true],
|
||||||
input[type=text][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);
|
border: var(--thin-border-size) solid var(--danger-color);
|
||||||
}
|
}
|
||||||
select,
|
select,
|
||||||
textarea,
|
textarea,
|
||||||
input[type=text],
|
input[type=text],
|
||||||
input[type=number] {
|
input[type=number],
|
||||||
|
input[type=password] {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
transition: box-shadow calc(var(--animation-duration) / 2) linear;
|
transition: box-shadow calc(var(--animation-duration) / 2) linear;
|
||||||
}
|
}
|
||||||
select:focus,
|
select:focus,
|
||||||
textarea:focus,
|
textarea:focus,
|
||||||
input[type=text]: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);
|
box-shadow: 0 0 0 calc(2em / var(--font-size-no-units)) var(--input-outline-color);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
@ -787,15 +793,18 @@ select:invalid:focus,
|
|||||||
textarea:invalid:focus,
|
textarea:invalid:focus,
|
||||||
input[type=text]:invalid:focus,
|
input[type=text]:invalid:focus,
|
||||||
input[type=number]:invalid:focus,
|
input[type=number]:invalid:focus,
|
||||||
|
input[type=password]:invalid:focus,
|
||||||
select[data-invalid=true]:focus,
|
select[data-invalid=true]:focus,
|
||||||
textarea[data-invalid=true]:focus,
|
textarea[data-invalid=true]:focus,
|
||||||
input[type=text][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);
|
box-shadow: 0 0 0 calc(2em / var(--font-size-no-units)) var(--danger-color);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
input[type=text].code,
|
input[type=text].code,
|
||||||
input[type=number].code {
|
input[type=number].code,
|
||||||
|
input[type=password].code {
|
||||||
font-family: 'Courier New', Courier, monospace;
|
font-family: 'Courier New', Courier, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -807,6 +816,7 @@ input[type=number].code {
|
|||||||
}
|
}
|
||||||
.input-group>input[type=text],
|
.input-group>input[type=text],
|
||||||
.input-group>input[type=number],
|
.input-group>input[type=number],
|
||||||
|
.input-group>input[type=password],
|
||||||
.input-group>button.input-button {
|
.input-group>button.input-button {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
@ -815,6 +825,7 @@ input[type=number].code {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
.input-suffix,
|
.input-suffix,
|
||||||
|
.button.input-suffix,
|
||||||
button.input-suffix {
|
button.input-suffix {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
@ -828,11 +839,13 @@ button.input-suffix {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.input-suffix:not(:first-child),
|
.input-suffix:not(:first-child),
|
||||||
|
.button.input-suffix:not(:first-child),
|
||||||
button.input-suffix:not(:first-child) {
|
button.input-suffix:not(:first-child) {
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
}
|
}
|
||||||
.input-suffix:not(:last-child),
|
.input-suffix:not(:last-child),
|
||||||
|
.button.input-suffix:not(:last-child),
|
||||||
button.input-suffix:not(:last-child) {
|
button.input-suffix:not(:last-child) {
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
border-bottom-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=text]:invalid~.input-suffix:not(button),
|
||||||
input[type=number]: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=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-color: var(--danger-color);
|
||||||
border-width: var(--thin-border-size);
|
border-width: var(--thin-border-size);
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
@ -1079,8 +1094,10 @@ button.input-suffix.input-suffix-icon-button>.icon {
|
|||||||
}
|
}
|
||||||
input[type=text]:invalid~button.input-suffix,
|
input[type=text]:invalid~button.input-suffix,
|
||||||
input[type=number]: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=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-border-color: var(--danger-color);
|
||||||
--button-hover-border-color: var(--danger-color);
|
--button-hover-border-color: var(--danger-color);
|
||||||
--button-active-border-color: var(--danger-color);
|
--button-active-border-color: var(--danger-color);
|
||||||
|
@ -845,7 +845,8 @@
|
|||||||
"fieldTemplates",
|
"fieldTemplates",
|
||||||
"suspendNewCards",
|
"suspendNewCards",
|
||||||
"displayTags",
|
"displayTags",
|
||||||
"noteGuiMode"
|
"noteGuiMode",
|
||||||
|
"apiKey"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"enable": {
|
"enable": {
|
||||||
@ -965,6 +966,10 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["browse", "edit"],
|
"enum": ["browse", "edit"],
|
||||||
"default": "browse"
|
"default": "browse"
|
||||||
|
},
|
||||||
|
"apiKey": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -998,8 +998,11 @@ class Backend {
|
|||||||
|
|
||||||
const enabled = options.general.enable;
|
const enabled = options.general.enable;
|
||||||
|
|
||||||
|
let {apiKey} = options.anki;
|
||||||
|
if (apiKey === '') { apiKey = null; }
|
||||||
this._anki.server = options.anki.server;
|
this._anki.server = options.anki.server;
|
||||||
this._anki.enabled = options.anki.enable && enabled;
|
this._anki.enabled = options.anki.enable && enabled;
|
||||||
|
this._anki.apiKey = apiKey;
|
||||||
|
|
||||||
this._mecab.setEnabled(options.parsing.enableMecabParser && enabled);
|
this._mecab.setEnabled(options.parsing.enableMecabParser && enabled);
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ class AnkiConnect {
|
|||||||
this._localVersion = 2;
|
this._localVersion = 2;
|
||||||
this._remoteVersion = 0;
|
this._remoteVersion = 0;
|
||||||
this._versionCheckPromise = null;
|
this._versionCheckPromise = null;
|
||||||
|
this._apiKey = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get server() {
|
get server() {
|
||||||
@ -44,6 +45,14 @@ class AnkiConnect {
|
|||||||
this._enabled = value;
|
this._enabled = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get apiKey() {
|
||||||
|
return this._apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
set apiKey(value) {
|
||||||
|
this._apiKey = value;
|
||||||
|
}
|
||||||
|
|
||||||
async isConnected() {
|
async isConnected() {
|
||||||
try {
|
try {
|
||||||
await this._invoke('version');
|
await this._invoke('version');
|
||||||
@ -230,6 +239,8 @@ class AnkiConnect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _invoke(action, params) {
|
async _invoke(action, params) {
|
||||||
|
const body = {action, params, version: this._localVersion};
|
||||||
|
if (this._apiKey !== null) { body.key = this._apiKey; }
|
||||||
let response;
|
let response;
|
||||||
try {
|
try {
|
||||||
response = await fetch(this._server, {
|
response = await fetch(this._server, {
|
||||||
@ -242,7 +253,7 @@ class AnkiConnect {
|
|||||||
},
|
},
|
||||||
redirect: 'follow',
|
redirect: 'follow',
|
||||||
referrerPolicy: 'no-referrer',
|
referrerPolicy: 'no-referrer',
|
||||||
body: JSON.stringify({action, params, version: this._localVersion})
|
body: JSON.stringify(body)
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const error = new Error('Anki connection failure');
|
const error = new Error('Anki connection failure');
|
||||||
|
@ -952,8 +952,10 @@ class OptionsUtil {
|
|||||||
_updateVersion19(options) {
|
_updateVersion19(options) {
|
||||||
// Version 19 changes:
|
// Version 19 changes:
|
||||||
// Added anki.noteGuiMode.
|
// Added anki.noteGuiMode.
|
||||||
|
// Added anki.apiKey.
|
||||||
for (const profile of options.profiles) {
|
for (const profile of options.profiles) {
|
||||||
profile.options.anki.noteGuiMode = 'browse';
|
profile.options.anki.noteGuiMode = 'browse';
|
||||||
|
profile.options.anki.apiKey = '';
|
||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
@ -126,10 +126,9 @@ class DOMDataBinder {
|
|||||||
|
|
||||||
_createObserver(element) {
|
_createObserver(element) {
|
||||||
const metadata = this._createElementMetadata(element);
|
const metadata = this._createElementMetadata(element);
|
||||||
const nodeName = element.nodeName.toUpperCase();
|
|
||||||
const observer = {
|
const observer = {
|
||||||
element,
|
element,
|
||||||
type: (nodeName === 'INPUT' ? element.type : null),
|
type: this._getNormalizedElementType(element),
|
||||||
value: null,
|
value: null,
|
||||||
hasValue: false,
|
hasValue: false,
|
||||||
onChange: null,
|
onChange: null,
|
||||||
@ -157,28 +156,21 @@ class DOMDataBinder {
|
|||||||
|
|
||||||
_isObserverStale(element, observer) {
|
_isObserverStale(element, observer) {
|
||||||
const {type, metadata} = observer;
|
const {type, metadata} = observer;
|
||||||
const nodeName = element.nodeName.toUpperCase();
|
|
||||||
return !(
|
return !(
|
||||||
type === (nodeName === 'INPUT' ? element.type : null) &&
|
type === this._getNormalizedElementType(element) &&
|
||||||
this._compareElementMetadata(metadata, this._createElementMetadata(element))
|
this._compareElementMetadata(metadata, this._createElementMetadata(element))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_setElementValue(element, value) {
|
_setElementValue(element, value) {
|
||||||
switch (element.nodeName.toUpperCase()) {
|
switch (this._getNormalizedElementType(element)) {
|
||||||
case 'INPUT':
|
case 'checkbox':
|
||||||
switch (element.type) {
|
element.checked = value;
|
||||||
case 'checkbox':
|
|
||||||
element.checked = value;
|
|
||||||
break;
|
|
||||||
case 'text':
|
|
||||||
case 'number':
|
|
||||||
element.value = value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'TEXTAREA':
|
case 'text':
|
||||||
case 'SELECT':
|
case 'number':
|
||||||
|
case 'textarea':
|
||||||
|
case 'select':
|
||||||
element.value = value;
|
element.value = value;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -188,24 +180,37 @@ class DOMDataBinder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_getElementValue(element) {
|
_getElementValue(element) {
|
||||||
switch (element.nodeName.toUpperCase()) {
|
switch (this._getNormalizedElementType(element)) {
|
||||||
case 'INPUT':
|
case 'checkbox':
|
||||||
switch (element.type) {
|
return !!element.checked;
|
||||||
case 'checkbox':
|
case 'text':
|
||||||
return !!element.checked;
|
return `${element.value}`;
|
||||||
case 'text':
|
case 'number':
|
||||||
return `${element.value}`;
|
return DOMDataBinder.convertToNumber(element.value, element);
|
||||||
case 'number':
|
case 'textarea':
|
||||||
return DOMDataBinder.convertToNumber(element.value, element);
|
case 'select':
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'TEXTAREA':
|
|
||||||
case 'SELECT':
|
|
||||||
return element.value;
|
return element.value;
|
||||||
}
|
}
|
||||||
return null;
|
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
|
// Utilities
|
||||||
|
|
||||||
static convertToNumber(value, constraints) {
|
static convertToNumber(value, constraints) {
|
||||||
|
@ -61,6 +61,7 @@ class AnkiController {
|
|||||||
this._ankiErrorInvalidResponseInfo = document.querySelector('#anki-error-invalid-response-info');
|
this._ankiErrorInvalidResponseInfo = document.querySelector('#anki-error-invalid-response-info');
|
||||||
this._ankiEnableCheckbox = document.querySelector('[data-setting="anki.enable"]');
|
this._ankiEnableCheckbox = document.querySelector('[data-setting="anki.enable"]');
|
||||||
this._ankiCardPrimary = document.querySelector('#anki-card-primary');
|
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]');
|
const ankiCardPrimaryTypeRadios = document.querySelectorAll('input[type=radio][name=anki-card-primary-type]');
|
||||||
|
|
||||||
this._setupFieldMenus();
|
this._setupFieldMenus();
|
||||||
@ -79,9 +80,17 @@ class AnkiController {
|
|||||||
|
|
||||||
document.querySelector('#anki-error-log').addEventListener('click', this._onAnkiErrorLogLinkClick.bind(this));
|
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._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
|
||||||
this._onOptionsChanged({options});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getFieldMarkers(type) {
|
getFieldMarkers(type) {
|
||||||
@ -164,9 +173,17 @@ class AnkiController {
|
|||||||
|
|
||||||
// Private
|
// Private
|
||||||
|
|
||||||
|
async _updateOptions() {
|
||||||
|
const options = await this._settingsController.getOptions();
|
||||||
|
this._onOptionsChanged({options});
|
||||||
|
}
|
||||||
|
|
||||||
async _onOptionsChanged({options: {anki}}) {
|
async _onOptionsChanged({options: {anki}}) {
|
||||||
|
let {apiKey} = anki;
|
||||||
|
if (apiKey === '') { apiKey = null; }
|
||||||
this._ankiConnect.server = anki.server;
|
this._ankiConnect.server = anki.server;
|
||||||
this._ankiConnect.enabled = anki.enable;
|
this._ankiConnect.enabled = anki.enable;
|
||||||
|
this._ankiConnect.apiKey = apiKey;
|
||||||
|
|
||||||
this._selectorObserver.disconnect();
|
this._selectorObserver.disconnect();
|
||||||
this._selectorObserver.observe(document.documentElement, true);
|
this._selectorObserver.observe(document.documentElement, true);
|
||||||
@ -202,6 +219,14 @@ class AnkiController {
|
|||||||
this._testAnkiNoteViewerSafe(e.currentTarget.dataset.mode);
|
this._testAnkiNoteViewerSafe(e.currentTarget.dataset.mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onApiKeyInputFocus(e) {
|
||||||
|
e.currentTarget.type = 'text';
|
||||||
|
}
|
||||||
|
|
||||||
|
_onApiKeyInputBlur(e) {
|
||||||
|
e.currentTarget.type = 'password';
|
||||||
|
}
|
||||||
|
|
||||||
_setAnkiCardPrimaryType(ankiCardType, ankiCardMenu) {
|
_setAnkiCardPrimaryType(ankiCardType, ankiCardMenu) {
|
||||||
if (this._ankiCardPrimary === null) { return; }
|
if (this._ankiCardPrimary === null) { return; }
|
||||||
this._ankiCardPrimary.dataset.ankiCardType = ankiCardType;
|
this._ankiCardPrimary.dataset.ankiCardType = ankiCardType;
|
||||||
|
@ -1653,6 +1653,15 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div></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 advanced-only">
|
||||||
<div class="settings-item-inner">
|
<div class="settings-item-inner">
|
||||||
<div class="settings-item-left">
|
<div class="settings-item-left">
|
||||||
|
@ -454,7 +454,8 @@ function createProfileOptionsUpdatedTestData1() {
|
|||||||
checkForDuplicates: true,
|
checkForDuplicates: true,
|
||||||
fieldTemplates: null,
|
fieldTemplates: null,
|
||||||
suspendNewCards: false,
|
suspendNewCards: false,
|
||||||
noteGuiMode: 'browse'
|
noteGuiMode: 'browse',
|
||||||
|
apiKey: ''
|
||||||
},
|
},
|
||||||
sentenceParsing: {
|
sentenceParsing: {
|
||||||
scanExtent: 200,
|
scanExtent: 200,
|
||||||
|
Loading…
Reference in New Issue
Block a user