aboutsummaryrefslogtreecommitdiff
path: root/ext/js/display
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 /ext/js/display
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
Diffstat (limited to 'ext/js/display')
-rw-r--r--ext/js/display/display-audio.js158
-rw-r--r--ext/js/display/display.js21
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,