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 @@
+ +