summaryrefslogtreecommitdiff
path: root/ext/mixed/js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/mixed/js')
-rw-r--r--ext/mixed/js/audio.js60
-rw-r--r--ext/mixed/js/display.js107
-rw-r--r--ext/mixed/js/extension.js35
3 files changed, 161 insertions, 41 deletions
diff --git a/ext/mixed/js/audio.js b/ext/mixed/js/audio.js
new file mode 100644
index 00000000..b905140c
--- /dev/null
+++ b/ext/mixed/js/audio.js
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2019 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 audioGetFromUrl(url) {
+ return new Promise((resolve, reject) => {
+ const audio = new Audio(url);
+ audio.addEventListener('loadeddata', () => {
+ if (audio.duration === 5.694694 || audio.duration === 5.720718) {
+ // Hardcoded values for invalid audio
+ reject(new Error('Could not retrieve audio'));
+ } else {
+ resolve(audio);
+ }
+ });
+ audio.addEventListener('error', () => reject(audio.error));
+ });
+}
+
+async function audioGetFromSources(expression, sources, optionsContext, createAudioObject, cache=null) {
+ const key = `${expression.expression}:${expression.reading}`;
+ if (cache !== null && cache.hasOwnProperty(expression)) {
+ return cache[key];
+ }
+
+ for (let i = 0, ii = sources.length; i < ii; ++i) {
+ const source = sources[i];
+ const url = await apiAudioGetUrl(expression, source, optionsContext);
+ if (url === null) {
+ continue;
+ }
+
+ try {
+ const audio = createAudioObject ? await audioGetFromUrl(url) : null;
+ const result = {audio, url, source};
+ if (cache !== null) {
+ cache[key] = result;
+ }
+ return result;
+ } catch (e) {
+ // NOP
+ }
+ }
+ return {audio: null, source: null};
+}
diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js
index dc64dbea..22181301 100644
--- a/ext/mixed/js/display.js
+++ b/ext/mixed/js/display.js
@@ -26,6 +26,8 @@ class Display {
this.context = null;
this.sequence = 0;
this.index = 0;
+ this.audioPlaying = null;
+ this.audioFallback = null;
this.audioCache = {};
this.optionsContext = {};
this.eventListeners = [];
@@ -39,11 +41,11 @@ class Display {
}
onError(error) {
- throw 'Override me';
+ throw new Error('Override me');
}
onSearchClear() {
- throw 'Override me';
+ throw new Error('Override me');
}
onSourceTermView(e) {
@@ -133,7 +135,7 @@ class Display {
const entry = link.closest('.entry');
const definitionIndex = this.entryIndexFind(entry);
const expressionIndex = Display.indexOf(entry.querySelectorAll('.expression .action-play-audio'), link);
- this.audioPlay(this.definitions[definitionIndex], expressionIndex);
+ this.audioPlay(this.definitions[definitionIndex], expressionIndex, definitionIndex);
}
onNoteAdd(e) {
@@ -189,7 +191,7 @@ class Display {
addable: options.anki.enable,
grouped: options.general.resultOutputMode === 'group',
merged: options.general.resultOutputMode === 'merge',
- playback: options.general.audioSource !== 'disabled',
+ playback: options.audio.enabled,
compactGlossaries: options.general.compactGlossaries,
debug: options.general.debugInfo
};
@@ -209,7 +211,7 @@ class Display {
const {index, scroll} = context || {};
this.entryScrollIntoView(index || 0, scroll);
- if (this.options.general.autoPlayAudio && this.options.general.audioSource !== 'disabled') {
+ if (this.options.audio.enabled && this.options.audio.autoPlay) {
this.autoPlayAudio();
}
@@ -274,7 +276,7 @@ class Display {
}
autoPlayAudio() {
- this.audioPlay(this.definitions[0], this.firstExpressionIndex);
+ this.audioPlay(this.definitions[0], this.firstExpressionIndex, 0);
}
async adderButtonUpdate(modes, sequence) {
@@ -286,15 +288,23 @@ class Display {
for (let i = 0; i < states.length; ++i) {
const state = states[i];
+ let noteId = null;
for (const mode in state) {
const button = this.adderButtonFind(i, mode);
if (button === null) {
continue;
}
- button.classList.toggle('disabled', !state[mode]);
+ const info = state[mode];
+ if (!info.canAdd && noteId === null && info.noteId) {
+ noteId = info.noteId;
+ }
+ button.classList.toggle('disabled', !info.canAdd);
button.classList.remove('pending');
}
+ if (noteId !== null) {
+ this.viewerButtonShow(i, noteId);
+ }
}
} catch (e) {
this.onError(e);
@@ -380,13 +390,9 @@ class Display {
if (adderButton !== null) {
adderButton.classList.add('disabled');
}
- const viewerButton = this.viewerButtonFind(index);
- if (viewerButton !== null) {
- viewerButton.classList.remove('pending', 'disabled');
- viewerButton.dataset.noteId = noteId;
- }
+ this.viewerButtonShow(index, noteId);
} else {
- throw 'Note could note be added';
+ throw new Error('Note could not be added');
}
} catch (e) {
this.onError(e);
@@ -395,37 +401,44 @@ class Display {
}
}
- async audioPlay(definition, expressionIndex) {
+ async audioPlay(definition, expressionIndex, entryIndex) {
try {
this.setSpinnerVisible(true);
const expression = expressionIndex === -1 ? definition : definition.expressions[expressionIndex];
- let url = await apiAudioGetUrl(expression, this.options.general.audioSource);
- if (!url) {
- url = '/mixed/mp3/button.mp3';
- }
- for (const key in this.audioCache) {
- this.audioCache[key].pause();
+ if (this.audioPlaying !== null) {
+ this.audioPlaying.pause();
+ this.audioPlaying = null;
}
- let audio = this.audioCache[url];
- if (audio) {
- audio.currentTime = 0;
- audio.volume = this.options.general.audioVolume / 100.0;
- audio.play();
+ const sources = this.options.audio.sources;
+ let {audio, source} = await audioGetFromSources(expression, sources, this.optionsContext, true, this.audioCache);
+ let info;
+ if (audio === null) {
+ if (this.audioFallback === null) {
+ this.audioFallback = new Audio('/mixed/mp3/button.mp3');
+ }
+ audio = this.audioFallback;
+ info = 'Could not find audio';
} else {
- audio = new Audio(url);
- audio.onloadeddata = () => {
- if (audio.duration === 5.694694 || audio.duration === 5.720718) {
- audio = new Audio('/mixed/mp3/button.mp3');
- }
+ info = `From source ${1 + sources.indexOf(source)}: ${source}`;
+ }
- this.audioCache[url] = audio;
- audio.volume = this.options.general.audioVolume / 100.0;
- audio.play();
- };
+ const button = this.audioButtonFindImage(entryIndex);
+ if (button !== null) {
+ let titleDefault = button.dataset.titleDefault;
+ if (!titleDefault) {
+ titleDefault = button.title || "";
+ button.dataset.titleDefault = titleDefault;
+ }
+ button.title = `${titleDefault}\n${info}`;
}
+
+ this.audioPlaying = audio;
+ audio.currentTime = 0;
+ audio.volume = this.options.audio.volume / 100.0;
+ audio.play();
} catch (e) {
this.onError(e);
} finally {
@@ -445,7 +458,7 @@ class Display {
async getScreenshot() {
try {
- await this.setPopupVisible(false);
+ await this.setPopupVisibleOverride(false);
await Display.delay(1); // Wait for popup to be hidden.
const {format, quality} = this.options.anki.screenshot;
@@ -454,7 +467,7 @@ class Display {
return {dataUrl, format};
} finally {
- await this.setPopupVisible(true);
+ await this.setPopupVisibleOverride(null);
}
}
@@ -462,8 +475,8 @@ class Display {
return this.options.general.resultOutputMode === 'merge' ? 0 : -1;
}
- setPopupVisible(visible) {
- return apiForward('popupSetVisible', {visible});
+ setPopupVisibleOverride(visible) {
+ return apiForward('popupSetVisibleOverride', {visible});
}
setSpinnerVisible(visible) {
@@ -504,6 +517,20 @@ class Display {
return entry !== null ? entry.querySelector('.action-view-note') : null;
}
+ viewerButtonShow(index, noteId) {
+ const viewerButton = this.viewerButtonFind(index);
+ if (viewerButton === null) {
+ return;
+ }
+ viewerButton.classList.remove('pending', 'disabled');
+ viewerButton.dataset.noteId = noteId;
+ }
+
+ audioButtonFindImage(index) {
+ const entry = this.getEntry(index);
+ return entry !== null ? entry.querySelector('.action-play-audio>img') : null;
+ }
+
static delay(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
@@ -539,7 +566,7 @@ class Display {
static getKeyFromEvent(event) {
const key = event.key;
- return key.length === 1 ? key.toUpperCase() : key;
+ return (typeof key === 'string' ? (key.length === 1 ? key.toUpperCase() : key) : '');
}
}
@@ -633,7 +660,7 @@ Display.onKeyDownHandlers = {
if (e.altKey) {
const entry = self.getEntry(self.index);
if (entry !== null && entry.dataset.type === 'term') {
- self.audioPlay(self.definitions[self.index], self.firstExpressionIndex);
+ self.audioPlay(self.definitions[self.index], self.firstExpressionIndex, self.index);
}
return true;
}
diff --git a/ext/mixed/js/extension.js b/ext/mixed/js/extension.js
index 5c803132..861e52a5 100644
--- a/ext/mixed/js/extension.js
+++ b/ext/mixed/js/extension.js
@@ -34,7 +34,7 @@ function toIterable(value) {
}
}
- throw 'Could not convert to iterable';
+ throw new Error('Could not convert to iterable');
}
function extensionHasChrome() {
@@ -53,6 +53,39 @@ function extensionHasBrowser() {
}
}
+function errorToJson(error) {
+ return {
+ name: error.name,
+ message: error.message,
+ stack: error.stack
+ };
+}
+
+function jsonToError(jsonError) {
+ const error = new Error(jsonError.message);
+ error.name = jsonError.name;
+ error.stack = jsonError.stack;
+ return error;
+}
+
+function logError(error, alert) {
+ const manifest = chrome.runtime.getManifest();
+ let errorMessage = `${manifest.name} v${manifest.version} has encountered an error.\n`;
+ errorMessage += `Originating URL: ${window.location.href}\n`;
+
+ const errorString = `${error.toString ? error.toString() : error}`;
+ const stack = `${error.stack}`.trimRight();
+ errorMessage += (!stack.startsWith(errorString) ? `${errorString}\n${stack}` : `${stack}`);
+
+ errorMessage += '\n\nIssues can be reported at https://github.com/FooSoft/yomichan/issues';
+
+ console.error(errorMessage);
+
+ if (alert) {
+ window.alert(`${errorString}\n\nCheck the developer console for more details.`);
+ }
+}
+
const EXTENSION_IS_BROWSER_EDGE = (
extensionHasBrowser() &&
(!extensionHasChrome() || (typeof chrome.runtime === 'undefined' && typeof browser.runtime !== 'undefined'))