diff options
| author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2021-01-18 00:16:40 -0500 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-01-18 00:16:40 -0500 | 
| commit | 25568637fe82988522ddd5c4d8642702b898a293 (patch) | |
| tree | 00f84f306fdb4f7333d519b8a5c5723316987b6c | |
| parent | 887150e01257af0b5deb472263de116cac7d0c1c (diff) | |
Display audio (#1269)
* Update display definition/definition node handling
* Separate display audio controls into a separate class
| -rw-r--r-- | ext/bg/search.html | 1 | ||||
| -rw-r--r-- | ext/fg/float.html | 1 | ||||
| -rw-r--r-- | ext/mixed/display-templates.html | 4 | ||||
| -rw-r--r-- | ext/mixed/js/display-audio.js | 184 | ||||
| -rw-r--r-- | ext/mixed/js/display.js | 154 | 
5 files changed, 216 insertions, 128 deletions
| diff --git a/ext/bg/search.html b/ext/bg/search.html index dae657e8..f8e4d21c 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -88,6 +88,7 @@  <script src="/mixed/js/audio-system.js"></script>  <script src="/mixed/js/dictionary-data-util.js"></script>  <script src="/mixed/js/display.js"></script> +<script src="/mixed/js/display-audio.js"></script>  <script src="/mixed/js/display-generator.js"></script>  <script src="/mixed/js/display-history.js"></script>  <script src="/mixed/js/display-notification.js"></script> diff --git a/ext/fg/float.html b/ext/fg/float.html index e10659f2..52fe3e66 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -101,6 +101,7 @@  <script src="/mixed/js/audio-system.js"></script>  <script src="/mixed/js/dictionary-data-util.js"></script>  <script src="/mixed/js/display.js"></script> +<script src="/mixed/js/display-audio.js"></script>  <script src="/mixed/js/display-generator.js"></script>  <script src="/mixed/js/display-history.js"></script>  <script src="/mixed/js/display-notification.js"></script> diff --git a/ext/mixed/display-templates.html b/ext/mixed/display-templates.html index 39f3b978..66cd9785 100644 --- a/ext/mixed/display-templates.html +++ b/ext/mixed/display-templates.html @@ -10,7 +10,7 @@                      <button class="action-button action-view-note" hidden disabled data-icon="view-note" title="View added note (Alt + V)"></button>                      <button class="action-button action-add-note" hidden disabled data-icon="add-term-kanji" data-mode="term-kanji" title="Add expression (Alt + E)"></button>                      <button class="action-button action-add-note" hidden disabled data-icon="add-term-kana" data-mode="term-kana" title="Add reading (Alt + R)"></button> -                    <button class="action-button action-play-audio" data-icon="play-audio" title="Play audio (Alt + P)"></button> +                    <button class="action-button action-play-audio" data-icon="play-audio" title="Play audio" data-title-default="Play audio"></button>                      <span class="entry-current-indicator-icon" title="Current entry (Alt + Up/Down/Home/End/PgUp/PgDn)"></span>                  </div>                  <div class="term-expression-list"></div> @@ -44,7 +44,7 @@          </span>      </div>      <div class="term-expression-details"> -        <button class="action-button action-play-audio" data-icon="play-audio" title="Play audio"></button> +        <button class="action-button action-play-audio" data-icon="play-audio" title="Play audio" data-title-default="Play audio"></button>          <div class="tags tag-list"></div>      </div>  </div></template> diff --git a/ext/mixed/js/display-audio.js b/ext/mixed/js/display-audio.js new file mode 100644 index 00000000..c423446e --- /dev/null +++ b/ext/mixed/js/display-audio.js @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2021  Yomichan Authors + * + * 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 <https://www.gnu.org/licenses/>. + */ + +/* global + * AudioSystem + */ + +class DisplayAudio { +    constructor(display) { +        this._display = display; +        this._audioPlaying = null; +        this._audioSystem = new AudioSystem(true); +        this._autoPlayAudioTimer = null; +        this._autoPlayAudioDelay = 400; +        this._eventListeners = new EventListenerCollection(); +    } + +    get autoPlayAudioDelay() { +        return this._autoPlayAudioDelay; +    } + +    set autoPlayAudioDelay(value) { +        this._autoPlayAudioDelay = value; +    } + +    prepare() { +        this._audioSystem.prepare(); +    } + +    updateOptions(options) { +        const data = document.documentElement.dataset; +        data.audioEnabled = `${options.audio.enabled && options.audio.sources.length > 0}`; +    } + +    cleanupEntries() { +        this.clearAutoPlayTimer(); +        this._eventListeners.removeAllEventListeners(); +    } + +    setupEntry(entry, definitionIndex) { +        for (const button of entry.querySelectorAll('.action-play-audio')) { +            const expressionIndex = this._getAudioPlayButtonExpressionIndex(button); +            this._eventListeners.addEventListener(button, 'click', this._onAudioPlayButtonClick.bind(this, definitionIndex, expressionIndex), false); +        } +    } + +    setupEntriesComplete() { +        const {audio} = this._display.getOptions(); +        if (!audio.enabled || !audio.autoPlay) { return; } + +        this.clearAutoPlayTimer(); + +        const definitions = this._display.definitions; +        if (definitions.length === 0) { return; } + +        const firstDefinition = definitions[0]; +        if (firstDefinition.type === 'kanji') { return; } + +        const callback = () => { +            this._autoPlayAudioTimer = null; +            this.playAudio(0, 0); +        }; + +        if (this._autoPlayAudioDelay > 0) { +            this._autoPlayAudioTimer = setTimeout(callback, this._autoPlayAudioDelay); +        } else { +            callback(); +        } +    } + +    clearAutoPlayTimer() { +        if (this._autoPlayAudioTimer === null) { return; } +        clearTimeout(this._autoPlayAudioTimer); +        this._autoPlayAudioTimer = null; +    } + +    stopAudio() { +        if (this._audioPlaying === null) { return; } +        this._audioPlaying.pause(); +        this._audioPlaying = null; +    } + +    async playAudio(definitionIndex, expressionIndex) { +        this.stopAudio(); +        this.clearAutoPlayTimer(); + +        const {definitions} = this._display; +        if (definitionIndex < 0 || definitionIndex >= definitions.length) { return; } + +        const definition = definitions[definitionIndex]; +        if (definition.type === 'kanji') { return; } + +        const {expressions} = definition; +        if (expressionIndex < 0 || expressionIndex >= expressions.length) { return; } + +        const {expression, reading} = expressions[expressionIndex]; +        const {sources, textToSpeechVoice, customSourceUrl, volume} = this._display.getOptions().audio; + +        const progressIndicatorVisible = this._display.progressIndicatorVisible; +        const overrideToken = progressIndicatorVisible.setOverride(true); +        try { +            // Create audio +            let audio; +            let info; +            try { +                let index; +                ({audio, index} = await this._audioSystem.createDefinitionAudio(sources, expression, reading, {textToSpeechVoice, customSourceUrl})); +                info = `From source ${1 + index}: ${sources[index]}`; +            } catch (e) { +                audio = this._audioSystem.getFallbackAudio(); +                info = 'Could not find audio'; +            } + +            // Stop any currently playing audio +            this.stopAudio(); + +            // Update details +            for (const button of this._getAudioPlayButtons(definitionIndex, expressionIndex)) { +                const titleDefault = button.dataset.titleDefault || ''; +                button.title = `${titleDefault}\n${info}`; +            } + +            // Play +            audio.currentTime = 0; +            audio.volume = Number.isFinite(volume) ? Math.max(0.0, Math.min(1.0, volume / 100.0)) : 1.0; + +            const playPromise = audio.play(); +            this._audioPlaying = audio; + +            if (typeof playPromise !== 'undefined') { +                try { +                    await playPromise; +                } catch (e) { +                    // NOP +                } +            } +        } finally { +            progressIndicatorVisible.clearOverride(overrideToken); +        } +    } + +    // Private + +    _onAudioPlayButtonClick(definitionIndex, expressionIndex, e) { +        e.preventDefault(); +        this.playAudio(definitionIndex, expressionIndex); +    } + +    _getAudioPlayButtonExpressionIndex(button) { +        const expressionNode = button.closest('.term-expression'); +        if (expressionNode !== null) { +            const expressionIndex = parseInt(expressionNode.dataset.index, 10); +            if (Number.isFinite(expressionIndex)) { return expressionIndex; } +        } +        return 0; +    } + +    _getAudioPlayButtons(definitionIndex, expressionIndex) { +        const results = []; +        const {definitionNodes} = this._display; +        if (definitionIndex >= 0 && definitionIndex < definitionNodes.length) { +            const node = definitionNodes[definitionIndex]; +            const button1 = (expressionIndex === 0 ? node.querySelector('.action-play-audio') : null); +            const button2 = node.querySelector(`.term-expression:nth-of-type(${expressionIndex + 1}) .action-play-audio`); +            if (button1 !== null) { results.push(button1); } +            if (button2 !== null) { results.push(button2); } +        } +        return results; +    } +} diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 0b0236da..a9d59aff 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -17,7 +17,7 @@  /* global   * AnkiNoteBuilder - * AudioSystem + * DisplayAudio   * DisplayGenerator   * DisplayHistory   * DisplayNotification @@ -42,16 +42,13 @@ class Display extends EventDispatcher {          this._hotkeyHandler = hotkeyHandler;          this._container = document.querySelector('#definitions');          this._definitions = []; +        this._definitionNodes = [];          this._optionsContext = {depth: 0, url: window.location.href};          this._options = null;          this._index = 0; -        this._audioPlaying = null; -        this._audioSystem = new AudioSystem(true);          this._styleNode = null;          this._eventListeners = new EventListenerCollection();          this._setContentToken = null; -        this._autoPlayAudioTimer = null; -        this._autoPlayAudioDelay = 400;          this._mediaLoader = new MediaLoader();          this._displayGenerator = new DisplayGenerator({              japaneseUtil, @@ -110,6 +107,7 @@ class Display extends EventDispatcher {          this._frameResizeEventListeners = new EventListenerCollection();          this._tagNotification = null;          this._tagNotificationContainer = document.querySelector('#content-footer'); +        this._displayAudio = new DisplayAudio(this);          this._hotkeyHandler.registerActions([              ['close',             () => { this.close(); }], @@ -151,11 +149,11 @@ class Display extends EventDispatcher {      }      get autoPlayAudioDelay() { -        return this._autoPlayAudioDelay; +        return this._displayAudio.autoPlayAudioDelay;      }      set autoPlayAudioDelay(value) { -        this._autoPlayAudioDelay = value; +        this._displayAudio.autoPlayAudioDelay = value;      }      get queryParserVisible() { @@ -183,6 +181,18 @@ class Display extends EventDispatcher {          return this._hotkeyHandler;      } +    get definitions() { +        return this._definitions; +    } + +    get definitionNodes() { +        return this._definitionNodes; +    } + +    get progressIndicatorVisible() { +        return this._progressIndicatorVisible; +    } +      async prepare() {          // State setup          const {documentElement} = document; @@ -192,7 +202,7 @@ class Display extends EventDispatcher {          // Prepare          await this._displayGenerator.prepare(); -        this._audioSystem.prepare(); +        this._displayAudio.prepare();          this._queryParser.prepare();          this._history.prepare(); @@ -274,6 +284,7 @@ class Display extends EventDispatcher {          this._updateDocumentOptions(options);          this._updateTheme(options.general.popupTheme);          this.setCustomCss(options.general.customPopupCss); +        this._displayAudio.updateOptions(options);          this._queryParser.setOptions({              selectedParser: options.parsing.selectedParser, @@ -296,25 +307,8 @@ class Display extends EventDispatcher {          this._updateDefinitionTextScanner(options);      } -    autoPlayAudio() { -        this.clearAutoPlayTimer(); - -        if (this._definitions.length === 0) { return; } - -        const callback = () => this._playAudio(0, 0); - -        if (this._autoPlayAudioDelay > 0) { -            this._autoPlayAudioTimer = setTimeout(callback, this._autoPlayAudioDelay); -        } else { -            callback(); -        } -    } -      clearAutoPlayTimer() { -        if (this._autoPlayAudioTimer !== null) { -            clearTimeout(this._autoPlayAudioTimer); -            this._autoPlayAudioTimer = null; -        } +        this._displayAudio.clearAutoPlayTimer();      }      setContent(details) { @@ -518,7 +512,10 @@ class Display extends EventDispatcher {              this._closePopups();              this._eventListeners.removeAllEventListeners();              this._mediaLoader.unloadAll(); +            this._displayAudio.cleanupEntries();              this._hideTagNotification(false); +            this._definitions = []; +            this._definitionNodes = [];              // Prepare              const urlSearchParams = new URLSearchParams(location.search); @@ -688,15 +685,6 @@ class Display extends EventDispatcher {          }      } -    _onAudioPlay(e) { -        e.preventDefault(); -        const link = e.currentTarget; -        const definitionIndex = this._getClosestDefinitionIndex(link); -        if (definitionIndex < 0) { return; } -        const expressionIndex = Math.max(0, this._getClosestExpressionIndex(link)); -        this._playAudio(definitionIndex, expressionIndex); -    } -      _onNoteAdd(e) {          e.preventDefault();          const link = e.currentTarget; @@ -807,7 +795,6 @@ class Display extends EventDispatcher {      _updateDocumentOptions(options) {          const data = document.documentElement.dataset;          data.ankiEnabled = `${options.anki.enable}`; -        data.audioEnabled = `${options.audio.enabled && options.audio.sources.length > 0}`;          data.glossaryLayoutMode = `${options.general.glossaryLayoutMode}`;          data.compactTags = `${options.general.compactTags}`;          data.enableSearchTags = `${options.scanning.enableSearchTags}`; @@ -921,7 +908,9 @@ class Display extends EventDispatcher {                  this._displayGenerator.createKanjiEntry(definition)              );              entry.dataset.index = `${i}`; +            this._definitionNodes.push(entry);              this._addEntryEventListeners(entry); +            this._displayAudio.setupEntry(entry, i);              container.appendChild(entry);              if (focusEntry === i) {                  this._focusEntry(i, false); @@ -936,13 +925,7 @@ class Display extends EventDispatcher {              this._windowScroll.to(x, y);          } -        if ( -            isTerms && -            this._options.audio.enabled && -            this._options.audio.autoPlay -        ) { -            this.autoPlayAudio(); -        } +        this._displayAudio.setupEntriesComplete();          this._updateAdderButtons(token, isTerms, definitions);      } @@ -1209,76 +1192,12 @@ class Display extends EventDispatcher {          return true;      } -    async _playAudio(definitionIndex, expressionIndex) { -        if (definitionIndex < 0 || definitionIndex >= this._definitions.length) { return; } - -        const definition = this._definitions[definitionIndex]; -        if (definition.type === 'kanji') { return; } - -        const {expressions} = definition; -        if (expressionIndex < 0 || expressionIndex >= expressions.length) { return; } - -        const {expression, reading} = expressions[expressionIndex]; - -        const overrideToken = this._progressIndicatorVisible.setOverride(true); -        try { -            this._stopPlayingAudio(); - -            let audio, info; -            try { -                const {sources, textToSpeechVoice, customSourceUrl} = this._options.audio; -                let index; -                ({audio, index} = await this._audioSystem.createDefinitionAudio(sources, expression, reading, {textToSpeechVoice, customSourceUrl})); -                info = `From source ${1 + index}: ${sources[index]}`; -            } catch (e) { -                audio = this._audioSystem.getFallbackAudio(); -                info = 'Could not find audio'; -            } - -            const button = this._audioButtonFindImage(definitionIndex, expressionIndex); -            if (button !== null) { -                let titleDefault = button.dataset.titleDefault; -                if (!titleDefault) { -                    titleDefault = button.title || ''; -                    button.dataset.titleDefault = titleDefault; -                } -                button.title = `${titleDefault}\n${info}`; -            } - -            this._stopPlayingAudio(); - -            const volume = Math.max(0.0, Math.min(1.0, this._options.audio.volume / 100.0)); -            this._audioPlaying = audio; -            audio.currentTime = 0; -            audio.volume = Number.isFinite(volume) ? volume : 1.0; -            const playPromise = audio.play(); -            if (typeof playPromise !== 'undefined') { -                try { -                    await playPromise; -                } catch (e2) { -                    // NOP -                } -            } -        } catch (e) { -            this.onError(e); -        } finally { -            this._progressIndicatorVisible.clearOverride(overrideToken); -        } -    } -      async _playAudioCurrent() { -        return await this._playAudio(this._index, 0); -    } - -    _stopPlayingAudio() { -        if (this._audioPlaying !== null) { -            this._audioPlaying.pause(); -            this._audioPlaying = null; -        } +        return await this._displayAudio.playAudio(this._index, 0);      }      _getEntry(index) { -        const entries = this._container.querySelectorAll('.entry'); +        const entries = this._definitionNodes;          return index >= 0 && index < entries.length ? entries[index] : null;      } @@ -1293,10 +1212,6 @@ class Display extends EventDispatcher {          return this._getClosestIndex(element, '.entry');      } -    _getClosestExpressionIndex(element) { -        return this._getClosestIndex(element, '.term-expression'); -    } -      _getClosestIndex(element, selector) {          const node = element.closest(selector);          if (node === null) { return -1; } @@ -1324,18 +1239,6 @@ class Display extends EventDispatcher {          viewerButton.dataset.noteId = noteId;      } -    _audioButtonFindImage(index, expressionIndex) { -        const entry = this._getEntry(index); -        if (entry === null) { return null; } - -        const container = ( -            expressionIndex >= 0 ? -            entry.querySelector(`.term-expression:nth-of-type(${expressionIndex + 1})`) : -            entry -        ); -        return container !== null ? container.querySelector('.action-play-audio>img') : null; -    } -      _getElementTop(element) {          const elementRect = element.getBoundingClientRect();          const documentRect = this._contentScrollBodyElement.getBoundingClientRect(); @@ -1699,7 +1602,6 @@ class Display extends EventDispatcher {          this._eventListeners.addEventListener(entry, 'click', this._onEntryClick.bind(this));          this._addMultipleEventListeners(entry, '.action-add-note', 'click', this._onNoteAdd.bind(this));          this._addMultipleEventListeners(entry, '.action-view-note', 'click', this._onNoteView.bind(this)); -        this._addMultipleEventListeners(entry, '.action-play-audio', 'click', this._onAudioPlay.bind(this));          this._addMultipleEventListeners(entry, '.kanji-link', 'click', this._onKanjiLookup.bind(this));          this._addMultipleEventListeners(entry, '.debug-log-link', 'click', this._onDebugLogClick.bind(this));          this._addMultipleEventListeners(entry, '.tag', 'click', this._onTagClick.bind(this)); |