diff options
| author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2019-11-02 16:21:06 -0400 | 
|---|---|---|
| committer | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2019-11-07 20:30:55 -0500 | 
| commit | e091c7ebe2f6780b6a88df313c9f20170a8e5c1c (patch) | |
| tree | 54eaeb00c11fa591321dc4d6a934fd449cd4c5d3 /ext/bg | |
| parent | e355b839142a8bab0edc446c7da08256ad5b938c (diff) | |
Add support for deleting individual dictionaries
Diffstat (limited to 'ext/bg')
| -rw-r--r-- | ext/bg/css/settings.css | 18 | ||||
| -rw-r--r-- | ext/bg/js/database.js | 104 | ||||
| -rw-r--r-- | ext/bg/js/settings-dictionaries.js | 64 | ||||
| -rw-r--r-- | ext/bg/js/translator.js | 5 | ||||
| -rw-r--r-- | ext/bg/js/util.js | 4 | ||||
| -rw-r--r-- | ext/bg/settings.html | 30 | 
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">×</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> |