From 44638b7ceb8ec4e2a235ad4ffc9aa23ec66f21d2 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Tue, 29 Oct 2019 16:31:27 -0400 Subject: Simplify how option visibility is changed --- ext/bg/css/settings.css | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'ext/bg/css') diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css index b3d5b884..1036622d 100644 --- a/ext/bg/css/settings.css +++ b/ext/bg/css/settings.css @@ -17,9 +17,16 @@ */ -#anki-spinner, #anki-general, #anki-error, +#anki-spinner, #anki-error, #dict-spinner, #dict-error, #dict-warning, #dict-purge, #dict-import-progress, -#debug, .options-advanced, .storage-hidden, #storage-spinner { +.storage-hidden, #storage-spinner { + display: none; +} + +html:root:not([data-options-anki-enable=true]) #anki-general, +html:root:not([data-options-general-debug-info=true]) .debug, +html:root:not([data-options-general-show-advanced=true]) .options-advanced, +html:root:not([data-options-general-result-output-mode=merge]) #dict-main-group { display: none; } -- cgit v1.2.3 From e355b839142a8bab0edc446c7da08256ad5b938c Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 2 Nov 2019 14:39:37 -0400 Subject: Update how nodes are hidden --- ext/bg/css/settings.css | 2 +- ext/bg/js/settings-dictionaries.js | 17 ++++++++++------- ext/bg/settings.html | 6 +++--- 3 files changed, 14 insertions(+), 11 deletions(-) (limited to 'ext/bg/css') diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css index 1036622d..102d53de 100644 --- a/ext/bg/css/settings.css +++ b/ext/bg/css/settings.css @@ -18,7 +18,7 @@ #anki-spinner, #anki-error, -#dict-spinner, #dict-error, #dict-warning, #dict-purge, #dict-import-progress, +#dict-spinner, #dict-import-progress, .storage-hidden, #storage-spinner { display: none; } diff --git a/ext/bg/js/settings-dictionaries.js b/ext/bg/js/settings-dictionaries.js index 72a27475..2f33d1ac 100644 --- a/ext/bg/js/settings-dictionaries.js +++ b/ext/bg/js/settings-dictionaries.js @@ -270,6 +270,8 @@ async function onDatabaseUpdated(options) { const dictionaries = await utilDatabaseGetDictionaryInfo(); dictionaryUI.setDictionaries(dictionaries); + document.querySelector('#dict-warning').hidden = (dictionaries.length > 0); + updateMainDictionarySelect(options, dictionaries); const {counts, total} = await utilDatabaseGetDictionaryCounts(dictionaries.map(v => v.title), true); @@ -353,8 +355,8 @@ dictionaryErrorToString.overrides = [ ]; function dictionaryErrorsShow(errors) { - const dialog = $('#dict-error'); - dialog.show().text(''); + const dialog = document.querySelector('#dict-error'); + dialog.textContent = ''; if (errors !== null && errors.length > 0) { const uniqueErrors = {}; @@ -375,12 +377,12 @@ function dictionaryErrorsShow(errors) { } else { div.textContent = `${e}`; } - dialog.append($(div)); + dialog.appendChild(div); } - dialog.show(); + dialog.hidden = false; } else { - dialog.hide(); + dialog.hidden = true; } } @@ -410,7 +412,8 @@ async function onDictionaryPurge(e) { $('#dict-purge-modal').modal('hide'); const dictControls = $('#dict-importer, #dict-groups, #dict-groups-extra, #dict-main-group').hide(); - const dictProgress = $('#dict-purge').show(); + const dictProgress = document.querySelector('#dict-purge'); + dictProgress.hidden = false; try { dictionaryErrorsShow(null); @@ -432,7 +435,7 @@ async function onDictionaryPurge(e) { dictionarySpinnerShow(false); dictControls.show(); - dictProgress.hide(); + dictProgress.hidden = true; if (storageEstimate.mostRecent !== null) { storageUpdateStats(); diff --git a/ext/bg/settings.html b/ext/bg/settings.html index fb39e5c3..4fc20d77 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -426,9 +426,9 @@ -
Dictionary data is being purged, please be patient...
-
No dictionaries have been installed
-
+ + +
-- cgit v1.2.3 From e091c7ebe2f6780b6a88df313c9f20170a8e5c1c Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 2 Nov 2019 16:21:06 -0400 Subject: Add support for deleting individual dictionaries --- ext/bg/css/settings.css | 18 +++++++ ext/bg/js/database.js | 104 +++++++++++++++++++++++++++++++++++++ ext/bg/js/settings-dictionaries.js | 64 +++++++++++++++++++++++ ext/bg/js/translator.js | 5 ++ ext/bg/js/util.js | 4 ++ ext/bg/settings.html | 30 ++++++++++- 6 files changed, 224 insertions(+), 1 deletion(-) (limited to 'ext/bg/css') diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css index 102d53de..35b4a152 100644 --- a/ext/bg/css/settings.css +++ b/ext/bg/css/settings.css @@ -165,6 +165,24 @@ input[type=checkbox].storage-button-checkbox { height: 320px; } +.dict-delete-table { + display: table; + width: 100%; +} +.dict-delete-table>*:first-child { + display: table-cell; + vertical-align: middle; + padding-right: 1em; +} +.dict-delete-table>*:nth-child(n+2) { + display: table-cell; + width: 100%; + vertical-align: middle; +} +.dict-delete-table .progress { + margin: 0; +} + [data-show-for-browser], [data-show-for-operating-system] { display: none; diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js index fc0af049..dc2198ac 100644 --- a/ext/bg/js/database.js +++ b/ext/bg/js/database.js @@ -56,6 +56,42 @@ class Database { await this.prepare(); } + async deleteDictionary(dictionaryName, onProgress, progressSettings) { + this.validate(); + + const targets = [ + ['dictionaries', 'title'], + ['kanji', 'dictionary'], + ['kanjiMeta', 'dictionary'], + ['terms', 'dictionary'], + ['termMeta', 'dictionary'], + ['tagMeta', 'dictionary'] + ]; + const promises = []; + const progressData = { + count: 0, + processed: 0, + storeCount: targets.length, + storesProcesed: 0 + }; + let progressRate = (typeof progressSettings === 'object' && progressSettings !== null ? progressSettings.rate : 0); + if (typeof progressRate !== 'number' || progressRate <= 0) { + progressRate = 1000; + } + + const db = this.db.backendDB(); + + for (const [objectStoreName, index] of targets) { + const dbTransaction = db.transaction([objectStoreName], 'readwrite'); + const dbObjectStore = dbTransaction.objectStore(objectStoreName); + const dbIndex = dbObjectStore.index(index); + const only = IDBKeyRange.only(dictionaryName); + promises.push(Database.deleteValues(dbObjectStore, dbIndex, only, onProgress, progressData, progressRate)); + } + + await Promise.all(promises); + } + async findTermsBulk(termList, titles) { this.validate(); @@ -612,4 +648,72 @@ class Database { request.onsuccess = (e) => resolve(e.target.result); }); } + + static getAllKeys(dbIndex, query) { + const fn = typeof dbIndex.getAllKeys === 'function' ? Database.getAllKeysFast : Database.getAllKeysUsingCursor; + return fn(dbIndex, query); + } + + static getAllKeysFast(dbIndex, query) { + return new Promise((resolve, reject) => { + const request = dbIndex.getAllKeys(query); + request.onerror = (e) => reject(e); + request.onsuccess = (e) => resolve(e.target.result); + }); + } + + static getAllKeysUsingCursor(dbIndex, query) { + return new Promise((resolve, reject) => { + const primaryKeys = []; + const request = dbIndex.openKeyCursor(query, 'next'); + request.onerror = (e) => reject(e); + request.onsuccess = (e) => { + const cursor = e.target.result; + if (cursor) { + primaryKeys.push(cursor.primaryKey); + cursor.continue(); + } else { + resolve(primaryKeys); + } + }; + }); + } + + static async deleteValues(dbObjectStore, dbIndex, query, onProgress, progressData, progressRate) { + const hasProgress = (typeof onProgress === 'function'); + const count = await Database.getCount(dbIndex, query); + ++progressData.storesProcesed; + progressData.count += count; + if (hasProgress) { + onProgress(progressData); + } + + const onValueDeleted = ( + hasProgress ? + () => { + const p = ++progressData.processed; + if ((p % progressRate) === 0 || p === progressData.count) { + onProgress(progressData); + } + } : + () => {} + ); + + const promises = []; + const primaryKeys = await Database.getAllKeys(dbIndex, query); + for (const key of primaryKeys) { + const promise = Database.deleteValue(dbObjectStore, key).then(onValueDeleted); + promises.push(promise); + } + + await Promise.all(promises); + } + + static deleteValue(dbObjectStore, key) { + return new Promise((resolve, reject) => { + const request = dbObjectStore.delete(key); + request.onerror = (e) => reject(e); + request.onsuccess = () => resolve(); + }); + } } diff --git a/ext/bg/js/settings-dictionaries.js b/ext/bg/js/settings-dictionaries.js index 2f33d1ac..bf1b232f 100644 --- a/ext/bg/js/settings-dictionaries.js +++ b/ext/bg/js/settings-dictionaries.js @@ -30,6 +30,8 @@ class SettingsDictionaryListUI { this.dictionaryEntries = []; this.extra = null; + + document.querySelector('#dict-delete-confirm').addEventListener('click', (e) => this.onDictionaryConfirmDelete(e), false); } setDictionaries(dictionaries) { @@ -126,6 +128,19 @@ class SettingsDictionaryListUI { save() { // Overwrite } + + onDictionaryConfirmDelete(e) { + e.preventDefault(); + const n = document.querySelector('#dict-delete-modal'); + const title = n.dataset.dict; + delete n.dataset.dict; + $(n).modal('hide'); + + const index = this.dictionaryEntries.findIndex(e => e.dictionaryInfo.title === title); + if (index >= 0) { + this.dictionaryEntries[index].deleteDictionary(); + } + } } class SettingsDictionaryEntryUI { @@ -135,11 +150,13 @@ class SettingsDictionaryEntryUI { this.optionsDictionary = optionsDictionary; this.counts = null; this.eventListeners = []; + this.isDeleting = false; 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.deleteButton = this.content.querySelector('.dict-delete-button'); this.content.querySelector('.dict-title').textContent = this.dictionaryInfo.title; this.content.querySelector('.dict-revision').textContent = `rev.${this.dictionaryInfo.revision}`; @@ -149,6 +166,7 @@ class SettingsDictionaryEntryUI { 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); + this.addEventListener(this.deleteButton, 'click', (e) => this.onDeleteButtonClicked(e), false); } cleanup() { @@ -194,6 +212,38 @@ class SettingsDictionaryEntryUI { this.priorityInput.value = `${this.optionsDictionary.priority}`; } + async deleteDictionary() { + if (this.isDeleting) { + return; + } + + const progress = this.content.querySelector('.progress'); + progress.hidden = false; + const progressBar = this.content.querySelector('.progress-bar'); + this.isDeleting = true; + + try { + const onProgress = ({processed, count, storeCount, storesProcesed}) => { + let percent = 0.0; + if (count > 0 && storesProcesed > 0) { + percent = (processed / count) * (storesProcesed / storeCount) * 100.0; + } + progressBar.style.width = `${percent}%`; + }; + + await utilDatabaseDeleteDictionary(this.dictionaryInfo.title, onProgress, {rate: 1000}); + } catch (e) { + dictionaryErrorsShow([e]); + } finally { + this.isDeleting = false; + progress.hidden = true; + + const optionsContext = getOptionsContext(); + const options = await apiOptionsGet(optionsContext); + onDatabaseUpdated(options); + } + } + onEnabledChanged(e) { this.optionsDictionary.enabled = !!e.target.checked; this.save(); @@ -215,6 +265,20 @@ class SettingsDictionaryEntryUI { e.target.value = `${value}`; } + + onDeleteButtonClicked(e) { + e.preventDefault(); + + if (this.isDeleting) { + return; + } + + const title = this.dictionaryInfo.title; + const n = document.querySelector('#dict-delete-modal'); + n.dataset.dict = title; + document.querySelector('#dict-remove-modal-dict-name').textContent = title; + $(n).modal('show'); + } } class SettingsDictionaryExtraUI { diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js index 9d90136b..ff1d24f3 100644 --- a/ext/bg/js/translator.js +++ b/ext/bg/js/translator.js @@ -42,6 +42,11 @@ class Translator { await this.database.purge(); } + async deleteDictionary(dictionaryName) { + this.tagCache = {}; + await this.database.deleteDictionary(dictionaryName); + } + async findTermsGrouped(text, dictionaries, alphanumeric, options) { const titles = Object.keys(dictionaries); const {length, definitions} = await this.findTerms(text, dictionaries, alphanumeric); diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js index 3554ec3d..f9686943 100644 --- a/ext/bg/js/util.js +++ b/ext/bg/js/util.js @@ -100,6 +100,10 @@ function utilDatabasePurge() { return utilBackend().translator.purgeDatabase(); } +function utilDatabaseDeleteDictionary(dictionaryName, onProgress) { + return utilBackend().translator.database.deleteDictionary(dictionaryName, onProgress); +} + async function utilDatabaseImport(data, progress, exceptions) { // Edge cannot read data on the background page due to the File object // being created from a different window. Read on the same page instead. diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 4fc20d77..5842e97a 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -418,7 +418,6 @@

Yomichan can import and use a variety of dictionary formats. Unneeded dictionaries can be disabled. - Deleting individual dictionaries is not currently feasible due to limitations of browser database technology.

@@ -471,6 +470,25 @@
+ + -- cgit v1.2.3 From 184cc4cf28594f3bef9e141a8cbf9d7eb7a39e88 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 9 Nov 2019 16:34:39 -0500 Subject: Allow templates to be tested on the settings page --- ext/bg/css/settings.css | 18 ++++++---- ext/bg/js/dictionary.js | 3 +- ext/bg/js/settings.js | 91 +++++++++++++++++++++++++++++++++++++++++++++++-- ext/bg/settings.html | 57 +++++++++++++++++++++++++++++-- 4 files changed, 156 insertions(+), 13 deletions(-) (limited to 'ext/bg/css') diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css index 35b4a152..5dfbd931 100644 --- a/ext/bg/css/settings.css +++ b/ext/bg/css/settings.css @@ -37,12 +37,6 @@ html:root:not([data-options-general-result-output-mode=merge]) #dict-main-group padding: 10px; } -#field-templates { - font-family: monospace; - overflow-x: hidden; - white-space: pre; -} - .bottom-links { padding-bottom: 1em; } @@ -136,14 +130,24 @@ html:root:not([data-options-general-result-output-mode=merge]) #dict-main-group } #custom-popup-css, -#custom-popup-outer-css { +#custom-popup-outer-css, +#field-templates { width: 100%; min-height: 34px; + line-height: 18px; height: 96px; resize: vertical; font-family: 'Courier New', Courier, monospace; white-space: pre; } +#field-templates { + height: 240px; + border-bottom-left-radius: 0; +} +#field-templates-reset { + border-top-left-radius: 0; + border-top-right-radius: 0; +} .btn-inner-middle { vertical-align: middle; diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js index e786c4e2..a4cf34ed 100644 --- a/ext/bg/js/dictionary.js +++ b/ext/bg/js/dictionary.js @@ -326,7 +326,7 @@ function dictFieldSplit(field) { return field.length === 0 ? [] : field.split(' '); } -async function dictFieldFormat(field, definition, mode, options) { +async function dictFieldFormat(field, definition, mode, options, exceptions) { const data = { marker: null, definition, @@ -347,6 +347,7 @@ async function dictFieldFormat(field, definition, mode, options) { try { return await apiTemplateRender(options.anki.fieldTemplates, data, true); } catch (e) { + if (exceptions) { exceptions.push(e); } return `{${marker}-render-error}`; } }); diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js index daa997d4..9d95e358 100644 --- a/ext/bg/js/settings.js +++ b/ext/bg/js/settings.js @@ -134,6 +134,8 @@ async function formWrite(options) { $('#screenshot-quality').val(options.anki.screenshot.quality); $('#field-templates').val(options.anki.fieldTemplates); + onAnkiTemplatesValidateCompile(); + try { await ankiDeckAndModelPopulate(options); } catch (e) { @@ -144,7 +146,6 @@ async function formWrite(options) { } function formSetupEventListeners() { - $('#field-templates-reset').click(utilAsync(onAnkiFieldTemplatesReset)); $('input, select, textarea').not('.anki-model').not('.ignore-form-changes *').change(utilAsync(onFormOptionsChanged)); $('.anki-model').change(utilAsync(onAnkiModelChanged)); } @@ -202,6 +203,7 @@ async function onReady() { await audioSettingsInitialize(); await profileOptionsSetup(); await dictSettingsInitialize(); + ankiTemplatesInitialize(); storageInfoInitialize(); @@ -607,20 +609,105 @@ async function onAnkiModelChanged(e) { } } -async function onAnkiFieldTemplatesReset(e) { +function onAnkiFieldTemplatesReset(e) { + e.preventDefault(); + $('#field-template-reset-modal').modal('show'); +} + +async function onAnkiFieldTemplatesResetConfirm(e) { try { e.preventDefault(); + + $('#field-template-reset-modal').modal('hide'); + const optionsContext = getOptionsContext(); const options = await apiOptionsGet(optionsContext); const fieldTemplates = profileOptionsGetDefaultFieldTemplates(); options.anki.fieldTemplates = fieldTemplates; $('#field-templates').val(fieldTemplates); + onAnkiTemplatesValidateCompile(); await settingsSaveOptions(); } catch (e) { ankiErrorShow(e); } } +function ankiTemplatesInitialize() { + const markers = new Set(ankiGetFieldMarkers('terms').concat(ankiGetFieldMarkers('kanji'))); + const fragment = ankiGetFieldMarkersHtml(markers); + + const list = document.querySelector('#field-templates-list'); + list.appendChild(fragment); + for (const node of list.querySelectorAll('.marker-link')) { + node.addEventListener('click', onAnkiTemplateMarkerClicked, false); + } + + $('#field-templates').on('change', onAnkiTemplatesValidateCompile); + $('#field-template-render').on('click', onAnkiTemplateRender); + $('#field-templates-reset').on('click', onAnkiFieldTemplatesReset); + $('#field-templates-reset-confirm').on('click', onAnkiFieldTemplatesResetConfirm); +} + +const ankiTemplatesValidateGetDefinition = (() => { + let cachedValue = null; + let cachedText = null; + + return async (text, optionsContext) => { + if (cachedText !== text) { + const {definitions} = await apiTermsFind(text, optionsContext); + if (definitions.length === 0) { return null; } + + cachedValue = definitions[0]; + cachedText = text; + } + return cachedValue; + }; +})(); + +async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, invalidateInput) { + const text = document.querySelector('#field-templates-preview-text').value || ''; + const exceptions = []; + let result = `No definition found for ${text}`; + try { + const optionsContext = getOptionsContext(); + const definition = await ankiTemplatesValidateGetDefinition(text, optionsContext); + if (definition !== null) { + const options = await apiOptionsGet(optionsContext); + result = await dictFieldFormat(field, definition, mode, options, exceptions); + } + } catch (e) { + exceptions.push(e); + } + + const hasException = exceptions.length > 0; + infoNode.hidden = !(showSuccessResult || hasException); + infoNode.textContent = hasException ? exceptions.map(e => `${e}`).join('\n') : (showSuccessResult ? result : ''); + infoNode.classList.toggle('text-danger', hasException); + if (invalidateInput) { + const input = document.querySelector('#field-templates'); + input.classList.toggle('is-invalid', hasException); + } +} + +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); +} + /* * Storage diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 86d8935d..bdcc11d3 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -272,7 +272,7 @@
@@ -699,10 +699,60 @@

Fields are formatted using the Handlebars.js template rendering engine. Advanced users can modify these templates for ultimate control of what information gets included in - their Anki cards. If you encounter problems with your changes you can always reset to default - template settings. + their Anki cards. If you encounter problems with your changes, you can always reset to the default template settings.

+
+ +
+

+ + +

Templates can be tested using the inputs below.

+ +
+
+
+ + +
+
+ +
+
+ +
+ +
+ + +
+
+
+
+
+ +

+ +
+ +