aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ext/bg/background.html1
-rw-r--r--ext/bg/guide.html13
-rw-r--r--ext/bg/js/deinflector.js2
-rw-r--r--ext/bg/js/display-window.js13
-rw-r--r--ext/bg/js/popup.js11
-rw-r--r--ext/bg/js/templates.js12
-rw-r--r--ext/bg/js/util.js9
-rw-r--r--ext/bg/js/yomichan.js69
-rw-r--r--ext/bg/popup.html6
-rw-r--r--ext/bg/search.html3
-rw-r--r--ext/fg/frame.html31
-rw-r--r--ext/fg/js/display-frame.js12
-rw-r--r--ext/fg/js/driver.js31
-rw-r--r--ext/manifest.json22
-rw-r--r--ext/mixed/css/frame.css3
-rw-r--r--ext/mixed/img/entry-current.pngbin0 -> 743 bytes
-rw-r--r--ext/mixed/js/display.js324
-rw-r--r--ext/mixed/js/util.js104
-rw-r--r--tmpl/kanji.html7
-rw-r--r--tmpl/terms.html9
20 files changed, 479 insertions, 203 deletions
diff --git a/ext/bg/background.html b/ext/bg/background.html
index 8fb41a38..b5ae147b 100644
--- a/ext/bg/background.html
+++ b/ext/bg/background.html
@@ -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>
diff --git a/ext/bg/guide.html b/ext/bg/guide.html
index 130d6d68..3b3dcab7 100644
--- a/ext/bg/guide.html
+++ b/ext/bg/guide.html
@@ -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>
diff --git a/ext/bg/js/deinflector.js b/ext/bg/js/deinflector.js
index 6e480068..6484f953 100644
--- a/ext/bg/js/deinflector.js
+++ b/ext/bg/js/deinflector.js
@@ -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] : []
}];
}
diff --git a/ext/bg/js/display-window.js b/ext/bg/js/display-window.js
index ad193c5b..ae97cd36 100644
--- a/ext/bg/js/display-window.js
+++ b/ext/bg/js/display-window.js
@@ -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();
diff --git a/ext/bg/js/popup.js b/ext/bg/js/popup.js
index 9f2567df..8577dd96 100644
--- a/ext/bg/js/popup.js
+++ b/ext/bg/js/popup.js
@@ -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'));
});
});
diff --git a/ext/bg/js/templates.js b/ext/bg/js/templates.js
index 91518e84..0346e4c7 100644
--- a/ext/bg/js/templates.js
+++ b/ext/bg/js/templates.js
@@ -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>";
diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js
index 59eb9269..6999cae3 100644
--- a/ext/bg/js/util.js
+++ b/ext/bg/js/util.js
@@ -34,6 +34,15 @@ function promiseCallback(promise, callback) {
/*
+ * Commands
+ */
+
+function commandExec(command) {
+ instYomi().onCommand(command);
+}
+
+
+/*
* Instance
*/
diff --git a/ext/bg/js/yomichan.js b/ext/bg/js/yomichan.js
index a61be8be..39105c54 100644
--- a/ext/bg/js/yomichan.js
+++ b/ext/bg/js/yomichan.js
@@ -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);
diff --git a/ext/bg/popup.html b/ext/bg/popup.html
index 22fc0d40..e223e241 100644
--- a/ext/bg/popup.html
+++ b/ext/bg/popup.html
@@ -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>
diff --git a/ext/bg/search.html b/ext/bg/search.html
index e9c25e15..b8ab21b0 100644
--- a/ext/bg/search.html
+++ b/ext/bg/search.html
@@ -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>
diff --git a/ext/fg/frame.html b/ext/fg/frame.html
index ec0acf64..35bc0284 100644
--- a/ext/fg/frame.html
+++ b/ext/fg/frame.html
@@ -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>
diff --git a/ext/fg/js/display-frame.js b/ext/fg/js/display-frame.js
index 8f15b1bc..41c2fb53 100644
--- a/ext/fg/js/display-frame.js
+++ b/ext/fg/js/display-frame.js
@@ -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);
}
diff --git a/ext/fg/js/driver.js b/ext/fg/js/driver.js
index fbe89ab8..036dc2d8 100644
--- a/ext/fg/js/driver.js
+++ b/ext/fg/js/driver.js
@@ -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 {
}
}
+ onFrameMessage(e) {
+ const handlers = {
+ popupClose: () => {
+ this.searchClear();
+ }
+ };
+
+ const handler = handlers[e.data];
+ if (handler) {
+ handler();
+ }
+ }
+
onBgMessage({action, params}, sender, callback) {
const handlers = new class {
- api_optionsSet(options) {
+ optionsSet(options) {
this.options = options;
+ if (!this.options.enable) {
+ this.searchClear();
+ }
}
};
- const method = handlers[`api_${action}`];
+ 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();
}
diff --git a/ext/manifest.json b/ext/manifest.json
index 9c311b2f..ae7cf8e3 100644
--- a/ext/manifest.json
+++ b/ext/manifest.json
@@ -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": {
diff --git a/ext/mixed/css/frame.css b/ext/mixed/css/frame.css
index af689cbe..a425aca8 100644
--- a/ext/mixed/css/frame.css
+++ b/ext/mixed/css/frame.css
@@ -52,7 +52,8 @@ hr {
*/
.entry {
- padding: 15px 0px 15px 0px;
+ padding-top: 10px;
+ padding-bottom: 10px;
}
.tag-default {
diff --git a/ext/mixed/img/entry-current.png b/ext/mixed/img/entry-current.png
new file mode 100644
index 00000000..bab7cc9b
--- /dev/null
+++ b/ext/mixed/img/entry-current.png
Binary files differ
diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js
index 63620dc6..db14a43c 100644
--- a/ext/mixed/js/display.js
+++ b/ext/mixed/js/display.js
@@ -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) {
+ entryScroll(index, smooth) {
+ index = Math.min(index, this.definitions.length - 1);
+ index = Math.max(index, 0);
+
+ $('.current').hide().eq(index).show();
+
+ 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);
+ }
+
+ this.index = index;
+ }
+
+ onSourceTerm(e) {
+ e.preventDefault();
+ this.sourceBack();
+ }
+
+ onKanjiLookup(e) {
e.preventDefault();
- this.spinner.show();
+ 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 mode = link.data('mode');
const index = Display.entryIndexFind(link);
- const definition = this.definitions[index];
+ this.noteAdd(this.definitions[index], link.data('mode'));
+ }
- 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(() => {});
+ 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;
+ },
- 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');
+ 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'));
}
diff --git a/ext/mixed/js/util.js b/ext/mixed/js/util.js
new file mode 100644
index 00000000..4ce60e4f
--- /dev/null
+++ b/ext/mixed/js/util.js
@@ -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);
+}
diff --git a/tmpl/kanji.html b/tmpl/kanji.html
index 4bb524cf..acd79036 100644
--- a/tmpl/kanji.html
+++ b/tmpl/kanji.html
@@ -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>
diff --git a/tmpl/terms.html b/tmpl/terms.html
index fa32cea1..dc50efe2 100644
--- a/tmpl/terms.html
+++ b/tmpl/terms.html
@@ -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>