Merge branch 'dev'

This commit is contained in:
Alex Yatskov 2016-12-22 18:50:58 -08:00
commit 39fa11f72b
24 changed files with 3936 additions and 712 deletions

View File

@ -9,7 +9,7 @@
<script src="js/ankinull.js"></script>
<script src="js/templates.js"></script>
<script src="js/util.js"></script>
<script src="js/dictionary.js"></script>
<script src="js/database.js"></script>
<script src="js/deinflector.js"></script>
<script src="js/translator.js"></script>
<script src="js/options.js"></script>

2799
ext/bg/data/deinflect.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,14 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Yomichan Dictionary Import</title>
<title>Welcome to Yomichan!</title>
<link rel="stylesheet" type="text/css" href="../lib/bootstrap-3.3.6-dist/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="../lib/bootstrap-3.3.6-dist/css/bootstrap-theme.min.css">
<style>
div.alert {
display: none;
}
</style>
</head>
<body>
<div class="container">
@ -19,40 +14,23 @@
<p>Thank you for downloading this extension! I sincerely hope that it will assist you on your language learning journey.</p>
<div>
<h2>Dictionary Import</h2>
<p>
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.
</p>
<div class="progress">
<div class="progress-bar progress-bar-striped" style="width: 0%"></div>
</div>
<div class="alert alert-success">Dictionary import complete!</div>
</div>
<div>
<h2>Quick Guide</h2>
<p>
Please read the steps outlined below to get quickly get up and running with Yomichan. For complete documentation,
visit the <a href="https://foosoft.net/projects/yomichan-chrome/">official homepage</a>.
visit the <a href="https://foosoft.net/projects/yomichan-chrome/" target="_blank">official homepage</a>.
</p>
<ol>
<li>Left-click on the <img src="../img/icon16.png" alt> icon to enable or disable Yomichan for the current browser instance.</li>
<li>Right-click on the <img src="../img/icon16.png" alt> icon and select <em>Options</em> to open the Yomichan options page.</li>
<li>Import any dictionaries (bundled or custom) you wish to use for Kanji and term searches.</li>
<li>Hold down <kbd>Shift</kbd> (or the middle mouse button) as you hover over text to see term definitions.</li>
<li>Resize the definitions window by dragging the bottom-left corner inwards or outwards.</li>
<li>Click on Kanji in the definition window to view additional information about that character.</li>
</ol>
</div>
</div>
<script src="../lib/jquery-2.2.2.min.js"></script>
<script src="js/import.js"></script>
</body>
</html>

BIN
ext/bg/img/paypal.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

297
ext/bg/js/database.js Normal file
View File

@ -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);
}
}

View File

@ -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() : []);
}
}

View File

@ -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;
});
}
}

View File

@ -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();
});

View File

@ -16,15 +16,291 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
//
// 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($('<option/>', {value: name, text: name})));
const ankiModel = $('.anki-model');
ankiModel.find('option').remove();
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);
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(() => {
return anki().getModelNames();
}).then(names => {
names.forEach(name => ankiModel.append($('<option/>', {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 = $('<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 groupBtn = $('<div>', {class: 'input-group-btn'});
groupBtn.append(button.append($('<span>', {class: 'caret'})));
groupBtn.append(markerItems);
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);
const row = $('<tr>');
row.append($('<td>', {class: 'col-sm-2'}).text(name));
row.append($('<td>', {class: 'col-sm-10'}).append(group));
tbody.append(row);
const html = Handlebars.templates['model.html']({name, markers, value: opts[optKey][name] || ''});
container.append($(html));
});
table.append(tbody);
});
}
function onOptionsChanged(e) {
if (!e.originalEvent && !e.isTrigger) {
return;
}
getFormValues().then(({optsNew, optsOld}) => {
saveOptions(optsNew).then(() => {
yomichan().setOptions(optsNew);
updateVisibility(optsNew);
const loginChanged =
optsNew.ankiUsername !== optsOld.ankiUsername ||
optsNew.ankiPassword !== optsOld.ankiPassword;
if (loginChanged && optsNew.ankiMethod === 'ankiweb') {
anki().logout().then(() => populateAnkiDeckAndModel(optsNew)).catch(error => {
$('#anki-error').show().find('span').text(error);
});
} else if (loginChanged || optsNew.ankiMethod !== optsOld.ankiMethod) {
populateAnkiDeckAndModel(optsNew);
}
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');
});
});
}
@ -236,45 +405,48 @@ function onAnkiModelChanged(e) {
return;
}
showAnkiError(null);
showAnkiSpinner(true);
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);
showAnkiError(error);
}).then(() => {
$('#anki-error').hide();
ankiSpinner.hide();
showAnkiSpinner(false);
});
});
}
$(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);
function onOptionsChanged(e) {
if (!e.originalEvent && !e.isTrigger) {
return;
}
$('#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);
getFormValues().then(({optsNew, optsOld}) => {
return saveOptions(optsNew).then(() => {
yomichan().setOptions(optsNew);
updateVisibility(optsNew);
$('#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);
const loginChanged =
optsNew.ankiUsername !== optsOld.ankiUsername ||
optsNew.ankiPassword !== optsOld.ankiPassword;
$('input, select').not('.anki-model').change(onOptionsChanged);
$('.anki-model').change(onAnkiModelChanged);
populateAnkiDeckAndModel(opts);
updateVisibility(opts);
if (loginChanged && optsNew.ankiMethod === 'ankiweb') {
showAnkiError(null);
showAnkiSpinner(true);
return anki().logout().then(() => populateAnkiDeckAndModel(optsNew));
} else if (loginChanged || optsNew.ankiMethod !== optsOld.ankiMethod) {
showAnkiError(null);
showAnkiSpinner(true);
return populateAnkiDeckAndModel(optsNew);
}
});
}).catch(error => {
showAnkiError(error);
}).then(() => {
showAnkiSpinner(false);
});
});
}

View File

@ -28,6 +28,8 @@ function sanitizeOptions(options) {
scanDelay: 15,
scanLength: 20,
dictionaries: {},
ankiMethod: 'disabled',
ankiUsername: '',
ankiPassword: '',

View File

@ -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});
})();

View File

@ -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;
}
const promises = [
loadJsonInt('bg/data/deinflect.json'),
this.database.prepare()
];
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;
}
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);
}
processDeinflection(groups, {source, rules, reasons, definitions}, dictionaries) {
for (const definition of definitions) {
if (definition.id in groups) {
continue;
}
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 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);
}
definition.tags = sortTags(tagItems);
const tags = definition.tags.map(tag => buildTag(tag, definition.tagMeta));
definition.tags = sortTags(tags);
}
return definitions;

View File

@ -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;
});
}

View File

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

View File

@ -6,7 +6,9 @@
<link rel="stylesheet" type="text/css" href="../lib/bootstrap-3.3.6-dist/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="../lib/bootstrap-3.3.6-dist/css/bootstrap-theme.min.css">
<style>
#anki-spinner, #anki-general, #anki-error, #options-advanced {
#anki-spinner, #anki-general, #anki-error,
#dict-spinner, #dict-error, #dict-warning, #dict-purge-progress, #dict-import-progress, .dict-delete-progress,
#options-advanced {
display: none;
}
@ -62,12 +64,71 @@
</div>
</div>
<div>
<div>
<img src="img/spinner.gif" class="pull-right" id="dict-spinner" alt>
<h3>Dictionaries</h3>
</div>
<p class="help-block">
Yomichan can utilize both bundled and custom (see the <a href="https://foosoft.net/projects/yomichan-import">Yomichan Import</a>
page for details) dictionaries. You can disable dictionaries that you no longer wish to use, or you can simply
<a href="#" id="dict-purge">purge the database</a> to delete everything. Please make sure to wait for import
and delete operations to complete before closing this page.
</p>
<div id="dict-purge-progress" class="text-danger">Dictionary data is being purged, please be patient...</div>
<div class="alert alert-warning" id="dict-warning">
<strong>Warning:</strong>
<span>No dictionaries found, use the importer below to install packaged and external dictionaries</span>
</div>
<div class="alert alert-danger" id="dict-error">
<strong>Error:</strong>
<span></span>
</div>
<div id="dict-groups"></div>
<div id="dict-import-progress">
Dictionary data is being imported, please be patient...
<div class="progress">
<div class="progress-bar progress-bar-striped" style="width: 0%"></div>
</div>
</div>
<div class="input-group" id="dict-importer">
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button>
<ul class="dropdown-menu">
<li><a href="#" data-url="edict">JMdict</a></li>
<li><a href="#" data-url="enamdict">JMnedict</a></li>
<li><a href="#" data-url="kanjidic">KANJIDIC2</a></li>
<li role="separator" class="divider"></li>
<li><a href="#" data-url="http://localhost:9876/index.json">Local dictionary</a></li>
</ul>
</div>
<input type="text" id="dict-url" class="form-control" placeholder="Dictionary import URL">
<div class="input-group-btn">
<button type="button" id="dict-import" class="btn btn-primary" disabled>Import</button>
</div>
</div>
</div>
<div>
<div>
<img src="img/spinner.gif" class="pull-right" id="anki-spinner" alt>
<h3>Anki Options</h3>
</div>
<p class="help-block">
Yomichan features automatic flashcard creation for <a href="http://ankisrs.net/">Anki</a>, a free application
designed to help you retain knowledge. While the <a href="https://foosoft.net/projects/anki-connect/">AnkiConnect</a> plugin
offers the best experience, it is also possible to create flashcards through <a href="https://ankiweb.net/">AnkiWeb</a>,
provided you already have an account.
</p>
<div class="alert alert-danger" id="anki-error">
<strong>Error:</strong>
<span></span>
@ -80,12 +141,6 @@
<option value="ankiconnect">AnkiConnect (requires the AnkiConnect plugin)</option>
<option value="ankiweb">AnkiWeb (requires an account on AnkiWeb)</option>
</select>
<p class="help-block">
Yomichan features automatic flashcard creation for <a href="http://ankisrs.net/">Anki</a>, a free application
designed to help you retain knowledge. While the <a href="https://foosoft.net/projects/anki-connect/">AnkiConnect</a> plugin
offers the best experience, it is also possible to create flashcards through <a href="https://ankiweb.net/">AnkiWeb</a>,
provided you already have an account.
</p>
</div>
<div id="anki-general">
@ -158,6 +213,19 @@
</div>
</div>
</div>
<div>
<h3>Support Development</h3>
<p class="help-block">
If you find Yomichan useful, please consider making a small donation as a way to show your appreciation for the countless hours
that I have devoted to this extension. Seeing that people care about my work is great motivation for continuing to
improve Yomichan!
</p>
<p>
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=4DBTN9A3CUAFN"><img src="img/paypal.gif" alt></a>
</p>
</div>
</div>
<div class="pull-right">
@ -167,6 +235,8 @@
<script src="../lib/jquery-2.2.2.min.js"></script>
<script src="../lib/bootstrap-3.3.6-dist/js/bootstrap.min.js"></script>
<script src="../lib/handlebars.min.js"></script>
<script src="js/templates.js"></script>
<script src="js/options.js"></script>
<script src="js/options-form.js"></script>
</body>

View File

@ -100,7 +100,7 @@ body {
text-decoration: none;
}
.term-rules {
.term-reasons {
color: #777;
display: inline-block;
}
@ -117,6 +117,11 @@ body {
color: #000;
}
.term-glossary p {
overflow-x: auto;
white-space: pre;
}
/* kanji styles */
.kanji-glyph {
@ -153,3 +158,8 @@ body {
.kanji-glossary li span {
color: #000;
}
.kanji-glossary p {
overflow-x: auto;
white-space: pre;
}

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Yomichan",
"version": "0.997",
"version": "0.998",
"description": "Japanese dictionary with Anki integration",
"icons": {"16": "img/icon16.png", "48": "img/icon48.png", "128": "img/icon128.png"},

26
tmpl/dictionary.html Normal file
View File

@ -0,0 +1,26 @@
<div class="dict-group well well-sm" data-title="{{title}}">
<h4><span class="text-muted glyphicon glyphicon-book"></span> {{title}} <small>v.{{version}}</small></h4>
<!-- <div class="row"> -->
<!-- <div class="col-xs-8"> -->
<!-- <h4><span class="text-muted glyphicon glyphicon-book"></span> {{title}} <small>v.{{version}}</small></h4> -->
<!-- </div> -->
<!-- <div class="col-xs-4 text-right disabled"> -->
<!-- <button type="button" class="dict-group-controls dict-delete btn btn-danger">Delete</button> -->
<!-- </div> -->
<!-- </div> -->
<div class="dict-delete-progress">
Dictionary data is being deleted, please be patient...
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-danger" style="width: 0%"></div>
</div>
</div>
<div class="checkbox dict-group-controls {{#unless hasTerms}}disabled{{/unless}}">
<label><input type="checkbox" class="dict-enable-terms" {{#unless hasTerms}}disabled{{/unless}} {{#if enableTerms}}checked{{/if}}> Enable term search</label>
</div>
<div class="checkbox dict-group-controls {{#unless hasKanji}}disabled{{/unless}}">
<label><input type="checkbox" class="dict-enable-kanji" {{#unless hasKanji}}disabled{{/unless}} {{#if enableKanji}}checked{{/if}}> Enable Kanji search</label>
</div>
</div>

View File

@ -1,5 +1,9 @@
{{> header.html}}
{{#each definitions}}
{{> kanji.html addable=../addable root=../root options=../options sequence=../sequence}}
{{/each}}
{{#if definitions}}
{{#each definitions}}
{{> kanji.html addable=../addable root=../root options=../options sequence=../sequence}}
{{/each}}
{{else}}
<p>No results found</p>
{{/if}}
{{> footer.html}}

View File

@ -30,15 +30,21 @@
<div class="kanji-tags">
{{#each tags}}
<span class="tag tag-{{class}}" title="{{desc}}">{{name}}</span>
<span class="tag tag-{{category}}" title="{{notes}}">{{name}}</span>
{{/each}}
</div>
<div class="kanji-glossary">
{{#if glossary.[1]}}
<ol>
{{#each glossary}}
<li><span>{{.}}</span></li>
{{/each}}
</ol>
{{else}}
<p>
{{glossary.[0]}}
</p>
{{/if}}
</div>
</div>

18
tmpl/model.html Normal file
View File

@ -0,0 +1,18 @@
<tr>
<td class="col-sm-2">{{name}}</td>
<td class="col-sm-10">
<div class="input-group">
<input type="text" class="anki-field-value form-control" data-field="{{name}}" value="{{value}}">
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
{{#each markers}}
<li><a class="marker-link" href="#">{{.}}</a></li>
{{/each}}
</ul>
</div>
</div>
</td>
</tr>

View File

@ -1,5 +1,9 @@
{{> header.html}}
{{#each definitions}}
{{> term.html addable=../addable root=../root options=../options sequence=../sequence}}
{{/each}}
{{#if definitions}}
{{#each definitions}}
{{> term.html addable=../addable root=../root options=../options sequence=../sequence}}
{{/each}}
{{else}}
<p>No results found</p>
{{/if}}
{{> footer.html}}

View File

@ -15,23 +15,27 @@
<div class="term-expression">{{#kanjiLinks}}{{expression}}{{/kanjiLinks}}</div>
{{/if}}
<div class="term-rules">
{{#each rules}}
<span class="rule">{{.}}</span> {{#unless @last}}&laquo;{{/unless}}
<div class="term-reasons">
{{#each reasons}}
<span class="reasons">{{.}}</span> {{#unless @last}}&laquo;{{/unless}}
{{/each}}
</div>
<div class="term-tags">
{{#each tags}}
<span class="tag tag-{{class}}" title="{{desc}}">{{name}}</span>
<span class="tag tag-{{category}}" title="{{notes}}">{{name}}</span>
{{/each}}
</div>
<div class="term-glossary">
{{#if glossary.[1]}}
<ol>
{{#each glossary}}
<li><span>{{.}}</span></li>
{{/each}}
</ol>
{{else}}
<p>{{glossary.[0]}}</p>
{{/if}}
</div>
</div>