Add support for deleting individual dictionaries

This commit is contained in:
toasted-nutbread 2019-11-02 16:21:06 -04:00
parent e355b83914
commit e091c7ebe2
6 changed files with 224 additions and 1 deletions

View File

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

View File

@ -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();
});
}
}

View File

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

View File

@ -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);

View File

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

View File

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