diff options
author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2021-02-15 21:34:10 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-02-15 21:34:10 -0500 |
commit | 55f5182ca93778b74105c9c9097174d3138cad9e (patch) | |
tree | fd02c62d1de5accd2a4d1ddfa37cd5a568314ce5 | |
parent | f2a387237bac02d93d1664ed7acb6a10108915b6 (diff) |
Audio popup menu primary card audio selection (#1406)
* Add card icon to audio menu items
* Update cache data format
* Create _getCacheItem
* Add _playAudioFromSource function
* Implement default card audio info
* Specify exact audio to download when an override is assigned
* Abstract using _getMenuItemSourceInfo
* Update downloadability check
* Update the main audio menu buttons to also assign the default source
-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, |