summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2021-02-15 21:34:10 -0500
committerGitHub <noreply@github.com>2021-02-15 21:34:10 -0500
commit55f5182ca93778b74105c9c9097174d3138cad9e (patch)
treefd02c62d1de5accd2a4d1ddfa37cd5a568314ce5
parentf2a387237bac02d93d1664ed7acb6a10108915b6 (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.css38
-rw-r--r--ext/css/material.css7
-rw-r--r--ext/display-templates.html5
-rw-r--r--ext/js/display/display-audio.js158
-rw-r--r--ext/js/display/display.js21
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,