Refactor display audio options (#1717)

* Update how options are updated and stored in DisplayAudio

* Add source list

* Improve menus for custom json

* Clear cache after options update

* Move function

* Update public API

* Simplify playing audio from a specific source

* Simplify audio list

* Refactor audio source usage

* Refactoring

* Refactor argument names

* Fix incorrect source usage

* Remove unused

* Remove return value

* Simplify details

* Simplify Anki card audio details

* Update the data that is passed to AudioDownloader

* Simplify schema handling

* Remove unnecessary details
This commit is contained in:
toasted-nutbread 2021-05-30 12:15:07 -04:00 committed by GitHub
parent 0f0e80aadb
commit efd35de67f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 279 additions and 292 deletions

View File

@ -519,8 +519,8 @@ class Backend {
return this._runCommand(command, params); return this._runCommand(command, params);
} }
async _onApiGetTermAudioInfoList({source, term, reading, details}) { async _onApiGetTermAudioInfoList({source, term, reading}) {
return await this._audioDownloader.getTermAudioInfoList(source, term, reading, details); return await this._audioDownloader.getTermAudioInfoList(source, term, reading);
} }
_onApiSendMessageToFrame({frameId: targetFrameId, action, params}, sender) { _onApiSendMessageToFrame({frameId: targetFrameId, action, params}, sender) {
@ -1742,7 +1742,7 @@ class Backend {
return null; return null;
} }
const {sources, preferredAudioIndex, customSourceUrl} = details; const {sources, preferredAudioIndex} = details;
let data; let data;
let contentType; let contentType;
try { try {
@ -1750,13 +1750,7 @@ class Backend {
sources, sources,
preferredAudioIndex, preferredAudioIndex,
term, term,
reading, reading
{
textToSpeechVoice: null,
customSourceUrl,
binary: true,
disableCache: true
}
)); ));
} catch (e) { } catch (e) {
// No audio // No audio

View File

@ -68,8 +68,8 @@ class API {
return this._invoke('suspendAnkiCardsForNote', {noteId}); return this._invoke('suspendAnkiCardsForNote', {noteId});
} }
getTermAudioInfoList(source, term, reading, details) { getTermAudioInfoList(source, term, reading) {
return this._invoke('getTermAudioInfoList', {source, term, reading, details}); return this._invoke('getTermAudioInfoList', {source, term, reading});
} }
commandExec(command, params) { commandExec(command, params) {

View File

@ -25,6 +25,8 @@ class DisplayAudio {
this._display = display; this._display = display;
this._audioPlaying = null; this._audioPlaying = null;
this._audioSystem = new AudioSystem(); this._audioSystem = new AudioSystem();
this._playbackVolume = 1.0;
this._autoPlay = false;
this._autoPlayAudioTimer = null; this._autoPlayAudioTimer = null;
this._autoPlayAudioDelay = 400; this._autoPlayAudioDelay = 400;
this._eventListeners = new EventListenerCollection(); this._eventListeners = new EventListenerCollection();
@ -32,6 +34,16 @@ class DisplayAudio {
this._menuContainer = document.querySelector('#popup-menus'); this._menuContainer = document.querySelector('#popup-menus');
this._entriesToken = {}; this._entriesToken = {};
this._openMenus = new Set(); this._openMenus = new Set();
this._audioSources = [];
this._audioSourceTypeNames = new Map([
['jpod101', 'JapanesePod101'],
['jpod101-alternate', 'JapanesePod101 (Alternate)'],
['jisho', 'Jisho.org'],
['text-to-speech', 'Text-to-speech'],
['text-to-speech-reading', 'Text-to-speech (Kana reading)'],
['custom', 'Custom URL'],
['custom-json', 'Custom URL (JSON)']
]);
} }
get autoPlayAudioDelay() { get autoPlayAudioDelay() {
@ -44,11 +56,8 @@ class DisplayAudio {
prepare() { prepare() {
this._audioSystem.prepare(); this._audioSystem.prepare();
} this._display.on('optionsUpdated', this._onOptionsUpdated.bind(this));
this._onOptionsUpdated({options: this._display.getOptions()});
updateOptions(options) {
const data = document.documentElement.dataset;
data.audioEnabled = `${options.audio.enabled && options.audio.sources.length > 0}`;
} }
cleanupEntries() { cleanupEntries() {
@ -68,8 +77,7 @@ class DisplayAudio {
} }
setupEntriesComplete() { setupEntriesComplete() {
const audioOptions = this._getAudioOptions(); if (!this._autoPlay) { return; }
if (!audioOptions.enabled || !audioOptions.autoPlay) { return; }
this.clearAutoPlayTimer(); this.clearAutoPlayTimer();
@ -103,85 +111,81 @@ class DisplayAudio {
this._audioPlaying = null; this._audioPlaying = null;
} }
async playAudio(dictionaryEntryIndex, headwordIndex, sources=null, sourceDetailsMap=null) { async playAudio(dictionaryEntryIndex, headwordIndex, sourceType=null) {
this.stopAudio(); let sources = this._audioSources;
this.clearAutoPlayTimer(); if (sourceType !== null) {
sources = [];
const headword = this._getHeadword(dictionaryEntryIndex, headwordIndex); for (const source of this._audioSources) {
if (headword === null) { if (source.type === sourceType) {
return {audio: null, source: null, valid: false}; sources.push(source);
}
}
}
await this._playAudio(dictionaryEntryIndex, headwordIndex, sources, null);
} }
const buttons = this._getAudioPlayButtons(dictionaryEntryIndex, headwordIndex); getAnkiNoteMediaAudioDetails(term, reading) {
const sources = [];
const {term, reading} = headword; let preferredAudioIndex = null;
const audioOptions = this._getAudioOptions(); const primaryCardAudio = this._getPrimaryCardAudio(term, reading);
const {textToSpeechVoice, customSourceUrl, volume} = audioOptions; if (primaryCardAudio !== null) {
if (!Array.isArray(sources)) { const {index, subIndex} = primaryCardAudio;
({sources} = audioOptions); const source = this._audioSources[index];
} sources.push(this._getSourceData(source));
if (!(sourceDetailsMap instanceof Map)) { preferredAudioIndex = subIndex;
sourceDetailsMap = null;
}
const progressIndicatorVisible = this._display.progressIndicatorVisible;
const overrideToken = progressIndicatorVisible.setOverride(true);
try {
// Create audio
let audio;
let title;
let source = null;
const info = await this._createTermAudio(sources, sourceDetailsMap, term, reading, {textToSpeechVoice, customSourceUrl});
const valid = (info !== null);
if (valid) {
({audio, source} = info);
const sourceIndex = sources.indexOf(source);
title = `From source ${1 + sourceIndex}: ${source}`;
} else { } else {
audio = this._audioSystem.getFallbackAudio(); for (const source of this._audioSources) {
title = 'Could not find audio'; if (!source.isInOptions) { continue; }
} sources.push(this._getSourceData(source));
// Stop any currently playing audio
this.stopAudio();
// Update details
const potentialAvailableAudioCount = this._getPotentialAvailableAudioCount(term, reading);
for (const button of buttons) {
const titleDefault = button.dataset.titleDefault || '';
button.title = `${titleDefault}\n${title}`;
this._updateAudioPlayButtonBadge(button, potentialAvailableAudioCount);
}
// Play
audio.currentTime = 0;
audio.volume = Number.isFinite(volume) ? Math.max(0.0, Math.min(1.0, volume / 100.0)) : 1.0;
const playPromise = audio.play();
this._audioPlaying = audio;
if (typeof playPromise !== 'undefined') {
try {
await playPromise;
} catch (e) {
// NOP
} }
} }
return {sources, preferredAudioIndex};
return {audio, source, valid};
} finally {
progressIndicatorVisible.clearOverride(overrideToken);
}
}
getPrimaryCardAudio(term, reading) {
const cacheEntry = this._getCacheItem(term, reading, false);
const primaryCardAudio = typeof cacheEntry !== 'undefined' ? cacheEntry.primaryCardAudio : null;
return primaryCardAudio;
} }
// Private // Private
_onOptionsUpdated({options}) {
if (options === null) { return; }
const {enabled, autoPlay, textToSpeechVoice, customSourceUrl, volume, sources} = options.audio;
this._autoPlay = enabled && autoPlay;
this._playbackVolume = Number.isFinite(volume) ? Math.max(0.0, Math.min(1.0, volume / 100.0)) : 1.0;
const requiredAudioSources = new Set([
'jpod101',
'jpod101-alternate',
'jisho'
]);
this._audioSources.length = 0;
for (const type of sources) {
this._addAudioSourceInfo(type, customSourceUrl, textToSpeechVoice, true);
requiredAudioSources.delete(type);
}
for (const type of requiredAudioSources) {
this._addAudioSourceInfo(type, '', '', false);
}
const data = document.documentElement.dataset;
data.audioEnabled = `${enabled && sources.length > 0}`;
this._cache.clear();
}
_addAudioSourceInfo(type, url, voice, isInOptions) {
const index = this._audioSources.length;
const downloadable = this._sourceIsDownloadable(type);
let displayName = this._audioSourceTypeNames.get(type);
if (typeof displayName === 'undefined') { displayName = 'Unknown'; }
this._audioSources.push({
index,
type,
url,
voice,
isInOptions,
downloadable,
displayName
});
}
_onAudioPlayButtonClick(dictionaryEntryIndex, headwordIndex, e) { _onAudioPlayButtonClick(dictionaryEntryIndex, headwordIndex, e) {
e.preventDefault(); e.preventDefault();
@ -229,29 +233,93 @@ class DisplayAudio {
_getMenuItemSourceInfo(item) { _getMenuItemSourceInfo(item) {
const group = item.closest('.popup-menu-item-group'); const group = item.closest('.popup-menu-item-group');
if (group === null) { return null; } if (group !== null) {
let {index, subIndex} = group.dataset;
let {source, index} = group.dataset;
if (typeof index !== 'undefined') {
index = Number.parseInt(index, 10); index = Number.parseInt(index, 10);
if (index >= 0 && index < this._audioSources.length) {
const source = this._audioSources[index];
if (typeof subIndex === 'string') {
subIndex = Number.parseInt(subIndex, 10);
} else {
subIndex = null;
} }
const hasIndex = (Number.isFinite(index) && Math.floor(index) === index); return {source, subIndex};
if (!hasIndex) { }
index = 0; }
return {source: null, subIndex: null};
}
async _playAudio(dictionaryEntryIndex, headwordIndex, sources, audioInfoListIndex) {
this.stopAudio();
this.clearAutoPlayTimer();
const headword = this._getHeadword(dictionaryEntryIndex, headwordIndex);
if (headword === null) {
return {audio: null, source: null, valid: false};
}
const buttons = this._getAudioPlayButtons(dictionaryEntryIndex, headwordIndex);
const {term, reading} = headword;
const progressIndicatorVisible = this._display.progressIndicatorVisible;
const overrideToken = progressIndicatorVisible.setOverride(true);
try {
// Create audio
let audio;
let title;
let source = null;
let subIndex = 0;
const info = await this._createTermAudio(term, reading, sources, audioInfoListIndex);
const valid = (info !== null);
if (valid) {
({audio, source, subIndex} = info);
const sourceIndex = sources.indexOf(source);
title = `From source ${1 + sourceIndex}: ${source}`;
} else {
audio = this._audioSystem.getFallbackAudio();
title = 'Could not find audio';
}
// Stop any currently playing audio
this.stopAudio();
// Update details
const potentialAvailableAudioCount = this._getPotentialAvailableAudioCount(term, reading);
for (const button of buttons) {
const titleDefault = button.dataset.titleDefault || '';
button.title = `${titleDefault}\n${title}`;
this._updateAudioPlayButtonBadge(button, potentialAvailableAudioCount);
}
// Play
audio.currentTime = 0;
audio.volume = this._playbackVolume;
const playPromise = audio.play();
this._audioPlaying = audio;
if (typeof playPromise !== 'undefined') {
try {
await playPromise;
} catch (e) {
// NOP
}
}
return {audio, source, subIndex, valid};
} finally {
progressIndicatorVisible.clearOverride(overrideToken);
} }
return {source, index, hasIndex};
} }
async _playAudioFromSource(dictionaryEntryIndex, headwordIndex, item) { async _playAudioFromSource(dictionaryEntryIndex, headwordIndex, item) {
const sourceInfo = this._getMenuItemSourceInfo(item); const {source, subIndex} = this._getMenuItemSourceInfo(item);
if (sourceInfo === null) { return; } if (source === null) { return; }
const {source, index, hasIndex} = sourceInfo;
const sourceDetailsMap = hasIndex ? new Map([[source, {start: index, end: index + 1}]]) : null;
try { try {
const token = this._entriesToken; const token = this._entriesToken;
const {valid} = await this.playAudio(dictionaryEntryIndex, headwordIndex, [source], sourceDetailsMap); const {valid} = await this._playAudio(dictionaryEntryIndex, headwordIndex, [source], subIndex);
if (valid && token === this._entriesToken) { if (valid && token === this._entriesToken) {
this._setPrimaryAudio(dictionaryEntryIndex, headwordIndex, item, null, false); this._setPrimaryAudio(dictionaryEntryIndex, headwordIndex, item, null, false);
} }
@ -261,11 +329,8 @@ class DisplayAudio {
} }
_setPrimaryAudio(dictionaryEntryIndex, headwordIndex, item, menu, canToggleOff) { _setPrimaryAudio(dictionaryEntryIndex, headwordIndex, item, menu, canToggleOff) {
const sourceInfo = this._getMenuItemSourceInfo(item); const {source, subIndex} = this._getMenuItemSourceInfo(item);
if (sourceInfo === null) { return; } if (source === null || !source.downloadable) { return; }
const {source, index} = sourceInfo;
if (!this._sourceIsDownloadable(source)) { return; }
const headword = this._getHeadword(dictionaryEntryIndex, headwordIndex); const headword = this._getHeadword(dictionaryEntryIndex, headwordIndex);
if (headword === null) { return; } if (headword === null) { return; }
@ -274,7 +339,12 @@ class DisplayAudio {
const cacheEntry = this._getCacheItem(term, reading, true); const cacheEntry = this._getCacheItem(term, reading, true);
let {primaryCardAudio} = cacheEntry; let {primaryCardAudio} = cacheEntry;
primaryCardAudio = (!canToggleOff || primaryCardAudio === null || primaryCardAudio.source !== source || primaryCardAudio.index !== index) ? {source, index} : null; primaryCardAudio = (
!canToggleOff ||
primaryCardAudio === null ||
primaryCardAudio.source !== source ||
primaryCardAudio.index !== subIndex
) ? {index: source.index, subIndex} : null;
cacheEntry.primaryCardAudio = primaryCardAudio; cacheEntry.primaryCardAudio = primaryCardAudio;
if (menu !== null) { if (menu !== null) {
@ -304,19 +374,19 @@ class DisplayAudio {
return results; return results;
} }
async _createTermAudio(sources, sourceDetailsMap, term, reading, details) { async _createTermAudio(term, reading, sources, audioInfoListIndex) {
const {sourceMap} = this._getCacheItem(term, reading, true); const {sourceMap} = this._getCacheItem(term, reading, true);
for (let i = 0, ii = sources.length; i < ii; ++i) { for (const source of sources) {
const source = sources[i]; const {index} = source;
let cacheUpdated = false; let cacheUpdated = false;
let infoListPromise; let infoListPromise;
let sourceInfo = sourceMap.get(source); let sourceInfo = sourceMap.get(index);
if (typeof sourceInfo === 'undefined') { if (typeof sourceInfo === 'undefined') {
infoListPromise = this._getTermAudioInfoList(source, term, reading, details); infoListPromise = this._getTermAudioInfoList(source, term, reading);
sourceInfo = {infoListPromise, infoList: null}; sourceInfo = {infoListPromise, infoList: null};
sourceMap.set(source, sourceInfo); sourceMap.set(index, sourceInfo);
cacheUpdated = true; cacheUpdated = true;
} }
@ -326,29 +396,29 @@ class DisplayAudio {
sourceInfo.infoList = infoList; sourceInfo.infoList = infoList;
} }
let start = 0; const {audio, index: subIndex, cacheUpdated: cacheUpdated2} = await this._createAudioFromInfoList(source, infoList, audioInfoListIndex);
let end = infoList.length;
if (sourceDetailsMap !== null) {
const sourceDetails = sourceDetailsMap.get(source);
if (typeof sourceDetails !== 'undefined') {
const {start: start2, end: end2} = sourceDetails;
if (this._isInteger(start2)) { start = this._clamp(start2, start, end); }
if (this._isInteger(end2)) { end = this._clamp(end2, start, end); }
}
}
const {result, cacheUpdated: cacheUpdated2} = await this._createAudioFromInfoList(source, infoList, start, end);
if (cacheUpdated || cacheUpdated2) { this._updateOpenMenu(); } if (cacheUpdated || cacheUpdated2) { this._updateOpenMenu(); }
if (result !== null) { return result; } if (audio !== null) {
return {audio, source, subIndex};
}
} }
return null; return null;
} }
async _createAudioFromInfoList(source, infoList, start, end) { async _createAudioFromInfoList(source, infoList, audioInfoListIndex) {
let result = null; let start = 0;
let cacheUpdated = false; let end = infoList.length;
if (audioInfoListIndex !== null) {
start = Math.max(0, Math.min(end, audioInfoListIndex));
end = Math.max(0, Math.min(end, audioInfoListIndex + 1));
}
const result = {
audio: null,
index: -1,
cacheUpdated: false
};
for (let i = start; i < end; ++i) { for (let i = start; i < end; ++i) {
const item = infoList[i]; const item = infoList[i];
@ -361,7 +431,7 @@ class DisplayAudio {
item.audioPromise = audioPromise; item.audioPromise = audioPromise;
} }
cacheUpdated = true; result.cacheUpdated = true;
try { try {
audio = await audioPromise; audio = await audioPromise;
@ -375,17 +445,18 @@ class DisplayAudio {
} }
if (audio !== null) { if (audio !== null) {
result = {audio, source, infoListIndex: i}; result.audio = audio;
result.index = i;
break; break;
} }
} }
return {result, cacheUpdated}; return result;
} }
async _createAudioFromInfo(info, source) { async _createAudioFromInfo(info, source) {
switch (info.type) { switch (info.type) {
case 'url': case 'url':
return await this._audioSystem.createAudio(info.url, source); return await this._audioSystem.createAudio(info.url, source.type);
case 'tts': case 'tts':
return this._audioSystem.createTextToSpeechAudio(info.text, info.voice); return this._audioSystem.createTextToSpeechAudio(info.text, info.voice);
default: default:
@ -393,8 +464,9 @@ class DisplayAudio {
} }
} }
async _getTermAudioInfoList(source, term, reading, details) { async _getTermAudioInfoList(source, term, reading) {
const infoList = await yomichan.api.getTermAudioInfoList(source, term, reading, details); const sourceData = this._getSourceData(source);
const infoList = await yomichan.api.getTermAudioInfoList(sourceData, term, reading);
return infoList.map((info) => ({info, audioPromise: null, audioResolved: false, audio: null})); return infoList.map((info) => ({info, audioPromise: null, audioResolved: false, audio: null}));
} }
@ -415,22 +487,6 @@ class DisplayAudio {
return JSON.stringify([term, reading]); return JSON.stringify([term, reading]);
} }
_getAudioOptions() {
return this._display.getOptions().audio;
}
_isInteger(value) {
return (
typeof value === 'number' &&
Number.isFinite(value) &&
Math.floor(value) === value
);
}
_clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
_updateAudioPlayButtonBadge(button, potentialAvailableAudioCount) { _updateAudioPlayButtonBadge(button, potentialAvailableAudioCount) {
if (potentialAvailableAudioCount === null) { if (potentialAvailableAudioCount === null) {
delete button.dataset.potentialAvailableAudioCount; delete button.dataset.potentialAvailableAudioCount;
@ -501,55 +557,6 @@ class DisplayAudio {
} }
} }
_getAudioSources(audioOptions) {
const {sources, textToSpeechVoice, customSourceUrl} = audioOptions;
const ttsSupported = (textToSpeechVoice.length > 0);
const customSupported = (customSourceUrl.length > 0);
const sourceIndexMap = new Map();
const optionsSourcesCount = sources.length;
for (let i = 0; i < optionsSourcesCount; ++i) {
sourceIndexMap.set(sources[i], i);
}
const rawSources = [
['jpod101', 'JapanesePod101', true],
['jpod101-alternate', 'JapanesePod101 (Alternate)', true],
['jisho', 'Jisho.org', true],
['text-to-speech', 'Text-to-speech', ttsSupported],
['text-to-speech-reading', 'Text-to-speech (Kana reading)', ttsSupported],
['custom', 'Custom URL', customSupported],
['custom-json', 'Custom URL (JSON)', customSupported]
];
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) {
optionsIndex = optionsSourcesCount;
}
results.push({
source,
displayName,
index: results.length,
optionsIndex,
isInOptions,
downloadable
});
}
// Sort according to source order in options
results.sort((a, b) => {
const i = a.optionsIndex - b.optionsIndex;
return i !== 0 ? i : a.index - b.index;
});
return results;
}
_createMenu(sourceButton, term, reading) { _createMenu(sourceButton, term, reading) {
// Create menu // Create menu
const menuContainerNode = this._display.displayGenerator.instantiateTemplate('audio-button-popup-menu'); const menuContainerNode = this._display.displayGenerator.instantiateTemplate('audio-button-popup-menu');
@ -569,15 +576,15 @@ class DisplayAudio {
} }
_createMenuItems(menuContainerNode, menuItemContainer, term, reading) { _createMenuItems(menuContainerNode, menuItemContainer, term, reading) {
const sources = this._getAudioSources(this._getAudioOptions());
const {displayGenerator} = this._display; const {displayGenerator} = this._display;
let showIcons = false; let showIcons = false;
const currentItems = [...menuItemContainer.children]; const currentItems = [...menuItemContainer.children];
for (const {source, displayName, isInOptions, downloadable} of sources) { for (const source of this._audioSources) {
const {index, displayName, isInOptions, downloadable} = source;
const entries = this._getMenuItemEntries(source, term, reading); const entries = this._getMenuItemEntries(source, term, 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: subIndex, name} = entries[i];
let node = this._getOrCreateMenuItem(currentItems, source, index); let node = this._getOrCreateMenuItem(currentItems, index, subIndex);
if (node === null) { if (node === null) {
node = displayGenerator.instantiateTemplate('audio-button-popup-menu-item'); node = displayGenerator.instantiateTemplate('audio-button-popup-menu-item');
} }
@ -596,9 +603,9 @@ class DisplayAudio {
icon.dataset.icon = valid ? 'checkmark' : 'cross'; icon.dataset.icon = valid ? 'checkmark' : 'cross';
showIcons = true; showIcons = true;
} }
node.dataset.source = source;
if (index !== null) {
node.dataset.index = `${index}`; node.dataset.index = `${index}`;
if (subIndex !== null) {
node.dataset.subIndex = `${subIndex}`;
} }
node.dataset.valid = `${valid}`; node.dataset.valid = `${valid}`;
node.dataset.sourceInOptions = `${isInOptions}`; node.dataset.sourceInOptions = `${isInOptions}`;
@ -615,16 +622,16 @@ class DisplayAudio {
menuContainerNode.dataset.showIcons = `${showIcons}`; menuContainerNode.dataset.showIcons = `${showIcons}`;
} }
_getOrCreateMenuItem(currentItems, source, index) { _getOrCreateMenuItem(currentItems, index, subIndex) {
if (index === null) { index = 0; }
index = `${index}`; index = `${index}`;
subIndex = `${subIndex !== null ? subIndex : 0}`;
for (let i = 0, ii = currentItems.length; i < ii; ++i) { for (let i = 0, ii = currentItems.length; i < ii; ++i) {
const node = currentItems[i]; const node = currentItems[i];
if (source !== node.dataset.source) { continue; } if (index !== node.dataset.index) { continue; }
let index2 = node.dataset.index; let subIndex2 = node.dataset.subIndex;
if (typeof index2 === 'undefined') { index2 = '0'; } if (typeof subIndex2 === 'undefined') { subIndex2 = '0'; }
if (index !== index2) { continue; } if (subIndex !== subIndex2) { continue; }
currentItems.splice(i, 1); currentItems.splice(i, 1);
return node; return node;
@ -636,7 +643,7 @@ class DisplayAudio {
const cacheEntry = this._getCacheItem(term, reading, false); const cacheEntry = this._getCacheItem(term, reading, false);
if (typeof cacheEntry !== 'undefined') { if (typeof cacheEntry !== 'undefined') {
const {sourceMap} = cacheEntry; const {sourceMap} = cacheEntry;
const sourceInfo = sourceMap.get(source); const sourceInfo = sourceMap.get(source.index);
if (typeof sourceInfo !== 'undefined') { if (typeof sourceInfo !== 'undefined') {
const {infoList} = sourceInfo; const {infoList} = sourceInfo;
if (infoList !== null) { if (infoList !== null) {
@ -659,23 +666,20 @@ class DisplayAudio {
return [{valid: null, index: null, name: null}]; return [{valid: null, index: null, name: null}];
} }
_updateMenuPrimaryCardAudio(menuBodyNode, term, reading) { _getPrimaryCardAudio(term, reading) {
const primaryCardAudio = this.getPrimaryCardAudio(term, reading); const cacheEntry = this._getCacheItem(term, reading, false);
const {source: primaryCardAudioSource, index: primaryCardAudioIndex} = (primaryCardAudio !== null ? primaryCardAudio : {source: null, index: -1}); return typeof cacheEntry !== 'undefined' ? cacheEntry.primaryCardAudio : null;
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); _updateMenuPrimaryCardAudio(menuBodyNode, term, reading) {
const primaryCardAudio = this._getPrimaryCardAudio(term, reading);
const primaryCardAudioIndex = (primaryCardAudio !== null ? primaryCardAudio.index : null);
const primaryCardAudioSubIndex = (primaryCardAudio !== null ? primaryCardAudio.subIndex : null);
const itemGroups = menuBodyNode.querySelectorAll('.popup-menu-item-group');
for (const node of itemGroups) {
const index = Number.parseInt(node.dataset.index, 10);
const subIndex = Number.parseInt(node.dataset.subIndex, 10);
const isPrimaryCardAudio = (index === primaryCardAudioIndex && subIndex === primaryCardAudioSubIndex);
node.dataset.isPrimaryCardAudio = `${isPrimaryCardAudio}`; node.dataset.isPrimaryCardAudio = `${isPrimaryCardAudio}`;
} }
} }
@ -688,4 +692,9 @@ class DisplayAudio {
menu.updatePosition(); menu.updatePosition();
} }
} }
_getSourceData(source) {
const {type, url, voice} = source;
return {type, url, voice};
}
} }

View File

@ -296,7 +296,6 @@ class Display extends EventDispatcher {
this._updateDocumentOptions(options); this._updateDocumentOptions(options);
this._updateTheme(options.general.popupTheme); this._updateTheme(options.general.popupTheme);
this.setCustomCss(options.general.customPopupCss); this.setCustomCss(options.general.customPopupCss);
this._displayAudio.updateOptions(options);
this._hotkeyHelpController.setOptions(options); this._hotkeyHelpController.setOptions(options);
this._displayGenerator.updateHotkeys(); this._displayGenerator.updateHotkeys();
this._hotkeyHelpController.setupNode(document.documentElement); this._hotkeyHelpController.setupNode(document.documentElement);
@ -1330,7 +1329,7 @@ class Display extends EventDispatcher {
} }
async _playAudioCurrent() { async _playAudioCurrent() {
return await this._displayAudio.playAudio(this._index, 0); await this._displayAudio.playAudio(this._index, 0);
} }
_getEntry(index) { _getEntry(index) {
@ -1552,26 +1551,17 @@ class Display extends EventDispatcher {
} }
async _injectAnkiNoteMedia(dictionaryEntry, options, fields) { async _injectAnkiNoteMedia(dictionaryEntry, options, fields) {
const { const {anki: {screenshot: {format, quality}}} = options;
anki: {screenshot: {format, quality}},
audio: {sources, customSourceUrl}
} = options;
const timestamp = Date.now(); const timestamp = Date.now();
const dictionaryEntryDetails = this._getDictionaryEntryDetailsForNote(dictionaryEntry); const dictionaryEntryDetails = this._getDictionaryEntryDetailsForNote(dictionaryEntry);
let audioDetails = null; const audioDetails = (
if (dictionaryEntryDetails.type !== 'kanji' && AnkiUtil.fieldsObjectContainsMarker(fields, 'audio')) { dictionaryEntryDetails.type !== 'kanji' && AnkiUtil.fieldsObjectContainsMarker(fields, 'audio') ?
const primaryCardAudio = this._displayAudio.getPrimaryCardAudio(dictionaryEntryDetails.term, dictionaryEntryDetails.reading); this._displayAudio.getAnkiNoteMediaAudioDetails(dictionaryEntryDetails.term, dictionaryEntryDetails.reading) :
let preferredAudioIndex = null; null
let sources2 = sources; );
if (primaryCardAudio !== null) {
sources2 = [primaryCardAudio.source];
preferredAudioIndex = primaryCardAudio.index;
}
audioDetails = {sources: sources2, preferredAudioIndex, customSourceUrl};
}
const screenshotDetails = ( const screenshotDetails = (
AnkiUtil.fieldsObjectContainsMarker(fields, 'screenshot') && typeof this._contentOriginTabId === 'number' ? AnkiUtil.fieldsObjectContainsMarker(fields, 'screenshot') && typeof this._contentOriginTabId === 'number' ?
@ -1906,7 +1896,7 @@ class Display extends EventDispatcher {
} }
_onHotkeyActionPlayAudioFromSource(source) { _onHotkeyActionPlayAudioFromSource(source) {
this._displayAudio.playAudio(this._index, 0, [source]); this._displayAudio.playAudio(this._index, 0, source);
} }
_closeAllPopupMenus() { _closeAllPopupMenus() {

View File

@ -26,7 +26,6 @@ class AudioDownloader {
this._japaneseUtil = japaneseUtil; this._japaneseUtil = japaneseUtil;
this._requestBuilder = requestBuilder; this._requestBuilder = requestBuilder;
this._customAudioListSchema = null; this._customAudioListSchema = null;
this._customAudioListSchema = null;
this._getInfoHandlers = new Map([ this._getInfoHandlers = new Map([
['jpod101', this._getInfoJpod101.bind(this)], ['jpod101', this._getInfoJpod101.bind(this)],
['jpod101-alternate', this._getInfoJpod101Alternate.bind(this)], ['jpod101-alternate', this._getInfoJpod101Alternate.bind(this)],
@ -38,11 +37,11 @@ class AudioDownloader {
]); ]);
} }
async getTermAudioInfoList(source, term, reading, details) { async getTermAudioInfoList(source, term, reading) {
const handler = this._getInfoHandlers.get(source); const handler = this._getInfoHandlers.get(source.type);
if (typeof handler === 'function') { if (typeof handler === 'function') {
try { try {
return await handler(term, reading, details); return await handler(term, reading, source);
} catch (e) { } catch (e) {
// NOP // NOP
} }
@ -50,9 +49,9 @@ class AudioDownloader {
return []; return [];
} }
async downloadTermAudio(sources, preferredAudioIndex, term, reading, details) { async downloadTermAudio(sources, preferredAudioIndex, term, reading) {
for (const source of sources) { for (const source of sources) {
let infoList = await this.getTermAudioInfoList(source, term, reading, details); let infoList = await this.getTermAudioInfoList(source, term, reading);
if (typeof preferredAudioIndex === 'number') { if (typeof preferredAudioIndex === 'number') {
infoList = (preferredAudioIndex >= 0 && preferredAudioIndex < infoList.length ? [infoList[preferredAudioIndex]] : []); infoList = (preferredAudioIndex >= 0 && preferredAudioIndex < infoList.length ? [infoList[preferredAudioIndex]] : []);
} }
@ -60,7 +59,7 @@ class AudioDownloader {
switch (info.type) { switch (info.type) {
case 'url': case 'url':
try { try {
return await this._downloadAudioFromUrl(info.url, source); return await this._downloadAudioFromUrl(info.url, source.type);
} catch (e) { } catch (e) {
// NOP // NOP
} }
@ -178,27 +177,27 @@ class AudioDownloader {
throw new Error('Failed to find audio URL'); throw new Error('Failed to find audio URL');
} }
async _getInfoTextToSpeech(term, reading, {textToSpeechVoice}) { async _getInfoTextToSpeech(term, reading, {voice}) {
if (!textToSpeechVoice) { if (!voice) {
throw new Error('No voice'); throw new Error('No voice');
} }
return [{type: 'tts', text: term, voice: textToSpeechVoice}]; return [{type: 'tts', text: term, voice: voice}];
} }
async _getInfoTextToSpeechReading(term, reading, {textToSpeechVoice}) { async _getInfoTextToSpeechReading(term, reading, {voice}) {
if (!textToSpeechVoice) { if (!voice) {
throw new Error('No voice'); throw new Error('No voice');
} }
return [{type: 'tts', text: reading, voice: textToSpeechVoice}]; return [{type: 'tts', text: reading, voice: voice}];
} }
async _getInfoCustom(term, reading, {customSourceUrl}) { async _getInfoCustom(term, reading, {url}) {
const url = this._getCustomUrl(term, reading, customSourceUrl); url = this._getCustomUrl(term, reading, url);
return [{type: 'url', url}]; return [{type: 'url', url}];
} }
async _getInfoCustomJson(term, reading, {customSourceUrl}) { async _getInfoCustomJson(term, reading, {url}) {
const url = this._getCustomUrl(term, reading, customSourceUrl); url = this._getCustomUrl(term, reading, url);
const response = await this._requestBuilder.fetchAnonymous(url, { const response = await this._requestBuilder.fetchAnonymous(url, {
method: 'GET', method: 'GET',
@ -230,15 +229,15 @@ class AudioDownloader {
return results; return results;
} }
_getCustomUrl(term, reading, customSourceUrl) { _getCustomUrl(term, reading, url) {
if (typeof customSourceUrl !== 'string') { if (typeof url !== 'string') {
throw new Error('No custom URL defined'); throw new Error('No custom URL defined');
} }
const data = {term, reading}; const data = {term, reading};
return customSourceUrl.replace(/\{([^}]*)\}/g, (m0, m1) => (Object.prototype.hasOwnProperty.call(data, m1) ? `${data[m1]}` : m0)); return url.replace(/\{([^}]*)\}/g, (m0, m1) => (Object.prototype.hasOwnProperty.call(data, m1) ? `${data[m1]}` : m0));
} }
async _downloadAudioFromUrl(url, source) { async _downloadAudioFromUrl(url, sourceType) {
const response = await this._requestBuilder.fetchAnonymous(url, { const response = await this._requestBuilder.fetchAnonymous(url, {
method: 'GET', method: 'GET',
mode: 'cors', mode: 'cors',
@ -254,7 +253,7 @@ class AudioDownloader {
const arrayBuffer = await response.arrayBuffer(); const arrayBuffer = await response.arrayBuffer();
if (!await this._isAudioBinaryValid(arrayBuffer, source)) { if (!await this._isAudioBinaryValid(arrayBuffer, sourceType)) {
throw new Error('Could not retrieve audio'); throw new Error('Could not retrieve audio');
} }
@ -263,8 +262,8 @@ class AudioDownloader {
return {data, contentType}; return {data, contentType};
} }
async _isAudioBinaryValid(arrayBuffer, source) { async _isAudioBinaryValid(arrayBuffer, sourceType) {
switch (source) { switch (sourceType) {
case 'jpod101': case 'jpod101':
{ {
const digest = await this._arrayBufferDigest(arrayBuffer); const digest = await this._arrayBufferDigest(arrayBuffer);
@ -304,8 +303,6 @@ class AudioDownloader {
} }
async _getCustomAudioListSchema() { async _getCustomAudioListSchema() {
let schema = this._customAudioListSchema;
if (schema === null) {
const url = chrome.runtime.getURL('/data/schemas/custom-audio-list-schema.json'); const url = chrome.runtime.getURL('/data/schemas/custom-audio-list-schema.json');
const response = await fetch(url, { const response = await fetch(url, {
method: 'GET', method: 'GET',
@ -315,9 +312,6 @@ class AudioDownloader {
redirect: 'follow', redirect: 'follow',
referrerPolicy: 'no-referrer' referrerPolicy: 'no-referrer'
}); });
schema = await response.json(); return await response.json();
this._customAudioListSchema = schema;
}
return schema;
} }
} }

View File

@ -42,11 +42,11 @@ class AudioSystem extends EventDispatcher {
return this._fallbackAudio; return this._fallbackAudio;
} }
createAudio(url, source) { createAudio(url, sourceType) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const audio = new Audio(url); const audio = new Audio(url);
audio.addEventListener('loadeddata', () => { audio.addEventListener('loadeddata', () => {
if (!this._isAudioValid(audio, source)) { if (!this._isAudioValid(audio, sourceType)) {
reject(new Error('Could not retrieve audio')); reject(new Error('Could not retrieve audio'));
} else { } else {
resolve(audio); resolve(audio);
@ -70,8 +70,8 @@ class AudioSystem extends EventDispatcher {
this.trigger('voiceschanged', e); this.trigger('voiceschanged', e);
} }
_isAudioValid(audio, source) { _isAudioValid(audio, sourceType) {
switch (source) { switch (sourceType) {
case 'jpod101': case 'jpod101':
{ {
const duration = audio.duration; const duration = audio.duration;