summaryrefslogtreecommitdiff
path: root/ext/bg/js/settings/anki-controller.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/bg/js/settings/anki-controller.js')
-rw-r--r--ext/bg/js/settings/anki-controller.js305
1 files changed, 305 insertions, 0 deletions
diff --git a/ext/bg/js/settings/anki-controller.js b/ext/bg/js/settings/anki-controller.js
new file mode 100644
index 00000000..0965e633
--- /dev/null
+++ b/ext/bg/js/settings/anki-controller.js
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2019-2020 Yomichan Authors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/* global
+ * api
+ */
+
+class AnkiController {
+ constructor(settingsController) {
+ this._settingsController = settingsController;
+ }
+
+ async prepare() {
+ for (const element of document.querySelectorAll('#anki-fields-container input,#anki-fields-container select')) {
+ element.addEventListener('change', this._onFieldsChanged.bind(this), false);
+ }
+
+ for (const element of document.querySelectorAll('#anki-terms-model,#anki-kanji-model')) {
+ element.addEventListener('change', this._onModelChanged.bind(this), false);
+ }
+
+ this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
+
+ const options = await this._settingsController.getOptions();
+ this._onOptionsChanged({options});
+ }
+
+ getFieldMarkers(type) {
+ switch (type) {
+ case 'terms':
+ return [
+ 'audio',
+ 'clipboard-image',
+ 'cloze-body',
+ 'cloze-prefix',
+ 'cloze-suffix',
+ 'dictionary',
+ 'document-title',
+ 'expression',
+ 'furigana',
+ 'furigana-plain',
+ 'glossary',
+ 'glossary-brief',
+ 'pitch-accents',
+ 'pitch-accent-graphs',
+ 'pitch-accent-positions',
+ 'reading',
+ 'screenshot',
+ 'sentence',
+ 'tags',
+ 'url'
+ ];
+ case 'kanji':
+ return [
+ 'character',
+ 'clipboard-image',
+ 'cloze-body',
+ 'cloze-prefix',
+ 'cloze-suffix',
+ 'dictionary',
+ 'document-title',
+ 'glossary',
+ 'kunyomi',
+ 'onyomi',
+ 'screenshot',
+ 'sentence',
+ 'tags',
+ 'url'
+ ];
+ default:
+ return [];
+ }
+ }
+
+ getFieldMarkersHtml(markers) {
+ const template = document.querySelector('#anki-field-marker-template').content;
+ const fragment = document.createDocumentFragment();
+ for (const marker of markers) {
+ const markerNode = document.importNode(template, true).firstChild;
+ markerNode.querySelector('.marker-link').textContent = marker;
+ fragment.appendChild(markerNode);
+ }
+ return fragment;
+ }
+
+ // Private
+
+ async _onOptionsChanged({options}) {
+ if (!options.anki.enable) {
+ return;
+ }
+
+ await this._deckAndModelPopulate(options);
+ await Promise.all([
+ this._populateFields('terms', options.anki.terms.fields),
+ this._populateFields('kanji', options.anki.kanji.fields)
+ ]);
+ }
+
+ _fieldsToDict(elements) {
+ const result = {};
+ for (const element of elements) {
+ result[element.dataset.field] = element.value;
+ }
+ return result;
+ }
+
+ _spinnerShow(show) {
+ const spinner = document.querySelector('#anki-spinner');
+ spinner.hidden = !show;
+ }
+
+ _setError(error) {
+ const node = document.querySelector('#anki-error');
+ const node2 = document.querySelector('#anki-invalid-response-error');
+ if (error) {
+ const errorString = `${error}`;
+ if (node !== null) {
+ node.hidden = false;
+ node.textContent = errorString;
+ this._setErrorData(node, error);
+ }
+
+ if (node2 !== null) {
+ node2.hidden = (errorString.indexOf('Invalid response') < 0);
+ }
+ } else {
+ if (node !== null) {
+ node.hidden = true;
+ node.textContent = '';
+ }
+
+ if (node2 !== null) {
+ node2.hidden = true;
+ }
+ }
+ }
+
+ _setErrorData(node, error) {
+ const data = error.data;
+ let message = '';
+ if (typeof data !== 'undefined') {
+ message += `${JSON.stringify(data, null, 4)}\n\n`;
+ }
+ message += `${error.stack}`.trimRight();
+
+ const button = document.createElement('a');
+ button.className = 'error-data-show-button';
+
+ const content = document.createElement('div');
+ content.className = 'error-data-container';
+ content.textContent = message;
+ content.hidden = true;
+
+ button.addEventListener('click', () => content.hidden = !content.hidden, false);
+
+ node.appendChild(button);
+ node.appendChild(content);
+ }
+
+ _setDropdownOptions(dropdown, optionValues) {
+ const fragment = document.createDocumentFragment();
+ for (const optionValue of optionValues) {
+ const option = document.createElement('option');
+ option.value = optionValue;
+ option.textContent = optionValue;
+ fragment.appendChild(option);
+ }
+ dropdown.textContent = '';
+ dropdown.appendChild(fragment);
+ }
+
+ async _deckAndModelPopulate(options) {
+ const termsDeck = {value: options.anki.terms.deck, selector: '#anki-terms-deck'};
+ const kanjiDeck = {value: options.anki.kanji.deck, selector: '#anki-kanji-deck'};
+ const termsModel = {value: options.anki.terms.model, selector: '#anki-terms-model'};
+ const kanjiModel = {value: options.anki.kanji.model, selector: '#anki-kanji-model'};
+ try {
+ this._spinnerShow(true);
+ const [deckNames, modelNames] = await Promise.all([api.getAnkiDeckNames(), api.getAnkiModelNames()]);
+ deckNames.sort();
+ modelNames.sort();
+ termsDeck.values = deckNames;
+ kanjiDeck.values = deckNames;
+ termsModel.values = modelNames;
+ kanjiModel.values = modelNames;
+ this._setError(null);
+ } catch (error) {
+ this._setError(error);
+ } finally {
+ this._spinnerShow(false);
+ }
+
+ for (const {value, values, selector} of [termsDeck, kanjiDeck, termsModel, kanjiModel]) {
+ const node = document.querySelector(selector);
+ this._setDropdownOptions(node, Array.isArray(values) ? values : [value]);
+ node.value = value;
+ }
+ }
+
+ _createFieldTemplate(name, value, markers) {
+ const template = document.querySelector('#anki-field-template').content;
+ const content = document.importNode(template, true).firstChild;
+
+ content.querySelector('.anki-field-name').textContent = name;
+
+ const field = content.querySelector('.anki-field-value');
+ field.dataset.field = name;
+ field.value = value;
+
+ content.querySelector('.anki-field-marker-list').appendChild(this.getFieldMarkersHtml(markers));
+
+ return content;
+ }
+
+ async _populateFields(tabId, fields) {
+ const tab = document.querySelector(`.tab-pane[data-anki-card-type=${tabId}]`);
+ const container = tab.querySelector('tbody');
+ const markers = this.getFieldMarkers(tabId);
+
+ const fragment = document.createDocumentFragment();
+ for (const [name, value] of Object.entries(fields)) {
+ const html = this._createFieldTemplate(name, value, markers);
+ fragment.appendChild(html);
+ }
+
+ container.textContent = '';
+ container.appendChild(fragment);
+
+ for (const node of container.querySelectorAll('.anki-field-value')) {
+ node.addEventListener('change', this._onFieldsChanged.bind(this), false);
+ }
+ for (const node of container.querySelectorAll('.marker-link')) {
+ node.addEventListener('click', this._onMarkerClicked.bind(this), false);
+ }
+ }
+
+ _onMarkerClicked(e) {
+ e.preventDefault();
+ const link = e.currentTarget;
+ const input = link.closest('.input-group').querySelector('.anki-field-value');
+ input.value = `{${link.textContent}}`;
+ input.dispatchEvent(new Event('change'));
+ }
+
+ async _onModelChanged(e) {
+ const node = e.currentTarget;
+ let fieldNames;
+ try {
+ const modelName = node.value;
+ fieldNames = await api.getAnkiModelFieldNames(modelName);
+ this._setError(null);
+ } catch (error) {
+ this._setError(error);
+ return;
+ } finally {
+ this._spinnerShow(false);
+ }
+
+ const tabId = node.dataset.ankiCardType;
+ if (tabId !== 'terms' && tabId !== 'kanji') { return; }
+
+ const fields = {};
+ for (const name of fieldNames) {
+ fields[name] = '';
+ }
+
+ await this._settingsController.setProfileSetting(`anki["${tabId}"].fields`, fields);
+ await this._populateFields(tabId, fields);
+ }
+
+ async _onFieldsChanged() {
+ const termsDeck = document.querySelector('#anki-terms-deck').value;
+ const termsModel = document.querySelector('#anki-terms-model').value;
+ const termsFields = this._fieldsToDict(document.querySelectorAll('#terms .anki-field-value'));
+ const kanjiDeck = document.querySelector('#anki-kanji-deck').value;
+ const kanjiModel = document.querySelector('#anki-kanji-model').value;
+ const kanjiFields = this._fieldsToDict(document.querySelectorAll('#kanji .anki-field-value'));
+
+ const targets = [
+ {action: 'set', path: 'anki.terms.deck', value: termsDeck},
+ {action: 'set', path: 'anki.terms.model', value: termsModel},
+ {action: 'set', path: 'anki.terms.fields', value: termsFields},
+ {action: 'set', path: 'anki.kanji.deck', value: kanjiDeck},
+ {action: 'set', path: 'anki.kanji.model', value: kanjiModel},
+ {action: 'set', path: 'anki.kanji.fields', value: kanjiFields}
+ ];
+
+ await this._settingsController.modifyProfileSettings(targets);
+ }
+}