diff options
Diffstat (limited to 'ext/mixed')
-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 |
4 files changed, 301 insertions, 130 deletions
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); +} |