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 /ext/js | |
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
Diffstat (limited to 'ext/js')
-rw-r--r-- | ext/js/display/display-audio.js | 158 | ||||
-rw-r--r-- | ext/js/display/display.js | 21 |
2 files changed, 143 insertions, 36 deletions
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, |