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 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; display: none;
} }
.audio-button-popup-menu .popup-menu-item-icon[data-icon=checkmark] { .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 { .audio-button-popup-menu .popup-menu-item-group[data-source-in-options=false][data-valid=null] .popup-menu-item {
color: var(--text-color-light1); 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 */ /* Anki errors */

View File

@ -947,11 +947,11 @@ button.icon-button:active {
} }
button.popup-menu-item { button.popup-menu-item {
padding: 0.625em 1.5em; padding: 0.625em 1.5em;
flex: 1 1 auto;
border-radius: 0; border-radius: 0;
background-color: transparent; background-color: transparent;
color: var(--text-color); color: var(--text-color);
border: none; border: none;
width: 100%;
text-align: left; text-align: left;
font-size: 1em; font-size: 1em;
font-weight: normal; font-weight: normal;
@ -977,12 +977,14 @@ button.popup-menu-item:disabled {
width: calc(16em / 14); width: calc(16em / 14);
height: calc(16em / 14); height: calc(16em / 14);
background-color: var(--text-color); background-color: var(--text-color);
margin-right: 0.5em;
flex: 0 0 auto; flex: 0 0 auto;
} }
.popup-menu-item-icon:not([hidden]) { .popup-menu-item-icon:not([hidden]) {
display: block; 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, :root[data-page-type=popup] .popup-menu.popup-menu-auto-size,
.popup-menu.popup-menu-small { .popup-menu.popup-menu-small {
border-radius: calc(var(--menu-border-radius) * 0.75); border-radius: calc(var(--menu-border-radius) * 0.75);
@ -995,6 +997,7 @@ button.popup-menu-item:disabled {
font-size: var(--font-size-small); font-size: var(--font-size-small);
} }
.popup-menu-item-group { .popup-menu-item-group {
position: relative;
display: flex; display: flex;
flex-flow: row nowrap; flex-flow: row nowrap;
align-items: stretch; align-items: stretch;

View File

@ -158,6 +158,9 @@
<!-- Popup menu --> <!-- 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-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> </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 // Private
_onAudioPlayButtonClick(definitionIndex, expressionIndex, e) { _onAudioPlayButtonClick(definitionIndex, expressionIndex, e) {
@ -185,27 +191,78 @@ class DisplayAudio {
} }
_onAudioPlayMenuCloseClick(definitionIndex, expressionIndex, e) { _onAudioPlayMenuCloseClick(definitionIndex, expressionIndex, e) {
const {detail: {action, item}} = e; const {detail: {action, item, menu}} = e;
switch (action) { switch (action) {
case 'playAudioFromSource': case 'playAudioFromSource':
{ this._playAudioFromSource(definitionIndex, expressionIndex, item, menu);
const group = item.closest('.popup-menu-item-group'); break;
if (group === null) { break; } case 'setPrimaryAudio':
e.preventDefault();
const {source, index} = group.dataset; this._setPrimaryAudio(definitionIndex, expressionIndex, item, menu, true);
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; 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) { _getAudioPlayButtonExpressionIndex(button) {
const expressionNode = button.closest('.term-expression'); const expressionNode = button.closest('.term-expression');
if (expressionNode !== null) { if (expressionNode !== null) {
@ -229,13 +286,7 @@ class DisplayAudio {
} }
async _createExpressionAudio(sources, sourceDetailsMap, expression, reading, details) { async _createExpressionAudio(sources, sourceDetailsMap, expression, reading, details) {
const key = this._getExpressionReadingKey(expression, reading); const {sourceMap} = this._getCacheItem(expression, reading, true);
let sourceMap = this._cache.get(key);
if (typeof sourceMap === 'undefined') {
sourceMap = new Map();
this._cache.set(key, sourceMap);
}
for (let i = 0, ii = sources.length; i < ii; ++i) { for (let i = 0, ii = sources.length; i < ii; ++i) {
const source = sources[i]; const source = sources[i];
@ -383,10 +434,10 @@ class DisplayAudio {
} }
_getPotentialAvailableAudioCount(expression, reading) { _getPotentialAvailableAudioCount(expression, reading) {
const key = this._getExpressionReadingKey(expression, reading); const cacheEntry = this._getCacheItem(expression, reading, false);
const sourceMap = this._cache.get(key); if (typeof cacheEntry === 'undefined') { return null; }
if (typeof sourceMap === 'undefined') { return null; }
const {sourceMap} = cacheEntry;
let count = 0; let count = 0;
for (const {infoList} of sourceMap.values()) { for (const {infoList} of sourceMap.values()) {
if (infoList === null) { continue; } if (infoList === null) { continue; }
@ -408,6 +459,16 @@ class DisplayAudio {
popupMenu.prepare(); popupMenu.prepare();
} }
_sourceIsDownloadable(source) {
switch (source) {
case 'text-to-speech':
case 'text-to-speech-reading':
return false;
default:
return true;
}
}
_getAudioSources(audioOptions) { _getAudioSources(audioOptions) {
const {sources, textToSpeechVoice, customSourceUrl} = audioOptions; const {sources, textToSpeechVoice, customSourceUrl} = audioOptions;
const ttsSupported = (textToSpeechVoice.length > 0); const ttsSupported = (textToSpeechVoice.length > 0);
@ -431,6 +492,7 @@ class DisplayAudio {
const results = []; const results = [];
for (const [source, displayName, supported] of rawSources) { for (const [source, displayName, supported] of rawSources) {
if (!supported) { continue; } if (!supported) { continue; }
const downloadable = this._sourceIsDownloadable(source);
let optionsIndex = sourceIndexMap.get(source); let optionsIndex = sourceIndexMap.get(source);
const isInOptions = typeof optionsIndex !== 'undefined'; const isInOptions = typeof optionsIndex !== 'undefined';
if (!isInOptions) { if (!isInOptions) {
@ -441,7 +503,8 @@ class DisplayAudio {
displayName, displayName,
index: results.length, index: results.length,
optionsIndex, optionsIndex,
isInOptions isInOptions,
downloadable
}); });
} }
@ -461,24 +524,27 @@ class DisplayAudio {
// Create menu // Create menu
const {displayGenerator} = this._display; const {displayGenerator} = this._display;
const menuNode = displayGenerator.instantiateTemplate('audio-button-popup-menu'); 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 // Set up items based on options and cache data
let showIcons = false; 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); const entries = this._getMenuItemEntries(source, expression, reading);
for (let i = 0, ii = entries.length; i < ii; ++i) { for (let i = 0, ii = entries.length; i < ii; ++i) {
const {valid, index, name} = entries[i]; const {valid, index, name} = entries[i];
const node = displayGenerator.instantiateTemplate('audio-button-popup-menu-item'); 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; let label = displayName;
if (ii > 1) { label = `${label} ${i + 1}`; } if (ii > 1) { label = `${label} ${i + 1}`; }
if (typeof name === 'string' && name.length > 0) { label += `: ${name}`; } if (typeof name === 'string' && name.length > 0) { label += `: ${name}`; }
labelNode.textContent = label; labelNode.textContent = label;
const cardButton = node.querySelector('.popup-menu-item-set-primary-audio-button');
cardButton.hidden = !downloadable;
if (valid !== null) { 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'; icon.dataset.icon = valid ? 'checkmark' : 'cross';
showIcons = true; showIcons = true;
} }
@ -488,20 +554,25 @@ class DisplayAudio {
} }
node.dataset.valid = `${valid}`; node.dataset.valid = `${valid}`;
node.dataset.sourceInOptions = `${isInOptions}`; node.dataset.sourceInOptions = `${isInOptions}`;
node.dataset.downloadable = `${downloadable}`;
menuBody.appendChild(node); menuBodyNode.appendChild(node);
} }
} }
menuNode.dataset.showIcons = `${showIcons}`; menuNode.dataset.showIcons = `${showIcons}`;
// Update primary card audio display
this._updateMenuPrimaryCardAudio(menuBodyNode, expression, reading);
// Create popup menu // Create popup menu
this._menuContainer.appendChild(menuNode); this._menuContainer.appendChild(menuNode);
return new PopupMenu(sourceButton, menuNode); return new PopupMenu(sourceButton, menuNode);
} }
_getMenuItemEntries(source, expression, reading) { _getMenuItemEntries(source, expression, reading) {
const sourceMap = this._cache.get(this._getExpressionReadingKey(expression, reading)); const cacheEntry = this._getCacheItem(expression, reading, false);
if (typeof sourceMap !== 'undefined') { if (typeof cacheEntry !== 'undefined') {
const {sourceMap} = cacheEntry;
const sourceInfo = sourceMap.get(source); const sourceInfo = sourceMap.get(source);
if (typeof sourceInfo !== 'undefined') { if (typeof sourceInfo !== 'undefined') {
const {infoList} = sourceInfo; const {infoList} = sourceInfo;
@ -524,4 +595,25 @@ class DisplayAudio {
} }
return [{valid: null, index: null, name: null}]; 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; let injectedMedia = null;
if (injectMedia) { if (injectMedia) {
let errors2; 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)) { if (Array.isArray(errors)) {
for (const error of errors2) { for (const error of errors2) {
errors.push(deserializeError(error)); errors.push(deserializeError(error));
@ -1479,20 +1479,35 @@ class Display extends EventDispatcher {
}); });
} }
async _injectAnkiNoteMedia(definition, mode, options, fields) { async _injectAnkiNoteMedia(definition, options, fields) {
const { const {
anki: {screenshot: {format, quality}}, anki: {screenshot: {format, quality}},
audio: {sources, customSourceUrl, customSourceType} audio: {sources, customSourceUrl, customSourceType}
} = options; } = options;
const timestamp = Date.now(); const timestamp = Date.now();
const definitionDetails = this._getDefinitionDetailsForNote(definition); 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 screenshotDetails = (this._ankiNoteBuilder.containsMarker(fields, 'screenshot') ? {tabId: this._contentOriginTabId, frameId: this._contentOriginFrameId, format, quality} : null);
const clipboardDetails = { const clipboardDetails = {
image: this._ankiNoteBuilder.containsMarker(fields, 'clipboard-image'), image: this._ankiNoteBuilder.containsMarker(fields, 'clipboard-image'),
text: this._ankiNoteBuilder.containsMarker(fields, 'clipboard-text') text: this._ankiNoteBuilder.containsMarker(fields, 'clipboard-text')
}; };
return await yomichan.api.injectAnkiNoteMedia( return await yomichan.api.injectAnkiNoteMedia(
timestamp, timestamp,
definitionDetails, definitionDetails,