Merge branch 'dev'

This commit is contained in:
Alex Yatskov 2017-03-25 18:19:38 -07:00
commit 37321e8d52
20 changed files with 485 additions and 209 deletions

View File

@ -7,6 +7,7 @@
<script src="/mixed/lib/handlebars.min.js"></script>
<script src="/mixed/lib/dexie.min.js"></script>
<script src="/mixed/lib/wanakana.min.js"></script>
<script src="/mixed/js/util.js"></script>
<script src="/bg/js/templates.js"></script>
<script src="/bg/js/util.js"></script>
<script src="/bg/js/anki-connect.js"></script>

View File

@ -7,7 +7,7 @@
<link rel="stylesheet" type="text/css" href="/mixed/lib/bootstrap-3.3.7-dist/css/bootstrap-theme.min.css">
</head>
<body>
<div class="container-fluid">
<div class="container">
<div class="page-header">
<h1>Welcome to Yomichan!</h1>
</div>
@ -23,11 +23,12 @@
</p>
<ol>
<li>Click on the <img src="/mixed/img/icon16.png" alt> icon in the browser toolbar to open the Yomichan options page.</li>
<li>Import the dictionaries (bundled or custom) you wish to use for term and Kanji searches.</li>
<li>Hold down <kbd>Shift</kbd> (or the middle mouse button) as you hover over text to see term definitions.</li>
<li>Click on the <img src="/mixed/img/play-audio.png" alt> icon to hear the term pronounced by a native speaker (if audio is available).</li>
<li>Click on Kanji in the definition window to view additional information about that character.</li>
<li>Click on the <img src="/mixed/img/icon16.png" alt> icon in the browser toolbar to open the Yomichan actions dialog.</li>
<li>Click on the <em>monkey wrench</em> icon in the middle to open the options page.</li>
<li>Import the dictionaries you wish to use for term and Kanji searches.</li>
<li>Hold down <kbd>Shift</kbd> key or the middle mouse button as you move your mouse over text to display definitions.</li>
<li>Click on the <img src="/mixed/img/play-audio.png" alt> icon to hear the term pronounced by a native speaker.</li>
<li>Click on individual Kanji in the term definition results to view additional information about those characters.</li>
</ol>
</div>
</div>

View File

@ -90,7 +90,7 @@ class Deinflection {
source: this.term,
rules: this.rules,
definitions: this.definitions,
reasons: [this.reason]
reasons: this.reason.length > 0 ? [this.reason] : []
}];
}

View File

@ -20,8 +20,13 @@
window.displayWindow = new class extends Display {
constructor() {
super($('#spinner'), $('#content'));
$('#search').click(this.onSearch.bind(this));
window.wanakana.bind($('#query').get(0));
const search = $('#search');
search.click(this.onSearch.bind(this));
const query = $('#query');
query.on('input', () => search.prop('disabled', query.val().length === 0));
window.wanakana.bind(query.get(0));
}
definitionAdd(definition, mode) {
@ -44,6 +49,10 @@ window.displayWindow = new class extends Display {
window.alert(`Error: ${error}`);
}
clearSearch() {
$('#query').focus().select();
}
onSearch(e) {
e.preventDefault();
$('#intro').slideUp();

View File

@ -18,17 +18,14 @@
$(document).ready(() => {
$('#open-search').click(() => window.open(chrome.extension.getURL('/bg/search.html')));
$('#open-options').click(() => chrome.runtime.openOptionsPage());
$('#open-help').click(() => window.open('http://foosoft.net/projects/yomichan'));
$('#open-search').click(() => commandExec('search'));
$('#open-options').click(() => commandExec('options'));
$('#open-help').click(() => commandExec('help'));
optionsLoad().then(options => {
const toggle = $('#enable-search');
toggle.prop('checked', options.general.enable).change();
toggle.bootstrapToggle();
toggle.change(() => {
options.general.enable = toggle.prop('checked');
optionsSave(options).then(() => instYomi().optionsSet(options));
});
toggle.change(() => commandExec('toggle'));
});
});

View File

@ -284,7 +284,7 @@ templates['fields.html'] = template({"1":function(container,depth0,helpers,parti
templates['kanji.html'] = template({"1":function(container,depth0,helpers,partials,data) {
var stack1, helper, alias1=depth0 != null ? depth0 : {};
return "<div class=\"entry\">\n <div class=\"actions\">\n"
return "<div class=\"entry\" data-type=\"kanji\">\n <div class=\"actions\">\n <img src=\"/mixed/img/entry-current.png\" class=\"current\" title=\"Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)\" alt>\n"
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.source : depth0),{"name":"if","hash":{},"fn":container.program(4, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " </div>\n\n <div class=\"glyph\">"
@ -299,9 +299,9 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia
+ ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.glossary : depth0)) != null ? stack1["1"] : stack1),{"name":"if","hash":{},"fn":container.program(11, data, 0),"inverse":container.program(15, data, 0),"data":data})) != null ? stack1 : "")
+ " </div>\n</div>\n";
},"2":function(container,depth0,helpers,partials,data) {
return " <a href=\"#\" title=\"Add Kanji\" class=\"action-add-note pending disabled\" data-mode=\"kanji\"><img src=\"/mixed/img/add-kanji.png\" alt></a>\n";
return " <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"kanji\"><img src=\"/mixed/img/add-kanji.png\" title=\"Add Kanji (Alt + K)\" alt></a>\n";
},"4":function(container,depth0,helpers,partials,data) {
return " <a href=\"#\" title=\"Source term\" class=\"source-term\"><img src=\"/mixed/img/source-term.png\" alt></a>\n";
return " <a href=\"#\" class=\"source-term\"><img src=\"/mixed/img/source-term.png\" title=\"Source term (Alt + B)\" alt></a>\n";
},"6":function(container,depth0,helpers,partials,data) {
var stack1;
@ -442,7 +442,7 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia
},"12":function(container,depth0,helpers,partials,data) {
var stack1, alias1=depth0 != null ? depth0 : {};
return "<div class=\"entry\">\n <div class=\"actions\">\n"
return "<div class=\"entry\" data-type=\"term\">\n <div class=\"actions\">\n <img src=\"/mixed/img/entry-current.png\" class=\"current\" title=\"Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)\" alt>\n"
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.addable : depth0),{"name":"if","hash":{},"fn":container.program(13, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.playback : depth0),{"name":"if","hash":{},"fn":container.program(15, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ " </div>\n\n"
@ -453,9 +453,9 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.grouped : depth0),{"name":"if","hash":{},"fn":container.program(26, data, 0),"inverse":container.program(32, data, 0),"data":data})) != null ? stack1 : "")
+ " </div>\n</div>\n";
},"13":function(container,depth0,helpers,partials,data) {
return " <a href=\"#\" title=\"Add expression\" class=\"action-add-note pending disabled\" data-mode=\"term-kanji\"><img src=\"/mixed/img/add-term-kanji.png\" alt></a>\n <a href=\"#\" title=\"Add reading\" class=\"action-add-note pending disabled\" data-mode=\"term-kana\"><img src=\"/mixed/img/add-term-kana.png\" alt></a>\n";
return " <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"term-kanji\"><img src=\"/mixed/img/add-term-kanji.png\" title=\"Add expression (Alt + E)\" alt></a>\n <a href=\"#\" class=\"action-add-note pending disabled\" data-mode=\"term-kana\"><img src=\"/mixed/img/add-term-kana.png\" title=\"Add reading (Alt + R)\" alt></a>\n";
},"15":function(container,depth0,helpers,partials,data) {
return " <a href=\"#\" title=\"Play audio\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.png\" alt></a>\n";
return " <a href=\"#\" class=\"action-play-audio\"><img src=\"/mixed/img/play-audio.png\" title=\"Play audio (Alt + P)\" alt></a>\n";
},"17":function(container,depth0,helpers,partials,data) {
var stack1, helper, options, alias1=depth0 != null ? depth0 : {}, alias2=helpers.helperMissing, alias3="function", buffer =
" <div class=\"expression\"><ruby>";

View File

@ -33,6 +33,15 @@ function promiseCallback(promise, callback) {
}
/*
* Commands
*/
function commandExec(command) {
instYomi().onCommand(command);
}
/*
* Instance
*/

View File

@ -25,12 +25,13 @@ window.yomichan = new class {
this.anki = new AnkiNull();
this.options = null;
chrome.runtime.onMessage.addListener(this.onMessage.bind(this));
if (chrome.runtime.onInstalled) {
chrome.runtime.onInstalled.addListener(this.onInstalled.bind(this));
}
this.translator.prepare().then(optionsLoad).then(this.optionsSet.bind(this));
this.translator.prepare().then(optionsLoad).then(this.optionsSet.bind(this)).then(() => {
chrome.commands.onCommand.addListener(this.onCommand.bind(this));
chrome.runtime.onMessage.addListener(this.onMessage.bind(this));
if (chrome.runtime.onInstalled) {
chrome.runtime.onInstalled.addListener(this.onInstalled.bind(this));
}
});
}
optionsSet(options) {
@ -55,7 +56,10 @@ window.yomichan = new class {
}
noteFormat(definition, mode) {
const note = {fields: {}, tags: this.options.anki.tags};
const note = {
fields: {},
tags: this.options.anki.tags
};
let fields = [];
if (mode === 'kanji') {
@ -116,8 +120,15 @@ window.yomichan = new class {
}
definitionAdd(definition, mode) {
const note = this.noteFormat(definition, mode);
return this.anki.addNote(note);
let promise = Promise.resolve();
if (mode !== 'kanji') {
promise = audioInject(definition, this.options.anki.terms.fields);
}
return promise.then(() => {
const note = this.noteFormat(definition, mode);
return this.anki.addNote(note);
});
}
definitionsAddable(definitions, modes) {
@ -153,34 +164,60 @@ window.yomichan = new class {
}
}
onCommand(command) {
const handlers = {
search: () => {
chrome.tabs.create({url: chrome.extension.getURL('/bg/search.html')});
},
help: () => {
chrome.tabs.create({url: 'https://foosoft.net/projects/yomichan/'});
},
options: () => {
chrome.runtime.openOptionsPage();
},
toggle: () => {
this.options.general.enable = !this.options.general.enable;
optionsSave(this.options).then(() => this.optionsSet(this.options));
}
};
const handler = handlers[command];
if (handler) {
handler();
}
}
onMessage(request, sender, callback) {
const handlers = new class {
api_optionsGet({callback}) {
optionsGet({callback}) {
promiseCallback(optionsLoad(), callback);
}
api_kanjiFind({text, callback}) {
kanjiFind({text, callback}) {
promiseCallback(this.kanjiFind(text), callback);
}
api_termsFind({text, callback}) {
termsFind({text, callback}) {
promiseCallback(this.termsFind(text), callback);
}
api_templateRender({template, data, callback}) {
templateRender({template, data, callback}) {
promiseCallback(this.templateRender(template, data), callback);
}
api_definitionAdd({definition, mode, callback}) {
definitionAdd({definition, mode, callback}) {
promiseCallback(this.definitionAdd(definition, mode), callback);
}
api_definitionsAddable({definitions, modes, callback}) {
definitionsAddable({definitions, modes, callback}) {
promiseCallback(this.definitionsAddable(definitions, modes), callback);
}
};
const {action, params} = request, method = handlers[`api_${action}`];
const {action, params} = request, method = handlers[action];
if (typeof(method) === 'function') {
params.callback = callback;
method.call(this, params);

View File

@ -18,12 +18,12 @@
</head>
<body>
<p>
<input type="checkbox" id="enable-search">
<input type="checkbox" id="enable-search" title="Toggle (Alt + Delete)">
</p>
<p>
<div class="btn-group" style="white-space: nowrap">
<button type="button" id="open-search" title="Search" class="btn btn-default btn-xs glyphicon glyphicon-search"></button>
<button type="button" id="open-options" title="Options" class="btn btn-default btn-xs glyphicon glyphicon-wrench"></button>
<button type="button" id="open-search" title="Search (Alt + Insert)" class="btn btn-default btn-xs glyphicon glyphicon-search"></button>
<button type="button" id="open-options" title="Options (Alt + End)" class="btn btn-default btn-xs glyphicon glyphicon-wrench"></button>
<button type="button" id="open-help" title="Help" class="btn btn-default btn-xs glyphicon glyphicon-question-sign"></button>
</div>
</p>

View File

@ -20,7 +20,7 @@
<form class="input-group">
<input type="text" class="form-control" placeholder="Search for..." id="query" autofocus>
<span class="input-group-btn">
<input type="submit" class="btn btn-default form-control" id="search" value="Search">
<input type="submit" class="btn btn-default form-control" id="search" value="Search" disabled>
</span>
</form>
</p>
@ -34,6 +34,7 @@
<script src="/mixed/lib/jquery-3.1.1.min.js"></script>
<script src="/bg/js/util.js"></script>
<script src="/mixed/js/util.js"></script>
<script src="/mixed/js/display.js"></script>
<script src="/mixed/lib/wanakana.min.js"></script>
<script src="/bg/js/display-window.js"></script>

View File

@ -6,28 +6,35 @@
<link rel="stylesheet" href="/mixed/lib/bootstrap-3.3.7-dist/css/bootstrap.min.css">
<link rel="stylesheet" href="/mixed/lib/bootstrap-3.3.7-dist/css/bootstrap-theme.min.css">
<link rel="stylesheet" href="/mixed/css/frame.css">
<style type="text/css">
.entry {
padding-left: 10px;
padding-right: 10px;
}
</style>
</head>
<body>
<div class="container-fluid">
<div id="spinner">
<img src="/mixed/img/spinner.gif">
</div>
<div id="spinner">
<img src="/mixed/img/spinner.gif">
</div>
<div id="content"></div>
<div id="content"></div>
<div id="orphan">
<div id="orphan">
<div class="container-fluid">
<h1>Yomichan Updated!</h1>
<p>
The Yomichan extension has been updated to a new version! In order to continue
viewing definitions on this page you must reload this tab or restart your browser.
</p>
</div>
<script src="/mixed/lib/jquery-3.1.1.min.js"></script>
<script src="/mixed/lib/wanakana.min.js"></script>
<script src="/fg/js/util.js"></script>
<script src="/mixed/js/display.js"></script>
<script src="/fg/js/display-frame.js"></script>
</div>
<script src="/mixed/lib/jquery-3.1.1.min.js"></script>
<script src="/mixed/lib/wanakana.min.js"></script>
<script src="/fg/js/util.js"></script>
<script src="/mixed/js/util.js"></script>
<script src="/mixed/js/display.js"></script>
<script src="/fg/js/display-frame.js"></script>
</body>
</html>

View File

@ -47,6 +47,10 @@ window.displayFrame = new class extends Display {
}
}
clearSearch() {
window.parent.postMessage('popupClose', '*');
}
showOrphaned() {
$('#content').hide();
$('#orphan').show();
@ -54,20 +58,20 @@ window.displayFrame = new class extends Display {
onMessage(e) {
const handlers = new class {
api_showTermDefs({definitions, options, context}) {
showTermDefs({definitions, options, context}) {
this.showTermDefs(definitions, options, context);
}
api_showKanjiDefs({definitions, options, context}) {
showKanjiDefs({definitions, options, context}) {
this.showKanjiDefs(definitions, options, context);
}
api_showOrphaned() {
showOrphaned() {
this.showOrphaned();
}
};
const {action, params} = e.originalEvent.data, method = handlers[`api_${action}`];
const {action, params} = e.originalEvent.data, method = handlers[action];
if (typeof(method) === 'function') {
method.call(this, params);
}

View File

@ -35,6 +35,7 @@ window.driver = new class {
window.addEventListener('mouseup', this.onMouseUp.bind(this));
window.addEventListener('mousemove', this.onMouseMove.bind(this));
window.addEventListener('resize', e => this.searchClear());
window.addEventListener('message', this.onFrameMessage.bind(this));
chrome.runtime.onMessage.addListener(this.onBgMessage.bind(this));
}).catch(this.handleError.bind(this));
}
@ -45,14 +46,14 @@ window.driver = new class {
}
popupTimerClear() {
if (this.popupTimer !== null) {
if (this.popupTimer) {
window.clearTimeout(this.popupTimer);
this.popupTimer = null;
}
}
onMouseOver(e) {
if (e.target === this.popup.container && this.popuptimer !== null) {
if (e.target === this.popup.container && this.popupTimer) {
this.popupTimerClear();
}
}
@ -101,14 +102,30 @@ window.driver = new class {
}
}
onBgMessage({action, params}, sender, callback) {
const handlers = new class {
api_optionsSet(options) {
this.options = options;
onFrameMessage(e) {
const handlers = {
popupClose: () => {
this.searchClear();
}
};
const method = handlers[`api_${action}`];
const handler = handlers[e.data];
if (handler) {
handler();
}
}
onBgMessage({action, params}, sender, callback) {
const handlers = new class {
optionsSet(options) {
this.options = options;
if (!this.options.enable) {
this.searchClear();
}
}
};
const method = handlers[action];
if (typeof(method) === 'function') {
method.call(this, params);
}
@ -122,11 +139,11 @@ window.driver = new class {
}
const textSource = docRangeFromPoint(point, this.options.scanning.imposter);
if (textSource === null || !textSource.containsPoint(point)) {
if (!textSource || !textSource.containsPoint(point)) {
return;
}
if (this.lastTextSource !== null && this.lastTextSource.equals(textSource)) {
if (this.lastTextSource && this.lastTextSource.equals(textSource)) {
return;
}
@ -200,7 +217,7 @@ window.driver = new class {
docImposterDestroy();
this.popup.hide();
if (this.options.scanning.selectText && this.lastTextSource !== null) {
if (this.options.scanning.selectText && this.lastTextSource) {
this.lastTextSource.deselect();
}

View File

@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Yomichan",
"version": "1.1.7",
"version": "1.1.8",
"description": "Japanese dictionary with Anki integration",
"icons": {"16": "mixed/img/icon16.png", "48": "mixed/img/icon48.png", "128": "mixed/img/icon128.png"},
@ -31,6 +31,26 @@
"<all_urls>",
"storage"
],
"commands": {
"toggle": {
"suggested_key": {
"default": "Alt+Delete"
},
"description": "Toggle text scanning"
},
"search": {
"suggested_key": {
"default": "Alt+Insert"
},
"description": "Open search window"
},
"options": {
"suggested_key": {
"default": "Alt+End"
},
"description": "Open options page"
}
},
"web_accessible_resources": ["fg/frame.html"],
"applications": {
"gecko": {

View File

@ -52,7 +52,8 @@ hr {
*/
.entry {
padding: 15px 0px 15px 0px;
padding-top: 10px;
padding-bottom: 10px;
}
.tag-default {

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 B

View File

@ -25,6 +25,9 @@ class Display {
this.audioCache = {};
this.responseCache = {};
this.sequence = 0;
this.index = 0;
$(document).keydown(this.onKeyDown.bind(this));
}
definitionAdd(definition, mode) {
@ -47,9 +50,17 @@ class Display {
throw 'override me';
}
clearSearch() {
throw 'override me';
}
showTermDefs(definitions, options, context) {
window.focus();
this.spinner.hide();
this.definitions = definitions;
this.options = options;
this.context = context;
const sequence = ++this.sequence;
const params = {
@ -68,43 +79,23 @@ class Display {
this.templateRender('terms.html', params).then(content => {
this.container.html(content);
this.entryScroll(context && context.index || 0);
let offset = 0;
if (context && context.hasOwnProperty('index') && context.index < definitions.length) {
const entry = $('.entry').eq(context.index);
offset = entry.offset().top;
}
window.scrollTo(0, offset);
$('.action-add-note').click(this.onActionAddNote.bind(this));
$('.action-play-audio').click(e => {
e.preventDefault();
const index = Display.entryIndexFind($(e.currentTarget));
this.audioPlay(this.definitions[index]);
});
$('.kanji-link').click(e => {
e.preventDefault();
const link = $(e.target);
context = context || {};
context.source = {
definitions,
index: Display.entryIndexFind(link)
};
this.kanjiFind(link.text()).then(kanjiDefs => {
this.showKanjiDefs(kanjiDefs, options, context);
}).catch(this.handleError.bind(this));
});
$('.action-add-note').click(this.onAddNote.bind(this));
$('.action-play-audio').click(this.onPlayAudio.bind(this));
$('.kanji-link').click(this.onKanjiLookup.bind(this));
return this.adderButtonsUpdate(['term-kanji', 'term-kana'], sequence);
}).catch(this.handleError.bind(this));
}
showKanjiDefs(definitions, options, context) {
window.focus();
this.spinner.hide();
this.definitions = definitions;
this.options = options;
this.context = context;
const sequence = ++this.sequence;
const params = {
@ -122,17 +113,10 @@ class Display {
this.templateRender('kanji.html', params).then(content => {
this.container.html(content);
window.scrollTo(0, 0);
this.entryScroll(context && context.index || 0);
$('.action-add-note').click(this.onActionAddNote.bind(this));
$('.source-term').click(e => {
e.preventDefault();
if (context && context.source) {
context.index = context.source.index;
this.showTermDefs(context.source.definitions, options, context);
}
});
$('.action-add-note').click(this.onAddNote.bind(this));
$('.source-term').click(this.onSourceTerm.bind(this));
return this.adderButtonsUpdate(['kanji'], sequence);
}).catch(this.handleError.bind(this));
@ -159,31 +143,186 @@ class Display {
});
}
onActionAddNote(e) {
e.preventDefault();
this.spinner.show();
entryScroll(index, smooth) {
index = Math.min(index, this.definitions.length - 1);
index = Math.max(index, 0);
const link = $(e.currentTarget);
const mode = link.data('mode');
const index = Display.entryIndexFind(link);
const definition = this.definitions[index];
$('.current').hide().eq(index).show();
let promise = Promise.resolve();
if (mode !== 'kanji') {
const filename = Display.audioBuildFilename(definition);
if (filename) {
promise = this.audioBuildUrl(definition).then(url => definition.audio = {url, filename}).catch(() => {});
}
const container = $('html,body').stop();
const entry = $('.entry').eq(index);
const target = index === 0 ? 0 : entry.offset().top;
if (smooth) {
container.animate({scrollTop: target}, 200);
} else {
container.scrollTop(target);
}
promise.then(() => {
return this.definitionAdd(definition, mode).then(success => {
if (success) {
Display.adderButtonFind(index, mode).addClass('disabled');
} else {
this.handleError('note could not be added');
this.index = index;
}
onSourceTerm(e) {
e.preventDefault();
this.sourceBack();
}
onKanjiLookup(e) {
e.preventDefault();
const link = $(e.target);
const context = {
source: {
definitions: this.definitions,
index: Display.entryIndexFind(link)
}
};
if (this.context) {
context.sentence = this.context.sentence;
context.url = this.context.url;
}
this.kanjiFind(link.text()).then(kanjiDefs => {
this.showKanjiDefs(kanjiDefs, this.options, context);
}).catch(this.handleError.bind(this));
}
onPlayAudio(e) {
e.preventDefault();
const index = Display.entryIndexFind($(e.currentTarget));
this.audioPlay(this.definitions[index]);
}
onAddNote(e) {
e.preventDefault();
const link = $(e.currentTarget);
const index = Display.entryIndexFind(link);
this.noteAdd(this.definitions[index], link.data('mode'));
}
onKeyDown(e) {
const noteTryAdd = mode => {
const button = Display.adderButtonFind(this.index, mode);
if (button.length !== 0 && !button.hasClass('disabled')) {
this.noteAdd(this.definitions[this.index], mode);
}
};
const handlers = {
27: /* escape */ () => {
this.clearSearch();
return true;
},
33: /* page up */ () => {
if (e.altKey) {
this.entryScroll(this.index - 3, true);
return true;
}
});
},
34: /* page down */ () => {
if (e.altKey) {
this.entryScroll(this.index + 3, true);
return true;
}
},
35: /* end */ () => {
if (e.altKey) {
this.entryScroll(this.definitions.length - 1, true);
return true;
}
},
36: /* home */ () => {
if (e.altKey) {
this.entryScroll(0, true);
return true;
}
},
38: /* up */ () => {
if (e.altKey) {
this.entryScroll(this.index - 1, true);
return true;
}
},
40: /* down */ () => {
if (e.altKey) {
this.entryScroll(this.index + 1, true);
return true;
}
},
66: /* b */ () => {
if (e.altKey) {
this.sourceBack();
return true;
}
},
69: /* e */ () => {
if (e.altKey) {
noteTryAdd('term-kanji');
return true;
}
},
75: /* k */ () => {
if (e.altKey) {
noteTryAdd('kanji');
return true;
}
},
82: /* r */ () => {
if (e.altKey) {
noteTryAdd('term-kana');
return true;
}
},
80: /* p */ () => {
if (e.altKey) {
if ($('.entry').eq(this.index).data('type') === 'term') {
this.audioPlay(this.definitions[this.index]);
}
return true;
}
}
};
const handler = handlers[e.keyCode];
if (handler && handler()) {
e.preventDefault();
}
}
sourceBack() {
if (this.context && this.context.source) {
const context = {
url: this.context.source.url,
sentence: this.context.source.sentence,
index: this.context.source.index
};
this.showTermDefs(this.context.source.definitions, this.options, context);
}
}
noteAdd(definition, mode) {
this.spinner.show();
return this.definitionAdd(definition, mode).then(success => {
if (success) {
const index = this.definitions.indexOf(definition);
Display.adderButtonFind(index, mode).addClass('disabled');
} else {
this.handleError('note could not be added');
}
}).catch(this.handleError.bind(this)).then(() => this.spinner.hide());
}
@ -194,7 +333,7 @@ class Display {
this.audioCache[key].pause();
}
this.audioBuildUrl(definition).then(url => {
audioBuildUrl(definition, this.responseCache).then(url => {
if (!url) {
url = '/mixed/mp3/button.mp3';
}
@ -217,79 +356,6 @@ class Display {
}).catch(this.handleError.bind(this)).then(() => this.spinner.hide());
}
audioBuildUrl(definition) {
return new Promise((resolve, reject) => {
const response = this.responseCache[definition.expression];
if (response) {
resolve(response);
return;
}
const data = {
post: 'dictionary_reference',
match_type: 'exact',
search_query: definition.expression
};
const params = [];
for (const key in data) {
params.push(`${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`);
}
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://www.japanesepod101.com/learningcenter/reference/dictionary_post');
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.addEventListener('error', () => reject('failed to scrape audio data'));
xhr.addEventListener('load', () => {
this.responseCache[definition.expression] = xhr.responseText;
resolve(xhr.responseText);
});
xhr.send(params.join('&'));
}).then(response => {
const dom = new DOMParser().parseFromString(response, 'text/html');
const entries = [];
for (const row of dom.getElementsByClassName('dc-result-row')) {
try {
const url = row.getElementsByClassName('ill-onebuttonplayer').item(0).getAttribute('data-url');
const expression = dom.getElementsByClassName('dc-vocab').item(0).innerText;
const reading = dom.getElementsByClassName('dc-vocab_kana').item(0).innerText;
if (url && expression && reading) {
entries.push({url, expression, reading});
}
} catch (e) {
// NOP
}
}
return entries;
}).then(entries => {
for (const entry of entries) {
if (!definition.reading || definition.reading === entry.reading) {
return entry.url;
}
}
});
}
static audioBuildFilename(definition) {
if (!definition.reading && !definition.expression) {
return;
}
let filename = 'yomichan';
if (definition.reading) {
filename += `_${definition.reading}`;
}
if (definition.expression) {
filename += `_${definition.expression}`;
}
return filename += '.mp3';
}
static entryIndexFind(element) {
return $('.entry').index(element.closest('.entry'));
}

104
ext/mixed/js/util.js Normal file
View File

@ -0,0 +1,104 @@
/*
* Copyright (C) 2017 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/>.
*/
/*
* Audio
*/
function audioBuildUrl(definition, cache={}) {
return new Promise((resolve, reject) => {
const response = cache[definition.expression];
if (response) {
resolve(response);
} else {
const data = {
post: 'dictionary_reference',
match_type: 'exact',
search_query: definition.expression
};
const params = [];
for (const key in data) {
params.push(`${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`);
}
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://www.japanesepod101.com/learningcenter/reference/dictionary_post');
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.addEventListener('error', () => reject('failed to scrape audio data'));
xhr.addEventListener('load', () => {
cache[definition.expression] = xhr.responseText;
resolve(xhr.responseText);
});
xhr.send(params.join('&'));
}
}).then(response => {
const dom = new DOMParser().parseFromString(response, 'text/html');
for (const row of dom.getElementsByClassName('dc-result-row')) {
try {
const url = row.getElementsByClassName('ill-onebuttonplayer').item(0).getAttribute('data-url');
const reading = row.getElementsByClassName('dc-vocab_kana').item(0).innerText;
if (url && reading && (!definition.reading || definition.reading === reading)) {
return url;
}
} catch (e) {
// NOP
}
}
});
}
function audioBuildFilename(definition) {
if (definition.reading && definition.expression) {
let filename = 'yomichan';
if (definition.reading) {
filename += `_${definition.reading}`;
}
if (definition.expression) {
filename += `_${definition.expression}`;
}
return filename += '.mp3';
}
}
function audioInject(definition, fields) {
const filename = audioBuildFilename(definition);
if (!filename) {
return Promise.resolve(true);
}
let usesAudio = false;
for (const name in fields) {
if (fields[name].includes('{audio}')) {
usesAudio = true;
break;
}
}
if (!usesAudio) {
return Promise.resolve(true);
}
return audioBuildUrl(definition).then(url => {
definition.audio = {url, filename};
return true;
}).catch(() => false);
}

View File

@ -1,11 +1,12 @@
{{#*inline "kanji"}}
<div class="entry">
<div class="entry" data-type="kanji">
<div class="actions">
<img src="/mixed/img/entry-current.png" class="current" title="Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)" alt>
{{#if addable}}
<a href="#" title="Add Kanji" class="action-add-note pending disabled" data-mode="kanji"><img src="/mixed/img/add-kanji.png" alt></a>
<a href="#" class="action-add-note pending disabled" data-mode="kanji"><img src="/mixed/img/add-kanji.png" title="Add Kanji (Alt + K)" alt></a>
{{/if}}
{{#if source}}
<a href="#" title="Source term" class="source-term"><img src="/mixed/img/source-term.png" alt></a>
<a href="#" class="source-term"><img src="/mixed/img/source-term.png" title="Source term (Alt + B)" alt></a>
{{/if}}
</div>

View File

@ -18,14 +18,15 @@
{{/inline}}
{{#*inline "term"}}
<div class="entry">
<div class="entry" data-type="term">
<div class="actions">
<img src="/mixed/img/entry-current.png" class="current" title="Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)" alt>
{{#if addable}}
<a href="#" title="Add expression" class="action-add-note pending disabled" data-mode="term-kanji"><img src="/mixed/img/add-term-kanji.png" alt></a>
<a href="#" title="Add reading" class="action-add-note pending disabled" data-mode="term-kana"><img src="/mixed/img/add-term-kana.png" alt></a>
<a href="#" class="action-add-note pending disabled" data-mode="term-kanji"><img src="/mixed/img/add-term-kanji.png" title="Add expression (Alt + E)" alt></a>
<a href="#" class="action-add-note pending disabled" data-mode="term-kana"><img src="/mixed/img/add-term-kana.png" title="Add reading (Alt + R)" alt></a>
{{/if}}
{{#if playback}}
<a href="#" title="Play audio" class="action-play-audio"><img src="/mixed/img/play-audio.png" alt></a>
<a href="#" class="action-play-audio"><img src="/mixed/img/play-audio.png" title="Play audio (Alt + P)" alt></a>
{{/if}}
</div>