aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2019-11-02 16:21:06 -0400
committertoasted-nutbread <toasted-nutbread@users.noreply.github.com>2019-11-07 20:30:55 -0500
commite091c7ebe2f6780b6a88df313c9f20170a8e5c1c (patch)
tree54eaeb00c11fa591321dc4d6a934fd449cd4c5d3
parente355b839142a8bab0edc446c7da08256ad5b938c (diff)
Add support for deleting individual dictionaries
-rw-r--r--ext/bg/css/settings.css18
-rw-r--r--ext/bg/js/database.js104
-rw-r--r--ext/bg/js/settings-dictionaries.js64
-rw-r--r--ext/bg/js/translator.js5
-rw-r--r--ext/bg/js/util.js4
-rw-r--r--ext/bg/settings.html30
6 files changed, 224 insertions, 1 deletions
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 @@
<p class="help-block">
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.
</p>
<div class="form-group" id="dict-main-group">
@@ -471,6 +470,25 @@
</div>
</div>
+ <div class="modal fade" tabindex="-1" role="dialog" id="dict-delete-modal">
+ <div class="modal-dialog modal-dialog-centered">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+ <h4 class="modal-title">Confirm dictionary deletion</h4>
+ </div>
+ <div class="modal-body">
+ Are you sure you want to delete the dictionary <em id="dict-remove-modal-dict-name"></em>?
+ This operation may take some time and the responsiveness of this browser tab may be reduced.
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
+ <button type="button" class="btn btn-danger" id="dict-delete-confirm">Delete Dictionary</button>
+ </div>
+ </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>
@@ -485,6 +503,16 @@
<label class="dict-result-priority-label">Result priority</label>
<input type="number" class="form-control dict-priority">
</div>
+ <div class="dict-delete-table">
+ <div>
+ <button class="btn btn-default dict-delete-button">Delete Dictionary</button>
+ </div>
+ <div>
+ <div class="progress" hidden>
+ <div class="progress-bar progress-bar-striped" style="width: 0%"></div>
+ </div>
+ </div>
+ </div>
<pre class="debug dict-counts" hidden></pre>
</div></template>