aboutsummaryrefslogtreecommitdiff
path: root/ext/mixed
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2021-01-23 21:13:01 -0500
committerGitHub <noreply@github.com>2021-01-23 21:13:01 -0500
commitef577b88754523abeab3844115506a0b6e914874 (patch)
tree78f181897afc89904f2df7c3370030a955219c5b /ext/mixed
parent9fbdb9757b22c2bb9afe5061137bfe4b3b755e91 (diff)
Audio button menu (#1302)
* Fix popup menus not stoping events * Ensure non-stale use of buttons * Enable popup menus on the popup/search pages * Add audio menu
Diffstat (limited to 'ext/mixed')
-rw-r--r--ext/mixed/css/display.css15
-rw-r--r--ext/mixed/display-templates.html14
-rw-r--r--ext/mixed/js/display-audio.js185
-rw-r--r--ext/mixed/js/display-generator.js4
-rw-r--r--ext/mixed/js/display.js23
-rw-r--r--ext/mixed/js/popup-menu.js4
6 files changed, 240 insertions, 5 deletions
diff --git a/ext/mixed/css/display.css b/ext/mixed/css/display.css
index 003d0962..ce5cac6c 100644
--- a/ext/mixed/css/display.css
+++ b/ext/mixed/css/display.css
@@ -1620,6 +1620,21 @@ button.footer-notification-close-button:active {
}
+/* Audio menu */
+.audio-button-popup-menu[data-show-icons=false] .popup-menu-item-icon {
+ display: none;
+}
+.popup-menu-item-icon[data-icon=checkmark] {
+ background-color: var(--success-color);
+}
+.popup-menu-item-icon[data-icon=cross] {
+ background-color: var(--danger-color);
+}
+.popup-menu-item[data-source-in-options=false][data-valid=null] {
+ color: var(--text-color-light1);
+}
+
+
/* Conditional styles */
:root:not([data-enable-search-tags=true]) .tag[data-category=search] {
display: none;
diff --git a/ext/mixed/display-templates.html b/ext/mixed/display-templates.html
index 40716469..2d363b7b 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" data-hotkey='["viewNote","title","View added note ({0})"]'></button>
<button class="action-button action-add-note" hidden disabled data-icon="add-term-kanji" data-mode="term-kanji" title="Add expression" data-hotkey='["addNoteTermKanji","title","Add expression ({0})"]'></button>
<button class="action-button action-add-note" hidden disabled data-icon="add-term-kana" data-mode="term-kana" title="Add reading" data-hotkey='["addNoteTermKana","title","Add reading ({0})"]'></button>
- <button class="action-button action-play-audio" data-icon="play-audio" title="Play audio" data-title-default="Play audio" data-hotkey='["playAudio",["title","data-title-default"],"Play audio ({0})"]'><div class="action-button-badge icon" hidden></div></button>
+ <button class="action-button action-play-audio" data-icon="play-audio" title="Play audio" data-title-default="Play audio" data-hotkey='["playAudio",["title","data-title-default"],"Play audio ({0})"]' data-menu-position="left below h-cover v-cover"><div class="action-button-badge icon" hidden></div></button>
<span class="entry-current-indicator-icon" title="Current entry"></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" data-title-default="Play audio" data-hotkey='["playAudio",["title","data-title-default"],"Play audio ({0})"]'><div class="action-button-badge icon" hidden></div></button>
+ <button class="action-button action-play-audio" data-icon="play-audio" title="Play audio" data-title-default="Play audio" data-hotkey='["playAudio",["title","data-title-default"],"Play audio ({0})"]' data-menu-position="right below h-cover v-cover"><div class="action-button-badge icon" hidden></div></button>
<div class="tags tag-list"></div>
</div>
</div></template>
@@ -149,4 +149,14 @@
<div class="profile-list-item-name"></div>
</label></template>
+<!-- Popup menu -->
+<template id="audio-button-popup-menu-template"><div class="popup-menu-container scan-disable audio-button-popup-menu" tabindex="-1" role="dialog"><div class="popup-menu popup-menu-auto-size"><div class="popup-menu-body">
+ <button class="popup-menu-item" data-menu-action="playAudioFromSource" data-source="jpod101"><div class="popup-menu-item-icon icon" data-icon="none"></div><span class="popup-menu-item-label">JapanesePod101</span></button>
+ <button class="popup-menu-item" data-menu-action="playAudioFromSource" data-source="jpod101-alternate"><div class="popup-menu-item-icon icon" data-icon="none"></div><span class="popup-menu-item-label">JapanesePod101 (Alternate)</span></button>
+ <button class="popup-menu-item" data-menu-action="playAudioFromSource" data-source="jisho"><div class="popup-menu-item-icon icon" data-icon="none"></div><span class="popup-menu-item-label">Jisho.org</span></button>
+ <button class="popup-menu-item" data-menu-action="playAudioFromSource" data-source="text-to-speech"><div class="popup-menu-item-icon icon" data-icon="none"></div><span class="popup-menu-item-label">Text-to-speech</span></button>
+ <button class="popup-menu-item" data-menu-action="playAudioFromSource" data-source="text-to-speech-reading"><div class="popup-menu-item-icon icon" data-icon="none"></div><span class="popup-menu-item-label">Text-to-speech (Kana reading)</span></button>
+ <button class="popup-menu-item" data-menu-action="playAudioFromSource" data-source="custom"><div class="popup-menu-item-icon icon" data-icon="none"></div><span class="popup-menu-item-label">Custom</span></button>
+</div></div></div></template>
+
</body></html>
diff --git a/ext/mixed/js/display-audio.js b/ext/mixed/js/display-audio.js
index e1a9e250..c60831b1 100644
--- a/ext/mixed/js/display-audio.js
+++ b/ext/mixed/js/display-audio.js
@@ -17,6 +17,7 @@
/* global
* AudioSystem
+ * PopupMenu
* api
*/
@@ -29,6 +30,7 @@ class DisplayAudio {
this._autoPlayAudioDelay = 400;
this._eventListeners = new EventListenerCollection();
this._cache = new Map();
+ this._menuContainer = document.querySelector('#popup-menus');
}
get autoPlayAudioDelay() {
@@ -58,6 +60,8 @@ class DisplayAudio {
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);
+ this._eventListeners.addEventListener(button, 'contextmenu', this._onAudioPlayButtonContextMenu.bind(this, definitionIndex, expressionIndex), false);
+ this._eventListeners.addEventListener(button, 'menuClose', this._onAudioPlayMenuCloseClick.bind(this, definitionIndex, expressionIndex), false);
}
}
@@ -104,6 +108,8 @@ class DisplayAudio {
const expressionReading = this._getExpressionAndReading(definitionIndex, expressionIndex);
if (expressionReading === null) { return; }
+ const buttons = this._getAudioPlayButtons(definitionIndex, expressionIndex);
+
const {expression, reading} = expressionReading;
const audioOptions = this._getAudioOptions();
const {textToSpeechVoice, customSourceUrl, volume} = audioOptions;
@@ -136,7 +142,7 @@ class DisplayAudio {
// Update details
const potentialAvailableAudioCount = this._getPotentialAvailableAudioCount(expression, reading);
- for (const button of this._getAudioPlayButtons(definitionIndex, expressionIndex)) {
+ for (const button of buttons) {
const titleDefault = button.dataset.titleDefault || '';
button.title = `${titleDefault}\n${title}`;
this._updateAudioPlayButtonBadge(button, potentialAvailableAudioCount);
@@ -165,7 +171,37 @@ class DisplayAudio {
_onAudioPlayButtonClick(definitionIndex, expressionIndex, e) {
e.preventDefault();
- this.playAudio(definitionIndex, expressionIndex);
+
+ if (e.shiftKey) {
+ this._showAudioMenu(e.currentTarget, definitionIndex, expressionIndex);
+ } else {
+ this.playAudio(definitionIndex, expressionIndex);
+ }
+ }
+
+ _onAudioPlayButtonContextMenu(definitionIndex, expressionIndex, e) {
+ e.preventDefault();
+
+ this._showAudioMenu(e.currentTarget, definitionIndex, expressionIndex);
+ }
+
+ _onAudioPlayMenuCloseClick(definitionIndex, expressionIndex, e) {
+ const {detail: {action, item}} = e;
+ switch (action) {
+ case 'playAudioFromSource':
+ {
+ const {source, index} = item.dataset;
+ let sourceDetailsMap = null;
+ if (typeof index !== 'undefined') {
+ const index2 = Number.parseInt(index, 10);
+ sourceDetailsMap = new Map([
+ [source, {start: index2, end: index2 + 1}]
+ ]);
+ }
+ this.playAudio(definitionIndex, expressionIndex, [source], sourceDetailsMap);
+ }
+ break;
+ }
}
_getAudioPlayButtonExpressionIndex(button) {
@@ -360,4 +396,149 @@ class DisplayAudio {
}
return count;
}
+
+ _showAudioMenu(button, definitionIndex, expressionIndex) {
+ const expressionReading = this._getExpressionAndReading(definitionIndex, expressionIndex);
+ if (expressionReading === null) { return; }
+
+ const {expression, reading} = expressionReading;
+ const popupMenu = this._createMenu(button, expression, reading);
+ popupMenu.prepare();
+ }
+
+ _createMenu(button, expression, reading) {
+ // Options
+ const {sources, textToSpeechVoice, customSourceUrl} = this._getAudioOptions();
+ const sourceIndexMap = new Map();
+ for (let i = 0, ii = sources.length; i < ii; ++i) {
+ sourceIndexMap.set(sources[i], i);
+ }
+
+ // Create menu
+ const menuNode = this._display.displayGenerator.createPopupMenu('audio-button');
+
+ // Create menu item metadata
+ const menuItems = [];
+ const menuItemNodes = menuNode.querySelectorAll('.popup-menu-item');
+ for (let i = 0, ii = menuItemNodes.length; i < ii; ++i) {
+ const node = menuItemNodes[i];
+ const {source} = node.dataset;
+ let optionsIndex = sourceIndexMap.get(source);
+ if (typeof optionsIndex === 'undefined') { optionsIndex = null; }
+ menuItems.push({node, source, index: i, optionsIndex});
+ }
+
+ // Sort according to source order in options
+ menuItems.sort((a, b) => {
+ const ai = a.optionsIndex;
+ const bi = b.optionsIndex;
+ if (ai !== null) {
+ if (bi !== null) {
+ const i = ai - bi;
+ if (i !== 0) { return i; }
+ } else {
+ return -1;
+ }
+ } else {
+ if (bi !== null) {
+ return 1;
+ }
+ }
+ return a.index - b.index;
+ });
+
+ // Set up items based on cache data
+ const sourceMap = this._cache.get(this._getExpressionReadingKey(expression, reading));
+ const menuEntryMap = new Map();
+ let showIcons = false;
+ for (let i = 0, ii = menuItems.length; i < ii; ++i) {
+ const {node, source, optionsIndex} = menuItems[i];
+ const entries = this._getMenuItemEntries(node, sourceMap, source);
+ menuEntryMap.set(source, entries);
+ for (const {node: node2, valid, index} of entries) {
+ if (valid !== null) {
+ const icon = node2.querySelector('.popup-menu-item-icon');
+ icon.dataset.icon = valid ? 'checkmark' : 'cross';
+ showIcons = true;
+ }
+ if (index !== null) {
+ node2.dataset.index = `${index}`;
+ }
+ node2.dataset.valid = `${valid}`;
+ node2.dataset.sourceInOptions = `${optionsIndex !== null}`;
+ node2.style.order = `${i}`;
+ }
+ }
+ menuNode.dataset.showIcons = `${showIcons}`;
+
+ // Hide options
+ if (textToSpeechVoice.length === 0) {
+ this._setMenuItemEntriesHidden(menuEntryMap, 'text-to-speech', true);
+ this._setMenuItemEntriesHidden(menuEntryMap, 'text-to-speech-reading', true);
+ }
+ if (customSourceUrl.length === 0) {
+ this._setMenuItemEntriesHidden(menuEntryMap, 'custom', true);
+ }
+
+ // Create popup menu
+ this._menuContainer.appendChild(menuNode);
+ return new PopupMenu(button, menuNode);
+ }
+
+ _getMenuItemEntries(node, sourceMap, source) {
+ const entries = [{node, valid: null, index: null}];
+
+ const nextNode = node.nextSibling;
+
+ if (typeof sourceMap === 'undefined') { return entries; }
+
+ const sourceInfo = sourceMap.get(source);
+ if (typeof sourceInfo === 'undefined') { return entries; }
+
+ const {infoList} = sourceInfo;
+ if (infoList === null) { return entries; }
+
+ if (infoList.length === 0) {
+ entries[0].valid = false;
+ return entries;
+ }
+
+ const defaultLabel = node.querySelector('.popup-menu-item-label').textContent;
+
+ for (let i = 0, ii = infoList.length; i < ii; ++i) {
+ // Get/create entry
+ let entry;
+ if (i < entries.length) {
+ entry = entries[i];
+ } else {
+ const node2 = node.cloneNode(true);
+ nextNode.parentNode.insertBefore(node2, nextNode);
+ entry = {node: node2, valid: null, index: null};
+ entries.push(entry);
+ }
+
+ // Entry info
+ entry.index = i;
+
+ const {audio, audioResolved, title} = infoList[i];
+ if (audioResolved) { entry.valid = (audio !== null); }
+
+ const labelNode = entry.node.querySelector('.popup-menu-item-label');
+ let label = defaultLabel;
+ if (ii > 1) { label = `${label} ${i + 1}`; }
+ if (typeof title === 'string' && title.length > 0) { label += `: ${title}`; }
+ labelNode.textContent = label;
+ }
+
+ return entries;
+ }
+
+ _setMenuItemEntriesHidden(menuEntryMap, source, hidden) {
+ const entries = menuEntryMap.get(source);
+ if (typeof entries === 'undefined') { return; }
+
+ for (const {node} of entries) {
+ node.hidden = hidden;
+ }
+ }
}
diff --git a/ext/mixed/js/display-generator.js b/ext/mixed/js/display-generator.js
index 0324f16a..e9eaa68f 100644
--- a/ext/mixed/js/display-generator.js
+++ b/ext/mixed/js/display-generator.js
@@ -175,6 +175,10 @@ class DisplayGenerator {
return this._templates.instantiate('profile-list-item');
}
+ createPopupMenu(name) {
+ return this._templates.instantiate(`${name}-popup-menu`);
+ }
+
// Private
_createTermExpression(details) {
diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js
index 6af35074..eb8b2900 100644
--- a/ext/mixed/js/display.js
+++ b/ext/mixed/js/display.js
@@ -27,6 +27,7 @@
* HotkeyHelpController
* MediaLoader
* PopupFactory
+ * PopupMenu
* QueryParser
* TextScanner
* WindowScroll
@@ -113,7 +114,7 @@ class Display extends EventDispatcher {
this._displayAudio = new DisplayAudio(this);
this._hotkeyHandler.registerActions([
- ['close', () => { this.close(); }],
+ ['close', () => { this._onHotkeyClose(); }],
['nextEntry', () => { this._focusEntry(this._index + 1, true); }],
['nextEntry3', () => { this._focusEntry(this._index + 3, true); }],
['previousEntry', () => { this._focusEntry(this._index - 1, true); }],
@@ -517,6 +518,7 @@ class Display extends EventDispatcher {
try {
// Clear
this._closePopups();
+ this._closeAllPopupMenus();
this._eventListeners.removeAllEventListeners();
this._mediaLoader.unloadAll();
this._displayAudio.cleanupEntries();
@@ -1806,4 +1808,23 @@ class Display extends EventDispatcher {
});
});
}
+
+ _onHotkeyClose() {
+ if (this._closeSinglePopupMenu()) { return; }
+ this.close();
+ }
+
+ _closeAllPopupMenus() {
+ for (const popupMenu of PopupMenu.openMenus) {
+ popupMenu.close();
+ }
+ }
+
+ _closeSinglePopupMenu() {
+ for (const popupMenu of PopupMenu.openMenus) {
+ popupMenu.close();
+ return true;
+ }
+ return false;
+ }
}
diff --git a/ext/mixed/js/popup-menu.js b/ext/mixed/js/popup-menu.js
index 124c1984..9ad4e260 100644
--- a/ext/mixed/js/popup-menu.js
+++ b/ext/mixed/js/popup-menu.js
@@ -76,12 +76,16 @@ class PopupMenu extends EventDispatcher {
_onMenuContainerClick(e) {
if (e.currentTarget !== e.target) { return; }
+ e.stopPropagation();
+ e.preventDefault();
this._close(null, 'outside', true);
}
_onMenuItemClick(e) {
const item = e.currentTarget;
if (item.disabled) { return; }
+ e.stopPropagation();
+ e.preventDefault();
this._close(item, 'item', true);
}