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}) { |