diff options
Diffstat (limited to 'ext/bg/js')
| -rw-r--r-- | ext/bg/js/database.js | 297 | ||||
| -rw-r--r-- | ext/bg/js/deinflector.js | 117 | ||||
| -rw-r--r-- | ext/bg/js/dictionary.js | 217 | ||||
| -rw-r--r-- | ext/bg/js/import.js | 36 | ||||
| -rw-r--r-- | ext/bg/js/options-form.js | 470 | ||||
| -rw-r--r-- | ext/bg/js/options.js | 2 | ||||
| -rw-r--r-- | ext/bg/js/templates.js | 118 | ||||
| -rw-r--r-- | ext/bg/js/translator.js | 199 | ||||
| -rw-r--r-- | ext/bg/js/util.js | 100 | ||||
| -rw-r--r-- | ext/bg/js/yomichan.js | 42 | 
10 files changed, 952 insertions, 646 deletions
| diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js new file mode 100644 index 00000000..7ad7d410 --- /dev/null +++ b/ext/bg/js/database.js @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2016  Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <http://www.gnu.org/licenses/>. + */ + + +class Database { +    constructor() { +        this.db = null; +        this.tagMetaCache = {}; +    } + +    prepare() { +        if (this.db !== null) { +            return Promise.reject('database already initialized'); +        } + +        this.db = new Dexie('dict'); +        this.db.version(1).stores({ +            terms: '++id,dictionary,expression,reading', +            kanji: '++,dictionary,character', +            tagMeta: '++,dictionary', +            dictionaries: '++,title,version', +        }); + +        return this.db.open(); +    } + +    purge() { +        if (this.db === null) { +            return Promise.reject('database not initialized'); +        } + +        this.db.close(); +        return this.db.delete().then(() => { +            this.db = null; +            this.tagMetaCache = {}; +            return this.prepare(); +        }); +    } + +    findTerm(term, dictionaries) { +        if (this.db === null) { +            return Promise.reject('database not initialized'); +        } + +        const results = []; +        return this.db.terms.where('expression').equals(term).or('reading').equals(term).each(row => { +            if (dictionaries.includes(row.dictionary)) { +                results.push({ +                    expression: row.expression, +                    reading: row.reading, +                    tags: splitField(row.tags), +                    rules: splitField(row.rules), +                    glossary: row.glossary, +                    score: row.score, +                    dictionary: row.dictionary, +                    id: row.id +                }); +            } +        }).then(() => { +            return this.cacheTagMeta(dictionaries); +        }).then(() => { +            for (const result of results) { +                result.tagMeta = this.tagMetaCache[result.dictionary] || {}; +            } + +            return results; +        }); +    } + +    findKanji(kanji, dictionaries) { +        if (this.db === null) { +            return Promise.reject('database not initialized'); +        } + +        const results = []; +        return this.db.kanji.where('character').equals(kanji).each(row => { +            if (dictionaries.includes(row.dictionary)) { +                results.push({ +                    character: row.character, +                    onyomi: splitField(row.onyomi), +                    kunyomi: splitField(row.kunyomi), +                    tags: splitField(row.tags), +                    glossary: row.meanings, +                    dictionary: row.dictionary +                }); +            } +        }).then(() => { +            return this.cacheTagMeta(dictionaries); +        }).then(() => { +            for (const result of results) { +                result.tagMeta = this.tagMetaCache[result.dictionary] || {}; +            } + +            return results; +        }); +    } + +    cacheTagMeta(dictionaries) { +        if (this.db === null) { +            return Promise.reject('database not initialized'); +        } + +        const promises = []; +        for (const dictionary of dictionaries) { +            if (this.tagMetaCache[dictionary]) { +                continue; +            } + +            const tagMeta = {}; +            promises.push( +                this.db.tagMeta.where('dictionary').equals(dictionary).each(row => { +                    tagMeta[row.name] = {category: row.category, notes: row.notes, order: row.order}; +                }).then(() => { +                    this.tagMetaCache[dictionary] = tagMeta; +                }) +            ); +        } + +        return Promise.all(promises); +    } + +    getDictionaries() { +        if (this.db === null) { +            return Promise.reject('database not initialized'); +        } + +        return this.db.dictionaries.toArray(); +    } + +    deleteDictionary(title, callback) { +        if (this.db === null) { +            return Promise.reject('database not initialized'); +        } + +        return this.db.dictionaries.where('title').equals(title).first(info => { +            if (!info) { +                return; +            } + +            let termCounter = Promise.resolve(0); +            if (info.hasTerms) { +                termCounter = this.db.terms.where('dictionary').equals(title).count(); +            } + +            let kanjiCounter = Promise.resolve(0); +            if (info.hasKanji) { +                kanjiCounter = this.db.kanji.where('dictionary').equals(title).count(); +            } + +            return Promise.all([termCounter, kanjiCounter]).then(([termCount, kanjiCount]) => { +                const rowLimit = 500; +                const totalCount = termCount + kanjiCount; +                let deletedCount = 0; + +                let termDeleter = Promise.resolve(); +                if (info.hasTerms) { +                    const termDeleterFunc = () => { +                        return this.db.terms.where('dictionary').equals(title).limit(rowLimit).delete().then(count => { +                            if (count === 0) { +                                return Promise.resolve(); +                            } + +                            deletedCount += count; +                            if (callback) { +                                callback(totalCount, deletedCount); +                            } + +                            return termDeleterFunc(); +                        }); +                    }; + +                    termDeleter = termDeleterFunc(); +                } + +                let kanjiDeleter = Promise.resolve(); +                if (info.hasKanji) { +                    const kanjiDeleterFunc = () => { +                        return this.db.kanji.where('dictionary').equals(title).limit(rowLimit).delete().then(count => { +                            if (count === 0) { +                                return Promise.resolve(); +                            } + +                            deletedCount += count; +                            if (callback) { +                                callback(totalCount, deletedCount); +                            } + +                            return kanjiDeleterFunc(); +                        }); +                    }; + +                    kanjiDeleter = kanjiDeleterFunc(); +                } + +                return Promise.all([termDeleter, kanjiDeleter]); +            }); +        }).then(() => { +            return this.db.tagMeta.where('dictionary').equals(title).delete(); +        }).then(() => { +            return this.db.dictionaries.where('title').equals(title).delete(); +        }).then(() => { +            delete this.cacheTagMeta[title]; +        }); +    } + +    importDictionary(indexUrl, callback) { +        if (this.db === null) { +            return Promise.reject('database not initialized'); +        } + +        let summary = null; +        const indexLoaded = (title, version, tagMeta, hasTerms, hasKanji) => { +            summary = {title, hasTerms, hasKanji, version}; +            return this.db.dictionaries.where('title').equals(title).count().then(count => { +                if (count > 0) { +                    return Promise.reject(`dictionary "${title}" is already imported`); +                } + +                return this.db.dictionaries.add({title, version, hasTerms, hasKanji}).then(() => { +                    const rows = []; +                    for (const tag in tagMeta || {}) { +                        const meta = tagMeta[tag]; +                        const row = sanitizeTag({ +                            name: tag, +                            category: meta.category, +                            notes: meta.notes, +                            order: meta.order, +                            dictionary: title +                        }); + +                        rows.push(row); +                    } + +                    return this.db.tagMeta.bulkAdd(rows); +                }); +            }); +        }; + +        const termsLoaded = (title, entries, total, current) => { +            const rows = []; +            for (const [expression, reading, tags, rules, score, ...glossary] of entries) { +                rows.push({ +                    expression, +                    reading, +                    tags, +                    rules, +                    score, +                    glossary, +                    dictionary: title +                }); +            } + +            return this.db.terms.bulkAdd(rows).then(() => { +                if (callback) { +                    callback(total, current, indexUrl); +                } +            }); +        }; + +        const kanjiLoaded = (title, entries, total, current)  => { +            const rows = []; +            for (const [character, onyomi, kunyomi, tags, ...meanings] of entries) { +                rows.push({ +                    character, +                    onyomi, +                    kunyomi, +                    tags, +                    meanings, +                    dictionary: title +                }); +            } + +            return this.db.kanji.bulkAdd(rows).then(() => { +                if (callback) { +                    callback(total, current, indexUrl); +                } +            }); +        }; + +        return importJsonDb(indexUrl, indexLoaded, termsLoaded, kanjiLoaded).then(() => summary); +    } +} diff --git a/ext/bg/js/deinflector.js b/ext/bg/js/deinflector.js index 1474e56d..6e480068 100644 --- a/ext/bg/js/deinflector.js +++ b/ext/bg/js/deinflector.js @@ -18,50 +18,47 @@  class Deinflection { -    constructor(term, tags=[], rule='') { -        this.children = []; +    constructor(term, {rules=[], definitions=[], reason=''} = {}) {          this.term = term; -        this.tags = tags; -        this.rule = rule; +        this.rules = rules; +        this.definitions = definitions; +        this.reason = reason; +        this.children = [];      } -    validate(validator) { -        return validator(this.term).then(sets => { -            for (const tags of sets) { -                if (this.tags.length === 0) { -                    return true; -                } - -                for (const tag of this.tags) { -                    if (tags.includes(tag)) { -                        return true; +    deinflect(definer, reasons) { +        const define = () => { +            return definer(this.term).then(definitions => { +                if (this.rules.length === 0) { +                    this.definitions = definitions; +                } else { +                    for (const rule of this.rules) { +                        for (const definition of definitions) { +                            if (definition.rules.includes(rule)) { +                                this.definitions.push(definition); +                            } +                        }                      }                  } -            } - -            return false; -        }); -    } -    deinflect(validator, rules) { -        const promises = [ -            this.validate(validator).then(valid => { -                const child = new Deinflection(this.term, this.tags); -                this.children.push(child); -            }) -        ]; - -        for (const rule in rules) { -            for (const variant of rules[rule]) { -                let allowed = this.tags.length === 0; -                for (const tag of this.tags) { -                    if (variant.tagsIn.includes(tag)) { -                        allowed = true; -                        break; +                return this.definitions.length > 0; +            }); +        }; + +        const promises = []; +        for (const reason in reasons) { +            for (const variant of reasons[reason]) { +                let accept = this.rules.length === 0; +                if (!accept) { +                    for (const rule of this.rules) { +                        if (variant.rulesIn.includes(rule)) { +                            accept = true; +                            break; +                        }                      }                  } -                if (!allowed || !this.term.endsWith(variant.kanaIn)) { +                if (!accept || !this.term.endsWith(variant.kanaIn)) {                      continue;                  } @@ -70,55 +67,61 @@ class Deinflection {                      continue;                  } -                const child = new Deinflection(term, variant.tagsOut, rule); +                const child = new Deinflection(term, {reason, rules: variant.rulesOut});                  promises.push( -                    child.deinflect(validator, rules).then(valid => { -                        if (valid) { -                            this.children.push(child); -                        } -                    } -                )); +                    child.deinflect(definer, reasons).then(valid => valid && this.children.push(child)) +                );              }          } -        return Promise.all(promises).then(() => { -            return this.children.length > 0; +        return Promise.all(promises).then(define).then(valid => { +            if (valid && this.children.length > 0) { +                const child = new Deinflection(this.term, {rules: this.rules, definitions: this.definitions}); +                this.children.push(child); +            } + +            return valid || this.children.length > 0;          });      }      gather() {          if (this.children.length === 0) { -            return [{root: this.term, tags: this.tags, rules: []}]; +            return [{ +                source: this.term, +                rules: this.rules, +                definitions: this.definitions, +                reasons: [this.reason] +            }];          } -        const paths = []; +        const results = [];          for (const child of this.children) { -            for (const path of child.gather()) { -                if (this.rule.length > 0) { -                    path.rules.push(this.rule); +            for (const result of child.gather()) { +                if (this.reason.length > 0) { +                    result.reasons.push(this.reason);                  } -                path.source = this.term; -                paths.push(path); +                result.source = this.term; +                results.push(result);              }          } -        return paths; +        return results;      }  }  class Deinflector {      constructor() { -        this.rules = {}; +        this.reasons = {};      } -    setRules(rules) { -        this.rules = rules; +    setReasons(reasons) { +        this.reasons = reasons;      } -    deinflect(term, validator) { +    deinflect(term, definer) {          const node = new Deinflection(term); -        return node.deinflect(validator, this.rules).then(success => success ? node.gather() : []); +        return node.deinflect(definer, this.reasons).then(success => success ? node.gather() : []);      }  } diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js deleted file mode 100644 index 1d54190e..00000000 --- a/ext/bg/js/dictionary.js +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright (C) 2016  Alex Yatskov <alex@foosoft.net> - * Author: Alex Yatskov <alex@foosoft.net> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program.  If not, see <http://www.gnu.org/licenses/>. - */ - - -class Dictionary { -    constructor() { -        this.db = null; -        this.dbVer = 2; -        this.entities = null; -    } - -    initDb() { -        if (this.db !== null) { -            return Promise.reject('database already initialized'); -        } - -        this.db = new Dexie('dict'); -        this.db.version(1).stores({ -            terms: '++id,expression,reading', -            entities: '++,name', -            kanji: '++,character', -            meta: 'name,value', -        }); -    } - -    prepareDb() { -        this.initDb(); - -        return this.db.meta.get('version').then(row => { -            return row ? row.value : 0; -        }).catch(() => { -            return 0; -        }).then(version => { -            if (this.dbVer === version) { -                return true; -            } - -            const db = this.db; -            this.db.close(); -            this.db = null; - -            return db.delete().then(() => { -                this.initDb(); -                return false; -            }); -        }); -    } - -    sealDb() { -        if (this.db === null) { -            return Promise.reject('database not initialized'); -        } - -        return this.db.meta.put({name: 'version', value: this.dbVer}); -    } - -    findTerm(term) { -        if (this.db === null) { -            return Promise.reject('database not initialized'); -        } - -        const results = []; -        return this.db.terms.where('expression').equals(term).or('reading').equals(term).each(row => { -            results.push({ -                expression: row.expression, -                reading: row.reading, -                tags: splitField(row.tags), -                glossary: row.glossary, -                id: row.id -            }); -        }).then(() => { -            return this.getEntities(); -        }).then(entities => { -            for (const result of results) { -                result.entities = entities; -            } - -            return results; -        }); -    } - -    findKanji(kanji) { -        if (this.db === null) { -            return Promise.reject('database not initialized'); -        } - -        const results = []; -        return this.db.kanji.where('character').equals(kanji).each(row => { -            results.push({ -                character: row.character, -                onyomi: splitField(row.onyomi), -                kunyomi: splitField(row.kunyomi), -                tags: splitField(row.tags), -                glossary: row.meanings -            }); -        }).then(() => results); -    } - -    getEntities(tags) { -        if (this.db === null) { -            return Promise.reject('database not initialized'); -        } - -        if (this.entities !== null) { -            return Promise.resolve(this.entities); -        } - -        return this.db.entities.toArray(rows => { -            this.entities = {}; -            for (const row of rows) { -                this.entities[row.name] = row.value; -            } - -            return this.entities; -        }); -    } - -    importTermDict(indexUrl, callback) { -        if (this.db === null) { -            return Promise.reject('database not initialized'); -        } - -        const indexDir = indexUrl.slice(0, indexUrl.lastIndexOf('/')); -        return loadJson(indexUrl).then(index => { -            const entities = []; -            for (const [name, value] of index.ents) { -                entities.push({name, value}); -            } - -            return this.db.entities.bulkAdd(entities).then(() => { -                if (this.entities === null) { -                    this.entities = {}; -                } - -                for (const entity of entities) { -                    this.entities[entity.name] = entity.value; -                } -            }).then(() => { -                const loaders = []; -                for (let i = 1; i <= index.banks; ++i) { -                    const bankUrl = `${indexDir}/bank_${i}.json`; -                    loaders.push(() => { -                        return loadJson(bankUrl).then(definitions => { -                            const rows = []; -                            for (const [expression, reading, tags, ...glossary] of definitions) { -                                rows.push({expression, reading, tags, glossary}); -                            } - -                            return this.db.terms.bulkAdd(rows).then(() => { -                                if (callback) { -                                    callback(i, index.banks, indexUrl); -                                } -                            }); -                        }); -                    }); -                } - -                let chain = Promise.resolve(); -                for (const loader of loaders) { -                    chain = chain.then(loader); -                } - -                return chain; -            }); -        }); -    } - -    importKanjiDict(indexUrl, callback) { -        if (this.db === null) { -            return Promise.reject('database not initialized'); -        } - -        const indexDir = indexUrl.slice(0, indexUrl.lastIndexOf('/')); -        return loadJson(indexUrl).then(index => { -            const loaders = []; -            for (let i = 1; i <= index.banks; ++i) { -                const bankUrl = `${indexDir}/bank_${i}.json`; -                loaders.push(() => { -                    return loadJson(bankUrl).then(definitions => { -                        const rows = []; -                        for (const [character, onyomi, kunyomi, tags, ...meanings] of definitions) { -                            rows.push({character, onyomi, kunyomi, tags, meanings}); -                        } - -                        return this.db.kanji.bulkAdd(rows).then(() => { -                            if (callback) { -                                callback(i, index.banks, indexUrl); -                            } -                        }); -                    }); -                }); -            } - -            let chain = Promise.resolve(); -            for (const loader of loaders) { -                chain = chain.then(loader); -            } - -            return chain; -        }); -    } -} diff --git a/ext/bg/js/import.js b/ext/bg/js/import.js deleted file mode 100644 index 0601cb9f..00000000 --- a/ext/bg/js/import.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2016  Alex Yatskov <alex@foosoft.net> - * Author: Alex Yatskov <alex@foosoft.net> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program.  If not, see <http://www.gnu.org/licenses/>. - */ - - -function api_setProgress(progress) { -    $('.progress-bar').css('width', `${progress}%`); - -    if (progress === 100.0) { -        $('.progress').hide(); -        $('.alert').show(); -    } -} - -chrome.runtime.onMessage.addListener(({action, params}, sender, callback) => { -    const method = this['api_' + action]; -    if (typeof(method) === 'function') { -        method.call(this, params); -    } - -    callback(); -}); diff --git a/ext/bg/js/options-form.js b/ext/bg/js/options-form.js index eb562142..fb81e83a 100644 --- a/ext/bg/js/options-form.js +++ b/ext/bg/js/options-form.js @@ -16,55 +16,14 @@   * along with this program.  If not, see <http://www.gnu.org/licenses/>.   */ +// +//  General +//  function yomichan() {      return chrome.extension.getBackgroundPage().yomichan;  } -function anki() { -    return yomichan().anki; -} - -function fieldsToDict(selection) { -    const result = {}; -    selection.each((index, element) => { -        result[$(element).data('field')] = $(element).val(); -    }); - -    return result; -} - -function modelIdToFieldOptKey(id) { -    return { -        'anki-term-model': 'ankiTermFields', -        'anki-kanji-model': 'ankiKanjiFields' -    }[id]; -} - -function modelIdToMarkers(id) { -    return { -        'anki-term-model': [ -            'audio', -            'expression', -            'expression-furigana', -            'glossary', -            'glossary-list', -            'reading', -            'sentence', -            'tags', -            'url' -        ], -        'anki-kanji-model': [ -            'character', -            'glossary', -            'glossary-list', -            'kunyomi', -            'onyomi', -            'url' -        ], -    }[id]; -} -  function getFormValues() {      return loadOptions().then(optsOld => {          const optsNew = $.extend({}, optsOld); @@ -91,6 +50,14 @@ function getFormValues() {          optsNew.ankiKanjiModel = $('#anki-kanji-model').val();          optsNew.ankiKanjiFields = fieldsToDict($('#kanji .anki-field-value')); +        $('.dict-group').each((index, element) => { +            const dictionary = $(element); +            const title = dictionary.data('title'); +            const enableTerms = dictionary.find('.dict-enable-terms').prop('checked'); +            const enableKanji = dictionary.find('.dict-enable-kanji').prop('checked'); +            optsNew.dictionaries[title] = {enableTerms, enableKanji}; +        }); +          return {              optsNew: sanitizeOptions(optsNew),              optsOld: sanitizeOptions(optsOld) @@ -120,43 +87,294 @@ function updateVisibility(opts) {      }  } -function populateAnkiDeckAndModel(opts) { -    const ankiSpinner = $('#anki-spinner'); -    ankiSpinner.show(); +$(document).ready(() => { +    Handlebars.partials = Handlebars.templates; + +    loadOptions().then(opts => { +        $('#activate-on-startup').prop('checked', opts.activateOnStartup); +        $('#enable-audio-playback').prop('checked', opts.enableAudioPlayback); +        $('#enable-soft-katakana-search').prop('checked', opts.enableSoftKatakanaSearch); +        $('#show-advanced-options').prop('checked', opts.showAdvancedOptions); + +        $('#hold-shift-to-scan').prop('checked', opts.holdShiftToScan); +        $('#select-matched-text').prop('checked', opts.selectMatchedText); +        $('#scan-delay').val(opts.scanDelay); +        $('#scan-length').val(opts.scanLength); + +        $('#anki-method').val(opts.ankiMethod); +        $('#anki-username').val(opts.ankiUsername); +        $('#anki-password').val(opts.ankiPassword); +        $('#anki-card-tags').val(opts.ankiCardTags.join(' ')); +        $('#sentence-extent').val(opts.sentenceExtent); + +        $('input, select').not('.anki-model').change(onOptionsChanged); +        $('.anki-model').change(onAnkiModelChanged); + +        $('#dict-purge').click(onDictionaryPurge); +        $('#dict-importer a').click(onDictionarySetUrl); +        $('#dict-import').click(onDictionaryImport); +        $('#dict-url').on('input', onDictionaryUpdateUrl); -    const ankiFormat = $('#anki-format'); -    ankiFormat.hide(); +        populateDictionaries(opts); +        populateAnkiDeckAndModel(opts); +        updateVisibility(opts); +    }); +}); -    const ankiDeck = $('.anki-deck'); -    ankiDeck.find('option').remove(); +// +//  Dictionary +// -    const ankiModel = $('.anki-model'); -    ankiModel.find('option').remove(); +function database() { +    return yomichan().translator.database; +} -    return anki().getDeckNames().then(names => { -        names.forEach(name => ankiDeck.append($('<option/>', {value: name, text: name}))); -        $('#anki-term-deck').val(opts.ankiTermDeck); -        $('#anki-kanji-deck').val(opts.ankiKanjiDeck); +function showDictionaryError(error) { +    const dialog = $('#dict-error'); +    if (error) { +        dialog.show().find('span').text(error); +    } else { +        dialog.hide(); +    } +} + +function showDictionarySpinner(show) { +    const spinner = $('#dict-spinner'); +    if (show) { +        spinner.show(); +    } else { +        spinner.hide(); +    } +} + +function populateDictionaries(opts) { +    showDictionaryError(null); +    showDictionarySpinner(true); + +    const dictGroups = $('#dict-groups').empty(); +    const dictWarning = $('#dict-warning').hide(); + +    let dictCount = 0; +    return database().getDictionaries().then(rows => { +        rows.forEach(row => { +            const dictOpts = opts.dictionaries[row.title] || {enableTerms: false, enableKanji: false}; +            const html = Handlebars.templates['dictionary.html']({ +                title: row.title, +                version: row.version, +                hasTerms: row.hasTerms, +                hasKanji: row.hasKanji, +                enableTerms: dictOpts.enableTerms, +                enableKanji: dictOpts.enableKanji +            }); + +            dictGroups.append($(html)); +            ++dictCount; +        }); + +        $('.dict-enable-terms, .dict-enable-kanji').change(onOptionsChanged); +        $('.dict-delete').click(onDictionaryDelete); +    }).catch(error => { +        showDictionaryError(error);      }).then(() => { -        return anki().getModelNames(); -    }).then(names => { -        names.forEach(name => ankiModel.append($('<option/>', {value: name, text: name}))); -        return populateAnkiFields($('#anki-term-model').val(opts.ankiTermModel), opts); +        showDictionarySpinner(false); +        if (dictCount === 0) { +            dictWarning.show(); +        } +    }); +} + +function onDictionaryPurge(e) { +    e.preventDefault(); + +    showDictionaryError(null); +    showDictionarySpinner(true); + +    const dictControls = $('#dict-importer, #dict-groups').hide(); +    const dictProgress = $('#dict-purge-progress').show(); + +    return database().purge().catch(error => { +        showDictionaryError(error); +    }).then(() => { +        showDictionarySpinner(false); +        dictControls.show(); +        dictProgress.hide(); +        return loadOptions().then(opts => populateDictionaries(opts)); +    }); +} + +function onDictionaryDelete() { +    showDictionaryError(null); +    showDictionarySpinner(true); + +    const dictGroup = $(this).closest('.dict-group'); +    const dictProgress = dictGroup.find('.dict-delete-progress').show(); +    const dictControls = dictGroup.find('.dict-group-controls').hide(); +    const setProgress = percent => { +        dictProgress.find('.progress-bar').css('width', `${percent}%`); +    }; + +    setProgress(0.0); + +    database().deleteDictionary(dictGroup.data('title'), (total, current) => setProgress(current / total * 100.0)).catch(error => { +        showDictionaryError(error);      }).then(() => { -        return populateAnkiFields($('#anki-kanji-model').val(opts.ankiKanjiModel), opts); +        showDictionarySpinner(false); +        dictProgress.hide(); +        dictControls.show(); +        return loadOptions().then(opts => populateDictionaries(opts)); +    }); +} + +function onDictionaryImport() { +    showDictionaryError(null); +    showDictionarySpinner(true); + +    const dictUrl = $('#dict-url'); +    const dictImporter = $('#dict-importer').hide(); +    const dictProgress = $('#dict-import-progress').show(); +    const setProgress = percent => { +        dictProgress.find('.progress-bar').css('width', `${percent}%`); +    }; + +    setProgress(0.0); + +    loadOptions().then(opts => { +        database().importDictionary(dictUrl.val(), (total, current) => setProgress(current / total * 100.0)).then(summary => { +            opts.dictionaries[summary.title] = {enableTerms: summary.hasTerms, enableKanji: summary.hasKanji}; +            return saveOptions(opts).then(() => yomichan().setOptions(opts)); +        }).then(() => { +            return populateDictionaries(opts); +        }).catch(error => { +            showDictionaryError(error); +        }).then(() => { +            showDictionarySpinner(false); +            dictProgress.hide(); +            dictImporter.show(); +            dictUrl.val(''); +            dictUrl.trigger('input'); +        }); +    }); +} + +function onDictionarySetUrl(e) { +    e.preventDefault(); + +    const dictUrl = $('#dict-url'); +    const url = $(this).data('url'); +    if (url.includes('/')) { +        dictUrl.val(url); +    } else { +        dictUrl.val(chrome.extension.getURL(`bg/data/${url}/index.json`)); +    } + +    dictUrl.trigger('input'); +} + +function onDictionaryUpdateUrl() { +    $('#dict-import').prop('disabled', $(this).val().length === 0); +} + +// +//  Anki +// + +function anki() { +    return yomichan().anki; +} + +function showAnkiSpinner(show) { +    const spinner = $('#anki-spinner'); +    if (show) { +        spinner.show(); +    } else { +        spinner.hide(); +    } +} + +function showAnkiError(error) { +    const dialog = $('#anki-error'); +    if (error) { +        dialog.show().find('span').text(error); +    } +    else { +        dialog.hide(); +    } +} + +function fieldsToDict(selection) { +    const result = {}; +    selection.each((index, element) => { +        result[$(element).data('field')] = $(element).val(); +    }); + +    return result; +} + +function modelIdToFieldOptKey(id) { +    return { +        'anki-term-model': 'ankiTermFields', +        'anki-kanji-model': 'ankiKanjiFields' +    }[id]; +} + +function modelIdToMarkers(id) { +    return { +        'anki-term-model': [ +            'audio', +            'expression', +            'expression-furigana', +            'glossary', +            'glossary-list', +            'reading', +            'sentence', +            'tags', +            'url' +        ], +        'anki-kanji-model': [ +            'character', +            'glossary', +            'glossary-list', +            'kunyomi', +            'onyomi', +            'url' +        ], +    }[id]; +} + +function populateAnkiDeckAndModel(opts) { +    showAnkiError(null); +    showAnkiSpinner(true); + +    const ankiFormat = $('#anki-format').hide(); + +    return Promise.all([anki().getDeckNames(), anki().getModelNames()]).then(([deckNames, modelNames]) => { +        const ankiDeck = $('.anki-deck'); +        ankiDeck.find('option').remove(); +        deckNames.forEach(name => ankiDeck.append($('<option/>', {value: name, text: name}))); + +        $('#anki-term-deck').val(opts.ankiTermDeck); +        $('#anki-kanji-deck').val(opts.ankiKanjiDeck); + +        const ankiModel = $('.anki-model'); +        ankiModel.find('option').remove(); +        modelNames.forEach(name => ankiModel.append($('<option/>', {value: name, text: name}))); + +        return Promise.all([ +            populateAnkiFields($('#anki-term-model').val(opts.ankiTermModel), opts), +            populateAnkiFields($('#anki-kanji-model').val(opts.ankiKanjiModel), opts) +        ]);      }).then(() => { -        $('#anki-error').hide();          ankiFormat.show();      }).catch(error => { -        $('#anki-error').show().find('span').text(error); +        showAnkiError(error);      }).then(() => { -        ankiSpinner.hide(); +        showAnkiSpinner(false);      });  }  function populateAnkiFields(element, opts) { -    const table = element.closest('.tab-pane').find('.anki-fields'); -    table.find('tbody').remove(); +    const tab = element.closest('.tab-pane'); +    const container = tab.find('tbody').empty();      const modelName = element.val();      if (modelName === null) { @@ -168,41 +386,37 @@ function populateAnkiFields(element, opts) {      const markers = modelIdToMarkers(modelId);      return anki().getModelFieldNames(modelName).then(names => { -        const tbody = $('<tbody>');          names.forEach(name => { -            const button = $('<button>', {type: 'button', class: 'btn btn-default dropdown-toggle'}); -            button.attr('data-toggle', 'dropdown').dropdown(); - -            const markerItems = $('<ul>', {class: 'dropdown-menu dropdown-menu-right'}); -            for (const marker of markers) { -                const link = $('<a>', {href: '#'}).text(`{${marker}}`); -                link.click(e => { -                    e.preventDefault(); -                    link.closest('.input-group').find('.anki-field-value').val(link.text()).trigger('change'); -                }); -                markerItems.append($('<li>').append(link)); -            } +            const html = Handlebars.templates['model.html']({name, markers, value: opts[optKey][name] || ''}); +            container.append($(html)); +        }); -            const groupBtn = $('<div>', {class: 'input-group-btn'}); -            groupBtn.append(button.append($('<span>', {class: 'caret'}))); -            groupBtn.append(markerItems); +        tab.find('.anki-field-value').change(onOptionsChanged); +        tab.find('.marker-link').click(e => { +            e.preventDefault(); +            const link = e.target; +            $(link).closest('.input-group').find('.anki-field-value').val(`{${link.text}}`).trigger('change'); +        }); +    }); +} -            const group = $('<div>', {class: 'input-group'}); -            group.append($('<input>', { -                type: 'text', -                class: 'anki-field-value form-control', -                value: opts[optKey][name] || '' -            }).data('field', name).change(onOptionsChanged)); -            group.append(groupBtn); +function onAnkiModelChanged(e) { +    if (!e.originalEvent) { +        return; +    } -            const row = $('<tr>'); -            row.append($('<td>', {class: 'col-sm-2'}).text(name)); -            row.append($('<td>', {class: 'col-sm-10'}).append(group)); +    showAnkiError(null); +    showAnkiSpinner(true); -            tbody.append(row); +    getFormValues().then(({optsNew, optsOld}) => { +        optsNew[modelIdToFieldOptKey($(this).id)] = {}; +        populateAnkiFields($(this), optsNew).then(() => { +            saveOptions(optsNew).then(() => yomichan().setOptions(optsNew)); +        }).catch(error => { +            showAnkiError(error); +        }).then(() => { +            showAnkiSpinner(false);          }); - -        table.append(tbody);      });  } @@ -212,7 +426,7 @@ function onOptionsChanged(e) {      }      getFormValues().then(({optsNew, optsOld}) => { -        saveOptions(optsNew).then(() => { +        return saveOptions(optsNew).then(() => {              yomichan().setOptions(optsNew);              updateVisibility(optsNew); @@ -221,60 +435,18 @@ function onOptionsChanged(e) {                  optsNew.ankiPassword !== optsOld.ankiPassword;              if (loginChanged && optsNew.ankiMethod === 'ankiweb') { -                anki().logout().then(() => populateAnkiDeckAndModel(optsNew)).catch(error => { -                    $('#anki-error').show().find('span').text(error); -                }); +                showAnkiError(null); +                showAnkiSpinner(true); +                return anki().logout().then(() => populateAnkiDeckAndModel(optsNew));              } else if (loginChanged || optsNew.ankiMethod !== optsOld.ankiMethod) { -                populateAnkiDeckAndModel(optsNew); +                showAnkiError(null); +                showAnkiSpinner(true); +                return populateAnkiDeckAndModel(optsNew);              }          }); +    }).catch(error => { +        showAnkiError(error); +    }).then(() => { +        showAnkiSpinner(false);      });  } - -function onAnkiModelChanged(e) { -    if (!e.originalEvent) { -        return; -    } - -    getFormValues().then(({optsNew, optsOld}) => { -        optsNew[modelIdToFieldOptKey($(this).id)] = {}; - -        const ankiSpinner = $('#anki-spinner'); -        ankiSpinner.show(); - -        populateAnkiFields($(this), optsNew).then(() => { -            saveOptions(optsNew).then(() => yomichan().setOptions(optsNew)); -        }).catch(error => { -            $('#anki-error').show().find('span').text(error); -        }).then(() => { -            $('#anki-error').hide(); -            ankiSpinner.hide(); -        }); -    }); -} - -$(document).ready(() => { -    loadOptions().then(opts => { -        $('#activate-on-startup').prop('checked', opts.activateOnStartup); -        $('#enable-audio-playback').prop('checked', opts.enableAudioPlayback); -        $('#enable-soft-katakana-search').prop('checked', opts.enableSoftKatakanaSearch); -        $('#show-advanced-options').prop('checked', opts.showAdvancedOptions); - -        $('#hold-shift-to-scan').prop('checked', opts.holdShiftToScan); -        $('#select-matched-text').prop('checked', opts.selectMatchedText); -        $('#scan-delay').val(opts.scanDelay); -        $('#scan-length').val(opts.scanLength); - -        $('#anki-method').val(opts.ankiMethod); -        $('#anki-username').val(opts.ankiUsername); -        $('#anki-password').val(opts.ankiPassword); -        $('#anki-card-tags').val(opts.ankiCardTags.join(' ')); -        $('#sentence-extent').val(opts.sentenceExtent); - -        $('input, select').not('.anki-model').change(onOptionsChanged); -        $('.anki-model').change(onAnkiModelChanged); - -        populateAnkiDeckAndModel(opts); -        updateVisibility(opts); -    }); -}); diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 15288afc..28448b96 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -28,6 +28,8 @@ function sanitizeOptions(options) {          scanDelay: 15,          scanLength: 20, +        dictionaries: {}, +          ankiMethod: 'disabled',          ankiUsername: '',          ankiPassword: '', diff --git a/ext/bg/js/templates.js b/ext/bg/js/templates.js index 70920cec..1480bb17 100644 --- a/ext/bg/js/templates.js +++ b/ext/bg/js/templates.js @@ -1,5 +1,36 @@  (function() {    var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; +templates['dictionary.html'] = template({"1":function(container,depth0,helpers,partials,data) { +    return "disabled"; +},"3":function(container,depth0,helpers,partials,data) { +    return "checked"; +},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { +    var stack1, helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; + +  return "<div class=\"dict-group well well-sm\" data-title=\"" +    + alias4(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper))) +    + "\">\n    <h4><span class=\"text-muted glyphicon glyphicon-book\"></span> " +    + alias4(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper))) +    + " <small>v." +    + alias4(((helper = (helper = helpers.version || (depth0 != null ? depth0.version : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"version","hash":{},"data":data}) : helper))) +    + "</small></h4>\n\n    <!-- <div class=\"row\"> -->\n    <!--     <div class=\"col-xs-8\"> -->\n    <!--         <h4><span class=\"text-muted glyphicon glyphicon-book\"></span> " +    + alias4(((helper = (helper = helpers.title || (depth0 != null ? depth0.title : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"title","hash":{},"data":data}) : helper))) +    + " <small>v." +    + alias4(((helper = (helper = helpers.version || (depth0 != null ? depth0.version : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"version","hash":{},"data":data}) : helper))) +    + "</small></h4> -->\n    <!--     </div> -->\n    <!--     <div class=\"col-xs-4 text-right disabled\"> -->\n    <!--         <button type=\"button\" class=\"dict-group-controls dict-delete btn btn-danger\">Delete</button> -->\n    <!--     </div> -->\n    <!-- </div> -->\n\n    <div class=\"dict-delete-progress\">\n        Dictionary data is being deleted, please be patient...\n        <div class=\"progress\">\n            <div class=\"progress-bar progress-bar-striped progress-bar-danger\" style=\"width: 0%\"></div>\n        </div>\n    </div>\n\n    <div class=\"checkbox dict-group-controls " +    + ((stack1 = helpers.unless.call(alias1,(depth0 != null ? depth0.hasTerms : depth0),{"name":"unless","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "\">\n        <label><input type=\"checkbox\" class=\"dict-enable-terms\" " +    + ((stack1 = helpers.unless.call(alias1,(depth0 != null ? depth0.hasTerms : depth0),{"name":"unless","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + " " +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.enableTerms : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "> Enable term search</label>\n    </div>\n    <div class=\"checkbox dict-group-controls " +    + ((stack1 = helpers.unless.call(alias1,(depth0 != null ? depth0.hasKanji : depth0),{"name":"unless","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "\">\n        <label><input type=\"checkbox\" class=\"dict-enable-kanji\" " +    + ((stack1 = helpers.unless.call(alias1,(depth0 != null ? depth0.hasKanji : depth0),{"name":"unless","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + " " +    + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.enableKanji : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "> Enable Kanji search</label>\n    </div>\n</div>\n"; +},"useData":true});  templates['footer.html'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {      var helper; @@ -39,16 +70,28 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia      var helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;    return "        <span class=\"tag tag-" -    + alias4(((helper = (helper = helpers["class"] || (depth0 != null ? depth0["class"] : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"class","hash":{},"data":data}) : helper))) +    + alias4(((helper = (helper = helpers.category || (depth0 != null ? depth0.category : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"category","hash":{},"data":data}) : helper)))      + "\" title=\"" -    + alias4(((helper = (helper = helpers.desc || (depth0 != null ? depth0.desc : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"desc","hash":{},"data":data}) : helper))) +    + alias4(((helper = (helper = helpers.notes || (depth0 != null ? depth0.notes : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"notes","hash":{},"data":data}) : helper)))      + "\">"      + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))      + "</span>\n";  },"8":function(container,depth0,helpers,partials,data) { +    var stack1; + +  return "        <ol>\n" +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(9, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "        </ol>\n"; +},"9":function(container,depth0,helpers,partials,data) {      return "            <li><span>"      + container.escapeExpression(container.lambda(depth0, depth0))      + "</span></li>\n"; +},"11":function(container,depth0,helpers,partials,data) { +    var stack1; + +  return "        <p>\n            " +    + container.escapeExpression(container.lambda(((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["0"] : stack1), depth0)) +    + "\n        </p>\n";  },"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {      var stack1, helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; @@ -64,9 +107,9 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia      + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.onyomi : depth0),{"name":"each","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "                </td>\n            </tr>\n        </table>\n    </div>\n\n    <div class=\"kanji-tags\">\n"      + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.tags : depth0),{"name":"each","hash":{},"fn":container.program(6, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + "    </div>\n\n    <div class=\"kanji-glossary\">\n        <ol>\n" -    + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(8, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + "        </ol>\n    </div>\n</div>\n"; +    + "    </div>\n\n    <div class=\"kanji-glossary\">\n" +    + ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(8, data, 0),"inverse":container.program(11, data, 0),"data":data})) != null ? stack1 : "") +    + "    </div>\n</div>\n";  },"useData":true});  templates['kanji-link.html'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {      var helper; @@ -78,14 +121,37 @@ templates['kanji-link.html'] = template({"compiler":[7,">= 4.0.0"],"main":functi  templates['kanji-list.html'] = template({"1":function(container,depth0,helpers,partials,data,blockParams,depths) {      var stack1; -  return ((stack1 = container.invokePartial(partials["kanji.html"],depth0,{"name":"kanji.html","hash":{"sequence":(depths[1] != null ? depths[1].sequence : depths[1]),"options":(depths[1] != null ? depths[1].options : depths[1]),"root":(depths[1] != null ? depths[1].root : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); +  return ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(2, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"2":function(container,depth0,helpers,partials,data,blockParams,depths) { +    var stack1; + +  return ((stack1 = container.invokePartial(partials["kanji.html"],depth0,{"name":"kanji.html","hash":{"sequence":(depths[1] != null ? depths[1].sequence : depths[1]),"options":(depths[1] != null ? depths[1].options : depths[1]),"root":(depths[1] != null ? depths[1].root : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1])},"data":data,"indent":"    ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); +},"4":function(container,depth0,helpers,partials,data) { +    return "    <p>No results found</p>\n";  },"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) {      var stack1;    return ((stack1 = container.invokePartial(partials["header.html"],depth0,{"name":"header.html","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") -    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0, blockParams, depths),"inverse":container.program(4, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "")      + ((stack1 = container.invokePartial(partials["footer.html"],depth0,{"name":"footer.html","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "");  },"usePartial":true,"useData":true,"useDepths":true}); +templates['model.html'] = template({"1":function(container,depth0,helpers,partials,data) { +    return "                    <li><a class=\"marker-link\" href=\"#\">" +    + container.escapeExpression(container.lambda(depth0, depth0)) +    + "</a></li>\n"; +},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) { +    var stack1, helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; + +  return "<tr>\n    <td class=\"col-sm-2\">" +    + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper))) +    + "</td>\n    <td class=\"col-sm-10\">\n        <div class=\"input-group\">\n            <input type=\"text\" class=\"anki-field-value form-control\" data-field=\"" +    + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper))) +    + "\" value=\"" +    + alias4(((helper = (helper = helpers.value || (depth0 != null ? depth0.value : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"value","hash":{},"data":data}) : helper))) +    + "\">\n            <div class=\"input-group-btn\">\n                <button type=\"button\" class=\"btn btn-default dropdown-toggle\" data-toggle=\"dropdown\">\n                    <span class=\"caret\"></span>\n                </button>\n                <ul class=\"dropdown-menu dropdown-menu-right\">\n" +    + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.markers : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "                </ul>\n            </div>\n        </div>\n    </td>\n</tr>\n"; +},"useData":true});  templates['term.html'] = template({"1":function(container,depth0,helpers,partials,data) {      var helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression; @@ -129,7 +195,7 @@ templates['term.html'] = template({"1":function(container,depth0,helpers,partial  },"10":function(container,depth0,helpers,partials,data) {      var stack1; -  return "        <span class=\"rule\">" +  return "        <span class=\"reasons\">"      + container.escapeExpression(container.lambda(depth0, depth0))      + "</span> "      + ((stack1 = helpers.unless.call(depth0 != null ? depth0 : {},(data && data.last),{"name":"unless","hash":{},"fn":container.program(11, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") @@ -140,16 +206,28 @@ templates['term.html'] = template({"1":function(container,depth0,helpers,partial      var helper, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;    return "        <span class=\"tag tag-" -    + alias4(((helper = (helper = helpers["class"] || (depth0 != null ? depth0["class"] : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"class","hash":{},"data":data}) : helper))) +    + alias4(((helper = (helper = helpers.category || (depth0 != null ? depth0.category : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"category","hash":{},"data":data}) : helper)))      + "\" title=\"" -    + alias4(((helper = (helper = helpers.desc || (depth0 != null ? depth0.desc : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"desc","hash":{},"data":data}) : helper))) +    + alias4(((helper = (helper = helpers.notes || (depth0 != null ? depth0.notes : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"notes","hash":{},"data":data}) : helper)))      + "\">"      + alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))      + "</span>\n";  },"15":function(container,depth0,helpers,partials,data) { +    var stack1; + +  return "        <ol>\n" +    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(16, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "        </ol>\n"; +},"16":function(container,depth0,helpers,partials,data) {      return "            <li><span>"      + container.escapeExpression(container.lambda(depth0, depth0))      + "</span></li>\n"; +},"18":function(container,depth0,helpers,partials,data) { +    var stack1; + +  return "        <p>" +    + container.escapeExpression(container.lambda(((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["0"] : stack1), depth0)) +    + "</p>\n";  },"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {      var stack1, helper, alias1=depth0 != null ? depth0 : {}; @@ -160,23 +238,29 @@ templates['term.html'] = template({"1":function(container,depth0,helpers,partial      + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "    </div>\n\n"      + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.reading : depth0),{"name":"if","hash":{},"fn":container.program(5, data, 0),"inverse":container.program(8, data, 0),"data":data})) != null ? stack1 : "") -    + "\n    <div class=\"term-rules\">\n" -    + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.rules : depth0),{"name":"each","hash":{},"fn":container.program(10, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + "\n    <div class=\"term-reasons\">\n" +    + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.reasons : depth0),{"name":"each","hash":{},"fn":container.program(10, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")      + "    </div>\n\n    <div class=\"term-tags\">\n"      + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.tags : depth0),{"name":"each","hash":{},"fn":container.program(13, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + "    </div>\n\n    <div class=\"term-glossary\">\n        <ol>\n" -    + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.glossary : depth0),{"name":"each","hash":{},"fn":container.program(15, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") -    + "        </ol>\n    </div>\n</div>\n"; +    + "    </div>\n\n    <div class=\"term-glossary\">\n" +    + ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(15, data, 0),"inverse":container.program(18, data, 0),"data":data})) != null ? stack1 : "") +    + "    </div>\n</div>\n";  },"useData":true});  templates['term-list.html'] = template({"1":function(container,depth0,helpers,partials,data,blockParams,depths) {      var stack1; -  return ((stack1 = container.invokePartial(partials["term.html"],depth0,{"name":"term.html","hash":{"sequence":(depths[1] != null ? depths[1].sequence : depths[1]),"options":(depths[1] != null ? depths[1].options : depths[1]),"root":(depths[1] != null ? depths[1].root : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); +  return ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(2, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : ""); +},"2":function(container,depth0,helpers,partials,data,blockParams,depths) { +    var stack1; + +  return ((stack1 = container.invokePartial(partials["term.html"],depth0,{"name":"term.html","hash":{"sequence":(depths[1] != null ? depths[1].sequence : depths[1]),"options":(depths[1] != null ? depths[1].options : depths[1]),"root":(depths[1] != null ? depths[1].root : depths[1]),"addable":(depths[1] != null ? depths[1].addable : depths[1])},"data":data,"indent":"    ","helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : ""); +},"4":function(container,depth0,helpers,partials,data) { +    return "    <p>No results found</p>\n";  },"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) {      var stack1;    return ((stack1 = container.invokePartial(partials["header.html"],depth0,{"name":"header.html","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") -    + ((stack1 = helpers.each.call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.definitions : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "") +    + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : {},(depth0 != null ? depth0.definitions : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0, blockParams, depths),"inverse":container.program(4, data, 0, blockParams, depths),"data":data})) != null ? stack1 : "")      + ((stack1 = container.invokePartial(partials["footer.html"],depth0,{"name":"footer.html","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "");  },"usePartial":true,"useData":true,"useDepths":true});  })();
\ No newline at end of file diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js index 44f37e31..f29b90c9 100644 --- a/ext/bg/js/translator.js +++ b/ext/bg/js/translator.js @@ -20,96 +20,33 @@  class Translator {      constructor() {          this.loaded = false; -        this.tagMeta = null; -        this.dictionary = new Dictionary(); +        this.ruleMeta = null; +        this.database = new Database();          this.deinflector = new Deinflector();      } -    loadData(callback) { +    prepare() {          if (this.loaded) {              return Promise.resolve();          } -        return loadJson('bg/data/rules.json').then(rules => { -            this.deinflector.setRules(rules); -            return loadJson('bg/data/tags.json'); -        }).then(tagMeta => { -            this.tagMeta = tagMeta; -            return this.dictionary.prepareDb(); -        }).then(exists => { -            if (exists) { -                return; -            } - -            if (callback) { -                callback({state: 'begin', progress: 0}); -            } - -            const banks = {}; -            const bankCallback = (loaded, total, indexUrl) => { -                banks[indexUrl] = {loaded, total}; - -                let percent = 0.0; -                for (const url in banks) { -                    percent += banks[url].loaded / banks[url].total; -                } +        const promises = [ +            loadJsonInt('bg/data/deinflect.json'), +            this.database.prepare() +        ]; -                percent /= 3.0; - -                if (callback) { -                    callback({state: 'update', progress: Math.ceil(100.0 * percent)}); -                } -            }; - -            return Promise.all([ -                this.dictionary.importTermDict('bg/data/edict/index.json', bankCallback), -                this.dictionary.importTermDict('bg/data/enamdict/index.json', bankCallback), -                this.dictionary.importKanjiDict('bg/data/kanjidic/index.json', bankCallback), -            ]).then(() => { -                return this.dictionary.sealDb(); -            }).then(() => { -                if (callback) { -                    callback({state: 'end', progress: 100.0}); -                } -            }); -        }).then(() => { +        return Promise.all(promises).then(([reasons]) => { +            this.deinflector.setReasons(reasons);              this.loaded = true;          });      } -    findTermGroups(text) { -        const deinflectGroups = {}; -        const deinflectPromises = []; - -        for (let i = text.length; i > 0; --i) { -            deinflectPromises.push( -                this.deinflector.deinflect(text.slice(0, i), term => { -                    return this.dictionary.findTerm(term).then(definitions => definitions.map(definition => definition.tags)); -                }).then(deinflects => { -                    const processPromises = []; -                    for (const deinflect of deinflects) { -                        processPromises.push(this.processTerm( -                            deinflectGroups, -                            deinflect.source, -                            deinflect.tags, -                            deinflect.rules, -                            deinflect.root -                        )); -                    } - -                    return Promise.all(processPromises); -                }) -            ); -        } - -        return Promise.all(deinflectPromises).then(() => deinflectGroups); -    } - -    findTerm(text, enableSoftKatakanaSearch) { -        return this.findTermGroups(text).then(groups => { +    findTerm(text, dictionaries, enableSoftKatakanaSearch) { +        const cache = {}; +        return this.findDeinflectionGroups(text, dictionaries, cache).then(groups => {              const textHiragana = wanakana._katakanaToHiragana(text);              if (text !== textHiragana && enableSoftKatakanaSearch) { -                return this.findTermGroups(textHiragana).then(groupsHiragana => { +                return this.findDeinflectionGroups(textHiragana, dictionaries, cache).then(groupsHiragana => {                      for (const key in groupsHiragana) {                          groups[key] = groups[key] || groupsHiragana[key];                      } @@ -137,13 +74,11 @@ class Translator {          });      } -    findKanji(text) { -        const processed = {}; -        const promises = []; - +    findKanji(text, dictionaries) { +        const processed = {}, promises = [];          for (const c of text) {              if (!processed[c]) { -                promises.push(this.dictionary.findKanji(c).then((definitions) => definitions)); +                promises.push(this.database.findKanji(c, dictionaries));                  processed[c] = true;              }          } @@ -151,73 +86,53 @@ class Translator {          return Promise.all(promises).then(sets => this.processKanji(sets.reduce((a, b) => a.concat(b), [])));      } -    processTerm(groups, source, tags, rules, root) { -        return this.dictionary.findTerm(root).then(definitions => { -            for (const definition of definitions) { -                if (definition.id in groups) { -                    continue; -                } - -                let matched = tags.length === 0; -                for (const tag of tags) { -                    if (definition.tags.includes(tag)) { -                        matched = true; -                        break; -                    } -                } - -                if (!matched) { -                    continue; -                } - -                const tagItems = []; -                for (const tag of definition.tags) { -                    const tagItem = { -                        name: tag, -                        class: 'default', -                        order: Number.MAX_SAFE_INTEGER, -                        score: 0, -                        desc: definition.entities[tag] || '', -                    }; - -                    applyTagMeta(tagItem, this.tagMeta); -                    tagItems.push(tagItem); -                } - -                let score = 0; -                for (const tagItem of tagItems) { -                    score += tagItem.score; -                } - -                groups[definition.id] = { -                    score, -                    source, -                    rules, -                    expression: definition.expression, -                    reading: definition.reading, -                    glossary: definition.glossary, -                    tags: sortTags(tagItems) -                }; +    findDeinflectionGroups(text, dictionaries, cache) { +        const definer = term => { +            if (cache.hasOwnProperty(term)) { +                return Promise.resolve(cache[term]);              } -        }); + +            return this.database.findTerm(term, dictionaries).then(definitions => cache[term] = definitions); +        }; + +        const groups = {}, promises = []; +        for (let i = text.length; i > 0; --i) { +            promises.push( +                this.deinflector.deinflect(text.slice(0, i), definer).then(deinflections => { +                    for (const deinflection of deinflections) { +                        this.processDeinflection(groups, deinflection); +                    } +                }) +            ); +        } + +        return Promise.all(promises).then(() => groups);      } -    processKanji(definitions) { +    processDeinflection(groups, {source, rules, reasons, definitions}, dictionaries) {          for (const definition of definitions) { -            const tagItems = []; -            for (const tag of definition.tags) { -                const tagItem = { -                    name: tag, -                    class: 'default', -                    order: Number.MAX_SAFE_INTEGER, -                    desc: '', -                }; - -                applyTagMeta(tagItem, this.tagMeta); -                tagItems.push(tagItem); +            if (definition.id in groups) { +                continue;              } -            definition.tags = sortTags(tagItems); +            const tags = definition.tags.map(tag => buildTag(tag, definition.tagMeta)); +            groups[definition.id] = { +                source, +                reasons, +                score: definition.score, +                dictionary: definition.dictionary, +                expression: definition.expression, +                reading: definition.reading, +                glossary: definition.glossary, +                tags: sortTags(tags) +            }; +        } +    } + +    processKanji(definitions) { +        for (const definition of definitions) { +            const tags = definition.tags.map(tag => buildTag(tag, definition.tagMeta)); +            definition.tags = sortTags(tags);          }          return definitions; diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js index 4e0cc671..a0fca270 100644 --- a/ext/bg/js/util.js +++ b/ext/bg/js/util.js @@ -39,19 +39,11 @@ function promiseCallback(promise, callback) {      return promise.then(result => {         callback({result});      }).catch(error => { +        console.log(error);          callback({error});      });  } -function loadJson(url) { -    return new Promise((resolve, reject) => { -        const xhr = new XMLHttpRequest(); -        xhr.addEventListener('load', () => resolve(JSON.parse(xhr.responseText))); -        xhr.open('GET', chrome.extension.getURL(url)); -        xhr.send(); -    }); -} -  function sortTags(tags) {      return tags.sort((v1, v2) => {          const order1 = v1.order; @@ -92,8 +84,8 @@ function sortTermDefs(definitions) {              return 1;          } -        const rl1 = v1.rules.length; -        const rl2 = v2.rules.length; +        const rl1 = v1.reasons.length; +        const rl2 = v2.reasons.length;          if (rl1 < rl2) {              return -1;          } else if (rl1 > rl2) { @@ -104,15 +96,97 @@ function sortTermDefs(definitions) {      });  } -function applyTagMeta(tag, meta) { -    const symbol = tag.name.split(':')[0]; +function buildTag(name, meta) { +    const tag = {name}; +    const symbol = name.split(':')[0];      for (const prop in meta[symbol] || {}) {          tag[prop] = meta[symbol][prop];      } +    return sanitizeTag(tag); +} + +function sanitizeTag(tag) { +    tag.name = tag.name || 'untitled'; +    tag.category = tag.category || 'default'; +    tag.notes = tag.notes || ''; +    tag.order = tag.order || 0;      return tag;  }  function splitField(field) {      return field.length === 0 ? [] : field.split(' ');  } + +function loadJson(url) { +    return new Promise((resolve, reject) => { +        const xhr = new XMLHttpRequest(); +        xhr.addEventListener('load', () => resolve(xhr.responseText)); +        xhr.addEventListener('error', () => reject('failed to execute network request')); +        xhr.open('GET', url); +        xhr.send(); +    }).then(responseText => { +        try { +            return JSON.parse(responseText); +        } +        catch (e) { +            return Promise.reject('invalid JSON response'); +        } +    }); +} + +function loadJsonInt(url) { +    return loadJson(chrome.extension.getURL(url)); +} + +function importJsonDb(indexUrl, indexLoaded, termsLoaded, kanjiLoaded) { +    const indexDir = indexUrl.slice(0, indexUrl.lastIndexOf('/')); +    return loadJson(indexUrl).then(index => { +        if (!index.title || !index.version) { +            return Promise.reject('unrecognized dictionary format'); +        } + +        if (indexLoaded !== null) { +            return indexLoaded( +                index.title, +                index.version, +                index.tagMeta, +                index.termBanks > 0, +                index.kanjiBanks > 0 +            ).then(() => index); +        } + +        return index; +    }).then(index => { +        const loaders = []; +        const banksTotal = index.termBanks + index.kanjiBanks; +        let banksLoaded = 0; + +        for (let i = 1; i <= index.termBanks; ++i) { +            const bankUrl = `${indexDir}/term_bank_${i}.json`; +            loaders.push(() => loadJson(bankUrl).then(entries => termsLoaded( +                index.title, +                entries, +                banksTotal, +                banksLoaded++ +            ))); +        } + +        for (let i = 1; i <= index.kanjiBanks; ++i) { +            const bankUrl = `${indexDir}/kanji_bank_${i}.json`; +            loaders.push(() => loadJson(bankUrl).then(entries => kanjiLoaded( +                index.title, +                entries, +                banksTotal, +                banksLoaded++ +            ))); +        } + +        let chain = Promise.resolve(); +        for (const loader of loaders) { +            chain = chain.then(loader); +        } + +        return chain; +    }); +} diff --git a/ext/bg/js/yomichan.js b/ext/bg/js/yomichan.js index 7bca579d..e8956057 100644 --- a/ext/bg/js/yomichan.js +++ b/ext/bg/js/yomichan.js @@ -25,11 +25,11 @@ class Yomichan {          this.translator = new Translator();          this.anki = new AnkiNull();          this.options = null; -        this.importTabId = null;          this.setState('disabled');          chrome.runtime.onMessage.addListener(this.onMessage.bind(this));          chrome.browserAction.onClicked.addListener(this.onBrowserAction.bind(this)); +        chrome.runtime.onInstalled.addListener(this.onInstalled.bind(this));          loadOptions().then(opts => {              this.setOptions(opts); @@ -39,17 +39,9 @@ class Yomichan {          });      } -    onImport({state, progress}) { -        if (state === 'begin') { -            chrome.tabs.create({url: chrome.extension.getURL('bg/import.html')}, tab => this.importTabId = tab.id); -        } - -        if (this.importTabId !== null) { -            this.tabInvoke(this.importTabId, 'setProgress', progress); -        } - -        if (state === 'end') { -            this.importTabId = null; +    onInstalled(details) { +        if (details.reason === 'install') { +            chrome.tabs.create({url: chrome.extension.getURL('bg/guide.html')});          }      } @@ -91,7 +83,7 @@ class Yomichan {                  break;              case 'loading':                  chrome.browserAction.setBadgeText({text: '...'}); -                this.translator.loadData(this.onImport.bind(this)).then(() => this.setState('enabled')); +                this.translator.prepare().then(this.setState('enabled'));                  break;          } @@ -239,11 +231,31 @@ class Yomichan {      }      api_findKanji({text, callback}) { -        promiseCallback(this.translator.findKanji(text), callback); +        const dictionaries = []; +        for (const title in this.options.dictionaries) { +            if (this.options.dictionaries[title].enableKanji) { +                dictionaries.push(title); +            } +        } + +        promiseCallback( +            this.translator.findKanji(text, dictionaries), +            callback +        );      }      api_findTerm({text, callback}) { -        promiseCallback(this.translator.findTerm(text, this.options.enableSoftKatakanaSearch), callback); +        const dictionaries = []; +        for (const title in this.options.dictionaries) { +            if (this.options.dictionaries[title].enableTerms) { +                dictionaries.push(title); +            } +        } + +        promiseCallback( +            this.translator.findTerm(text, dictionaries, this.options.enableSoftKatakanaSearch), +            callback +        );      }      api_renderText({template, data, callback}) { |