- Yomichan Dictionary Import
+ Welcome to Yomichan!
-
@@ -19,40 +14,23 @@
Thank you for downloading this extension! I sincerely hope that it will assist you on your language learning journey.
-
-
Dictionary Import
-
-
- Before it can be used for the first time, Yomichan must import the Japanese dictionary data included with this extension. This process can take a
- couple of minutes to finish so please be patient! Please do not completely exit out of your browser until this process completes.
-
-
-
-
-
-
-
Dictionary import complete!
-
-
Quick Guide
Please read the steps outlined below to get quickly get up and running with Yomichan. For complete documentation,
- visit the official homepage.
+ visit the official homepage.
Left-click on the icon to enable or disable Yomichan for the current browser instance.
Right-click on the icon and select Options to open the Yomichan options page.
+
Import any dictionaries (bundled or custom) you wish to use for Kanji and term searches.
Hold down Shift (or the middle mouse button) as you hover over text to see term definitions.
Resize the definitions window by dragging the bottom-left corner inwards or outwards.
Click on Kanji in the definition window to view additional information about that character.
-
-
-
diff --git a/ext/bg/img/paypal.gif b/ext/bg/img/paypal.gif
new file mode 100644
index 00000000..43cef691
Binary files /dev/null and b/ext/bg/img/paypal.gif differ
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
+ * Author: Alex Yatskov
+ *
+ * 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 .
+ */
+
+
+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;
- }
- }
- }
-
- 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;
+ 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);
+ }
+ }
}
}
- if (!allowed || !this.term.endsWith(variant.kanaIn)) {
+ 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 (!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
- * Author: Alex Yatskov
- *
- * 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 .
- */
-
-
-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
- * Author: Alex Yatskov
- *
- * 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 .
- */
-
-
-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,15 +16,291 @@
* along with this program. If not, see .
*/
+//
+// General
+//
function yomichan() {
return chrome.extension.getBackgroundPage().yomichan;
}
+function getFormValues() {
+ return loadOptions().then(optsOld => {
+ const optsNew = $.extend({}, optsOld);
+
+ optsNew.activateOnStartup = $('#activate-on-startup').prop('checked');
+ optsNew.enableAudioPlayback = $('#enable-audio-playback').prop('checked');
+ optsNew.showAdvancedOptions = $('#show-advanced-options').prop('checked');
+ optsNew.enableSoftKatakanaSearch = $('#enable-soft-katakana-search').prop('checked');
+
+ optsNew.holdShiftToScan = $('#hold-shift-to-scan').prop('checked');
+ optsNew.selectMatchedText = $('#select-matched-text').prop('checked');
+ optsNew.scanDelay = parseInt($('#scan-delay').val(), 10);
+ optsNew.scanLength = parseInt($('#scan-length').val(), 10);
+
+ optsNew.ankiMethod = $('#anki-method').val();
+ optsNew.ankiUsername = $('#anki-username').val();
+ optsNew.ankiPassword = $('#anki-password').val();
+ optsNew.ankiCardTags = $('#anki-card-tags').val().split(/[,; ]+/);
+ optsNew.sentenceExtent = parseInt($('#sentence-extent').val(), 10);
+ optsNew.ankiTermDeck = $('#anki-term-deck').val();
+ optsNew.ankiTermModel = $('#anki-term-model').val();
+ optsNew.ankiTermFields = fieldsToDict($('#term .anki-field-value'));
+ optsNew.ankiKanjiDeck = $('#anki-kanji-deck').val();
+ 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)
+ };
+ });
+}
+
+function updateVisibility(opts) {
+ switch (opts.ankiMethod) {
+ case 'ankiweb':
+ $('#anki-general').show();
+ $('.anki-login').show();
+ break;
+ case 'ankiconnect':
+ $('#anki-general').show();
+ $('.anki-login').hide();
+ break;
+ default:
+ $('#anki-general').hide();
+ break;
+ }
+
+ if (opts.showAdvancedOptions) {
+ $('.options-advanced').show();
+ } else {
+ $('.options-advanced').hide();
+ }
+}
+
+$(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);
+
+ populateDictionaries(opts);
+ populateAnkiDeckAndModel(opts);
+ updateVisibility(opts);
+ });
+});
+
+//
+// Dictionary
+//
+
+function database() {
+ return yomichan().translator.database;
+}
+
+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(() => {
+ 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(() => {
+ 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) => {
@@ -65,98 +341,40 @@ function modelIdToMarkers(id) {
}[id];
}
-function getFormValues() {
- return loadOptions().then(optsOld => {
- const optsNew = $.extend({}, optsOld);
-
- optsNew.activateOnStartup = $('#activate-on-startup').prop('checked');
- optsNew.enableAudioPlayback = $('#enable-audio-playback').prop('checked');
- optsNew.showAdvancedOptions = $('#show-advanced-options').prop('checked');
- optsNew.enableSoftKatakanaSearch = $('#enable-soft-katakana-search').prop('checked');
-
- optsNew.holdShiftToScan = $('#hold-shift-to-scan').prop('checked');
- optsNew.selectMatchedText = $('#select-matched-text').prop('checked');
- optsNew.scanDelay = parseInt($('#scan-delay').val(), 10);
- optsNew.scanLength = parseInt($('#scan-length').val(), 10);
-
- optsNew.ankiMethod = $('#anki-method').val();
- optsNew.ankiUsername = $('#anki-username').val();
- optsNew.ankiPassword = $('#anki-password').val();
- optsNew.ankiCardTags = $('#anki-card-tags').val().split(/[,; ]+/);
- optsNew.sentenceExtent = parseInt($('#sentence-extent').val(), 10);
- optsNew.ankiTermDeck = $('#anki-term-deck').val();
- optsNew.ankiTermModel = $('#anki-term-model').val();
- optsNew.ankiTermFields = fieldsToDict($('#term .anki-field-value'));
- optsNew.ankiKanjiDeck = $('#anki-kanji-deck').val();
- optsNew.ankiKanjiModel = $('#anki-kanji-model').val();
- optsNew.ankiKanjiFields = fieldsToDict($('#kanji .anki-field-value'));
-
- return {
- optsNew: sanitizeOptions(optsNew),
- optsOld: sanitizeOptions(optsOld)
- };
- });
-}
-
-function updateVisibility(opts) {
- switch (opts.ankiMethod) {
- case 'ankiweb':
- $('#anki-general').show();
- $('.anki-login').show();
- break;
- case 'ankiconnect':
- $('#anki-general').show();
- $('.anki-login').hide();
- break;
- default:
- $('#anki-general').hide();
- break;
- }
-
- if (opts.showAdvancedOptions) {
- $('.options-advanced').show();
- } else {
- $('.options-advanced').hide();
- }
-}
-
function populateAnkiDeckAndModel(opts) {
- const ankiSpinner = $('#anki-spinner');
- ankiSpinner.show();
+ showAnkiError(null);
+ showAnkiSpinner(true);
- const ankiFormat = $('#anki-format');
- ankiFormat.hide();
+ const ankiFormat = $('#anki-format').hide();
- const ankiDeck = $('.anki-deck');
- ankiDeck.find('option').remove();
+ return Promise.all([anki().getDeckNames(), anki().getModelNames()]).then(([deckNames, modelNames]) => {
+ const ankiDeck = $('.anki-deck');
+ ankiDeck.find('option').remove();
+ deckNames.forEach(name => ankiDeck.append($('', {value: name, text: name})));
- const ankiModel = $('.anki-model');
- ankiModel.find('option').remove();
-
- return anki().getDeckNames().then(names => {
- names.forEach(name => ankiDeck.append($('', {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($('', {value: name, text: name})));
+
+ return Promise.all([
+ populateAnkiFields($('#anki-term-model').val(opts.ankiTermModel), opts),
+ populateAnkiFields($('#anki-kanji-model').val(opts.ankiKanjiModel), opts)
+ ]);
}).then(() => {
- return anki().getModelNames();
- }).then(names => {
- names.forEach(name => ankiModel.append($('', {value: name, text: name})));
- return populateAnkiFields($('#anki-term-model').val(opts.ankiTermModel), opts);
- }).then(() => {
- return 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,65 +386,16 @@ function populateAnkiFields(element, opts) {
const markers = modelIdToMarkers(modelId);
return anki().getModelFieldNames(modelName).then(names => {
- const tbody = $('');
names.forEach(name => {
- const button = $('