diff options
| -rw-r--r-- | ext/css/display.css | 38 | ||||
| -rw-r--r-- | ext/css/material.css | 7 | ||||
| -rw-r--r-- | ext/display-templates.html | 5 | ||||
| -rw-r--r-- | ext/js/display/display-audio.js | 158 | ||||
| -rw-r--r-- | ext/js/display/display.js | 21 | 
5 files changed, 189 insertions, 40 deletions
| diff --git a/ext/css/display.css b/ext/css/display.css index 607368fc..7953f6ef 100644 --- a/ext/css/display.css +++ b/ext/css/display.css @@ -1670,7 +1670,7 @@ button.footer-notification-close-button:active {  /* Audio menu */ -.audio-button-popup-menu[data-show-icons=false] .popup-menu-item-icon { +.audio-button-popup-menu[data-show-icons=false] .popup-menu-item-audio-button .popup-menu-item-icon {      display: none;  }  .audio-button-popup-menu .popup-menu-item-icon[data-icon=checkmark] { @@ -1682,6 +1682,42 @@ button.footer-notification-close-button:active {  .audio-button-popup-menu .popup-menu-item-group[data-source-in-options=false][data-valid=null] .popup-menu-item {      color: var(--text-color-light1);  } +.popup-menu-item-audio-button .popup-menu-item-label { +    padding-right: 2.5em; +} +.popup-menu-item-set-primary-audio-button { +    flex-flow: row nowrap; +    align-items: center; +    justify-content: center; +    position: absolute; +    right: 0; +    top: 0; +    bottom: 0; +    width: 2.5em; +} +.popup-menu-item-set-primary-audio-button:not([hidden]) { +    display: flex; +} +.popup-menu-item-set-primary-audio-button .popup-menu-item-icon { +    opacity: 0; +    transition: opacity var(--animation-duration) linear; +} +.popup-menu-item-group:hover .popup-menu-item-set-primary-audio-button .popup-menu-item-icon { +    opacity: 0.25; +} +.popup-menu-item-group .popup-menu-item-set-primary-audio-button:hover .popup-menu-item-icon, +.popup-menu-item-group .popup-menu-item-set-primary-audio-button:active .popup-menu-item-icon, +.popup-menu-item-group .popup-menu-item-set-primary-audio-button:focus .popup-menu-item-icon { +    opacity: 0.375; +} +.popup-menu-item-group[data-is-primary-card-audio=true] .popup-menu-item-set-primary-audio-button .popup-menu-item-icon { +    opacity: 1; +} +.popup-menu-item-group[data-is-primary-card-audio=true] .popup-menu-item-set-primary-audio-button:hover .popup-menu-item-icon, +.popup-menu-item-group[data-is-primary-card-audio=true] .popup-menu-item-set-primary-audio-button:active .popup-menu-item-icon, +.popup-menu-item-group[data-is-primary-card-audio=true] .popup-menu-item-set-primary-audio-button:focus .popup-menu-item-icon { +    opacity: 1; +}  /* Anki errors */ diff --git a/ext/css/material.css b/ext/css/material.css index 2c7195cb..f9ab59bf 100644 --- a/ext/css/material.css +++ b/ext/css/material.css @@ -947,11 +947,11 @@ button.icon-button:active {  }  button.popup-menu-item {      padding: 0.625em 1.5em; +    flex: 1 1 auto;      border-radius: 0;      background-color: transparent;      color: var(--text-color);      border: none; -    width: 100%;      text-align: left;      font-size: 1em;      font-weight: normal; @@ -977,12 +977,14 @@ button.popup-menu-item:disabled {      width: calc(16em / 14);      height: calc(16em / 14);      background-color: var(--text-color); -    margin-right: 0.5em;      flex: 0 0 auto;  }  .popup-menu-item-icon:not([hidden]) {      display: block;  } +.popup-menu-item-icon+.popup-menu-item-label { +    margin-left: 0.5em; +}  :root[data-page-type=popup] .popup-menu.popup-menu-auto-size,  .popup-menu.popup-menu-small {      border-radius: calc(var(--menu-border-radius) * 0.75); @@ -995,6 +997,7 @@ button.popup-menu-item:disabled {      font-size: var(--font-size-small);  }  .popup-menu-item-group { +    position: relative;      display: flex;      flex-flow: row nowrap;      align-items: stretch; diff --git a/ext/display-templates.html b/ext/display-templates.html index 82a9b97f..3074e287 100644 --- a/ext/display-templates.html +++ b/ext/display-templates.html @@ -158,6 +158,9 @@  <!-- 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"></div></div></div></template> -<template id="audio-button-popup-menu-item-template"><div class="popup-menu-item-group"><button class="popup-menu-item" data-menu-action="playAudioFromSource"><div class="popup-menu-item-icon icon" data-icon="none"></div><span class="popup-menu-item-label"></span></button></div></template> +<template id="audio-button-popup-menu-item-template"><div class="popup-menu-item-group"> +    <button class="popup-menu-item popup-menu-item-audio-button" data-menu-action="playAudioFromSource"><div class="popup-menu-item-icon icon" data-icon="none"></div><span class="popup-menu-item-label"></span></button> +    <button class="popup-menu-item popup-menu-item-set-primary-audio-button" data-menu-action="setPrimaryAudio" title="Use as audio for Anki card"><div class="popup-menu-item-icon icon" data-icon="note-card"></div></button> +</div></template>  </body></html> diff --git a/ext/js/display/display-audio.js b/ext/js/display/display-audio.js index 0ccb4eef..cbc7cffc 100644 --- a/ext/js/display/display-audio.js +++ b/ext/js/display/display-audio.js @@ -166,6 +166,12 @@ class DisplayAudio {          }      } +    getPrimaryCardAudio(expression, reading) { +        const cacheEntry = this._getCacheItem(expression, reading, false); +        const primaryCardAudio = typeof cacheEntry !== 'undefined' ? cacheEntry.primaryCardAudio : null; +        return primaryCardAudio; +    } +      // Private      _onAudioPlayButtonClick(definitionIndex, expressionIndex, e) { @@ -185,27 +191,78 @@ class DisplayAudio {      }      _onAudioPlayMenuCloseClick(definitionIndex, expressionIndex, e) { -        const {detail: {action, item}} = e; +        const {detail: {action, item, menu}} = e;          switch (action) {              case 'playAudioFromSource': -                { -                    const group = item.closest('.popup-menu-item-group'); -                    if (group === null) { break; } - -                    const {source, index} = group.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); -                } +                this._playAudioFromSource(definitionIndex, expressionIndex, item, menu); +                break; +            case 'setPrimaryAudio': +                e.preventDefault(); +                this._setPrimaryAudio(definitionIndex, expressionIndex, item, menu, true);                  break;          }      } +    _getCacheItem(expression, reading, create) { +        const key = this._getExpressionReadingKey(expression, reading); +        let cacheEntry = this._cache.get(key); +        if (typeof cacheEntry === 'undefined' && create) { +            cacheEntry = { +                sourceMap: new Map(), +                primaryCardAudio: null +            }; +            this._cache.set(key, cacheEntry); +        } +        return cacheEntry; +    } + +    _getMenuItemSourceInfo(item) { +        const group = item.closest('.popup-menu-item-group'); +        if (group === null) { return null; } + +        let {source, index} = group.dataset; +        if (typeof index !== 'undefined') { +            index = Number.parseInt(index, 10); +        } +        const hasIndex = (Number.isFinite(index) && Math.floor(index) === index); +        if (!hasIndex) { +            index = 0; +        } +        return {source, index, hasIndex}; +    } + +    _playAudioFromSource(definitionIndex, expressionIndex, item, menu) { +        const sourceInfo = this._getMenuItemSourceInfo(item); +        if (sourceInfo === null) { return; } + +        const {source, index, hasIndex} = sourceInfo; +        const sourceDetailsMap = hasIndex ? new Map([[source, {start: index, end: index + 1}]]) : null; + +        this._setPrimaryAudio(definitionIndex, expressionIndex, item, menu, false); + +        this.playAudio(definitionIndex, expressionIndex, [source], sourceDetailsMap); +    } + +    _setPrimaryAudio(definitionIndex, expressionIndex, item, menu, canToggleOff) { +        const sourceInfo = this._getMenuItemSourceInfo(item); +        if (sourceInfo === null) { return; } + +        const {source, index} = sourceInfo; +        if (!this._sourceIsDownloadable(source)) { return; } + +        const expressionReading = this._getExpressionAndReading(definitionIndex, expressionIndex); +        if (expressionReading === null) { return; } + +        const {expression, reading} = expressionReading; +        const cacheEntry = this._getCacheItem(expression, reading, true); + +        let {primaryCardAudio} = cacheEntry; +        primaryCardAudio = (!canToggleOff || primaryCardAudio === null || primaryCardAudio.source !== source || primaryCardAudio.index !== index) ? {source, index} : null; +        cacheEntry.primaryCardAudio = primaryCardAudio; + +        this._updateMenuPrimaryCardAudio(menu.bodyNode, expression, reading); +    } +      _getAudioPlayButtonExpressionIndex(button) {          const expressionNode = button.closest('.term-expression');          if (expressionNode !== null) { @@ -229,13 +286,7 @@ class DisplayAudio {      }      async _createExpressionAudio(sources, sourceDetailsMap, expression, reading, details) { -        const key = this._getExpressionReadingKey(expression, reading); - -        let sourceMap = this._cache.get(key); -        if (typeof sourceMap === 'undefined') { -            sourceMap = new Map(); -            this._cache.set(key, sourceMap); -        } +        const {sourceMap} = this._getCacheItem(expression, reading, true);          for (let i = 0, ii = sources.length; i < ii; ++i) {              const source = sources[i]; @@ -383,10 +434,10 @@ class DisplayAudio {      }      _getPotentialAvailableAudioCount(expression, reading) { -        const key = this._getExpressionReadingKey(expression, reading); -        const sourceMap = this._cache.get(key); -        if (typeof sourceMap === 'undefined') { return null; } +        const cacheEntry = this._getCacheItem(expression, reading, false); +        if (typeof cacheEntry === 'undefined') { return null; } +        const {sourceMap} = cacheEntry;          let count = 0;          for (const {infoList} of sourceMap.values()) {              if (infoList === null) { continue; } @@ -408,6 +459,16 @@ class DisplayAudio {          popupMenu.prepare();      } +    _sourceIsDownloadable(source) { +        switch (source) { +            case 'text-to-speech': +            case 'text-to-speech-reading': +                return false; +            default: +                return true; +        } +    } +      _getAudioSources(audioOptions) {          const {sources, textToSpeechVoice, customSourceUrl} = audioOptions;          const ttsSupported = (textToSpeechVoice.length > 0); @@ -431,6 +492,7 @@ class DisplayAudio {          const results = [];          for (const [source, displayName, supported] of rawSources) {              if (!supported) { continue; } +            const downloadable = this._sourceIsDownloadable(source);              let optionsIndex = sourceIndexMap.get(source);              const isInOptions = typeof optionsIndex !== 'undefined';              if (!isInOptions) { @@ -441,7 +503,8 @@ class DisplayAudio {                  displayName,                  index: results.length,                  optionsIndex, -                isInOptions +                isInOptions, +                downloadable              });          } @@ -461,24 +524,27 @@ class DisplayAudio {          // Create menu          const {displayGenerator} = this._display;          const menuNode = displayGenerator.instantiateTemplate('audio-button-popup-menu'); -        const menuBody = menuNode.querySelector('.popup-menu-body'); +        const menuBodyNode = menuNode.querySelector('.popup-menu-body');          // Set up items based on options and cache data          let showIcons = false; -        for (const {source, displayName, isInOptions} of sources) { +        for (const {source, displayName, isInOptions, downloadable} of sources) {              const entries = this._getMenuItemEntries(source, expression, reading);              for (let i = 0, ii = entries.length; i < ii; ++i) {                  const {valid, index, name} = entries[i];                  const node = displayGenerator.instantiateTemplate('audio-button-popup-menu-item'); -                const labelNode = node.querySelector('.popup-menu-item-label'); +                const labelNode = node.querySelector('.popup-menu-item-audio-button .popup-menu-item-label');                  let label = displayName;                  if (ii > 1) { label = `${label} ${i + 1}`; }                  if (typeof name === 'string' && name.length > 0) { label += `: ${name}`; }                  labelNode.textContent = label; +                const cardButton = node.querySelector('.popup-menu-item-set-primary-audio-button'); +                cardButton.hidden = !downloadable; +                  if (valid !== null) { -                    const icon = node.querySelector('.popup-menu-item-icon'); +                    const icon = node.querySelector('.popup-menu-item-audio-button .popup-menu-item-icon');                      icon.dataset.icon = valid ? 'checkmark' : 'cross';                      showIcons = true;                  } @@ -488,20 +554,25 @@ class DisplayAudio {                  }                  node.dataset.valid = `${valid}`;                  node.dataset.sourceInOptions = `${isInOptions}`; +                node.dataset.downloadable = `${downloadable}`; -                menuBody.appendChild(node); +                menuBodyNode.appendChild(node);              }          }          menuNode.dataset.showIcons = `${showIcons}`; +        // Update primary card audio display +        this._updateMenuPrimaryCardAudio(menuBodyNode, expression, reading); +          // Create popup menu          this._menuContainer.appendChild(menuNode);          return new PopupMenu(sourceButton, menuNode);      }      _getMenuItemEntries(source, expression, reading) { -        const sourceMap = this._cache.get(this._getExpressionReadingKey(expression, reading)); -        if (typeof sourceMap !== 'undefined') { +        const cacheEntry = this._getCacheItem(expression, reading, false); +        if (typeof cacheEntry !== 'undefined') { +            const {sourceMap} = cacheEntry;              const sourceInfo = sourceMap.get(source);              if (typeof sourceInfo !== 'undefined') {                  const {infoList} = sourceInfo; @@ -524,4 +595,25 @@ class DisplayAudio {          }          return [{valid: null, index: null, name: null}];      } + +    _updateMenuPrimaryCardAudio(menuBodyNode, expression, reading) { +        const primaryCardAudio = this.getPrimaryCardAudio(expression, reading); +        const {source: primaryCardAudioSource, index: primaryCardAudioIndex} = (primaryCardAudio !== null ? primaryCardAudio : {source: null, index: -1}); + +        const itemGroups = menuBodyNode.querySelectorAll('.popup-menu-item-group'); +        let sourceIndex = 0; +        let sourcePre = null; +        for (const node of itemGroups) { +            const {source} = node.dataset; +            if (source !== sourcePre) { +                sourcePre = source; +                sourceIndex = 0; +            } else { +                ++sourceIndex; +            } + +            const isPrimaryCardAudio = (source === primaryCardAudioSource && sourceIndex === primaryCardAudioIndex); +            node.dataset.isPrimaryCardAudio = `${isPrimaryCardAudio}`; +        } +    }  } diff --git a/ext/js/display/display.js b/ext/js/display/display.js index 6a2a3766..35f22718 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -1452,7 +1452,7 @@ class Display extends EventDispatcher {          let injectedMedia = null;          if (injectMedia) {              let errors2; -            ({result: injectedMedia, errors: errors2} = await this._injectAnkiNoteMedia(definition, mode, options, fields)); +            ({result: injectedMedia, errors: errors2} = await this._injectAnkiNoteMedia(definition, options, fields));              if (Array.isArray(errors)) {                  for (const error of errors2) {                      errors.push(deserializeError(error)); @@ -1479,20 +1479,35 @@ class Display extends EventDispatcher {          });      } -    async _injectAnkiNoteMedia(definition, mode, options, fields) { +    async _injectAnkiNoteMedia(definition, options, fields) {          const {              anki: {screenshot: {format, quality}},              audio: {sources, customSourceUrl, customSourceType}          } = options;          const timestamp = Date.now(); +          const definitionDetails = this._getDefinitionDetailsForNote(definition); -        const audioDetails = (mode !== 'kanji' && this._ankiNoteBuilder.containsMarker(fields, 'audio') ? {sources, preferredAudioIndex: null, customSourceUrl, customSourceType} : null); + +        let audioDetails = null; +        if (definitionDetails.type !== 'kanji' && this._ankiNoteBuilder.containsMarker(fields, 'audio')) { +            const primaryCardAudio = this._displayAudio.getPrimaryCardAudio(definitionDetails.expression, definitionDetails.reading); +            let preferredAudioIndex = null; +            let sources2 = sources; +            if (primaryCardAudio !== null) { +                sources2 = [primaryCardAudio.source]; +                preferredAudioIndex = primaryCardAudio.index; +            } +            audioDetails = {sources: sources2, preferredAudioIndex, customSourceUrl, customSourceType}; +        } +          const screenshotDetails = (this._ankiNoteBuilder.containsMarker(fields, 'screenshot') ? {tabId: this._contentOriginTabId, frameId: this._contentOriginFrameId, format, quality} : null); +          const clipboardDetails = {              image: this._ankiNoteBuilder.containsMarker(fields, 'clipboard-image'),              text: this._ankiNoteBuilder.containsMarker(fields, 'clipboard-text')          }; +          return await yomichan.api.injectAnkiNoteMedia(              timestamp,              definitionDetails, |