Merge branch 'dev'
5
.gitattributes
vendored
@ -1,3 +1,4 @@
|
|||||||
util/data/*dic* filter=lfs diff=lfs merge=lfs -text
|
ext/bg/data/edict/*.json filter=lfs diff=lfs merge=lfs -text
|
||||||
ext/bg/data/*dic* filter=lfs diff=lfs merge=lfs -text
|
ext/bg/data/enamdict/*.json filter=lfs diff=lfs merge=lfs -text
|
||||||
|
ext/bg/data/kanjidic/*.json filter=lfs diff=lfs merge=lfs -text
|
||||||
*.ttf filter=lfs diff=lfs merge=lfs -text
|
*.ttf filter=lfs diff=lfs merge=lfs -text
|
||||||
|
@ -2,7 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body>
|
<body>
|
||||||
<script src="../lib/handlebars.min.js"></script>
|
<script src="../lib/handlebars.min.js"></script>
|
||||||
|
<script src="../lib/dexie.min.js"></script>
|
||||||
<script src="js/templates.js"></script>
|
<script src="js/templates.js"></script>
|
||||||
|
<script src="js/util.js"></script>
|
||||||
<script src="js/dictionary.js"></script>
|
<script src="js/dictionary.js"></script>
|
||||||
<script src="js/deinflector.js"></script>
|
<script src="js/deinflector.js"></script>
|
||||||
<script src="js/translator.js"></script>
|
<script src="js/translator.js"></script>
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Yomichan Guide</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">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
|
|
||||||
<div class="page-header">
|
|
||||||
<h1>Yomichan Guide</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>This is a minimal guide to get you started with Yomichan. For complete documentation, visit the <a href="https://foosoft.net/projects/yomichan-chrome/">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>Hold down <kbd>Shift</kbd> or the middle mouse button as you move your cursor over text to see definitions.</li>
|
|
||||||
<li>Resize the definition 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>
|
|
||||||
|
|
||||||
<p>Enjoy!</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
62
ext/bg/import.html
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Yomichan Dictionary Import</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">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>Welcome to Yomichan!</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>.
|
||||||
|
</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>Hold down <kbd>Shift</kbd> or the middle mouse button as you move your cursor over text to see definitions (or <kbd>Shift</kbd> + <kbd>Ctrl</kbd> for Kanji).</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>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<p>よろしくね!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../lib/jquery-2.2.2.min.js"></script>
|
||||||
|
<script src="js/import.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -26,32 +26,36 @@ class Deinflection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validate(validator) {
|
validate(validator) {
|
||||||
for (const tags of validator(this.term)) {
|
return validator(this.term).then(sets => {
|
||||||
if (this.tags.length === 0) {
|
for (const tags of sets) {
|
||||||
return true;
|
if (this.tags.length === 0) {
|
||||||
}
|
|
||||||
|
|
||||||
for (const tag of this.tags) {
|
|
||||||
if (tags.indexOf(tag) !== -1) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
for (const tag of this.tags) {
|
||||||
|
if (tags.includes(tag)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
deinflect(validator, rules) {
|
deinflect(validator, rules) {
|
||||||
if (this.validate(validator)) {
|
const promises = [
|
||||||
const child = new Deinflection(this.term, this.tags);
|
this.validate(validator).then(valid => {
|
||||||
this.children.push(child);
|
const child = new Deinflection(this.term, this.tags);
|
||||||
}
|
this.children.push(child);
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
for (const rule in rules) {
|
for (const rule in rules) {
|
||||||
for (const variant of rules[rule]) {
|
for (const variant of rules[rule]) {
|
||||||
let allowed = this.tags.length === 0;
|
let allowed = this.tags.length === 0;
|
||||||
for (const tag of this.tags) {
|
for (const tag of this.tags) {
|
||||||
if (variant.ti.indexOf(tag) !== -1) {
|
if (variant.ti.includes(tag)) {
|
||||||
allowed = true;
|
allowed = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -62,14 +66,24 @@ class Deinflection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const term = this.term.slice(0, -variant.ki.length) + variant.ko;
|
const term = this.term.slice(0, -variant.ki.length) + variant.ko;
|
||||||
const child = new Deinflection(term, variant.to, rule);
|
if (term.length === 0) {
|
||||||
if (child.deinflect(validator, rules)) {
|
continue;
|
||||||
this.children.push(child);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const child = new Deinflection(term, variant.to, rule);
|
||||||
|
promises.push(
|
||||||
|
child.deinflect(validator, rules).then(valid => {
|
||||||
|
if (valid) {
|
||||||
|
this.children.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.children.length > 0;
|
return Promise.all(promises).then(() => {
|
||||||
|
return this.children.length > 0;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
gather() {
|
gather() {
|
||||||
@ -105,10 +119,6 @@ class Deinflector {
|
|||||||
|
|
||||||
deinflect(term, validator) {
|
deinflect(term, validator) {
|
||||||
const node = new Deinflection(term);
|
const node = new Deinflection(term);
|
||||||
if (node.deinflect(validator, this.rules)) {
|
return node.deinflect(validator, this.rules).then(success => success ? node.gather() : []);
|
||||||
return node.gather();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,63 +19,199 @@
|
|||||||
|
|
||||||
class Dictionary {
|
class Dictionary {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.termDicts = {};
|
this.db = null;
|
||||||
this.kanjiDicts = {};
|
this.dbVer = 1;
|
||||||
|
this.entities = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
addTermDict(name, dict) {
|
initDb() {
|
||||||
this.termDicts[name] = dict;
|
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',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addKanjiDict(name, dict) {
|
prepareDb() {
|
||||||
this.kanjiDicts[name] = dict;
|
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) {
|
findTerm(term) {
|
||||||
let results = [];
|
if (this.db === null) {
|
||||||
|
return Promise.reject('database not initialized');
|
||||||
for (let name in this.termDicts) {
|
|
||||||
const dict = this.termDicts[name];
|
|
||||||
if (!(term in dict.i)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const indices = dict.i[term].split(' ').map(Number);
|
|
||||||
results = results.concat(
|
|
||||||
indices.map(index => {
|
|
||||||
const [e, r, t, ...g] = dict.d[index];
|
|
||||||
return {
|
|
||||||
expression: e,
|
|
||||||
reading: r,
|
|
||||||
tags: t.split(' '),
|
|
||||||
glossary: g,
|
|
||||||
entities: dict.e,
|
|
||||||
id: index
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
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: row.tags.split(' '),
|
||||||
|
glossary: row.glossary,
|
||||||
|
id: row.id
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
return this.getEntities();
|
||||||
|
}).then(entities => {
|
||||||
|
for (const result of results) {
|
||||||
|
result.entities = entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
findKanji(kanji) {
|
findKanji(kanji) {
|
||||||
const results = [];
|
if (this.db === null) {
|
||||||
|
return Promise.reject('database not initialized');
|
||||||
for (let name in this.kanjiDicts) {
|
|
||||||
const def = this.kanjiDicts[name].c[kanji];
|
|
||||||
if (def) {
|
|
||||||
const [k, o, t, ...g] = def;
|
|
||||||
results.push({
|
|
||||||
character: kanji,
|
|
||||||
kunyomi: k.split(' '),
|
|
||||||
onyomi: o.split(' '),
|
|
||||||
tags: t.split(' '),
|
|
||||||
glossary: g
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
const results = [];
|
||||||
|
return this.db.kanji.where('character').equals(kanji).each(row => {
|
||||||
|
results.push({
|
||||||
|
character: row.character,
|
||||||
|
onyomi: row.onyomi.split(' '),
|
||||||
|
kunyomi: row.kunyomi.split(' '),
|
||||||
|
tags: row.tags.split(' '),
|
||||||
|
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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,26 +17,20 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
function bgSendMessage(action, params) {
|
function api_setProgress(progress) {
|
||||||
return new Promise((resolve, reject) => chrome.runtime.sendMessage({action, params}, resolve));
|
$('.progress-bar').css('width', `${progress}%`);
|
||||||
|
|
||||||
|
if (progress === 100.0) {
|
||||||
|
$('.progress').hide();
|
||||||
|
$('.alert').show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function bgFindTerm(text) {
|
chrome.runtime.onMessage.addListener(({action, params}, sender, callback) => {
|
||||||
return bgSendMessage('findTerm', {text});
|
const method = this['api_' + action];
|
||||||
}
|
if (typeof(method) === 'function') {
|
||||||
|
method.call(this, params);
|
||||||
|
}
|
||||||
|
|
||||||
function bgFindKanji(text) {
|
callback();
|
||||||
return bgSendMessage('findKanji', {text});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function bgRenderText(data, template) {
|
|
||||||
return bgSendMessage('renderText', {data, template});
|
|
||||||
}
|
|
||||||
|
|
||||||
function bgCanAddDefinitions(definitions, modes) {
|
|
||||||
return bgSendMessage('canAddDefinitions', {definitions, modes});
|
|
||||||
}
|
|
||||||
|
|
||||||
function bgAddDefinition(definition, mode) {
|
|
||||||
return bgSendMessage('addDefinition', {definition, mode});
|
|
||||||
}
|
|
@ -32,7 +32,7 @@ function fieldsToDict(selection) {
|
|||||||
|
|
||||||
function modelIdToFieldOptKey(id) {
|
function modelIdToFieldOptKey(id) {
|
||||||
return {
|
return {
|
||||||
'anki-term-model': 'ankiTermFields',
|
'anki-term-model': 'ankiTermFields',
|
||||||
'anki-kanji-model': 'ankiKanjiFields'
|
'anki-kanji-model': 'ankiKanjiFields'
|
||||||
}[id];
|
}[id];
|
||||||
}
|
}
|
||||||
@ -60,15 +60,14 @@ function modelIdToMarkers(id) {
|
|||||||
}[id];
|
}[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
function formToOptions(section, callback) {
|
function formToOptions(section) {
|
||||||
loadOptions((optsOld) => {
|
return loadOptions().then(optsOld => {
|
||||||
const optsNew = $.extend({}, optsOld);
|
const optsNew = $.extend({}, optsOld);
|
||||||
|
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case 'general':
|
case 'general':
|
||||||
optsNew.scanLength = parseInt($('#scan-length').val(), 10);
|
optsNew.scanLength = parseInt($('#scan-length').val(), 10);
|
||||||
optsNew.activateOnStartup = $('#activate-on-startup').prop('checked');
|
optsNew.activateOnStartup = $('#activate-on-startup').prop('checked');
|
||||||
optsNew.loadEnamDict = $('#load-enamdict').prop('checked');
|
|
||||||
optsNew.selectMatchedText = $('#select-matched-text').prop('checked');
|
optsNew.selectMatchedText = $('#select-matched-text').prop('checked');
|
||||||
optsNew.showAdvancedOptions = $('#show-advanced-options').prop('checked');
|
optsNew.showAdvancedOptions = $('#show-advanced-options').prop('checked');
|
||||||
optsNew.enableAudioPlayback = $('#enable-audio-playback').prop('checked');
|
optsNew.enableAudioPlayback = $('#enable-audio-playback').prop('checked');
|
||||||
@ -86,7 +85,10 @@ function formToOptions(section, callback) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(sanitizeOptions(optsNew), sanitizeOptions(optsOld));
|
return {
|
||||||
|
optsNew: sanitizeOptions(optsNew),
|
||||||
|
optsOld: sanitizeOptions(optsOld)
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,9 +97,9 @@ function populateAnkiDeckAndModel(opts) {
|
|||||||
|
|
||||||
const ankiDeck = $('.anki-deck');
|
const ankiDeck = $('.anki-deck');
|
||||||
ankiDeck.find('option').remove();
|
ankiDeck.find('option').remove();
|
||||||
yomi.api_getDeckNames({callback: (names) => {
|
yomi.api_getDeckNames({callback: names => {
|
||||||
if (names !== null) {
|
if (names !== null) {
|
||||||
names.forEach((name) => ankiDeck.append($('<option/>', {value: name, text: name})));
|
names.forEach(name => ankiDeck.append($('<option/>', {value: name, text: name})));
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#anki-term-deck').val(opts.ankiTermDeck);
|
$('#anki-term-deck').val(opts.ankiTermDeck);
|
||||||
@ -106,9 +108,9 @@ function populateAnkiDeckAndModel(opts) {
|
|||||||
|
|
||||||
const ankiModel = $('.anki-model');
|
const ankiModel = $('.anki-model');
|
||||||
ankiModel.find('option').remove();
|
ankiModel.find('option').remove();
|
||||||
yomi.api_getModelNames({callback: (names) => {
|
yomi.api_getModelNames({callback: names => {
|
||||||
if (names !== null) {
|
if (names !== null) {
|
||||||
names.forEach((name) => ankiModel.append($('<option/>', {value: name, text: name})));
|
names.forEach(name => ankiModel.append($('<option/>', {value: name, text: name})));
|
||||||
}
|
}
|
||||||
|
|
||||||
populateAnkiFields($('#anki-term-model').val(opts.ankiTermModel), opts);
|
populateAnkiFields($('#anki-term-model').val(opts.ankiTermModel), opts);
|
||||||
@ -119,7 +121,7 @@ function populateAnkiDeckAndModel(opts) {
|
|||||||
function updateAnkiStatus() {
|
function updateAnkiStatus() {
|
||||||
$('.error-dlg').hide();
|
$('.error-dlg').hide();
|
||||||
|
|
||||||
yomichan().api_getVersion({callback: (version) => {
|
yomichan().api_getVersion({callback: version => {
|
||||||
if (version === null) {
|
if (version === null) {
|
||||||
$('.error-dlg-connection').show();
|
$('.error-dlg-connection').show();
|
||||||
$('.options-anki-controls').hide();
|
$('.options-anki-controls').hide();
|
||||||
@ -142,19 +144,19 @@ function populateAnkiFields(element, opts) {
|
|||||||
const optKey = modelIdToFieldOptKey(modelId);
|
const optKey = modelIdToFieldOptKey(modelId);
|
||||||
const markers = modelIdToMarkers(modelId);
|
const markers = modelIdToMarkers(modelId);
|
||||||
|
|
||||||
yomichan().api_getModelFieldNames({modelName, callback: (names) => {
|
yomichan().api_getModelFieldNames({modelName, callback: names => {
|
||||||
const table = element.closest('.tab-pane').find('.anki-fields');
|
const table = element.closest('.tab-pane').find('.anki-fields');
|
||||||
table.find('tbody').remove();
|
table.find('tbody').remove();
|
||||||
|
|
||||||
const tbody = $('<tbody>');
|
const tbody = $('<tbody>');
|
||||||
names.forEach((name) => {
|
names.forEach(name => {
|
||||||
const button = $('<button>', {type: 'button', class: 'btn btn-default dropdown-toggle'});
|
const button = $('<button>', {type: 'button', class: 'btn btn-default dropdown-toggle'});
|
||||||
button.attr('data-toggle', 'dropdown').dropdown();
|
button.attr('data-toggle', 'dropdown').dropdown();
|
||||||
|
|
||||||
const markerItems = $('<ul>', {class: 'dropdown-menu dropdown-menu-right'});
|
const markerItems = $('<ul>', {class: 'dropdown-menu dropdown-menu-right'});
|
||||||
for (const marker of markers) {
|
for (const marker of markers) {
|
||||||
const link = $('<a>', {href: '#'}).text(`{${marker}}`);
|
const link = $('<a>', {href: '#'}).text(`{${marker}}`);
|
||||||
link.click((e) => {
|
link.click(e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
link.closest('.input-group').find('.anki-field-value').val(link.text()).trigger('change');
|
link.closest('.input-group').find('.anki-field-value').val(link.text()).trigger('change');
|
||||||
});
|
});
|
||||||
@ -185,8 +187,8 @@ function onOptionsGeneralChanged(e) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
formToOptions('general', (optsNew, optsOld) => {
|
formToOptions('general').then(({optsNew, optsOld}) => {
|
||||||
saveOptions(optsNew, () => {
|
saveOptions(optsNew).then(() => {
|
||||||
yomichan().setOptions(optsNew);
|
yomichan().setOptions(optsNew);
|
||||||
if (!optsOld.enableAnkiConnect && optsNew.enableAnkiConnect) {
|
if (!optsOld.enableAnkiConnect && optsNew.enableAnkiConnect) {
|
||||||
updateAnkiStatus();
|
updateAnkiStatus();
|
||||||
@ -210,30 +212,29 @@ function onOptionsAnkiChanged(e) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
formToOptions('anki', (opts) => {
|
formToOptions('anki').then(({optsNew, optsOld}) => {
|
||||||
saveOptions(opts, () => yomichan().setOptions(opts));
|
saveOptions(optsNew).then(() => yomichan().setOptions(optsNew));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAnkiModelChanged(e) {
|
function onAnkiModelChanged(e) {
|
||||||
if (e.originalEvent) {
|
if (e.originalEvent) {
|
||||||
formToOptions('anki', (opts) => {
|
formToOptions('anki').then(({optsNew, optsOld}) => {
|
||||||
opts[modelIdToFieldOptKey($(this).id)] = {};
|
optsNew[modelIdToFieldOptKey($(this).id)] = {};
|
||||||
populateAnkiFields($(this), opts);
|
populateAnkiFields($(this), optsNew);
|
||||||
saveOptions(opts, () => yomichan().setOptions(opts));
|
saveOptions(optsNew).then(() => yomichan().setOptions(optsNew));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(() => {
|
$(document).ready(() => {
|
||||||
loadOptions((opts) => {
|
loadOptions().then(opts => {
|
||||||
$('#scan-length').val(opts.scanLength);
|
|
||||||
$('#activate-on-startup').prop('checked', opts.activateOnStartup);
|
$('#activate-on-startup').prop('checked', opts.activateOnStartup);
|
||||||
$('#load-enamdict').prop('checked', opts.loadEnamDict);
|
|
||||||
$('#select-matched-text').prop('checked', opts.selectMatchedText);
|
$('#select-matched-text').prop('checked', opts.selectMatchedText);
|
||||||
$('#show-advanced-options').prop('checked', opts.showAdvancedOptions);
|
|
||||||
$('#enable-audio-playback').prop('checked', opts.enableAudioPlayback);
|
$('#enable-audio-playback').prop('checked', opts.enableAudioPlayback);
|
||||||
$('#enable-anki-connect').prop('checked', opts.enableAnkiConnect);
|
$('#enable-anki-connect').prop('checked', opts.enableAnkiConnect);
|
||||||
|
$('#show-advanced-options').prop('checked', opts.showAdvancedOptions);
|
||||||
|
$('#scan-length').val(opts.scanLength);
|
||||||
|
|
||||||
$('#anki-card-tags').val(opts.ankiCardTags.join(' '));
|
$('#anki-card-tags').val(opts.ankiCardTags.join(' '));
|
||||||
$('#sentence-extent').val(opts.sentenceExtent);
|
$('#sentence-extent').val(opts.sentenceExtent);
|
||||||
|
@ -19,21 +19,22 @@
|
|||||||
|
|
||||||
function sanitizeOptions(options) {
|
function sanitizeOptions(options) {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
scanLength: 20,
|
activateOnStartup: true,
|
||||||
activateOnStartup: false,
|
selectMatchedText: true,
|
||||||
selectMatchedText: true,
|
|
||||||
showAdvancedOptions: false,
|
|
||||||
loadEnamDict: false,
|
|
||||||
enableAudioPlayback: true,
|
enableAudioPlayback: true,
|
||||||
enableAnkiConnect: false,
|
enableAnkiConnect: false,
|
||||||
ankiCardTags: ['yomichan'],
|
showAdvancedOptions: false,
|
||||||
sentenceExtent: 200,
|
scanLength: 20,
|
||||||
ankiTermDeck: '',
|
|
||||||
ankiTermModel: '',
|
ankiCardTags: ['yomichan'],
|
||||||
ankiTermFields: {},
|
sentenceExtent: 200,
|
||||||
ankiKanjiDeck: '',
|
|
||||||
ankiKanjiModel: '',
|
ankiTermDeck: '',
|
||||||
ankiKanjiFields: {}
|
ankiTermModel: '',
|
||||||
|
ankiTermFields: {},
|
||||||
|
ankiKanjiDeck: '',
|
||||||
|
ankiKanjiModel: '',
|
||||||
|
ankiKanjiFields: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const key in defaults) {
|
for (const key in defaults) {
|
||||||
@ -45,10 +46,16 @@ function sanitizeOptions(options) {
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadOptions(callback) {
|
function loadOptions() {
|
||||||
chrome.storage.sync.get(null, (items) => callback(sanitizeOptions(items)));
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.storage.sync.get(null, opts => {
|
||||||
|
resolve(sanitizeOptions(opts));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveOptions(opts, callback) {
|
function saveOptions(opts) {
|
||||||
chrome.storage.sync.set(sanitizeOptions(opts), callback);
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.storage.sync.set(sanitizeOptions(opts), resolve);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -25,171 +25,199 @@ class Translator {
|
|||||||
this.deinflector = new Deinflector();
|
this.deinflector = new Deinflector();
|
||||||
}
|
}
|
||||||
|
|
||||||
loadData({loadEnamDict=true}, callback) {
|
loadData(callback) {
|
||||||
if (this.loaded) {
|
if (this.loaded) {
|
||||||
callback();
|
return Promise.resolve();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Translator.loadData('bg/data/rules.json')
|
return loadJson('bg/data/rules.json').then(rules => {
|
||||||
.then((response) => {
|
this.deinflector.setRules(rules);
|
||||||
this.deinflector.setRules(JSON.parse(response));
|
return loadJson('bg/data/tags.json');
|
||||||
return Translator.loadData('bg/data/tags.json');
|
}).then(tagMeta => {
|
||||||
})
|
this.tagMeta = tagMeta;
|
||||||
.then((response) => {
|
return this.dictionary.prepareDb();
|
||||||
this.tagMeta = JSON.parse(response);
|
}).then(exists => {
|
||||||
return Translator.loadData('bg/data/edict.json');
|
if (exists) {
|
||||||
})
|
return;
|
||||||
.then((response) => {
|
}
|
||||||
this.dictionary.addTermDict('edict', JSON.parse(response));
|
|
||||||
return Translator.loadData('bg/data/kanjidic.json');
|
if (callback) {
|
||||||
})
|
callback({state: 'begin', progress: 0});
|
||||||
.then((response) => {
|
}
|
||||||
this.dictionary.addKanjiDict('kanjidic', JSON.parse(response));
|
|
||||||
return loadEnamDict ? Translator.loadData('bg/data/enamdict.json') : Promise.resolve(null);
|
const banks = {};
|
||||||
})
|
const bankCallback = (loaded, total, indexUrl) => {
|
||||||
.then((response) => {
|
banks[indexUrl] = {loaded, total};
|
||||||
if (response !== null) {
|
|
||||||
this.dictionary.addTermDict('enamdict', JSON.parse(response));
|
let percent = 0.0;
|
||||||
|
for (const url in banks) {
|
||||||
|
percent += banks[url].loaded / banks[url].total;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loaded = true;
|
percent /= 3.0;
|
||||||
callback();
|
|
||||||
|
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(() => {
|
||||||
|
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) {
|
findTerm(text) {
|
||||||
const groups = {};
|
return this.findTermGroups(text).then(deinflectGroups => {
|
||||||
for (let i = text.length; i > 0; --i) {
|
let definitions = [];
|
||||||
const term = text.slice(0, i);
|
for (const key in deinflectGroups) {
|
||||||
const dfs = this.deinflector.deinflect(term, t => {
|
definitions.push(deinflectGroups[key]);
|
||||||
const tags = [];
|
}
|
||||||
for (const d of this.dictionary.findTerm(t)) {
|
|
||||||
tags.push(d.tags);
|
definitions = definitions.sort((v1, v2) => {
|
||||||
|
const sl1 = v1.source.length;
|
||||||
|
const sl2 = v2.source.length;
|
||||||
|
if (sl1 > sl2) {
|
||||||
|
return -1;
|
||||||
|
} else if (sl1 < sl2) {
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return tags;
|
const s1 = v1.score;
|
||||||
|
const s2 = v2.score;
|
||||||
|
if (s1 > s2) {
|
||||||
|
return -1;
|
||||||
|
} else if (s1 < s2) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rl1 = v1.rules.length;
|
||||||
|
const rl2 = v2.rules.length;
|
||||||
|
if (rl1 < rl2) {
|
||||||
|
return -1;
|
||||||
|
} else if (rl1 > rl2) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return v2.expression.localeCompare(v1.expression);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dfs === null) {
|
let length = 0;
|
||||||
continue;
|
for (const result of definitions) {
|
||||||
|
length = Math.max(length, result.source.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const df of dfs) {
|
return {definitions, length};
|
||||||
this.processTerm(groups, df.source, df.tags, df.rules, df.root);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let definitions = [];
|
|
||||||
for (const key in groups) {
|
|
||||||
definitions.push(groups[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
definitions = definitions.sort((v1, v2) => {
|
|
||||||
const sl1 = v1.source.length;
|
|
||||||
const sl2 = v2.source.length;
|
|
||||||
if (sl1 > sl2) {
|
|
||||||
return -1;
|
|
||||||
} else if (sl1 < sl2) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const s1 = v1.score;
|
|
||||||
const s2 = v2.score;
|
|
||||||
if (s1 > s2) {
|
|
||||||
return -1;
|
|
||||||
} else if (s1 < s2) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rl1 = v1.rules.length;
|
|
||||||
const rl2 = v2.rules.length;
|
|
||||||
if (rl1 < rl2) {
|
|
||||||
return -1;
|
|
||||||
} else if (rl1 > rl2) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return v2.expression.localeCompare(v1.expression);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let length = 0;
|
|
||||||
for (const result of definitions) {
|
|
||||||
length = Math.max(length, result.source.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {definitions: definitions, length: length};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
findKanji(text) {
|
findKanji(text) {
|
||||||
let definitions = [];
|
|
||||||
const processed = {};
|
const processed = {};
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
for (const c of text) {
|
for (const c of text) {
|
||||||
if (!processed[c]) {
|
if (!processed[c]) {
|
||||||
definitions = definitions.concat(this.dictionary.findKanji(c));
|
promises.push(this.dictionary.findKanji(c).then((definitions) => definitions));
|
||||||
processed[c] = true;
|
processed[c] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.processKanji(definitions);
|
return Promise.all(promises).then(sets => this.processKanji(sets.reduce((a, b) => a.concat(b))));
|
||||||
}
|
}
|
||||||
|
|
||||||
processTerm(groups, source, tags, rules, root) {
|
processTerm(groups, source, tags, rules, root) {
|
||||||
for (const entry of this.dictionary.findTerm(root)) {
|
return this.dictionary.findTerm(root).then(definitions => {
|
||||||
if (entry.id in groups) {
|
for (const definition of definitions) {
|
||||||
continue;
|
if (definition.id in groups) {
|
||||||
}
|
continue;
|
||||||
|
|
||||||
let matched = tags.length === 0;
|
|
||||||
for (const tag of tags) {
|
|
||||||
if (entry.tags.indexOf(tag) !== -1) {
|
|
||||||
matched = true;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!matched) {
|
let matched = tags.length === 0;
|
||||||
continue;
|
for (const tag of tags) {
|
||||||
}
|
if (definition.tags.includes(tag)) {
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const tagItems = [];
|
if (!matched) {
|
||||||
for (const tag of entry.tags) {
|
continue;
|
||||||
const tagItem = {
|
}
|
||||||
name: tag,
|
|
||||||
class: 'default',
|
const tagItems = [];
|
||||||
order: Number.MAX_SAFE_INTEGER,
|
for (const tag of definition.tags) {
|
||||||
score: 0,
|
const tagItem = {
|
||||||
desc: entry.entities[tag] || '',
|
name: tag,
|
||||||
|
class: 'default',
|
||||||
|
order: Number.MAX_SAFE_INTEGER,
|
||||||
|
score: 0,
|
||||||
|
desc: definition.entities[tag] || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.applyTagMeta(tagItem);
|
||||||
|
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: Translator.sortTags(tagItems)
|
||||||
};
|
};
|
||||||
|
|
||||||
this.applyTagMeta(tagItem);
|
|
||||||
tagItems.push(tagItem);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
let score = 0;
|
|
||||||
for (const tagItem of tagItems) {
|
|
||||||
score += tagItem.score;
|
|
||||||
}
|
|
||||||
|
|
||||||
groups[entry.id] = {
|
|
||||||
score,
|
|
||||||
source,
|
|
||||||
rules,
|
|
||||||
expression: entry.expression,
|
|
||||||
reading: entry.reading,
|
|
||||||
glossary: entry.glossary,
|
|
||||||
tags: Translator.sortTags(tagItems)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
processKanji(entries) {
|
processKanji(definitions) {
|
||||||
const results = [];
|
for (const definition of definitions) {
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
const tagItems = [];
|
const tagItems = [];
|
||||||
for (const tag of entry.tags) {
|
for (const tag of definition.tags) {
|
||||||
const tagItem = {
|
const tagItem = {
|
||||||
name: tag,
|
name: tag,
|
||||||
class: 'default',
|
class: 'default',
|
||||||
@ -201,16 +229,10 @@ class Translator {
|
|||||||
tagItems.push(tagItem);
|
tagItems.push(tagItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
results.push({
|
definition.tags = Translator.sortTags(tagItems);
|
||||||
character: entry.character,
|
|
||||||
kunyomi: entry.kunyomi,
|
|
||||||
onyomi: entry.onyomi,
|
|
||||||
glossary: entry.glossary,
|
|
||||||
tags: Translator.sortTags(tagItems)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return definitions;
|
||||||
}
|
}
|
||||||
|
|
||||||
applyTagMeta(tag) {
|
applyTagMeta(tag) {
|
||||||
@ -241,18 +263,4 @@ class Translator {
|
|||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static isKanji(c) {
|
|
||||||
const code = c.charCodeAt(0);
|
|
||||||
return code >= 0x4e00 && code < 0x9fb0 || code >= 0x3400 && code < 0x4dc0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static loadData(url) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.addEventListener('load', () => resolve(xhr.responseText));
|
|
||||||
xhr.open('GET', chrome.extension.getURL(url), true);
|
|
||||||
xhr.send();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
46
ext/bg/js/util.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* 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 kanjiLinks(options) {
|
||||||
|
let result = '';
|
||||||
|
for (const c of options.fn(this)) {
|
||||||
|
if (isKanji(c)) {
|
||||||
|
result += Handlebars.templates['kanji-link.html']({kanji: c}).trim();
|
||||||
|
} else {
|
||||||
|
result += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
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), true);
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isKanji(c) {
|
||||||
|
const code = c.charCodeAt(0);
|
||||||
|
return code >= 0x4e00 && code < 0x9fb0 || code >= 0x3400 && code < 0x4dc0;
|
||||||
|
}
|
||||||
|
|
@ -20,31 +20,20 @@
|
|||||||
class Yomichan {
|
class Yomichan {
|
||||||
constructor() {
|
constructor() {
|
||||||
Handlebars.partials = Handlebars.templates;
|
Handlebars.partials = Handlebars.templates;
|
||||||
Handlebars.registerHelper('kanjiLinks', function(options) {
|
Handlebars.registerHelper('kanjiLinks', kanjiLinks);
|
||||||
let result = '';
|
|
||||||
for (const c of options.fn(this)) {
|
|
||||||
if (Translator.isKanji(c)) {
|
|
||||||
result += Handlebars.templates['kanji-link.html']({kanji: c}).trim();
|
|
||||||
} else {
|
|
||||||
result += c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.translator = new Translator();
|
this.translator = new Translator();
|
||||||
|
this.importTabId = null;
|
||||||
this.asyncPools = {};
|
this.asyncPools = {};
|
||||||
this.ankiConnectVer = 0;
|
this.ankiConnectVer = 0;
|
||||||
this.setState('disabled');
|
this.setState('disabled');
|
||||||
|
|
||||||
chrome.runtime.onInstalled.addListener(this.onInstalled.bind(this));
|
|
||||||
chrome.runtime.onMessage.addListener(this.onMessage.bind(this));
|
chrome.runtime.onMessage.addListener(this.onMessage.bind(this));
|
||||||
chrome.browserAction.onClicked.addListener(this.onBrowserAction.bind(this));
|
chrome.browserAction.onClicked.addListener(this.onBrowserAction.bind(this));
|
||||||
chrome.tabs.onCreated.addListener((tab) => this.onTabReady(tab.id));
|
chrome.tabs.onCreated.addListener(tab => this.onTabReady(tab.id));
|
||||||
chrome.tabs.onUpdated.addListener(this.onTabReady.bind(this));
|
chrome.tabs.onUpdated.addListener(this.onTabReady.bind(this));
|
||||||
|
|
||||||
loadOptions((opts) => {
|
loadOptions().then(opts => {
|
||||||
this.setOptions(opts);
|
this.setOptions(opts);
|
||||||
if (this.options.activateOnStartup) {
|
if (this.options.activateOnStartup) {
|
||||||
this.setState('loading');
|
this.setState('loading');
|
||||||
@ -52,9 +41,17 @@ class Yomichan {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onInstalled(details) {
|
onImport({state, progress}) {
|
||||||
if (details.reason === 'install') {
|
if (state === 'begin') {
|
||||||
chrome.tabs.create({url: chrome.extension.getURL('bg/guide.html')});
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +98,7 @@ class Yomichan {
|
|||||||
break;
|
break;
|
||||||
case 'loading':
|
case 'loading':
|
||||||
chrome.browserAction.setBadgeText({text: '...'});
|
chrome.browserAction.setBadgeText({text: '...'});
|
||||||
this.translator.loadData({loadEnamDict: this.options.loadEnamDict}, () => this.setState('enabled'));
|
this.translator.loadData(this.onImport.bind(this)).then(() => this.setState('enabled'));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +115,7 @@ class Yomichan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tabInvokeAll(action, params) {
|
tabInvokeAll(action, params) {
|
||||||
chrome.tabs.query({}, (tabs) => {
|
chrome.tabs.query({}, tabs => {
|
||||||
for (const tab of tabs) {
|
for (const tab of tabs) {
|
||||||
this.tabInvoke(tab.id, action, params);
|
this.tabInvoke(tab.id, action, params);
|
||||||
}
|
}
|
||||||
@ -133,7 +130,7 @@ class Yomichan {
|
|||||||
if (this.ankiConnectVer === this.getApiVersion()) {
|
if (this.ankiConnectVer === this.getApiVersion()) {
|
||||||
this.ankiInvoke(action, params, pool, callback);
|
this.ankiInvoke(action, params, pool, callback);
|
||||||
} else {
|
} else {
|
||||||
this.api_getVersion({callback: (version) => {
|
this.api_getVersion({callback: version => {
|
||||||
if (version === this.getApiVersion()) {
|
if (version === this.getApiVersion()) {
|
||||||
this.ankiConnectVer = version;
|
this.ankiConnectVer = version;
|
||||||
this.ankiInvoke(action, params, pool, callback);
|
this.ankiInvoke(action, params, pool, callback);
|
||||||
@ -209,7 +206,7 @@ class Yomichan {
|
|||||||
break;
|
break;
|
||||||
case 'tags':
|
case 'tags':
|
||||||
if (definition.tags) {
|
if (definition.tags) {
|
||||||
value = definition.tags.map((t) => t.name);
|
value = definition.tags.map(t => t.name);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -244,7 +241,7 @@ class Yomichan {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (const name in fields) {
|
for (const name in fields) {
|
||||||
if (fields[name].indexOf('{audio}') !== -1) {
|
if (fields[name].includes('{audio}')) {
|
||||||
audio.fields.push(name);
|
audio.fields.push(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -274,7 +271,7 @@ class Yomichan {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ankiInvokeSafe('canAddNotes', {notes}, 'notes', (results) => {
|
this.ankiInvokeSafe('canAddNotes', {notes}, 'notes', results => {
|
||||||
const states = [];
|
const states = [];
|
||||||
|
|
||||||
if (results !== null) {
|
if (results !== null) {
|
||||||
@ -293,11 +290,11 @@ class Yomichan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
api_findKanji({text, callback}) {
|
api_findKanji({text, callback}) {
|
||||||
callback(this.translator.findKanji(text));
|
this.translator.findKanji(text).then(result => callback(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
api_findTerm({text, callback}) {
|
api_findTerm({text, callback}) {
|
||||||
callback(this.translator.findTerm(text));
|
this.translator.findTerm(text).then(result => callback(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
api_getDeckNames({callback}) {
|
api_getDeckNames({callback}) {
|
||||||
|
@ -28,11 +28,6 @@
|
|||||||
<h3>General Options</h3>
|
<h3>General Options</h3>
|
||||||
|
|
||||||
<form class="form-horizontal">
|
<form class="form-horizontal">
|
||||||
<div class="form-group options-advanced">
|
|
||||||
<label for="scan-length" class="control-label col-sm-2">Scan length</label>
|
|
||||||
<div class="col-sm-10"><input type="number" min="1" id="scan-length" class="form-control"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-offset-2 col-sm-10">
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
@ -41,14 +36,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-offset-2 col-sm-10">
|
|
||||||
<div class="checkbox">
|
|
||||||
<label class="control-label"><input type="checkbox" id="load-enamdict"> Load <a href="http://www.edrdg.org/enamdict/enamdict_doc.html">ENAMDICT</a> (requires restart)</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-offset-2 col-sm-10">
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
@ -73,7 +60,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-offset-2 col-sm-10">
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
@ -81,6 +67,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group options-advanced">
|
||||||
|
<label for="scan-length" class="control-label col-sm-2">Scan length</label>
|
||||||
|
<div class="col-sm-10"><input type="number" min="1" id="scan-length" class="form-control"></div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,289 +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 Client {
|
|
||||||
constructor() {
|
|
||||||
this.popup = new Popup();
|
|
||||||
this.audio = {};
|
|
||||||
this.lastMousePos = null;
|
|
||||||
this.lastTextSource = null;
|
|
||||||
this.activateKey = 16;
|
|
||||||
this.activateBtn = 2;
|
|
||||||
this.enabled = false;
|
|
||||||
this.options = {};
|
|
||||||
this.definitions = null;
|
|
||||||
this.sequence = 0;
|
|
||||||
this.fgRoot = chrome.extension.getURL('fg');
|
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener(this.onBgMessage.bind(this));
|
|
||||||
window.addEventListener('message', this.onFrameMessage.bind(this));
|
|
||||||
window.addEventListener('mousedown', this.onMouseDown.bind(this));
|
|
||||||
window.addEventListener('mousemove', this.onMouseMove.bind(this));
|
|
||||||
window.addEventListener('keydown', this.onKeyDown.bind(this));
|
|
||||||
window.addEventListener('scroll', (e) => this.hidePopup());
|
|
||||||
window.addEventListener('resize', (e) => this.hidePopup());
|
|
||||||
}
|
|
||||||
|
|
||||||
onKeyDown(e) {
|
|
||||||
if (this.enabled && this.lastMousePos !== null && (e.keyCode === this.activateKey || e.charCode === this.activateKey)) {
|
|
||||||
this.searchAt(this.lastMousePos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseMove(e) {
|
|
||||||
this.lastMousePos = {x: e.clientX, y: e.clientY};
|
|
||||||
if (this.enabled && (e.shiftKey || e.which === this.activateBtn)) {
|
|
||||||
this.searchAt(this.lastMousePos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseDown(e) {
|
|
||||||
this.lastMousePos = {x: e.clientX, y: e.clientY};
|
|
||||||
if (this.enabled && (e.shiftKey || e.which === this.activateBtn)) {
|
|
||||||
this.searchAt(this.lastMousePos);
|
|
||||||
} else {
|
|
||||||
this.hidePopup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onBgMessage({action, params}, sender, callback) {
|
|
||||||
const method = this['api_' + action];
|
|
||||||
if (typeof(method) === 'function') {
|
|
||||||
method.call(this, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
|
|
||||||
onFrameMessage(e) {
|
|
||||||
const {action, params} = e.data, method = this['api_' + action];
|
|
||||||
if (typeof(method) === 'function') {
|
|
||||||
method.call(this, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
searchAt(point) {
|
|
||||||
const textSource = Client.textSourceFromPoint(point);
|
|
||||||
if (textSource === null || !textSource.containsPoint(point)) {
|
|
||||||
this.hidePopup();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.lastTextSource !== null && this.lastTextSource.equals(textSource)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
textSource.setEndOffset(this.options.scanLength);
|
|
||||||
|
|
||||||
let defs = [];
|
|
||||||
let seq = -1;
|
|
||||||
|
|
||||||
bgFindTerm(textSource.text())
|
|
||||||
.then(({definitions, length}) => {
|
|
||||||
if (length === 0) {
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
|
|
||||||
textSource.setEndOffset(length);
|
|
||||||
|
|
||||||
const sentence = Client.extractSentence(textSource, this.options.sentenceExtent);
|
|
||||||
definitions.forEach((definition) => {
|
|
||||||
definition.url = window.location.href;
|
|
||||||
definition.sentence = sentence;
|
|
||||||
});
|
|
||||||
|
|
||||||
defs = definitions;
|
|
||||||
seq = ++this.sequence;
|
|
||||||
|
|
||||||
return bgRenderText({definitions, root: this.fgRoot, options: this.options, sequence: seq}, 'term-list.html');
|
|
||||||
})
|
|
||||||
.then((content) => {
|
|
||||||
this.definitions = defs;
|
|
||||||
this.showPopup(textSource, content);
|
|
||||||
|
|
||||||
return bgCanAddDefinitions(defs, ['term_kanji', 'term_kana']);
|
|
||||||
})
|
|
||||||
.then((states) => {
|
|
||||||
if (states !== null) {
|
|
||||||
states.forEach((state, index) => this.popup.sendMessage('setActionState', {index, state, sequence: seq}));
|
|
||||||
}
|
|
||||||
}, () => this.hidePopup());
|
|
||||||
}
|
|
||||||
|
|
||||||
showPopup(textSource, content) {
|
|
||||||
this.popup.showNextTo(textSource.getRect(), content);
|
|
||||||
|
|
||||||
if (this.options.selectMatchedText) {
|
|
||||||
textSource.select();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lastTextSource = textSource;
|
|
||||||
}
|
|
||||||
|
|
||||||
hidePopup() {
|
|
||||||
this.popup.hide();
|
|
||||||
|
|
||||||
if (this.options.selectMatchedText && this.lastTextSource !== null) {
|
|
||||||
this.lastTextSource.deselect();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lastTextSource = null;
|
|
||||||
this.definitions = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
api_setOptions(opts) {
|
|
||||||
this.options = opts;
|
|
||||||
}
|
|
||||||
|
|
||||||
api_setEnabled(enabled) {
|
|
||||||
if (!(this.enabled = enabled)) {
|
|
||||||
this.hidePopup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api_addNote({index, mode}) {
|
|
||||||
const state = {[mode]: false};
|
|
||||||
bgAddDefinition(this.definitions[index], mode).then((success) => {
|
|
||||||
if (success) {
|
|
||||||
this.popup.sendMessage('setActionState', {index, state, sequence: this.sequence});
|
|
||||||
} else {
|
|
||||||
alert('Note could not be added');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
api_playAudio(index) {
|
|
||||||
const definition = this.definitions[index];
|
|
||||||
|
|
||||||
let url = `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kanji=${encodeURIComponent(definition.expression)}`;
|
|
||||||
if (definition.reading) {
|
|
||||||
url += `&kana=${encodeURIComponent(definition.reading)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key in this.audio) {
|
|
||||||
this.audio[key].pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
const audio = this.audio[url] || new Audio(url);
|
|
||||||
audio.currentTime = 0;
|
|
||||||
audio.play();
|
|
||||||
|
|
||||||
this.audio[url] = audio;
|
|
||||||
}
|
|
||||||
|
|
||||||
api_displayKanji(kanji) {
|
|
||||||
let defs = [];
|
|
||||||
let seq = -1;
|
|
||||||
|
|
||||||
bgFindKanji(kanji)
|
|
||||||
.then((definitions) => {
|
|
||||||
definitions.forEach((definition) => definition.url = window.location.href);
|
|
||||||
|
|
||||||
defs = definitions;
|
|
||||||
seq = ++this.sequence;
|
|
||||||
|
|
||||||
return bgRenderText({definitions, root: this.fgRoot, options: this.options, sequence: seq}, 'kanji-list.html');
|
|
||||||
})
|
|
||||||
.then((content) => {
|
|
||||||
this.definitions = defs;
|
|
||||||
this.popup.setContent(content, defs);
|
|
||||||
|
|
||||||
return bgCanAddDefinitions(defs, ['kanji']);
|
|
||||||
})
|
|
||||||
.then((states) => {
|
|
||||||
if (states !== null) {
|
|
||||||
states.forEach((state, index) => this.popup.sendMessage('setActionState', {index, state, sequence: seq}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static textSourceFromPoint(point) {
|
|
||||||
const element = document.elementFromPoint(point.x, point.y);
|
|
||||||
if (element !== null) {
|
|
||||||
const names = ['IMG', 'INPUT', 'BUTTON', 'TEXTAREA'];
|
|
||||||
if (names.indexOf(element.nodeName) !== -1) {
|
|
||||||
return new TextSourceElement(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const range = document.caretRangeFromPoint(point.x, point.y);
|
|
||||||
if (range !== null) {
|
|
||||||
return new TextSourceRange(range);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static extractSentence(source, extent) {
|
|
||||||
const quotesFwd = {'「': '」', '『': '』', "'": "'", '"': '"'};
|
|
||||||
const quotesBwd = {'」': '「', '』': '『', "'": "'", '"': '"'};
|
|
||||||
const terminators = '…。..??!!';
|
|
||||||
|
|
||||||
const sourceLocal = source.clone();
|
|
||||||
const position = sourceLocal.setStartOffset(extent);
|
|
||||||
sourceLocal.setEndOffset(position + extent);
|
|
||||||
const content = sourceLocal.text();
|
|
||||||
|
|
||||||
let quoteStack = [];
|
|
||||||
|
|
||||||
let startPos = 0;
|
|
||||||
for (let i = position; i >= startPos; --i) {
|
|
||||||
const c = content[i];
|
|
||||||
|
|
||||||
if (quoteStack.length === 0 && (terminators.indexOf(c) !== -1 || c in quotesFwd)) {
|
|
||||||
startPos = i + 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (quoteStack.length > 0 && c === quoteStack[0]) {
|
|
||||||
quoteStack.pop();
|
|
||||||
} else if (c in quotesBwd) {
|
|
||||||
quoteStack = [quotesBwd[c]].concat(quoteStack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
quoteStack = [];
|
|
||||||
|
|
||||||
let endPos = content.length;
|
|
||||||
for (let i = position; i < endPos; ++i) {
|
|
||||||
const c = content[i];
|
|
||||||
|
|
||||||
if (quoteStack.length === 0) {
|
|
||||||
if (terminators.indexOf(c) !== -1) {
|
|
||||||
endPos = i + 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else if (c in quotesBwd) {
|
|
||||||
endPos = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (quoteStack.length > 0 && c === quoteStack[0]) {
|
|
||||||
quoteStack.pop();
|
|
||||||
} else if (c in quotesFwd) {
|
|
||||||
quoteStack = [quotesFwd[c]].concat(quoteStack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return content.substring(startPos, endPos).trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.yomiClient = new Client();
|
|
244
ext/fg/js/driver.js
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
/*
|
||||||
|
* 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 Driver {
|
||||||
|
constructor() {
|
||||||
|
this.popup = new Popup();
|
||||||
|
this.audio = {};
|
||||||
|
this.lastMousePos = null;
|
||||||
|
this.lastTextSource = null;
|
||||||
|
this.pendingLookup = false;
|
||||||
|
this.enabled = false;
|
||||||
|
this.options = {};
|
||||||
|
this.definitions = null;
|
||||||
|
this.sequence = 0;
|
||||||
|
this.fgRoot = chrome.extension.getURL('fg');
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener(this.onBgMessage.bind(this));
|
||||||
|
window.addEventListener('message', this.onFrameMessage.bind(this));
|
||||||
|
window.addEventListener('mousedown', this.onMouseDown.bind(this));
|
||||||
|
window.addEventListener('mousemove', this.onMouseMove.bind(this));
|
||||||
|
window.addEventListener('keydown', this.onKeyDown.bind(this));
|
||||||
|
window.addEventListener('scroll', e => this.hidePopup());
|
||||||
|
window.addEventListener('resize', e => this.hidePopup());
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyDown(e) {
|
||||||
|
if (this.enabled && this.lastMousePos !== null && (e.keyCode === 16 || e.charCode === 16)) {
|
||||||
|
this.searchAt(this.lastMousePos, e.ctrlKey ? 'kanji' : 'terms');
|
||||||
|
} else {
|
||||||
|
this.hidePopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove(e) {
|
||||||
|
this.lastMousePos = {x: e.clientX, y: e.clientY};
|
||||||
|
if (this.enabled && (e.shiftKey || e.which === 2)) {
|
||||||
|
this.searchAt(this.lastMousePos, e.ctrlKey ? 'kanji' : 'terms');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseDown(e) {
|
||||||
|
this.lastMousePos = {x: e.clientX, y: e.clientY};
|
||||||
|
if (this.enabled && (e.shiftKey || e.which === 2)) {
|
||||||
|
this.searchAt(this.lastMousePos, e.ctrlKey ? 'kanji' : 'terms');
|
||||||
|
} else {
|
||||||
|
this.hidePopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBgMessage({action, params}, sender, callback) {
|
||||||
|
const method = this['api_' + action];
|
||||||
|
if (typeof(method) === 'function') {
|
||||||
|
method.call(this, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
onFrameMessage(e) {
|
||||||
|
const {action, params} = e.data, method = this['api_' + action];
|
||||||
|
if (typeof(method) === 'function') {
|
||||||
|
method.call(this, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
searchTerms(textSource) {
|
||||||
|
textSource.setEndOffset(this.options.scanLength);
|
||||||
|
|
||||||
|
this.pendingLookup = true;
|
||||||
|
findTerm(textSource.text()).then(({definitions, length}) => {
|
||||||
|
if (definitions.length === 0) {
|
||||||
|
this.pendingLookup = false;
|
||||||
|
this.hidePopup();
|
||||||
|
} else {
|
||||||
|
textSource.setEndOffset(length);
|
||||||
|
|
||||||
|
const sentence = extractSentence(textSource, this.options.sentenceExtent);
|
||||||
|
definitions.forEach(definition => {
|
||||||
|
definition.url = window.location.href;
|
||||||
|
definition.sentence = sentence;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sequence = ++this.sequence;
|
||||||
|
return renderText({definitions, sequence, root: this.fgRoot, options: this.options}, 'term-list.html').then(content => {
|
||||||
|
this.definitions = definitions;
|
||||||
|
this.pendingLookup = false;
|
||||||
|
this.showPopup(textSource, content);
|
||||||
|
return canAddDefinitions(definitions, ['term_kanji', 'term_kana']);
|
||||||
|
}).then(states => {
|
||||||
|
if (states !== null) {
|
||||||
|
states.forEach((state, index) => this.popup.invokeApi('setActionState', {index, state, sequence}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
searchKanji(textSource) {
|
||||||
|
textSource.setEndOffset(1);
|
||||||
|
|
||||||
|
this.pendingLookup = true;
|
||||||
|
findKanji(textSource.text()).then(definitions => {
|
||||||
|
if (definitions.length === 0) {
|
||||||
|
this.pendingLookup = false;
|
||||||
|
this.hidePopup();
|
||||||
|
} else {
|
||||||
|
definitions.forEach(definition => definition.url = window.location.href);
|
||||||
|
|
||||||
|
const sequence = ++this.sequence;
|
||||||
|
return renderText({definitions, sequence, root: this.fgRoot, options: this.options}, 'kanji-list.html').then(content => {
|
||||||
|
this.definitions = definitions;
|
||||||
|
this.pendingLookup = false;
|
||||||
|
this.showPopup(textSource, content);
|
||||||
|
return canAddDefinitions(definitions, ['kanji']);
|
||||||
|
}).then(states => {
|
||||||
|
if (states !== null) {
|
||||||
|
states.forEach((state, index) => this.popup.invokeApi('setActionState', {index, state, sequence}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
searchAt(point, mode) {
|
||||||
|
if (this.pendingLookup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textSource = textSourceFromPoint(point);
|
||||||
|
if (textSource === null || !textSource.containsPoint(point)) {
|
||||||
|
this.hidePopup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.lastTextSource !== null && this.lastTextSource.equals(textSource)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case 'terms':
|
||||||
|
this.searchTerms(textSource);
|
||||||
|
break;
|
||||||
|
case 'kanji':
|
||||||
|
this.searchKanji(textSource);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showPopup(textSource, content) {
|
||||||
|
this.popup.showNextTo(textSource.getRect(), content);
|
||||||
|
|
||||||
|
if (this.options.selectMatchedText) {
|
||||||
|
textSource.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastTextSource = textSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
hidePopup() {
|
||||||
|
this.popup.hide();
|
||||||
|
|
||||||
|
if (this.options.selectMatchedText && this.lastTextSource !== null) {
|
||||||
|
this.lastTextSource.deselect();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastTextSource = null;
|
||||||
|
this.definitions = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
api_setOptions(opts) {
|
||||||
|
this.options = opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
api_setEnabled(enabled) {
|
||||||
|
if (!(this.enabled = enabled)) {
|
||||||
|
this.hidePopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api_addNote({index, mode}) {
|
||||||
|
const state = {[mode]: false};
|
||||||
|
addDefinition(this.definitions[index], mode).then(success => {
|
||||||
|
if (success) {
|
||||||
|
this.popup.invokeApi('setActionState', {index, state, sequence: this.sequence});
|
||||||
|
} else {
|
||||||
|
alert('Note could not be added');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
api_playAudio(index) {
|
||||||
|
const definition = this.definitions[index];
|
||||||
|
|
||||||
|
let url = `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kanji=${encodeURIComponent(definition.expression)}`;
|
||||||
|
if (definition.reading) {
|
||||||
|
url += `&kana=${encodeURIComponent(definition.reading)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in this.audio) {
|
||||||
|
this.audio[key].pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
const audio = this.audio[url] || new Audio(url);
|
||||||
|
audio.currentTime = 0;
|
||||||
|
audio.play();
|
||||||
|
|
||||||
|
this.audio[url] = audio;
|
||||||
|
}
|
||||||
|
|
||||||
|
api_displayKanji(kanji) {
|
||||||
|
findKanji(kanji).then(definitions => {
|
||||||
|
definitions.forEach(definition => definition.url = window.location.href);
|
||||||
|
|
||||||
|
const sequence = ++this.sequence;
|
||||||
|
return renderText({definitions, sequence, root: this.fgRoot, options: this.options}, 'kanji-list.html').then(content => {
|
||||||
|
this.definitions = definitions;
|
||||||
|
this.popup.setContent(content, definitions);
|
||||||
|
return canAddDefinitions(definitions, ['kanji']);
|
||||||
|
}).then(states => {
|
||||||
|
if (states !== null) {
|
||||||
|
states.forEach((state, index) => this.popup.invokeApi('setActionState', {index, state, sequence}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.driver = new Driver();
|
@ -17,48 +17,39 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
function invokeApi(action, params, target) {
|
||||||
|
target.postMessage({action, params}, '*');
|
||||||
|
}
|
||||||
|
|
||||||
function registerKanjiLinks() {
|
function registerKanjiLinks() {
|
||||||
for (const link of Array.from(document.getElementsByClassName('kanji-link'))) {
|
for (const link of Array.from(document.getElementsByClassName('kanji-link'))) {
|
||||||
link.addEventListener('click', (e) => {
|
link.addEventListener('click', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.parent.postMessage({action: 'displayKanji', params: e.target.innerHTML}, '*');
|
invokeApi('displayKanji', e.target.innerHTML, window.parent);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerAddNoteLinks() {
|
function registerAddNoteLinks() {
|
||||||
for (const link of Array.from(document.getElementsByClassName('action-add-note'))) {
|
for (const link of Array.from(document.getElementsByClassName('action-add-note'))) {
|
||||||
link.addEventListener('click', (e) => {
|
link.addEventListener('click', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const ds = e.currentTarget.dataset;
|
const ds = e.currentTarget.dataset;
|
||||||
window.parent.postMessage({action: 'addNote', params: {index: ds.index, mode: ds.mode}}, '*');
|
invokeApi('addNote', {index: ds.index, mode: ds.mode}, window.parent);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerAudioLinks() {
|
function registerAudioLinks() {
|
||||||
for (const link of Array.from(document.getElementsByClassName('action-play-audio'))) {
|
for (const link of Array.from(document.getElementsByClassName('action-play-audio'))) {
|
||||||
link.addEventListener('click', (e) => {
|
link.addEventListener('click', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const ds = e.currentTarget.dataset;
|
const ds = e.currentTarget.dataset;
|
||||||
window.parent.postMessage({action: 'playAudio', params: ds.index}, '*');
|
invokeApi('playAudio', ds.index, window.parent);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDomContentLoaded() {
|
|
||||||
registerKanjiLinks();
|
|
||||||
registerAddNoteLinks();
|
|
||||||
registerAudioLinks();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMessage(e) {
|
|
||||||
const {action, params} = e.data, method = window['api_' + action];
|
|
||||||
if (typeof(method) === 'function') {
|
|
||||||
method(params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function api_setActionState({index, state, sequence}) {
|
function api_setActionState({index, state, sequence}) {
|
||||||
for (const mode in state) {
|
for (const mode in state) {
|
||||||
const matches = document.querySelectorAll(`.action-bar[data-sequence="${sequence}"] .action-add-note[data-index="${index}"][data-mode="${mode}"]`);
|
const matches = document.querySelectorAll(`.action-bar[data-sequence="${sequence}"] .action-add-note[data-index="${index}"][data-mode="${mode}"]`);
|
||||||
@ -75,5 +66,15 @@ function api_setActionState({index, state, sequence}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', onDomContentLoaded, false);
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
window.addEventListener('message', onMessage);
|
registerKanjiLinks();
|
||||||
|
registerAddNoteLinks();
|
||||||
|
registerAudioLinks();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('message', e => {
|
||||||
|
const {action, params} = e.data, method = window['api_' + action];
|
||||||
|
if (typeof(method) === 'function') {
|
||||||
|
method(params);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@ -68,7 +68,7 @@ class Popup {
|
|||||||
doc.close();
|
doc.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(action, params, callback) {
|
invokeApi(action, params) {
|
||||||
if (this.popup !== null) {
|
if (this.popup !== null) {
|
||||||
this.popup.contentWindow.postMessage({action, params}, '*');
|
this.popup.contentWindow.postMessage({action, params}, '*');
|
||||||
}
|
}
|
||||||
@ -81,8 +81,8 @@ class Popup {
|
|||||||
|
|
||||||
this.popup = document.createElement('iframe');
|
this.popup = document.createElement('iframe');
|
||||||
this.popup.id = 'yomichan-popup';
|
this.popup.id = 'yomichan-popup';
|
||||||
this.popup.addEventListener('mousedown', (e) => e.stopPropagation());
|
this.popup.addEventListener('mousedown', e => e.stopPropagation());
|
||||||
this.popup.addEventListener('scroll', (e) => e.stopPropagation());
|
this.popup.addEventListener('scroll', e => e.stopPropagation());
|
||||||
|
|
||||||
document.body.appendChild(this.popup);
|
document.body.appendChild(this.popup);
|
||||||
}
|
}
|
||||||
|
114
ext/fg/js/util.js
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
* 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 invokeApiBg(action, params) {
|
||||||
|
return new Promise((resolve, reject) => chrome.runtime.sendMessage({action, params}, resolve));
|
||||||
|
}
|
||||||
|
|
||||||
|
function findTerm(text) {
|
||||||
|
return invokeApiBg('findTerm', {text});
|
||||||
|
}
|
||||||
|
|
||||||
|
function findKanji(text) {
|
||||||
|
return invokeApiBg('findKanji', {text});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderText(data, template) {
|
||||||
|
return invokeApiBg('renderText', {data, template});
|
||||||
|
}
|
||||||
|
|
||||||
|
function canAddDefinitions(definitions, modes) {
|
||||||
|
return invokeApiBg('canAddDefinitions', {definitions, modes});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDefinition(definition, mode) {
|
||||||
|
return invokeApiBg('addDefinition', {definition, mode});
|
||||||
|
}
|
||||||
|
|
||||||
|
function textSourceFromPoint(point) {
|
||||||
|
const element = document.elementFromPoint(point.x, point.y);
|
||||||
|
if (element !== null) {
|
||||||
|
const names = ['IMG', 'INPUT', 'BUTTON', 'TEXTAREA'];
|
||||||
|
if (names.includes(element.nodeName)) {
|
||||||
|
return new TextSourceElement(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = document.caretRangeFromPoint(point.x, point.y);
|
||||||
|
if (range !== null) {
|
||||||
|
return new TextSourceRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractSentence(source, extent) {
|
||||||
|
const quotesFwd = {'「': '」', '『': '』', "'": "'", '"': '"'};
|
||||||
|
const quotesBwd = {'」': '「', '』': '『', "'": "'", '"': '"'};
|
||||||
|
const terminators = '…。..??!!';
|
||||||
|
|
||||||
|
const sourceLocal = source.clone();
|
||||||
|
const position = sourceLocal.setStartOffset(extent);
|
||||||
|
sourceLocal.setEndOffset(position + extent);
|
||||||
|
const content = sourceLocal.text();
|
||||||
|
|
||||||
|
let quoteStack = [];
|
||||||
|
|
||||||
|
let startPos = 0;
|
||||||
|
for (let i = position; i >= startPos; --i) {
|
||||||
|
const c = content[i];
|
||||||
|
|
||||||
|
if (quoteStack.length === 0 && (terminators.includes(c) || c in quotesFwd)) {
|
||||||
|
startPos = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quoteStack.length > 0 && c === quoteStack[0]) {
|
||||||
|
quoteStack.pop();
|
||||||
|
} else if (c in quotesBwd) {
|
||||||
|
quoteStack = [quotesBwd[c]].concat(quoteStack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
quoteStack = [];
|
||||||
|
|
||||||
|
let endPos = content.length;
|
||||||
|
for (let i = position; i < endPos; ++i) {
|
||||||
|
const c = content[i];
|
||||||
|
|
||||||
|
if (quoteStack.length === 0) {
|
||||||
|
if (terminators.includes(c)) {
|
||||||
|
endPos = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (c in quotesBwd) {
|
||||||
|
endPos = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quoteStack.length > 0 && c === quoteStack[0]) {
|
||||||
|
quoteStack.pop();
|
||||||
|
} else if (c in quotesFwd) {
|
||||||
|
quoteStack = [quotesFwd[c]].concat(quoteStack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return content.substring(startPos, endPos).trim();
|
||||||
|
}
|
Before Width: | Height: | Size: 274 B After Width: | Height: | Size: 594 B |
Before Width: | Height: | Size: 107 B After Width: | Height: | Size: 129 B |
Before Width: | Height: | Size: 111 B After Width: | Height: | Size: 135 B |
Before Width: | Height: | Size: 137 B After Width: | Height: | Size: 196 B |
Before Width: | Height: | Size: 147 B After Width: | Height: | Size: 223 B |
3
ext/lib/dexie.min.js
vendored
Normal file
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "Yomichan",
|
"name": "Yomichan",
|
||||||
"version": "0.97",
|
"version": "0.98",
|
||||||
|
|
||||||
"description": "Japanese dictionary with Anki integration",
|
"description": "Japanese dictionary with Anki integration",
|
||||||
"icons": {"16": "img/icon16.png", "48": "img/icon48.png", "128": "img/icon128.png"},
|
"icons": {"16": "img/icon16.png", "48": "img/icon48.png", "128": "img/icon128.png"},
|
||||||
@ -15,8 +15,8 @@
|
|||||||
"fg/js/source-range.js",
|
"fg/js/source-range.js",
|
||||||
"fg/js/source-element.js",
|
"fg/js/source-element.js",
|
||||||
"fg/js/popup.js",
|
"fg/js/popup.js",
|
||||||
"fg/js/api.js",
|
"fg/js/util.js",
|
||||||
"fg/js/client.js"
|
"fg/js/driver.js"
|
||||||
],
|
],
|
||||||
"css": ["fg/css/client.css"]
|
"css": ["fg/css/client.css"]
|
||||||
}],
|
}],
|
||||||
|