diff options
Diffstat (limited to 'ext')
| -rw-r--r-- | ext/bg/search.html | 3 | ||||
| -rw-r--r-- | ext/fg/float.html | 3 | ||||
| -rw-r--r-- | ext/mixed/css/display.css | 15 | ||||
| -rw-r--r-- | ext/mixed/display-templates.html | 14 | ||||
| -rw-r--r-- | ext/mixed/js/display-audio.js | 185 | ||||
| -rw-r--r-- | ext/mixed/js/display-generator.js | 4 | ||||
| -rw-r--r-- | ext/mixed/js/display.js | 23 | ||||
| -rw-r--r-- | ext/mixed/js/popup-menu.js | 4 | 
8 files changed, 246 insertions, 5 deletions
| diff --git a/ext/bg/search.html b/ext/bg/search.html index c08ad9cf..b5e8f746 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -70,6 +70,8 @@      </div>  </div> +<div id="popup-menus"></div> +  <!-- Scripts -->  <script src="/mixed/lib/wanakana.min.js"></script> @@ -96,6 +98,7 @@  <script src="/mixed/js/hotkey-help-controller.js"></script>  <script src="/mixed/js/hotkey-util.js"></script>  <script src="/mixed/js/media-loader.js"></script> +<script src="/mixed/js/popup-menu.js"></script>  <script src="/mixed/js/scroll.js"></script>  <script src="/mixed/js/text-scanner.js"></script>  <script src="/mixed/js/html-template-collection.js"></script> diff --git a/ext/fg/float.html b/ext/fg/float.html index c361c9eb..8e5bf550 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -86,6 +86,8 @@      </svg>  </div> +<div id="popup-menus"></div> +  <!-- Scripts -->  <script src="/mixed/js/core.js"></script>  <script src="/mixed/js/yomichan.js"></script> @@ -113,6 +115,7 @@  <script src="/mixed/js/hotkey-util.js"></script>  <script src="/mixed/js/media-loader.js"></script>  <script src="/mixed/js/panel-element.js"></script> +<script src="/mixed/js/popup-menu.js"></script>  <script src="/mixed/js/scroll.js"></script>  <script src="/mixed/js/text-scanner.js"></script>  <script src="/mixed/js/html-template-collection.js"></script> 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);      } |