diff options
Diffstat (limited to 'ext/mixed/js')
| -rw-r--r-- | ext/mixed/js/audio.js | 60 | ||||
| -rw-r--r-- | ext/mixed/js/display.js | 107 | ||||
| -rw-r--r-- | ext/mixed/js/extension.js | 35 | 
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')) |