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.js729
1 files changed, 0 insertions, 729 deletions
diff --git a/ext/bg/js/settings/anki-controller.js b/ext/bg/js/settings/anki-controller.js
deleted file mode 100644
index db3e3c14..00000000
--- a/ext/bg/js/settings/anki-controller.js
+++ /dev/null
@@ -1,729 +0,0 @@
-/*
- * Copyright (C) 2019-2021 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
- * AnkiConnect
- * AnkiNoteBuilder
- * ObjectPropertyAccessor
- * SelectorObserver
- */
-
-class AnkiController {
- constructor(settingsController) {
- this._settingsController = settingsController;
- this._ankiConnect = new AnkiConnect();
- this._ankiNoteBuilder = new AnkiNoteBuilder(false);
- this._selectorObserver = new SelectorObserver({
- selector: '.anki-card',
- ignoreSelector: null,
- onAdded: this._createCardController.bind(this),
- onRemoved: this._removeCardController.bind(this),
- isStale: this._isCardControllerStale.bind(this)
- });
- this._stringComparer = new Intl.Collator(); // Locale does not matter
- this._getAnkiDataPromise = null;
- this._ankiErrorContainer = null;
- this._ankiErrorMessageNode = null;
- this._ankiErrorMessageNodeDefaultContent = '';
- this._ankiErrorMessageDetailsNode = null;
- this._ankiErrorMessageDetailsContainer = null;
- this._ankiErrorMessageDetailsToggle = null;
- this._ankiErrorInvalidResponseInfo = null;
- this._ankiCardPrimary = null;
- this._ankiCardPrimaryType = null;
- this._validateFieldsToken = null;
- }
-
- get settingsController() {
- return this._settingsController;
- }
-
- async prepare() {
- this._ankiErrorContainer = document.querySelector('#anki-error');
- this._ankiErrorMessageNode = document.querySelector('#anki-error-message');
- this._ankiErrorMessageNodeDefaultContent = this._ankiErrorMessageNode.textContent;
- this._ankiErrorMessageDetailsNode = document.querySelector('#anki-error-message-details');
- this._ankiErrorMessageDetailsContainer = document.querySelector('#anki-error-message-details-container');
- this._ankiErrorMessageDetailsToggle = document.querySelector('#anki-error-message-details-toggle');
- this._ankiErrorInvalidResponseInfo = document.querySelector('#anki-error-invalid-response-info');
- this._ankiEnableCheckbox = document.querySelector('[data-setting="anki.enable"]');
- this._ankiCardPrimary = document.querySelector('#anki-card-primary');
- this._ankiCardPrimaryType = document.querySelector('#anki-card-primary-type');
-
- this._setupFieldMenus();
-
- this._ankiErrorMessageDetailsToggle.addEventListener('click', this._onAnkiErrorMessageDetailsToggleClick.bind(this), false);
- if (this._ankiEnableCheckbox !== null) { this._ankiEnableCheckbox.addEventListener('settingChanged', this._onAnkiEnableChanged.bind(this), false); }
- if (this._ankiCardPrimaryType !== null) { this._ankiCardPrimaryType.addEventListener('change', this._onAnkiCardPrimaryTypeChange.bind(this), false); }
-
- const options = await this._settingsController.getOptions();
- this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));
- this._onOptionsChanged({options});
- }
-
- getFieldMarkers(type) {
- switch (type) {
- case 'terms':
- return [
- 'audio',
- 'clipboard-image',
- 'clipboard-text',
- 'cloze-body',
- 'cloze-prefix',
- 'cloze-suffix',
- 'conjugation',
- 'dictionary',
- 'document-title',
- 'expression',
- 'frequencies',
- 'furigana',
- 'furigana-plain',
- 'glossary',
- 'glossary-brief',
- 'glossary-no-dictionary',
- 'pitch-accents',
- 'pitch-accent-graphs',
- 'pitch-accent-positions',
- 'reading',
- 'screenshot',
- 'sentence',
- 'tags',
- 'url'
- ];
- case 'kanji':
- return [
- 'character',
- 'clipboard-image',
- 'clipboard-text',
- 'cloze-body',
- 'cloze-prefix',
- 'cloze-suffix',
- 'dictionary',
- 'document-title',
- 'glossary',
- 'kunyomi',
- 'onyomi',
- 'screenshot',
- 'sentence',
- 'stroke-count',
- 'tags',
- 'url'
- ];
- default:
- return [];
- }
- }
-
- getFieldMarkersHtml(markers) {
- const fragment = document.createDocumentFragment();
- for (const marker of markers) {
- const markerNode = this._settingsController.instantiateTemplate('anki-card-field-marker');
- markerNode.querySelector('.marker-link').textContent = marker;
- fragment.appendChild(markerNode);
- }
- return fragment;
- }
-
- async getAnkiData() {
- let promise = this._getAnkiDataPromise;
- if (promise === null) {
- promise = this._getAnkiData();
- this._getAnkiDataPromise = promise;
- promise.finally(() => { this._getAnkiDataPromise = null; });
- }
- return promise;
- }
-
- async getModelFieldNames(model) {
- return await this._ankiConnect.getModelFieldNames(model);
- }
-
- getRequiredPermissions(fieldValue) {
- return this._settingsController.permissionsUtil.getRequiredPermissionsForAnkiFieldValue(fieldValue);
- }
-
- containsAnyMarker(field) {
- return this._ankiNoteBuilder.containsAnyMarker(field);
- }
-
- // Private
-
- async _onOptionsChanged({options: {anki}}) {
- this._ankiConnect.server = anki.server;
- this._ankiConnect.enabled = anki.enable;
-
- this._selectorObserver.disconnect();
- this._selectorObserver.observe(document.documentElement, true);
- }
-
- _onAnkiErrorMessageDetailsToggleClick() {
- const node = this._ankiErrorMessageDetailsContainer;
- node.hidden = !node.hidden;
- }
-
- _onAnkiEnableChanged({detail: {value}}) {
- if (this._ankiConnect.server === null) { return; }
- this._ankiConnect.enabled = value;
-
- for (const cardController of this._selectorObserver.datas()) {
- cardController.updateAnkiState();
- }
- }
-
- _onAnkiCardPrimaryTypeChange(e) {
- if (this._ankiCardPrimary === null) { return; }
- const node = e.currentTarget;
- let ankiCardMenu;
- if (node.selectedIndex >= 0) {
- const option = node.options[node.selectedIndex];
- ankiCardMenu = option.dataset.ankiCardMenu;
- }
-
- this._ankiCardPrimary.dataset.ankiCardType = node.value;
- if (typeof ankiCardMenu !== 'undefined') {
- this._ankiCardPrimary.dataset.ankiCardMenu = ankiCardMenu;
- } else {
- delete this._ankiCardPrimary.dataset.ankiCardMenu;
- }
- }
-
- _createCardController(node) {
- const cardController = new AnkiCardController(this._settingsController, this, node);
- cardController.prepare();
- return cardController;
- }
-
- _removeCardController(node, cardController) {
- cardController.cleanup();
- }
-
- _isCardControllerStale(node, cardController) {
- return cardController.isStale();
- }
-
- _setupFieldMenus() {
- const fieldMenuTargets = [
- [['terms'], '#anki-card-terms-field-menu-template'],
- [['kanji'], '#anki-card-kanji-field-menu-template'],
- [['terms', 'kanji'], '#anki-card-all-field-menu-template']
- ];
- for (const [types, selector] of fieldMenuTargets) {
- const element = document.querySelector(selector);
- if (element === null) { continue; }
-
- let markers = [];
- for (const type of types) {
- markers.push(...this.getFieldMarkers(type));
- }
- markers = [...new Set(markers)];
-
- const container = element.content.querySelector('.popup-menu-body');
- if (container === null) { return; }
-
- const fragment = document.createDocumentFragment();
- for (const marker of markers) {
- const option = document.createElement('button');
- option.textContent = marker;
- option.className = 'popup-menu-item';
- option.dataset.menuAction = 'setFieldMarker';
- option.dataset.marker = marker;
- fragment.appendChild(option);
- }
- container.appendChild(fragment);
- }
- }
-
- async _getAnkiData() {
- this._setAnkiStatusChanging();
- const [
- [deckNames, error1],
- [modelNames, error2]
- ] = await Promise.all([
- this._getDeckNames(),
- this._getModelNames()
- ]);
-
- if (error1 !== null) {
- this._showAnkiError(error1);
- } else if (error2 !== null) {
- this._showAnkiError(error2);
- } else {
- this._hideAnkiError();
- }
-
- return {deckNames, modelNames};
- }
-
- async _getDeckNames() {
- try {
- const result = await this._ankiConnect.getDeckNames();
- this._sortStringArray(result);
- return [result, null];
- } catch (e) {
- return [[], e];
- }
- }
-
- async _getModelNames() {
- try {
- const result = await this._ankiConnect.getModelNames();
- this._sortStringArray(result);
- return [result, null];
- } catch (e) {
- return [[], e];
- }
- }
-
- _setAnkiStatusChanging() {
- this._ankiErrorMessageNode.textContent = this._ankiErrorMessageNodeDefaultContent;
- this._ankiErrorMessageNode.classList.remove('danger-text');
- }
-
- _hideAnkiError() {
- if (this._ankiErrorContainer !== null) {
- this._ankiErrorContainer.hidden = true;
- }
- this._ankiErrorMessageDetailsContainer.hidden = true;
- this._ankiErrorMessageDetailsToggle.hidden = true;
- this._ankiErrorInvalidResponseInfo.hidden = true;
- this._ankiErrorMessageNode.textContent = (this._ankiConnect.enabled ? 'Connected' : 'Not enabled');
- this._ankiErrorMessageNode.classList.remove('danger-text');
- this._ankiErrorMessageDetailsNode.textContent = '';
- }
-
- _showAnkiError(error) {
- let errorString = typeof error === 'object' && error !== null ? error.message : null;
- if (!errorString) { errorString = `${error}`; }
- if (!/[.!?]$/.test(errorString)) { errorString += '.'; }
- this._ankiErrorMessageNode.textContent = errorString;
- this._ankiErrorMessageNode.classList.add('danger-text');
-
- const data = error.data;
- let details = '';
- if (typeof data !== 'undefined') {
- details += `${JSON.stringify(data, null, 4)}\n\n`;
- }
- details += `${error.stack}`.trimRight();
- this._ankiErrorMessageDetailsNode.textContent = details;
-
- if (this._ankiErrorContainer !== null) {
- this._ankiErrorContainer.hidden = false;
- }
- this._ankiErrorMessageDetailsContainer.hidden = true;
- this._ankiErrorInvalidResponseInfo.hidden = (errorString.indexOf('Invalid response') < 0);
- this._ankiErrorMessageDetailsToggle.hidden = false;
- }
-
- _sortStringArray(array) {
- const stringComparer = this._stringComparer;
- array.sort((a, b) => stringComparer.compare(a, b));
- }
-}
-
-class AnkiCardController {
- constructor(settingsController, ankiController, node) {
- this._settingsController = settingsController;
- this._ankiController = ankiController;
- this._node = node;
- this._cardType = node.dataset.ankiCardType;
- this._cardMenu = node.dataset.ankiCardMenu;
- this._eventListeners = new EventListenerCollection();
- this._fieldEventListeners = new EventListenerCollection();
- this._deck = null;
- this._model = null;
- this._fields = null;
- this._modelChangingTo = null;
- this._ankiCardDeckSelect = null;
- this._ankiCardModelSelect = null;
- this._ankiCardFieldsContainer = null;
- this._cleaned = false;
- this._fieldEntries = [];
- }
-
- async prepare() {
- const options = await this._settingsController.getOptions();
- const ankiOptions = options.anki;
- if (this._cleaned) { return; }
-
- const cardOptions = this._getCardOptions(ankiOptions, this._cardType);
- if (cardOptions === null) { return; }
- const {deck, model, fields} = cardOptions;
- this._deck = deck;
- this._model = model;
- this._fields = fields;
-
- this._ankiCardDeckSelect = this._node.querySelector('.anki-card-deck');
- this._ankiCardModelSelect = this._node.querySelector('.anki-card-model');
- this._ankiCardFieldsContainer = this._node.querySelector('.anki-card-fields');
-
- this._setupSelects([], []);
- this._setupFields();
-
- this._eventListeners.addEventListener(this._ankiCardDeckSelect, 'change', this._onCardDeckChange.bind(this), false);
- this._eventListeners.addEventListener(this._ankiCardModelSelect, 'change', this._onCardModelChange.bind(this), false);
- this._eventListeners.on(this._settingsController, 'permissionsChanged', this._onPermissionsChanged.bind(this));
-
- await this.updateAnkiState();
- }
-
- cleanup() {
- this._cleaned = true;
- this._fieldEntries = [];
- this._eventListeners.removeAllEventListeners();
- }
-
- async updateAnkiState() {
- if (this._fields === null) { return; }
- const {deckNames, modelNames} = await this._ankiController.getAnkiData();
- if (this._cleaned) { return; }
- this._setupSelects(deckNames, modelNames);
- }
-
- isStale() {
- return (this._cardType !== this._node.dataset.ankiCardType);
- }
-
- // Private
-
- _onCardDeckChange(e) {
- this._setDeck(e.currentTarget.value);
- }
-
- _onCardModelChange(e) {
- this._setModel(e.currentTarget.value);
- }
-
- _onFieldChange(index, e) {
- const node = e.currentTarget;
- this._validateFieldPermissions(node, index, true);
- this._validateField(node, index);
- }
-
- _onFieldInput(index, e) {
- const node = e.currentTarget;
- this._validateField(node, index);
- }
-
- _onFieldSettingChanged(index, e) {
- const node = e.currentTarget;
- this._validateFieldPermissions(node, index, false);
- }
-
- _onFieldMenuClose({currentTarget: button, detail: {action, item}}) {
- switch (action) {
- case 'setFieldMarker':
- this._setFieldMarker(button, item.dataset.marker);
- break;
- }
- }
-
- _onFieldMarkerLinkClick(e) {
- e.preventDefault();
- const link = e.currentTarget;
- this._setFieldMarker(link, link.textContent);
- }
-
- _validateField(node, index) {
- let valid = (node.dataset.hasPermissions !== 'false');
- if (valid && index === 0 && !this._ankiController.containsAnyMarker(node.value)) {
- valid = false;
- }
- node.dataset.invalid = `${!valid}`;
- }
-
- _setFieldMarker(element, marker) {
- const input = element.closest('.anki-card-field-value-container').querySelector('.anki-card-field-value');
- input.value = `{${marker}}`;
- input.dispatchEvent(new Event('change'));
- }
-
- _getCardOptions(ankiOptions, cardType) {
- switch (cardType) {
- case 'terms': return ankiOptions.terms;
- case 'kanji': return ankiOptions.kanji;
- default: return null;
- }
- }
-
- _setupSelects(deckNames, modelNames) {
- const deck = this._deck;
- const model = this._model;
- if (!deckNames.includes(deck)) { deckNames = [...deckNames, deck]; }
- if (!modelNames.includes(model)) { modelNames = [...modelNames, model]; }
-
- this._setSelectOptions(this._ankiCardDeckSelect, deckNames);
- this._ankiCardDeckSelect.value = deck;
-
- this._setSelectOptions(this._ankiCardModelSelect, modelNames);
- this._ankiCardModelSelect.value = model;
- }
-
- _setSelectOptions(select, optionValues) {
- const fragment = document.createDocumentFragment();
- for (const optionValue of optionValues) {
- const option = document.createElement('option');
- option.value = optionValue;
- option.textContent = optionValue;
- fragment.appendChild(option);
- }
- select.textContent = '';
- select.appendChild(fragment);
- }
-
- _setupFields() {
- this._fieldEventListeners.removeAllEventListeners();
-
- const markers = this._ankiController.getFieldMarkers(this._cardType);
- const totalFragment = document.createDocumentFragment();
- this._fieldEntries = [];
- let index = 0;
- for (const [fieldName, fieldValue] of Object.entries(this._fields)) {
- const content = this._settingsController.instantiateTemplateFragment('anki-card-field');
-
- const fieldNameContainerNode = content.querySelector('.anki-card-field-name-container');
- fieldNameContainerNode.dataset.index = `${index}`;
- const fieldNameNode = content.querySelector('.anki-card-field-name');
- fieldNameNode.textContent = fieldName;
-
- const valueContainer = content.querySelector('.anki-card-field-value-container');
- valueContainer.dataset.index = `${index}`;
-
- const inputField = content.querySelector('.anki-card-field-value');
- inputField.value = fieldValue;
- inputField.dataset.setting = ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'fields', fieldName]);
- this._validateFieldPermissions(inputField, index, false);
-
- this._fieldEventListeners.addEventListener(inputField, 'change', this._onFieldChange.bind(this, index), false);
- this._fieldEventListeners.addEventListener(inputField, 'input', this._onFieldInput.bind(this, index), false);
- this._fieldEventListeners.addEventListener(inputField, 'settingChanged', this._onFieldSettingChanged.bind(this, index), false);
- this._validateField(inputField, index);
-
- const markerList = content.querySelector('.anki-card-field-marker-list');
- if (markerList !== null) {
- const markersFragment = this._ankiController.getFieldMarkersHtml(markers);
- for (const element of markersFragment.querySelectorAll('.marker-link')) {
- this._fieldEventListeners.addEventListener(element, 'click', this._onFieldMarkerLinkClick.bind(this), false);
- }
- markerList.appendChild(markersFragment);
- }
-
- const menuButton = content.querySelector('.anki-card-field-value-menu-button');
- if (menuButton !== null) {
- if (typeof this._cardMenu !== 'undefined') {
- menuButton.dataset.menu = this._cardMenu;
- } else {
- delete menuButton.dataset.menu;
- }
- this._fieldEventListeners.addEventListener(menuButton, 'menuClose', this._onFieldMenuClose.bind(this), false);
- }
-
- totalFragment.appendChild(content);
- this._fieldEntries.push({fieldName, inputField, fieldNameContainerNode});
-
- ++index;
- }
-
- const ELEMENT_NODE = Node.ELEMENT_NODE;
- const container = this._ankiCardFieldsContainer;
- for (const node of [...container.childNodes]) {
- if (node.nodeType === ELEMENT_NODE && node.dataset.persistent === 'true') { continue; }
- container.removeChild(node);
- }
- container.appendChild(totalFragment);
-
- this._validateFields();
- }
-
- async _validateFields() {
- const token = {};
- this._validateFieldsToken = token;
-
- let fieldNames;
- try {
- fieldNames = await this._ankiController.getModelFieldNames(this._model);
- } catch (e) {
- return;
- }
-
- if (token !== this._validateFieldsToken) { return; }
-
- const fieldNamesSet = new Set(fieldNames);
- let index = 0;
- for (const {fieldName, fieldNameContainerNode} of this._fieldEntries) {
- fieldNameContainerNode.dataset.invalid = `${!fieldNamesSet.has(fieldName)}`;
- fieldNameContainerNode.dataset.orderMatches = `${index < fieldNames.length && fieldName === fieldNames[index]}`;
- ++index;
- }
- }
-
- async _setDeck(value) {
- if (this._deck === value) { return; }
- this._deck = value;
-
- await this._settingsController.modifyProfileSettings([{
- action: 'set',
- path: ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'deck']),
- value
- }]);
- }
-
- async _setModel(value) {
- if (this._modelChangingTo !== null) {
- // Revert
- this._ankiCardModelSelect.value = this._modelChangingTo;
- return;
- }
- if (this._model === value) { return; }
-
- let fieldNames;
- let options;
- try {
- this._modelChangingTo = value;
- fieldNames = await this._ankiController.getModelFieldNames(value);
- options = await this._ankiController.settingsController.getOptions();
- } catch (e) {
- // Revert
- this._ankiCardModelSelect.value = this._model;
- return;
- } finally {
- this._modelChangingTo = null;
- }
-
- const cardType = this._cardType;
- const cardOptions = this._getCardOptions(options.anki, cardType);
- const oldFields = cardOptions !== null ? cardOptions.fields : null;
-
- const fields = {};
- for (let i = 0, ii = fieldNames.length; i < ii; ++i) {
- const fieldName = fieldNames[i];
- fields[fieldName] = this._getDefaultFieldValue(fieldName, i, cardType, oldFields);
- }
-
- const targets = [
- {
- action: 'set',
- path: ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'model']),
- value
- },
- {
- action: 'set',
- path: ObjectPropertyAccessor.getPathString(['anki', this._cardType, 'fields']),
- value: fields
- }
- ];
-
- this._model = value;
- this._fields = fields;
-
- await this._settingsController.modifyProfileSettings(targets);
-
- this._setupFields();
- }
-
- async _requestPermissions(permissions) {
- try {
- await this._settingsController.permissionsUtil.setPermissionsGranted({permissions}, true);
- } catch (e) {
- yomichan.logError(e);
- }
- }
-
- async _validateFieldPermissions(node, index, request) {
- const fieldValue = node.value;
- const permissions = this._ankiController.getRequiredPermissions(fieldValue);
- if (permissions.length > 0) {
- node.dataset.requiredPermission = permissions.join(' ');
- const hasPermissions = await (
- request ?
- this._settingsController.permissionsUtil.setPermissionsGranted({permissions}, true) :
- this._settingsController.permissionsUtil.hasPermissions({permissions})
- );
- node.dataset.hasPermissions = `${hasPermissions}`;
- } else {
- delete node.dataset.requiredPermission;
- delete node.dataset.hasPermissions;
- }
-
- this._validateField(node, index);
- }
-
- _onPermissionsChanged({permissions: {permissions}}) {
- const permissionsSet = new Set(permissions);
- for (let i = 0, ii = this._fieldEntries.length; i < ii; ++i) {
- const {inputField} = this._fieldEntries[i];
- let {requiredPermission} = inputField.dataset;
- if (typeof requiredPermission !== 'string') { continue; }
- requiredPermission = (requiredPermission.length === 0 ? [] : requiredPermission.split(' '));
-
- let hasPermissions = true;
- for (const permission of requiredPermission) {
- if (!permissionsSet.has(permission)) {
- hasPermissions = false;
- break;
- }
- }
-
- inputField.dataset.hasPermissions = `${hasPermissions}`;
- this._validateField(inputField, i);
- }
- }
-
- _getDefaultFieldValue(fieldName, index, cardType, oldFields) {
- if (
- typeof oldFields === 'object' &&
- oldFields !== null &&
- Object.prototype.hasOwnProperty.call(oldFields, fieldName)
- ) {
- return oldFields[fieldName];
- }
-
- if (index === 0) {
- return (cardType === 'kanji' ? '{character}' : '{expression}');
- }
-
- const markers = this._ankiController.getFieldMarkers(cardType);
- const markerAliases = new Map([
- ['glossary', ['definition', 'meaning']],
- ['audio', ['sound']],
- ['dictionary', ['dict']]
- ]);
-
- const hyphenPattern = /-/g;
- for (const marker of markers) {
- const names = [marker];
- const aliases = markerAliases.get(marker);
- if (typeof aliases !== 'undefined') {
- names.push(...aliases);
- }
-
- let pattern = '^(?:';
- for (let i = 0, ii = names.length; i < ii; ++i) {
- const name = names[i];
- if (i > 0) { pattern += '|'; }
- pattern += name.replace(hyphenPattern, '[-_ ]*');
- }
- pattern += ')$';
- pattern = new RegExp(pattern, 'i');
-
- if (pattern.test(fieldName)) {
- return `{${marker}}`;
- }
- }
-
- return '';
- }
-}