diff options
-rw-r--r-- | ext/bg/background.html | 1 | ||||
-rw-r--r-- | ext/bg/guide.html | 13 | ||||
-rw-r--r-- | ext/bg/js/deinflector.js | 2 | ||||
-rw-r--r-- | ext/bg/js/display-window.js | 13 | ||||
-rw-r--r-- | ext/bg/js/popup.js | 11 | ||||
-rw-r--r-- | ext/bg/js/templates.js | 12 | ||||
-rw-r--r-- | ext/bg/js/util.js | 9 | ||||
-rw-r--r-- | ext/bg/js/yomichan.js | 69 | ||||
-rw-r--r-- | ext/bg/popup.html | 6 | ||||
-rw-r--r-- | ext/bg/search.html | 3 | ||||
-rw-r--r-- | ext/fg/frame.html | 31 | ||||
-rw-r--r-- | ext/fg/js/display-frame.js | 12 | ||||
-rw-r--r-- | ext/fg/js/driver.js | 31 | ||||
-rw-r--r-- | ext/manifest.json | 22 | ||||
-rw-r--r-- | ext/mixed/css/frame.css | 3 | ||||
-rw-r--r-- | ext/mixed/img/entry-current.png | bin | 0 -> 743 bytes | |||
-rw-r--r-- | ext/mixed/js/display.js | 324 | ||||
-rw-r--r-- | ext/mixed/js/util.js | 104 | ||||
-rw-r--r-- | tmpl/kanji.html | 7 | ||||
-rw-r--r-- | tmpl/terms.html | 9 |
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 Binary files differnew file mode 100644 index 00000000..bab7cc9b --- /dev/null +++ b/ext/mixed/img/entry-current.png 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> |