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
This commit is contained in:
toasted-nutbread 2021-02-15 21:34:10 -05:00 committed by GitHub
parent f2a387237b
commit 55f5182ca9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 189 additions and 40 deletions

View File

@ -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 */

View File

@ -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;

View File

@ -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>

View File

@ -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}`;
}
}
}

View File

@ -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,