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:
parent
f2a387237b
commit
55f5182ca9
@ -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 */
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user