summaryrefslogtreecommitdiff
path: root/ext/mixed/js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/mixed/js')
-rw-r--r--ext/mixed/js/display.js400
-rw-r--r--ext/mixed/js/japanese.js40
-rw-r--r--ext/mixed/js/util.js150
3 files changed, 263 insertions, 327 deletions
diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js
index 7982c69f..47efd195 100644
--- a/ext/mixed/js/display.js
+++ b/ext/mixed/js/display.js
@@ -27,190 +27,67 @@ class Display {
this.sequence = 0;
this.index = 0;
this.audioCache = {};
- this.responseCache = {};
$(document).keydown(this.onKeyDown.bind(this));
}
- definitionAdd(definition, mode) {
+ onError(error) {
throw 'override me';
}
- definitionsAddable(definitions, modes) {
+ onSearchClear() {
throw 'override me';
}
- templateRender(template, data) {
- throw 'override me';
- }
-
- kanjiFind(character) {
- throw 'override me';
- }
-
- handleError(error) {
- 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 = {
- definitions,
- addable: options.anki.enable,
- grouped: options.general.groupResults,
- playback: options.general.audioSource !== 'disabled',
- debug: options.general.debugInfo
- };
-
- if (context) {
- for (const definition of definitions) {
- if (context.sentence) {
- definition.cloze = clozeBuild(context.sentence, definition.source);
- }
-
- definition.url = context.url;
- }
- }
-
- this.templateRender('terms.html', params).then(content => {
- this.container.html(content);
- this.entryScroll(context && context.index || 0);
-
- $('.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 = {
- definitions,
- source: context && context.source,
- addable: options.anki.enable,
- debug: options.general.debugInfo
- };
-
- if (context) {
- for (const definition of definitions) {
- if (context.sentence) {
- definition.cloze = clozeBuild(context.sentence);
- }
-
- definition.url = context.url;
- }
- }
-
- this.templateRender('kanji.html', params).then(content => {
- this.container.html(content);
- this.entryScroll(context && context.index || 0);
-
- $('.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));
+ onSourceTermView(e) {
+ e.preventDefault();
+ this.sourceTermView();
}
- adderButtonsUpdate(modes, sequence) {
- return this.definitionsAddable(this.definitions, modes).then(states => {
- if (states === null || sequence !== this.sequence) {
- return;
- }
-
- states.forEach((state, index) => {
- for (const mode in state) {
- const button = Display.adderButtonFind(index, mode);
- if (state[mode]) {
- button.removeClass('disabled');
- } else {
- button.addClass('disabled');
- }
+ async onKanjiLookup(e) {
+ try {
+ e.preventDefault();
- button.removeClass('pending');
+ const link = $(e.target);
+ const context = {
+ source: {
+ definitions: this.definitions,
+ index: Display.entryIndexFind(link)
}
- });
- });
- }
-
- 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();
+ };
- 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;
}
- };
- if (this.context) {
- context.sentence = this.context.sentence;
- context.url = this.context.url;
+ const kanjiDefs = await apiKanjiFind(link.text());
+ this.kanjiShow(kanjiDefs, this.options, context);
+ } catch (e) {
+ this.onError(e);
}
-
- this.kanjiFind(link.text()).then(kanjiDefs => {
- this.showKanjiDefs(kanjiDefs, this.options, context);
- }).catch(this.handleError.bind(this));
}
- onPlayAudio(e) {
+ onAudioPlay(e) {
e.preventDefault();
const index = Display.entryIndexFind($(e.currentTarget));
this.audioPlay(this.definitions[index]);
}
- onAddNote(e) {
+ onNoteAdd(e) {
e.preventDefault();
const link = $(e.currentTarget);
const index = Display.entryIndexFind(link);
this.noteAdd(this.definitions[index], link.data('mode'));
}
+ onNoteView(e) {
+ e.preventDefault();
+ const link = $(e.currentTarget);
+ const index = Display.entryIndexFind(link);
+ apiNoteView(link.data('noteId'));
+ }
+
onKeyDown(e) {
const noteTryAdd = mode => {
const button = Display.adderButtonFind(this.index, mode);
@@ -219,57 +96,64 @@ class Display {
}
};
+ const noteTryView = mode => {
+ const button = Display.viewerButtonFind(this.index);
+ if (button.length !== 0 && !button.hasClass('disabled')) {
+ apiNoteView(button.data('noteId'));
+ }
+ };
+
const handlers = {
27: /* escape */ () => {
- this.clearSearch();
+ this.onSearchClear();
return true;
},
33: /* page up */ () => {
if (e.altKey) {
- this.entryScroll(this.index - 3, true);
+ this.entryScrollIntoView(this.index - 3, true);
return true;
}
},
34: /* page down */ () => {
if (e.altKey) {
- this.entryScroll(this.index + 3, true);
+ this.entryScrollIntoView(this.index + 3, true);
return true;
}
},
35: /* end */ () => {
if (e.altKey) {
- this.entryScroll(this.definitions.length - 1, true);
+ this.entryScrollIntoView(this.definitions.length - 1, true);
return true;
}
},
36: /* home */ () => {
if (e.altKey) {
- this.entryScroll(0, true);
+ this.entryScrollIntoView(0, true);
return true;
}
},
38: /* up */ () => {
if (e.altKey) {
- this.entryScroll(this.index - 1, true);
+ this.entryScrollIntoView(this.index - 1, true);
return true;
}
},
40: /* down */ () => {
if (e.altKey) {
- this.entryScroll(this.index + 1, true);
+ this.entryScrollIntoView(this.index + 1, true);
return true;
}
},
66: /* b */ () => {
if (e.altKey) {
- this.sourceBack();
+ this.sourceTermView();
return true;
}
},
@@ -303,6 +187,12 @@ class Display {
return true;
}
+ },
+
+ 86: /* v */ () => {
+ if (e.altKey) {
+ noteTryView();
+ }
}
};
@@ -312,7 +202,133 @@ class Display {
}
}
- sourceBack() {
+ async termsShow(definitions, options, context) {
+ try {
+ window.focus();
+
+ this.definitions = definitions;
+ this.options = options;
+ this.context = context;
+
+ const sequence = ++this.sequence;
+ const params = {
+ definitions,
+ addable: options.anki.enable,
+ grouped: options.general.groupResults,
+ playback: options.general.audioSource !== 'disabled',
+ debug: options.general.debugInfo
+ };
+
+ if (context) {
+ for (const definition of definitions) {
+ if (context.sentence) {
+ definition.cloze = Display.clozeBuild(context.sentence, definition.source);
+ }
+
+ definition.url = context.url;
+ }
+ }
+
+ const content = await apiTemplateRender('terms.html', params);
+ this.container.html(content);
+ this.entryScrollIntoView(context && context.index || 0);
+
+ $('.action-add-note').click(this.onNoteAdd.bind(this));
+ $('.action-view-note').click(this.onNoteView.bind(this));
+ $('.action-play-audio').click(this.onAudioPlay.bind(this));
+ $('.kanji-link').click(this.onKanjiLookup.bind(this));
+
+ await this.adderButtonUpdate(['term-kanji', 'term-kana'], sequence);
+ } catch (e) {
+ this.onError(e);
+ }
+ }
+
+ async kanjiShow(definitions, options, context) {
+ try {
+ window.focus();
+
+ this.definitions = definitions;
+ this.options = options;
+ this.context = context;
+
+ const sequence = ++this.sequence;
+ const params = {
+ definitions,
+ source: context && context.source,
+ addable: options.anki.enable,
+ debug: options.general.debugInfo
+ };
+
+ if (context) {
+ for (const definition of definitions) {
+ if (context.sentence) {
+ definition.cloze = Display.clozeBuild(context.sentence);
+ }
+
+ definition.url = context.url;
+ }
+ }
+
+ const content = await apiTemplateRender('kanji.html', params);
+ this.container.html(content);
+ this.entryScrollIntoView(context && context.index || 0);
+
+ $('.action-add-note').click(this.onNoteAdd.bind(this));
+ $('.action-view-note').click(this.onNoteView.bind(this));
+ $('.source-term').click(this.onSourceTermView.bind(this));
+
+ await this.adderButtonUpdate(['kanji'], sequence);
+ } catch (e) {
+ this.onError(e);
+ }
+ }
+
+ async adderButtonUpdate(modes, sequence) {
+ try {
+ const states = await apiDefinitionsAddable(this.definitions, modes);
+ if (!states || sequence !== this.sequence) {
+ return;
+ }
+
+ for (let i = 0; i < states.length; ++i) {
+ const state = states[i];
+ for (const mode in state) {
+ const button = Display.adderButtonFind(i, mode);
+ if (state[mode]) {
+ button.removeClass('disabled');
+ } else {
+ button.addClass('disabled');
+ }
+
+ button.removeClass('pending');
+ }
+ }
+ } catch (e) {
+ this.onError(e);
+ }
+ }
+
+ entryScrollIntoView(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;
+ }
+
+ sourceTermView() {
if (this.context && this.context.source) {
const context = {
url: this.context.source.url,
@@ -320,34 +336,42 @@ class Display {
index: this.context.source.index
};
- this.showTermDefs(this.context.source.definitions, this.options, context);
+ this.termsShow(this.context.source.definitions, this.options, context);
}
}
- noteAdd(definition, mode) {
- this.spinner.show();
- return this.definitionAdd(definition, mode).then(success => {
- if (success) {
+ async noteAdd(definition, mode) {
+ try {
+ this.spinner.show();
+
+ const noteId = await apiDefinitionAdd(definition, mode);
+ if (noteId) {
const index = this.definitions.indexOf(definition);
Display.adderButtonFind(index, mode).addClass('disabled');
+ Display.viewerButtonFind(index).removeClass('pending disabled').data('noteId', noteId);
} else {
- this.handleError('note could not be added');
+ throw 'note could note be added';
}
- }).catch(this.handleError.bind(this)).then(() => this.spinner.hide());
+ } catch (e) {
+ this.onError(e);
+ } finally {
+ this.spinner.hide();
+ }
}
- audioPlay(definition) {
- this.spinner.show();
+ async audioPlay(definition) {
+ try {
+ this.spinner.show();
- for (const key in this.audioCache) {
- this.audioCache[key].pause();
- }
-
- audioBuildUrl(definition, this.options.general.audioSource, this.responseCache).then(url => {
+ let url = await apiAudioGetUrl(definition, this.options.general.audioSource);
if (!url) {
url = '/mixed/mp3/button.mp3';
}
+ for (const key in this.audioCache) {
+ this.audioCache[key].pause();
+ }
+
let audio = this.audioCache[url];
if (audio) {
audio.currentTime = 0;
@@ -365,7 +389,25 @@ class Display {
audio.play();
};
}
- }).catch(this.handleError.bind(this)).then(() => this.spinner.hide());
+ } catch (e) {
+ this.onError(e);
+ } finally {
+ this.spinner.hide();
+ }
+ }
+
+ static clozeBuild(sentence, source) {
+ const result = {
+ sentence: sentence.text.trim()
+ };
+
+ if (source) {
+ result.prefix = sentence.text.substring(0, sentence.offset).trim();
+ result.body = source.trim();
+ result.suffix = sentence.text.substring(sentence.offset + source.length).trim();
+ }
+
+ return result;
}
static entryIndexFind(element) {
@@ -375,4 +417,8 @@ class Display {
static adderButtonFind(index, mode) {
return $('.entry').eq(index).find(`.action-add-note[data-mode="${mode}"]`);
}
+
+ static viewerButtonFind(index) {
+ return $('.entry').eq(index).find('.action-view-note');
+ }
}
diff --git a/ext/mixed/js/japanese.js b/ext/mixed/js/japanese.js
new file mode 100644
index 00000000..c11e955b
--- /dev/null
+++ b/ext/mixed/js/japanese.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 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/>.
+ */
+
+
+function jpIsKanji(c) {
+ const code = c.charCodeAt(0);
+ return code >= 0x4e00 && code < 0x9fb0 || code >= 0x3400 && code < 0x4dc0;
+}
+
+function jpIsKana(c) {
+ return wanakana.isKana(c);
+}
+
+function jpKatakanaToHiragana(text) {
+ let result = '';
+ for (const c of text) {
+ if (wanakana.isKatakana(c)) {
+ result += wanakana.toHiragana(c);
+ } else {
+ result += c;
+ }
+ }
+
+ return result;
+}
diff --git a/ext/mixed/js/util.js b/ext/mixed/js/util.js
deleted file mode 100644
index 5cf62000..00000000
--- a/ext/mixed/js/util.js
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * 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/>.
- */
-
-
-/*
- * Cloze
- */
-
-function clozeBuild(sentence, source) {
- const result = {
- sentence: sentence.text.trim()
- };
-
- if (source) {
- result.prefix = sentence.text.substring(0, sentence.offset).trim();
- result.body = source.trim();
- result.suffix = sentence.text.substring(sentence.offset + source.length).trim();
- }
-
- return result;
-}
-
-
-/*
- * Audio
- */
-
-function audioBuildUrl(definition, mode, cache={}) {
- if (mode === 'jpod101') {
- let kana = definition.reading;
- let kanji = definition.expression;
-
- if (!kana && wanakana.isHiragana(kanji)) {
- kana = kanji;
- kanji = null;
- }
-
- const params = [];
- if (kanji) {
- params.push(`kanji=${encodeURIComponent(kanji)}`);
- }
- if (kana) {
- params.push(`kana=${encodeURIComponent(kana)}`);
- }
-
- const url = `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?${params.join('&')}`;
- return Promise.resolve(url);
- } else if (mode === 'jpod101-alternate') {
- 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
- }
- }
- });
- } else {
- return Promise.reject('unsupported audio source');
- }
-}
-
-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, mode) {
- if (mode === 'disabled') {
- return Promise.resolve(true);
- }
-
- 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, mode).then(url => {
- definition.audio = {url, filename};
- return true;
- }).catch(() => false);
-}