Add support for deleting individual dictionaries
This commit is contained in:
parent
e355b83914
commit
e091c7ebe2
@ -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;
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
|
@ -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">×</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>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user