/* * 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', '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', '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); } }