Update how dictionaries are displayed on the settings page
This commit is contained in:
parent
79069d5908
commit
2ab871e7ee
482
ext/bg/js/settings-dictionaries.js
Normal file
482
ext/bg/js/settings-dictionaries.js
Normal file
@ -0,0 +1,482 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
|
||||||
|
* Author: Alex Yatskov <alex@foosoft.net>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
let dictionaryUI = null;
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsDictionaryListUI {
|
||||||
|
constructor(container, template, extraContainer, extraTemplate, optionsDictionaries) {
|
||||||
|
this.container = container;
|
||||||
|
this.template = template;
|
||||||
|
this.extraContainer = extraContainer;
|
||||||
|
this.extraTemplate = extraTemplate;
|
||||||
|
this.optionsDictionaries = optionsDictionaries;
|
||||||
|
|
||||||
|
this.dictionaryEntries = [];
|
||||||
|
this.extra = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDictionaries(dictionaries) {
|
||||||
|
for (const dictionaryEntry of this.dictionaryEntries) {
|
||||||
|
dictionaryEntry.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dictionaryEntries = [];
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
for (const dictionaryInfo of toIterable(dictionaries)) {
|
||||||
|
if (this.createEntry(dictionaryInfo)) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const titles = this.dictionaryEntries.map(e => e.dictionaryInfo.title);
|
||||||
|
const removeKeys = Object.keys(this.optionsDictionaries).filter(key => titles.indexOf(key) < 0);
|
||||||
|
if (removeKeys.length >= 0) {
|
||||||
|
for (const key of removeKeys) {
|
||||||
|
delete this.optionsDictionaries[key];
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createEntry(dictionaryInfo) {
|
||||||
|
const title = dictionaryInfo.title;
|
||||||
|
let changed = false;
|
||||||
|
let optionsDictionary;
|
||||||
|
const optionsDictionaries = this.optionsDictionaries;
|
||||||
|
if (optionsDictionaries.hasOwnProperty(title)) {
|
||||||
|
optionsDictionary = optionsDictionaries[title];
|
||||||
|
} else {
|
||||||
|
optionsDictionary = SettingsDictionaryListUI.createDictionaryOptions();
|
||||||
|
optionsDictionaries[title] = optionsDictionary;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = document.importNode(this.template.content, true).firstChild;
|
||||||
|
this.container.appendChild(content);
|
||||||
|
|
||||||
|
this.dictionaryEntries.push(new SettingsDictionaryEntryUI(this, dictionaryInfo, content, optionsDictionary));
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
static createDictionaryOptions() {
|
||||||
|
return utilBackgroundIsolate({
|
||||||
|
priority: 0,
|
||||||
|
enabled: false,
|
||||||
|
allowSecondarySearches: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createExtra(totalCounts, remainders, totalRemainder) {
|
||||||
|
const content = document.importNode(this.extraTemplate.content, true).firstChild;
|
||||||
|
this.extraContainer.appendChild(content);
|
||||||
|
return new SettingsDictionaryExtraUI(this, totalCounts, remainders, totalRemainder, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCounts(dictionaryCounts, totalCounts) {
|
||||||
|
const remainders = Object.assign({}, totalCounts);
|
||||||
|
const keys = Object.keys(remainders);
|
||||||
|
|
||||||
|
for (let i = 0, ii = Math.min(this.dictionaryEntries.length, dictionaryCounts.length); i < ii; ++i) {
|
||||||
|
const counts = dictionaryCounts[i];
|
||||||
|
this.dictionaryEntries[i].setCounts(counts);
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
remainders[key] -= counts[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalRemainder = 0;
|
||||||
|
for (const key of keys) {
|
||||||
|
totalRemainder += remainders[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.extra !== null) {
|
||||||
|
this.extra.cleanup();
|
||||||
|
this.extra = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalRemainder > 0) {
|
||||||
|
this.extra = this.createExtra(totalCounts, remainders, totalRemainder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
// Overwrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsDictionaryEntryUI {
|
||||||
|
constructor(parent, dictionaryInfo, content, optionsDictionary) {
|
||||||
|
this.parent = parent;
|
||||||
|
this.dictionaryInfo = dictionaryInfo;
|
||||||
|
this.optionsDictionary = optionsDictionary;
|
||||||
|
this.counts = null;
|
||||||
|
this.eventListeners = [];
|
||||||
|
|
||||||
|
this.content = content;
|
||||||
|
this.enabledCheckbox = this.content.querySelector('.dict-enabled');
|
||||||
|
this.allowSecondarySearchesCheckbox = this.content.querySelector('.dict-allow-secondary-searches');
|
||||||
|
this.priorityInput = this.content.querySelector('.dict-priority');
|
||||||
|
|
||||||
|
this.content.querySelector('.dict-title').textContent = this.dictionaryInfo.title;
|
||||||
|
this.content.querySelector('.dict-revision').textContent = `rev.${this.dictionaryInfo.revision}`;
|
||||||
|
|
||||||
|
this.applyValues();
|
||||||
|
|
||||||
|
this.addEventListener(this.enabledCheckbox, 'change', (e) => this.onEnabledChanged(e), false);
|
||||||
|
this.addEventListener(this.allowSecondarySearchesCheckbox, 'change', (e) => this.onAllowSecondarySearchesChanged(e), false);
|
||||||
|
this.addEventListener(this.priorityInput, 'change', (e) => this.onPriorityChanged(e), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if (this.content !== null) {
|
||||||
|
if (this.content.parentNode !== null) {
|
||||||
|
this.content.parentNode.removeChild(this.content);
|
||||||
|
}
|
||||||
|
this.content = null;
|
||||||
|
}
|
||||||
|
this.dictionaryInfo = null;
|
||||||
|
this.clearEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
setCounts(counts) {
|
||||||
|
this.counts = counts;
|
||||||
|
const node = this.content.querySelector('.dict-counts');
|
||||||
|
node.textContent = JSON.stringify({
|
||||||
|
info: this.dictionaryInfo,
|
||||||
|
counts
|
||||||
|
}, null, 4);
|
||||||
|
node.removeAttribute('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
this.parent.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener(node, type, listener, options) {
|
||||||
|
node.addEventListener(type, listener, options);
|
||||||
|
this.eventListeners.push([node, type, listener, options]);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearEventListeners() {
|
||||||
|
for (const [node, type, listener, options] of this.eventListeners) {
|
||||||
|
node.removeEventListener(type, listener, options);
|
||||||
|
}
|
||||||
|
this.eventListeners = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
applyValues() {
|
||||||
|
this.enabledCheckbox.checked = this.optionsDictionary.enabled;
|
||||||
|
this.allowSecondarySearchesCheckbox.checked = this.optionsDictionary.allowSecondarySearches;
|
||||||
|
this.priorityInput.value = `${this.optionsDictionary.priority}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnabledChanged(e) {
|
||||||
|
this.optionsDictionary.enabled = !!e.target.checked;
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
onAllowSecondarySearchesChanged(e) {
|
||||||
|
this.optionsDictionary.allowSecondarySearches = !!e.target.checked;
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
onPriorityChanged(e) {
|
||||||
|
let value = Number.parseFloat(e.target.value);
|
||||||
|
if (Number.isNaN(value)) {
|
||||||
|
value = this.optionsDictionary.priority;
|
||||||
|
} else {
|
||||||
|
this.optionsDictionary.priority = value;
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
e.target.value = `${value}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsDictionaryExtraUI {
|
||||||
|
constructor(parent, totalCounts, remainders, totalRemainder, content) {
|
||||||
|
this.parent = parent;
|
||||||
|
this.content = content;
|
||||||
|
|
||||||
|
this.content.querySelector('.dict-total-count').textContent = `${totalRemainder} item${totalRemainder !== 1 ? 's' : ''}`;
|
||||||
|
|
||||||
|
const node = this.content.querySelector('.dict-counts');
|
||||||
|
node.textContent = JSON.stringify({
|
||||||
|
counts: totalCounts,
|
||||||
|
remainders: remainders
|
||||||
|
}, null, 4);
|
||||||
|
node.removeAttribute('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if (this.content !== null) {
|
||||||
|
if (this.content.parentNode !== null) {
|
||||||
|
this.content.parentNode.removeChild(this.content);
|
||||||
|
}
|
||||||
|
this.content = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function dictSettingsInitialize() {
|
||||||
|
const optionsContext = getOptionsContext();
|
||||||
|
const options = await apiOptionsGet(optionsContext);
|
||||||
|
|
||||||
|
dictionaryUI = new SettingsDictionaryListUI(
|
||||||
|
document.querySelector('#dict-groups'),
|
||||||
|
document.querySelector('#dict-template'),
|
||||||
|
document.querySelector('#dict-groups-extra'),
|
||||||
|
document.querySelector('#dict-extra-template'),
|
||||||
|
options.dictionaries
|
||||||
|
);
|
||||||
|
dictionaryUI.save = () => apiOptionsSave();
|
||||||
|
|
||||||
|
document.querySelector('#dict-purge-link').addEventListener('click', (e) => onDictionaryPurge(e), false);
|
||||||
|
document.querySelector('#dict-file-button').addEventListener('click', (e) => onDictionaryImportButtonClick(e), false);
|
||||||
|
document.querySelector('#dict-file').addEventListener('change', (e) => onDictionaryImport(e), false);
|
||||||
|
document.querySelector('#dict-main').addEventListener('change', (e) => onDictionaryMainChanged(e), false);
|
||||||
|
|
||||||
|
onDatabaseUpdated(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDatabaseUpdated(options) {
|
||||||
|
try {
|
||||||
|
const dictionaries = await utilDatabaseGetDictionaryInfo();
|
||||||
|
dictionaryUI.setDictionaries(dictionaries);
|
||||||
|
|
||||||
|
updateMainDictionarySelect(options, dictionaries);
|
||||||
|
|
||||||
|
const {counts, total} = await utilDatabaseGetDictionaryCounts(dictionaries.map(v => v.title), true);
|
||||||
|
dictionaryUI.setCounts(counts, total);
|
||||||
|
} catch (e) {
|
||||||
|
dictionaryErrorsShow([e]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateMainDictionarySelect(options, dictionaries) {
|
||||||
|
const select = document.querySelector('#dict-main');
|
||||||
|
select.textContent = ''; // Empty
|
||||||
|
|
||||||
|
let option = document.createElement('option');
|
||||||
|
option.className = 'text-muted';
|
||||||
|
option.value = '';
|
||||||
|
option.textContent = 'Not selected';
|
||||||
|
select.appendChild(option);
|
||||||
|
|
||||||
|
let value = '';
|
||||||
|
const currentValue = options.general.mainDictionary;
|
||||||
|
for (const {title, sequenced} of toIterable(dictionaries)) {
|
||||||
|
if (!sequenced) { continue; }
|
||||||
|
|
||||||
|
option = document.createElement('option');
|
||||||
|
option.value = title;
|
||||||
|
option.textContent = title;
|
||||||
|
select.appendChild(option);
|
||||||
|
|
||||||
|
if (title === currentValue) {
|
||||||
|
value = title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select.value = value;
|
||||||
|
|
||||||
|
if (options.general.mainDictionary !== value) {
|
||||||
|
options.general.mainDictionary = value;
|
||||||
|
apiOptionsSave();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDictionaryMainChanged(e) {
|
||||||
|
const value = e.target.value;
|
||||||
|
const optionsContext = getOptionsContext();
|
||||||
|
const options = await apiOptionsGet(optionsContext);
|
||||||
|
options.general.mainDictionary = value;
|
||||||
|
apiOptionsSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function dictionaryErrorToString(error) {
|
||||||
|
if (error.toString) {
|
||||||
|
error = error.toString();
|
||||||
|
} else {
|
||||||
|
error = `${error}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [match, subst] of dictionaryErrorToString.overrides) {
|
||||||
|
if (error.includes(match)) {
|
||||||
|
error = subst;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
dictionaryErrorToString.overrides = [
|
||||||
|
[
|
||||||
|
'A mutation operation was attempted on a database that did not allow mutations.',
|
||||||
|
'Access to IndexedDB appears to be restricted. Firefox seems to require that the history preference is set to "Remember history" before IndexedDB use of any kind is allowed.'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'The operation failed for reasons unrelated to the database itself and not covered by any other error code.',
|
||||||
|
'Unable to access IndexedDB due to a possibly corrupt user profile. Try using the "Refresh Firefox" feature to reset your user profile.'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'BulkError',
|
||||||
|
'Unable to finish importing dictionary data into IndexedDB. This may indicate that you do not have sufficient disk space available to complete this operation.'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
function dictionaryErrorsShow(errors) {
|
||||||
|
const dialog = $('#dict-error');
|
||||||
|
dialog.show().text('');
|
||||||
|
|
||||||
|
if (errors !== null && errors.length > 0) {
|
||||||
|
const uniqueErrors = {};
|
||||||
|
for (let e of errors) {
|
||||||
|
console.error(e);
|
||||||
|
e = dictionaryErrorToString(e);
|
||||||
|
uniqueErrors[e] = uniqueErrors.hasOwnProperty(e) ? uniqueErrors[e] + 1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const e in uniqueErrors) {
|
||||||
|
const count = uniqueErrors[e];
|
||||||
|
const div = document.createElement('p');
|
||||||
|
if (count > 1) {
|
||||||
|
div.textContent = `${e} `;
|
||||||
|
const em = document.createElement('em');
|
||||||
|
em.textContent = `(${count})`;
|
||||||
|
div.appendChild(em);
|
||||||
|
} else {
|
||||||
|
div.textContent = `${e}`;
|
||||||
|
}
|
||||||
|
dialog.append($(div));
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.show();
|
||||||
|
} else {
|
||||||
|
dialog.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function dictionarySpinnerShow(show) {
|
||||||
|
const spinner = $('#dict-spinner');
|
||||||
|
if (show) {
|
||||||
|
spinner.show();
|
||||||
|
} else {
|
||||||
|
spinner.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDictionaryImportButtonClick() {
|
||||||
|
const dictFile = document.querySelector('#dict-file');
|
||||||
|
dictFile.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDictionaryPurge(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const dictControls = $('#dict-importer, #dict-groups, #dict-groups-extra, #dict-main-group').hide();
|
||||||
|
const dictProgress = $('#dict-purge').show();
|
||||||
|
|
||||||
|
try {
|
||||||
|
dictionaryErrorsShow(null);
|
||||||
|
dictionarySpinnerShow(true);
|
||||||
|
|
||||||
|
await utilDatabasePurge();
|
||||||
|
for (const options of toIterable(await getOptionsArray())) {
|
||||||
|
options.dictionaries = utilBackgroundIsolate({});
|
||||||
|
options.general.mainDictionary = '';
|
||||||
|
}
|
||||||
|
await settingsSaveOptions();
|
||||||
|
|
||||||
|
const optionsContext = getOptionsContext();
|
||||||
|
const options = await apiOptionsGet(optionsContext);
|
||||||
|
onDatabaseUpdated(options);
|
||||||
|
} catch (err) {
|
||||||
|
dictionaryErrorsShow([err]);
|
||||||
|
} finally {
|
||||||
|
dictionarySpinnerShow(false);
|
||||||
|
|
||||||
|
dictControls.show();
|
||||||
|
dictProgress.hide();
|
||||||
|
|
||||||
|
if (storageEstimate.mostRecent !== null) {
|
||||||
|
storageUpdateStats();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDictionaryImport(e) {
|
||||||
|
const dictFile = $('#dict-file');
|
||||||
|
const dictControls = $('#dict-importer').hide();
|
||||||
|
const dictProgress = $('#dict-import-progress').show();
|
||||||
|
|
||||||
|
try {
|
||||||
|
dictionaryErrorsShow(null);
|
||||||
|
dictionarySpinnerShow(true);
|
||||||
|
|
||||||
|
const setProgress = percent => dictProgress.find('.progress-bar').css('width', `${percent}%`);
|
||||||
|
const updateProgress = (total, current) => {
|
||||||
|
setProgress(current / total * 100.0);
|
||||||
|
if (storageEstimate.mostRecent !== null && !storageUpdateStats.isUpdating) {
|
||||||
|
storageUpdateStats();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setProgress(0.0);
|
||||||
|
|
||||||
|
const exceptions = [];
|
||||||
|
const summary = await utilDatabaseImport(e.target.files[0], updateProgress, exceptions);
|
||||||
|
for (const options of toIterable(await getOptionsArray())) {
|
||||||
|
const dictionaryOptions = SettingsDictionaryListUI.createDictionaryOptions();
|
||||||
|
dictionaryOptions.enabled = true;
|
||||||
|
options.dictionaries[summary.title] = dictionaryOptions;
|
||||||
|
if (summary.sequenced && options.general.mainDictionary === '') {
|
||||||
|
options.general.mainDictionary = summary.title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await settingsSaveOptions();
|
||||||
|
|
||||||
|
if (exceptions.length > 0) {
|
||||||
|
exceptions.push(`Dictionary may not have been imported properly: ${exceptions.length} error${exceptions.length === 1 ? '' : 's'} reported.`);
|
||||||
|
dictionaryErrorsShow(exceptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionsContext = getOptionsContext();
|
||||||
|
const options = await apiOptionsGet(optionsContext);
|
||||||
|
onDatabaseUpdated(options);
|
||||||
|
} catch (err) {
|
||||||
|
dictionaryErrorsShow([err]);
|
||||||
|
} finally {
|
||||||
|
dictionarySpinnerShow(false);
|
||||||
|
|
||||||
|
dictFile.val('');
|
||||||
|
dictControls.show();
|
||||||
|
dictProgress.hide();
|
||||||
|
}
|
||||||
|
}
|
@ -81,16 +81,6 @@ async function formRead(options) {
|
|||||||
options.anki.kanji.model = $('#anki-kanji-model').val();
|
options.anki.kanji.model = $('#anki-kanji-model').val();
|
||||||
options.anki.kanji.fields = utilBackgroundIsolate(ankiFieldsToDict($('#kanji .anki-field-value')));
|
options.anki.kanji.fields = utilBackgroundIsolate(ankiFieldsToDict($('#kanji .anki-field-value')));
|
||||||
}
|
}
|
||||||
|
|
||||||
options.general.mainDictionary = $('#dict-main').val();
|
|
||||||
$('.dict-group').each((index, element) => {
|
|
||||||
const dictionary = $(element);
|
|
||||||
options.dictionaries[dictionary.data('title')] = utilBackgroundIsolate({
|
|
||||||
priority: parseInt(dictionary.find('.dict-priority').val(), 10),
|
|
||||||
enabled: dictionary.find('.dict-enabled').prop('checked'),
|
|
||||||
allowSecondarySearches: dictionary.find('.dict-allow-secondary-searches').prop('checked')
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function formWrite(options) {
|
async function formWrite(options) {
|
||||||
@ -144,13 +134,6 @@ async function formWrite(options) {
|
|||||||
$('#screenshot-quality').val(options.anki.screenshot.quality);
|
$('#screenshot-quality').val(options.anki.screenshot.quality);
|
||||||
$('#field-templates').val(options.anki.fieldTemplates);
|
$('#field-templates').val(options.anki.fieldTemplates);
|
||||||
|
|
||||||
try {
|
|
||||||
await dictionaryGroupsPopulate(options);
|
|
||||||
await formMainDictionaryOptionsPopulate(options);
|
|
||||||
} catch (e) {
|
|
||||||
dictionaryErrorsShow([e]);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ankiDeckAndModelPopulate(options);
|
await ankiDeckAndModelPopulate(options);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -161,10 +144,6 @@ async function formWrite(options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formSetupEventListeners() {
|
function formSetupEventListeners() {
|
||||||
$('#dict-purge-link').click(utilAsync(onDictionaryPurge));
|
|
||||||
$('#dict-file').change(utilAsync(onDictionaryImport));
|
|
||||||
$('#dict-file-button').click(onDictionaryImportButtonClick);
|
|
||||||
|
|
||||||
$('#field-templates-reset').click(utilAsync(onAnkiFieldTemplatesReset));
|
$('#field-templates-reset').click(utilAsync(onAnkiFieldTemplatesReset));
|
||||||
$('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(utilAsync(onFormOptionsChanged));
|
$('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(utilAsync(onFormOptionsChanged));
|
||||||
$('.anki-model').change(utilAsync(onAnkiModelChanged));
|
$('.anki-model').change(utilAsync(onAnkiModelChanged));
|
||||||
@ -184,23 +163,6 @@ function formUpdateVisibility(options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function formMainDictionaryOptionsPopulate(options) {
|
|
||||||
const select = $('#dict-main').empty();
|
|
||||||
select.append($('<option class="text-muted" value="">Not selected</option>'));
|
|
||||||
|
|
||||||
let mainDictionary = '';
|
|
||||||
for (const dictRow of toIterable(await utilDatabaseSummarize())) {
|
|
||||||
if (dictRow.sequenced) {
|
|
||||||
select.append($(`<option value="${dictRow.title}">${dictRow.title}</option>`));
|
|
||||||
if (dictRow.title === options.general.mainDictionary) {
|
|
||||||
mainDictionary = dictRow.title;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
select.val(mainDictionary);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onFormOptionsChanged(e) {
|
async function onFormOptionsChanged(e) {
|
||||||
if (!e.originalEvent && !e.isTrigger) {
|
if (!e.originalEvent && !e.isTrigger) {
|
||||||
return;
|
return;
|
||||||
@ -239,6 +201,7 @@ async function onReady() {
|
|||||||
appearanceInitialize();
|
appearanceInitialize();
|
||||||
await audioSettingsInitialize();
|
await audioSettingsInitialize();
|
||||||
await profileOptionsSetup();
|
await profileOptionsSetup();
|
||||||
|
await dictSettingsInitialize();
|
||||||
|
|
||||||
storageInfoInitialize();
|
storageInfoInitialize();
|
||||||
|
|
||||||
@ -422,228 +385,6 @@ function onMessage({action, params}, sender, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Dictionary
|
|
||||||
*/
|
|
||||||
|
|
||||||
function dictionaryErrorToString(error) {
|
|
||||||
if (error.toString) {
|
|
||||||
error = error.toString();
|
|
||||||
} else {
|
|
||||||
error = `${error}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [match, subst] of dictionaryErrorToString.overrides) {
|
|
||||||
if (error.includes(match)) {
|
|
||||||
error = subst;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
dictionaryErrorToString.overrides = [
|
|
||||||
[
|
|
||||||
'A mutation operation was attempted on a database that did not allow mutations.',
|
|
||||||
'Access to IndexedDB appears to be restricted. Firefox seems to require that the history preference is set to "Remember history" before IndexedDB use of any kind is allowed.'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'The operation failed for reasons unrelated to the database itself and not covered by any other error code.',
|
|
||||||
'Unable to access IndexedDB due to a possibly corrupt user profile. Try using the "Refresh Firefox" feature to reset your user profile.'
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'BulkError',
|
|
||||||
'Unable to finish importing dictionary data into IndexedDB. This may indicate that you do not have sufficient disk space available to complete this operation.'
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
function dictionaryErrorsShow(errors) {
|
|
||||||
const dialog = $('#dict-error');
|
|
||||||
dialog.show().text('');
|
|
||||||
|
|
||||||
if (errors !== null && errors.length > 0) {
|
|
||||||
const uniqueErrors = {};
|
|
||||||
for (let e of errors) {
|
|
||||||
e = dictionaryErrorToString(e);
|
|
||||||
uniqueErrors[e] = uniqueErrors.hasOwnProperty(e) ? uniqueErrors[e] + 1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const e in uniqueErrors) {
|
|
||||||
const count = uniqueErrors[e];
|
|
||||||
const div = document.createElement('p');
|
|
||||||
if (count > 1) {
|
|
||||||
div.textContent = `${e} `;
|
|
||||||
const em = document.createElement('em');
|
|
||||||
em.textContent = `(${count})`;
|
|
||||||
div.appendChild(em);
|
|
||||||
} else {
|
|
||||||
div.textContent = `${e}`;
|
|
||||||
}
|
|
||||||
dialog.append($(div));
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog.show();
|
|
||||||
} else {
|
|
||||||
dialog.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function dictionarySpinnerShow(show) {
|
|
||||||
const spinner = $('#dict-spinner');
|
|
||||||
if (show) {
|
|
||||||
spinner.show();
|
|
||||||
} else {
|
|
||||||
spinner.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function dictionaryGroupsSort() {
|
|
||||||
const dictGroups = $('#dict-groups');
|
|
||||||
const dictGroupChildren = dictGroups.children('.dict-group').sort((ca, cb) => {
|
|
||||||
const pa = parseInt($(ca).find('.dict-priority').val(), 10);
|
|
||||||
const pb = parseInt($(cb).find('.dict-priority').val(), 10);
|
|
||||||
if (pa < pb) {
|
|
||||||
return 1;
|
|
||||||
} else if (pa > pb) {
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
dictGroups.append(dictGroupChildren);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dictionaryGroupsPopulate(options) {
|
|
||||||
const dictGroups = $('#dict-groups').empty();
|
|
||||||
const dictWarning = $('#dict-warning').hide();
|
|
||||||
|
|
||||||
const dictRows = toIterable(await utilDatabaseSummarize());
|
|
||||||
if (dictRows.length === 0) {
|
|
||||||
dictWarning.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const dictRow of toIterable(dictRowsSort(dictRows, options))) {
|
|
||||||
const dictOptions = options.dictionaries[dictRow.title] || {
|
|
||||||
enabled: false,
|
|
||||||
priority: 0,
|
|
||||||
allowSecondarySearches: false
|
|
||||||
};
|
|
||||||
|
|
||||||
const dictHtml = await apiTemplateRender('dictionary.html', {
|
|
||||||
enabled: dictOptions.enabled,
|
|
||||||
priority: dictOptions.priority,
|
|
||||||
allowSecondarySearches: dictOptions.allowSecondarySearches,
|
|
||||||
title: dictRow.title,
|
|
||||||
version: dictRow.version,
|
|
||||||
revision: dictRow.revision,
|
|
||||||
outdated: dictRow.version < 3
|
|
||||||
});
|
|
||||||
|
|
||||||
dictGroups.append($(dictHtml));
|
|
||||||
}
|
|
||||||
|
|
||||||
formUpdateVisibility(options);
|
|
||||||
|
|
||||||
$('.dict-enabled, .dict-priority, .dict-allow-secondary-searches').change(e => {
|
|
||||||
dictionaryGroupsSort();
|
|
||||||
onFormOptionsChanged(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onDictionaryPurge(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const dictControls = $('#dict-importer, #dict-groups, #dict-main-group').hide();
|
|
||||||
const dictProgress = $('#dict-purge').show();
|
|
||||||
|
|
||||||
try {
|
|
||||||
dictionaryErrorsShow(null);
|
|
||||||
dictionarySpinnerShow(true);
|
|
||||||
|
|
||||||
await utilDatabasePurge();
|
|
||||||
for (const options of toIterable(await getOptionsArray())) {
|
|
||||||
options.dictionaries = utilBackgroundIsolate({});
|
|
||||||
options.general.mainDictionary = '';
|
|
||||||
}
|
|
||||||
await settingsSaveOptions();
|
|
||||||
|
|
||||||
const optionsContext = getOptionsContext();
|
|
||||||
const options = await apiOptionsGet(optionsContext);
|
|
||||||
await dictionaryGroupsPopulate(options);
|
|
||||||
await formMainDictionaryOptionsPopulate(options);
|
|
||||||
} catch (e) {
|
|
||||||
dictionaryErrorsShow([e]);
|
|
||||||
} finally {
|
|
||||||
dictionarySpinnerShow(false);
|
|
||||||
|
|
||||||
dictControls.show();
|
|
||||||
dictProgress.hide();
|
|
||||||
|
|
||||||
if (storageEstimate.mostRecent !== null) {
|
|
||||||
storageUpdateStats();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDictionaryImportButtonClick() {
|
|
||||||
const dictFile = document.querySelector('#dict-file');
|
|
||||||
dictFile.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onDictionaryImport(e) {
|
|
||||||
const dictFile = $('#dict-file');
|
|
||||||
const dictControls = $('#dict-importer').hide();
|
|
||||||
const dictProgress = $('#dict-import-progress').show();
|
|
||||||
|
|
||||||
try {
|
|
||||||
dictionaryErrorsShow(null);
|
|
||||||
dictionarySpinnerShow(true);
|
|
||||||
|
|
||||||
const setProgress = percent => dictProgress.find('.progress-bar').css('width', `${percent}%`);
|
|
||||||
const updateProgress = (total, current) => {
|
|
||||||
setProgress(current / total * 100.0);
|
|
||||||
if (storageEstimate.mostRecent !== null && !storageUpdateStats.isUpdating) {
|
|
||||||
storageUpdateStats();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
setProgress(0.0);
|
|
||||||
|
|
||||||
const exceptions = [];
|
|
||||||
const summary = await utilDatabaseImport(e.target.files[0], updateProgress, exceptions);
|
|
||||||
for (const options of toIterable(await getOptionsArray())) {
|
|
||||||
options.dictionaries[summary.title] = utilBackgroundIsolate({
|
|
||||||
enabled: true,
|
|
||||||
priority: 0,
|
|
||||||
allowSecondarySearches: false
|
|
||||||
});
|
|
||||||
if (summary.sequenced && options.general.mainDictionary === '') {
|
|
||||||
options.general.mainDictionary = summary.title;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await settingsSaveOptions();
|
|
||||||
|
|
||||||
if (exceptions.length > 0) {
|
|
||||||
exceptions.push(`Dictionary may not have been imported properly: ${exceptions.length} error${exceptions.length === 1 ? '' : 's'} reported.`);
|
|
||||||
dictionaryErrorsShow(exceptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
const optionsContext = getOptionsContext();
|
|
||||||
const options = await apiOptionsGet(optionsContext);
|
|
||||||
await dictionaryGroupsPopulate(options);
|
|
||||||
await formMainDictionaryOptionsPopulate(options);
|
|
||||||
} catch (e) {
|
|
||||||
dictionaryErrorsShow([e]);
|
|
||||||
} finally {
|
|
||||||
dictionarySpinnerShow(false);
|
|
||||||
|
|
||||||
dictFile.val('');
|
|
||||||
dictControls.show();
|
|
||||||
dictProgress.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Anki
|
* Anki
|
||||||
*/
|
*/
|
||||||
|
@ -1,32 +1,5 @@
|
|||||||
(function() {
|
(function() {
|
||||||
var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
|
var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
|
||||||
templates['dictionary.html'] = template({"1":function(container,depth0,helpers,partials,data) {
|
|
||||||
return " <p class=\"text-warning\">This dictionary is outdated and may not support new extension features; please import the latest version.</p>\n";
|
|
||||||
},"3":function(container,depth0,helpers,partials,data) {
|
|
||||||
return "checked";
|
|
||||||
},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
|
|
||||||
var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
|
|
||||||
|
|
||||||
return "<div class=\"dict-group well well-sm\" data-title=\""
|
|
||||||
+ alias4(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper)))
|
|
||||||
+ "\">\n <h4><span class=\"text-muted glyphicon glyphicon-book\"></span> "
|
|
||||||
+ alias4(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper)))
|
|
||||||
+ " <small>rev."
|
|
||||||
+ alias4(((helper = (helper = helpers.revision || (depth0 != null ? depth0.revision : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"revision","hash":{},"data":data}) : helper)))
|
|
||||||
+ "</small></h4>\n"
|
|
||||||
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.outdated : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
|
|
||||||
+ "\n <div class=\"checkbox\">\n <label><input type=\"checkbox\" class=\"dict-enabled\" "
|
|
||||||
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.enabled : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
|
|
||||||
+ "> Enable search</label>\n </div>\n <div class=\"checkbox options-advanced\">\n <label><input type=\"checkbox\" class=\"dict-allow-secondary-searches\" "
|
|
||||||
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.allowSecondarySearches : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
|
|
||||||
+ "> Allow secondary searches</label>\n </div>\n <div class=\"form-group options-advanced\">\n <label for=\"dict-"
|
|
||||||
+ alias4(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper)))
|
|
||||||
+ "\">Result priority</label>\n <input type=\"number\" value=\""
|
|
||||||
+ alias4(((helper = (helper = helpers.priority || (depth0 != null ? depth0.priority : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"priority","hash":{},"data":data}) : helper)))
|
|
||||||
+ "\" id=\"dict-"
|
|
||||||
+ alias4(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper)))
|
|
||||||
+ "\" class=\"form-control dict-priority\">\n </div>\n</div>\n";
|
|
||||||
},"useData":true});
|
|
||||||
templates['kanji.html'] = template({"1":function(container,depth0,helpers,partials,data) {
|
templates['kanji.html'] = template({"1":function(container,depth0,helpers,partials,data) {
|
||||||
var stack1;
|
var stack1;
|
||||||
|
|
||||||
|
@ -84,6 +84,14 @@ function utilDatabaseSummarize() {
|
|||||||
return utilBackend().translator.database.summarize();
|
return utilBackend().translator.database.summarize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function utilDatabaseGetDictionaryInfo() {
|
||||||
|
return utilBackend().translator.database.getDictionaryInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
function utilDatabaseGetDictionaryCounts(dictionaryNames, getTotal) {
|
||||||
|
return utilBackend().translator.database.getDictionaryCounts(dictionaryNames, getTotal);
|
||||||
|
}
|
||||||
|
|
||||||
function utilAnkiGetModelFieldNames(modelName) {
|
function utilAnkiGetModelFieldNames(modelName) {
|
||||||
return utilBackend().anki.getModelFieldNames(modelName);
|
return utilBackend().anki.getModelFieldNames(modelName);
|
||||||
}
|
}
|
||||||
|
@ -410,7 +410,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="ignore-form-changes">
|
||||||
<div>
|
<div>
|
||||||
<img src="/mixed/img/spinner.gif" class="pull-right" id="dict-spinner" alt>
|
<img src="/mixed/img/spinner.gif" class="pull-right" id="dict-spinner" alt>
|
||||||
<h3>Dictionaries</h3>
|
<h3>Dictionaries</h3>
|
||||||
@ -434,6 +434,7 @@
|
|||||||
<div class="alert alert-danger" id="dict-error"></div>
|
<div class="alert alert-danger" id="dict-error"></div>
|
||||||
|
|
||||||
<div id="dict-groups"></div>
|
<div id="dict-groups"></div>
|
||||||
|
<div id="dict-groups-extra"></div>
|
||||||
|
|
||||||
<div id="dict-import-progress">
|
<div id="dict-import-progress">
|
||||||
Dictionary data is being imported, please be patient...
|
Dictionary data is being imported, please be patient...
|
||||||
@ -451,6 +452,32 @@
|
|||||||
<button class="btn btn-primary" id="dict-file-button">Import Dictionary</button>
|
<button class="btn btn-primary" id="dict-file-button">Import Dictionary</button>
|
||||||
<div hidden><input type="file" id="dict-file"></div>
|
<div hidden><input type="file" id="dict-file"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template id="dict-template"><div class="dict-group well well-sm">
|
||||||
|
<h4><span class="text-muted glyphicon glyphicon-book"></span> <span class="dict-title"></span> <small class="dict-revision"></small></h4>
|
||||||
|
<p class="text-warning" hidden>This dictionary is outdated and may not support new extension features; please import the latest version.</p>
|
||||||
|
|
||||||
|
<div class="checkbox">
|
||||||
|
<label><input type="checkbox" class="dict-enabled"> Enable search</label>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox options-advanced">
|
||||||
|
<label><input type="checkbox" class="dict-allow-secondary-searches"> Allow secondary searches</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group options-advanced">
|
||||||
|
<label class="dict-result-priority-label">Result priority</label>
|
||||||
|
<input type="number" class="form-control dict-priority">
|
||||||
|
</div>
|
||||||
|
<pre class="debug dict-counts" hidden></pre>
|
||||||
|
</div></template>
|
||||||
|
|
||||||
|
<template id="dict-extra-template"><div class="well well-sm">
|
||||||
|
<h4><span class="text-muted glyphicon glyphicon-alert"></span> <span class="dict-title">Unassociated Data</span> <small class="dict-total-count"></small></h4>
|
||||||
|
<p class="text-warning">
|
||||||
|
The database contains extra data which is not associated with any installed dictionary.
|
||||||
|
Purging the database can fix this issue.
|
||||||
|
</p>
|
||||||
|
<pre class="debug dict-counts" hidden></pre>
|
||||||
|
</div></template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="storage-info">
|
<div id="storage-info">
|
||||||
@ -700,6 +727,7 @@
|
|||||||
<script src="/bg/js/util.js"></script>
|
<script src="/bg/js/util.js"></script>
|
||||||
<script src="/mixed/js/audio.js"></script>
|
<script src="/mixed/js/audio.js"></script>
|
||||||
|
|
||||||
|
<script src="/bg/js/settings-dictionaries.js"></script>
|
||||||
<script src="/bg/js/settings-profiles.js"></script>
|
<script src="/bg/js/settings-profiles.js"></script>
|
||||||
<script src="/bg/js/settings.js"></script>
|
<script src="/bg/js/settings.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
<div class="dict-group well well-sm" data-title="{{title}}">
|
|
||||||
<h4><span class="text-muted glyphicon glyphicon-book"></span> {{title}} <small>rev.{{revision}}</small></h4>
|
|
||||||
{{#if outdated}}
|
|
||||||
<p class="text-warning">This dictionary is outdated and may not support new extension features; please import the latest version.</p>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<div class="checkbox">
|
|
||||||
<label><input type="checkbox" class="dict-enabled" {{#if enabled}}checked{{/if}}> Enable search</label>
|
|
||||||
</div>
|
|
||||||
<div class="checkbox options-advanced">
|
|
||||||
<label><input type="checkbox" class="dict-allow-secondary-searches" {{#if allowSecondarySearches}}checked{{/if}}> Allow secondary searches</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-group options-advanced">
|
|
||||||
<label for="dict-{{title}}">Result priority</label>
|
|
||||||
<input type="number" value="{{priority}}" id="dict-{{title}}" class="form-control dict-priority">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
Loading…
Reference in New Issue
Block a user