Anki settings controllers (#567)

* Convert anki-templates.js to a class

* Convert anki.js to a class
This commit is contained in:
toasted-nutbread 2020-05-29 19:52:51 -04:00 committed by GitHub
parent fde0072118
commit 5f9889fd26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 384 additions and 378 deletions

View File

@ -17,20 +17,58 @@
/* global /* global
* AnkiNoteBuilder * AnkiNoteBuilder
* ankiGetFieldMarkers
* ankiGetFieldMarkersHtml
* api * api
* getOptionsContext * getOptionsContext
* getOptionsMutable * getOptionsMutable
* settingsSaveOptions * settingsSaveOptions
*/ */
function onAnkiFieldTemplatesReset(e) { class AnkiTemplatesController {
constructor(ankiController) {
this._ankiController = ankiController;
this._cachedDefinitionValue = null;
this._cachedDefinitionText = null;
}
prepare() {
const markers = new Set([
...this._ankiController.getFieldMarkers('terms'),
...this._ankiController.getFieldMarkers('kanji')
]);
const fragment = this._ankiController.getFieldMarkersHtml(markers);
const list = document.querySelector('#field-templates-list');
list.appendChild(fragment);
for (const node of list.querySelectorAll('.marker-link')) {
node.addEventListener('click', this._onMarkerClicked.bind(this), false);
}
$('#field-templates').on('change', this._onChanged.bind(this));
$('#field-template-render').on('click', this._onRender.bind(this));
$('#field-templates-reset').on('click', this._onReset.bind(this));
$('#field-templates-reset-confirm').on('click', this._onResetConfirm.bind(this));
this.updateValue();
}
async updateValue() {
const optionsContext = getOptionsContext();
const options = await api.optionsGet(optionsContext);
let templates = options.anki.fieldTemplates;
if (typeof templates !== 'string') { templates = await api.getDefaultAnkiFieldTemplates(); }
$('#field-templates').val(templates);
this._onValidateCompile();
}
// Private
_onReset(e) {
e.preventDefault(); e.preventDefault();
$('#field-template-reset-modal').modal('show'); $('#field-template-reset-modal').modal('show');
} }
async function onAnkiFieldTemplatesResetConfirm(e) { async _onResetConfirm(e) {
e.preventDefault(); e.preventDefault();
$('#field-template-reset-modal').modal('hide'); $('#field-template-reset-modal').modal('hide');
@ -42,57 +80,61 @@ async function onAnkiFieldTemplatesResetConfirm(e) {
element.dispatchEvent(new Event('change')); element.dispatchEvent(new Event('change'));
} }
function ankiTemplatesInitialize() { async _onChanged(e) {
const markers = new Set(ankiGetFieldMarkers('terms').concat(ankiGetFieldMarkers('kanji'))); // Get value
const fragment = ankiGetFieldMarkersHtml(markers); let templates = e.currentTarget.value;
if (templates === await api.getDefaultAnkiFieldTemplates()) {
const list = document.querySelector('#field-templates-list'); // Default
list.appendChild(fragment); templates = null;
for (const node of list.querySelectorAll('.marker-link')) {
node.addEventListener('click', onAnkiTemplateMarkerClicked, false);
} }
$('#field-templates').on('change', onAnkiFieldTemplatesChanged); // Overwrite
$('#field-template-render').on('click', onAnkiTemplateRender);
$('#field-templates-reset').on('click', onAnkiFieldTemplatesReset);
$('#field-templates-reset-confirm').on('click', onAnkiFieldTemplatesResetConfirm);
ankiTemplatesUpdateValue();
}
async function ankiTemplatesUpdateValue() {
const optionsContext = getOptionsContext(); const optionsContext = getOptionsContext();
const options = await api.optionsGet(optionsContext); const options = await getOptionsMutable(optionsContext);
let templates = options.anki.fieldTemplates; options.anki.fieldTemplates = templates;
if (typeof templates !== 'string') { templates = await api.getDefaultAnkiFieldTemplates(); } await settingsSaveOptions();
$('#field-templates').val(templates);
onAnkiTemplatesValidateCompile(); // Compile
this._onValidateCompile();
} }
const ankiTemplatesValidateGetDefinition = (() => { _onValidateCompile() {
let cachedValue = null; const infoNode = document.querySelector('#field-template-compile-result');
let cachedText = null; this._validate(infoNode, '{expression}', 'term-kanji', false, true);
}
return async (text, optionsContext) => { _onMarkerClicked(e) {
if (cachedText !== text) { e.preventDefault();
document.querySelector('#field-template-render-text').value = `{${e.target.textContent}}`;
}
_onRender(e) {
e.preventDefault();
const field = document.querySelector('#field-template-render-text').value;
const infoNode = document.querySelector('#field-template-render-result');
infoNode.hidden = true;
this._validate(infoNode, field, 'term-kanji', true, false);
}
async _getDefinition(text, optionsContext) {
if (this._cachedDefinitionText !== text) {
const {definitions} = await api.termsFind(text, {}, optionsContext); const {definitions} = await api.termsFind(text, {}, optionsContext);
if (definitions.length === 0) { return null; } if (definitions.length === 0) { return null; }
cachedValue = definitions[0]; this._cachedDefinitionValue = definitions[0];
cachedText = text; this._cachedDefinitionText = text;
}
return this._cachedDefinitionValue;
} }
return cachedValue;
};
})();
async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, invalidateInput) { async _validate(infoNode, field, mode, showSuccessResult, invalidateInput) {
const text = document.querySelector('#field-templates-preview-text').value || ''; const text = document.querySelector('#field-templates-preview-text').value || '';
const exceptions = []; const exceptions = [];
let result = `No definition found for ${text}`; let result = `No definition found for ${text}`;
try { try {
const optionsContext = getOptionsContext(); const optionsContext = getOptionsContext();
const definition = await ankiTemplatesValidateGetDefinition(text, optionsContext); const definition = await this._getDefinition(text, optionsContext);
if (definition !== null) { if (definition !== null) {
const options = await api.optionsGet(optionsContext); const options = await api.optionsGet(optionsContext);
const context = { const context = {
@ -118,40 +160,4 @@ async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, i
input.classList.toggle('is-invalid', hasException); input.classList.toggle('is-invalid', hasException);
} }
} }
async function onAnkiFieldTemplatesChanged(e) {
// Get value
let templates = e.currentTarget.value;
if (templates === await api.getDefaultAnkiFieldTemplates()) {
// Default
templates = null;
}
// Overwrite
const optionsContext = getOptionsContext();
const options = await getOptionsMutable(optionsContext);
options.anki.fieldTemplates = templates;
await settingsSaveOptions();
// Compile
onAnkiTemplatesValidateCompile();
}
function onAnkiTemplatesValidateCompile() {
const infoNode = document.querySelector('#field-template-compile-result');
ankiTemplatesValidate(infoNode, '{expression}', 'term-kanji', false, true);
}
function onAnkiTemplateMarkerClicked(e) {
e.preventDefault();
document.querySelector('#field-template-render-text').value = `{${e.target.textContent}}`;
}
function onAnkiTemplateRender(e) {
e.preventDefault();
const field = document.querySelector('#field-template-render-text').value;
const infoNode = document.querySelector('#field-template-render-result');
infoNode.hidden = true;
ankiTemplatesValidate(infoNode, field, 'term-kanji', true, false);
} }

View File

@ -23,228 +23,35 @@
* utilBackgroundIsolate * utilBackgroundIsolate
*/ */
// Private class AnkiController {
prepare() {
$('#anki-fields-container input,#anki-fields-container select,#anki-fields-container textarea').change(this._onFieldsChanged.bind(this));
function _ankiSpinnerShow(show) { for (const node of document.querySelectorAll('#anki-terms-model,#anki-kanji-model')) {
const spinner = $('#anki-spinner'); node.addEventListener('change', this._onModelChanged.bind(this), false);
if (show) {
spinner.show();
} else {
spinner.hide();
}
} }
function _ankiSetError(error) { this.optionsChanged();
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;
_ankiSetErrorData(node, error);
} }
if (node2 !== null) { async optionsChanged(options=null) {
node2.hidden = (errorString.indexOf('Invalid response') < 0); if (options === null) {
} const optionsContext = getOptionsContext();
} else { options = await getOptionsMutable(optionsContext);
if (node !== null) {
node.hidden = true;
node.textContent = '';
} }
if (node2 !== null) { if (!options.anki.enable) {
node2.hidden = true;
}
}
}
function _ankiSetErrorData(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);
}
function _ankiSetDropdownOptions(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 function _ankiDeckAndModelPopulate(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 {
_ankiSpinnerShow(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;
_ankiSetError(null);
} catch (error) {
_ankiSetError(error);
} finally {
_ankiSpinnerShow(false);
}
for (const {value, values, selector} of [termsDeck, kanjiDeck, termsModel, kanjiModel]) {
const node = document.querySelector(selector);
_ankiSetDropdownOptions(node, Array.isArray(values) ? values : [value]);
node.value = value;
}
}
function _ankiCreateFieldTemplate(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(ankiGetFieldMarkersHtml(markers));
return content;
}
async function _ankiFieldsPopulate(tabId, options) {
const tab = document.querySelector(`.tab-pane[data-anki-card-type=${tabId}]`);
const container = tab.querySelector('tbody');
const markers = ankiGetFieldMarkers(tabId);
const fragment = document.createDocumentFragment();
const fields = options.anki[tabId].fields;
for (const name of Object.keys(fields)) {
const value = fields[name];
const html = _ankiCreateFieldTemplate(name, value, markers);
fragment.appendChild(html);
}
container.textContent = '';
container.appendChild(fragment);
for (const node of container.querySelectorAll('.anki-field-value')) {
node.addEventListener('change', _onAnkiFieldsChanged, false);
}
for (const node of container.querySelectorAll('.marker-link')) {
node.addEventListener('click', _onAnkiMarkerClicked, false);
}
}
function _onAnkiMarkerClicked(e) {
e.preventDefault();
const link = e.currentTarget;
const input = $(link).closest('.input-group').find('.anki-field-value')[0];
input.value = `{${link.textContent}}`;
input.dispatchEvent(new Event('change'));
}
async function _onAnkiModelChanged(e) {
const node = e.currentTarget;
let fieldNames;
try {
const modelName = node.value;
fieldNames = await api.getAnkiModelFieldNames(modelName);
_ankiSetError(null);
} catch (error) {
_ankiSetError(error);
return; return;
} finally {
_ankiSpinnerShow(false);
} }
const tabId = node.dataset.ankiCardType; await this._deckAndModelPopulate(options);
if (tabId !== 'terms' && tabId !== 'kanji') { return; } await Promise.all([
this._fieldsPopulate('terms', options),
const fields = {}; this._fieldsPopulate('kanji', options)
for (const name of fieldNames) { ]);
fields[name] = '';
} }
const optionsContext = getOptionsContext(); getFieldMarkers(type) {
const options = await getOptionsMutable(optionsContext);
options.anki[tabId].fields = utilBackgroundIsolate(fields);
await settingsSaveOptions();
await _ankiFieldsPopulate(tabId, options);
}
async function _onAnkiFieldsChanged() {
const optionsContext = getOptionsContext();
const options = await getOptionsMutable(optionsContext);
options.anki.terms.deck = $('#anki-terms-deck').val();
options.anki.terms.model = $('#anki-terms-model').val();
options.anki.terms.fields = utilBackgroundIsolate(ankiFieldsToDict(document.querySelectorAll('#terms .anki-field-value')));
options.anki.kanji.deck = $('#anki-kanji-deck').val();
options.anki.kanji.model = $('#anki-kanji-model').val();
options.anki.kanji.fields = utilBackgroundIsolate(ankiFieldsToDict(document.querySelectorAll('#kanji .anki-field-value')));
await settingsSaveOptions();
await onAnkiOptionsChanged(options);
}
// Public
function ankiErrorShown() {
const node = document.querySelector('#anki-error');
return node && !node.hidden;
}
function ankiFieldsToDict(elements) {
const result = {};
for (const element of elements) {
result[element.dataset.field] = element.value;
}
return result;
}
function ankiGetFieldMarkersHtml(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;
}
function ankiGetFieldMarkers(type) {
switch (type) { switch (type) {
case 'terms': case 'terms':
return [ return [
@ -283,27 +90,214 @@ function ankiGetFieldMarkers(type) {
} }
} }
getFieldMarkersHtml(markers) {
function ankiInitialize() { const template = document.querySelector('#anki-field-marker-template').content;
$('#anki-fields-container input,#anki-fields-container select,#anki-fields-container textarea').change(_onAnkiFieldsChanged); const fragment = document.createDocumentFragment();
for (const marker of markers) {
for (const node of document.querySelectorAll('#anki-terms-model,#anki-kanji-model')) { const markerNode = document.importNode(template, true).firstChild;
node.addEventListener('change', _onAnkiModelChanged, false); markerNode.querySelector('.marker-link').textContent = marker;
fragment.appendChild(markerNode);
}
return fragment;
} }
onAnkiOptionsChanged(); // Private
_fieldsToDict(elements) {
const result = {};
for (const element of elements) {
result[element.dataset.field] = element.value;
}
return result;
} }
async function onAnkiOptionsChanged(options=null) { _spinnerShow(show) {
if (options === null) { const spinner = $('#anki-spinner');
const optionsContext = getOptionsContext(); if (show) {
options = await getOptionsMutable(optionsContext); spinner.show();
} else {
spinner.hide();
}
} }
if (!options.anki.enable) { _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 _fieldsPopulate(tabId, options) {
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();
const fields = options.anki[tabId].fields;
for (const name of Object.keys(fields)) {
const value = fields[name];
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').find('.anki-field-value')[0];
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; return;
} finally {
this._spinnerShow(false);
} }
await _ankiDeckAndModelPopulate(options); const tabId = node.dataset.ankiCardType;
await Promise.all([_ankiFieldsPopulate('terms', options), _ankiFieldsPopulate('kanji', options)]); if (tabId !== 'terms' && tabId !== 'kanji') { return; }
const fields = {};
for (const name of fieldNames) {
fields[name] = '';
}
const optionsContext = getOptionsContext();
const options = await getOptionsMutable(optionsContext);
options.anki[tabId].fields = utilBackgroundIsolate(fields);
await settingsSaveOptions();
await this._fieldsPopulate(tabId, options);
}
async _onFieldsChanged() {
const optionsContext = getOptionsContext();
const options = await getOptionsMutable(optionsContext);
options.anki.terms.deck = $('#anki-terms-deck').val();
options.anki.terms.model = $('#anki-terms-model').val();
options.anki.terms.fields = utilBackgroundIsolate(this._fieldsToDict(document.querySelectorAll('#terms .anki-field-value')));
options.anki.kanji.deck = $('#anki-kanji-deck').val();
options.anki.kanji.model = $('#anki-kanji-model').val();
options.anki.kanji.fields = utilBackgroundIsolate(this._fieldsToDict(document.querySelectorAll('#kanji .anki-field-value')));
await settingsSaveOptions();
await this.optionsChanged(options);
}
} }

View File

@ -16,17 +16,15 @@
*/ */
/* global /* global
* AnkiController
* AnkiTemplatesController
* ProfileController * ProfileController
* SettingsBackup * SettingsBackup
* SettingsController * SettingsController
* ankiInitialize
* ankiTemplatesInitialize
* ankiTemplatesUpdateValue
* api * api
* appearanceInitialize * appearanceInitialize
* audioSettingsInitialize * audioSettingsInitialize
* dictSettingsInitialize * dictSettingsInitialize
* onAnkiOptionsChanged
* onDictionaryOptionsChanged * onDictionaryOptionsChanged
* storageInfoInitialize * storageInfoInitialize
* utilBackend * utilBackend
@ -269,9 +267,13 @@ async function onOptionsUpdated({source}) {
const options = await getOptionsMutable(optionsContext); const options = await getOptionsMutable(optionsContext);
document.querySelector('#enable-clipboard-popups').checked = options.general.enableClipboardPopups; document.querySelector('#enable-clipboard-popups').checked = options.general.enableClipboardPopups;
ankiTemplatesUpdateValue(); if (ankiTemplatesController !== null) {
ankiTemplatesController.updateValue();
}
onDictionaryOptionsChanged(); onDictionaryOptionsChanged();
onAnkiOptionsChanged(); if (ankiController !== null) {
ankiController.optionsChanged();
}
await formWrite(options); await formWrite(options);
} }
@ -302,6 +304,8 @@ async function settingsPopulateModifierKeys() {
} }
} }
let ankiController = null;
let ankiTemplatesController = null;
async function onReady() { async function onReady() {
api.forwardLogsToBackend(); api.forwardLogsToBackend();
@ -318,8 +322,10 @@ async function onReady() {
await audioSettingsInitialize(); await audioSettingsInitialize();
await (new ProfileController()).prepare(); await (new ProfileController()).prepare();
await dictSettingsInitialize(); await dictSettingsInitialize();
ankiInitialize(); ankiController = new AnkiController();
ankiTemplatesInitialize(); ankiController.prepare();
ankiTemplatesController = new AnkiTemplatesController(ankiController);
ankiTemplatesController.prepare();
new SettingsBackup().prepare(); new SettingsBackup().prepare();
storageInfoInitialize(); storageInfoInitialize();