summaryrefslogtreecommitdiff
path: root/ext/mixed
diff options
context:
space:
mode:
Diffstat (limited to 'ext/mixed')
-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
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
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);
+}