Display audio (#1269)
* Update display definition/definition node handling * Separate display audio controls into a separate class
This commit is contained in:
parent
887150e012
commit
25568637fe
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
184
ext/mixed/js/display-audio.js
Normal file
184
ext/mixed/js/display-audio.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
|
Loading…
Reference in New Issue
Block a user