diff options
author | Darius Jahandarie <djahandarie@gmail.com> | 2023-12-06 03:53:16 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-12-06 03:53:16 +0000 |
commit | bd5bc1a5db29903bc098995cd9262c4576bf76af (patch) | |
tree | c9214189e0214480fcf6539ad1c6327aef6cbd1c /ext/js/media | |
parent | fd6bba8a2a869eaf2b2c1fa49001f933fce3c618 (diff) | |
parent | 23e6fb76319c9ed7c9bcdc3efba39bc5dd38f288 (diff) |
Merge pull request #339 from toasted-nutbread/type-annotations
Type annotations
Diffstat (limited to 'ext/js/media')
-rw-r--r-- | ext/js/media/audio-downloader.js | 126 | ||||
-rw-r--r-- | ext/js/media/audio-system.js | 41 | ||||
-rw-r--r-- | ext/js/media/media-util.js | 2 | ||||
-rw-r--r-- | ext/js/media/text-to-speech-audio.js | 16 |
4 files changed, 165 insertions, 20 deletions
diff --git a/ext/js/media/audio-downloader.js b/ext/js/media/audio-downloader.js index 1720a5d9..e041cc67 100644 --- a/ext/js/media/audio-downloader.js +++ b/ext/js/media/audio-downloader.js @@ -17,17 +17,25 @@ */ import {RequestBuilder} from '../background/request-builder.js'; +import {ExtensionError} from '../core/extension-error.js'; import {JsonSchema} from '../data/json-schema.js'; import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js'; import {NativeSimpleDOMParser} from '../dom/native-simple-dom-parser.js'; import {SimpleDOMParser} from '../dom/simple-dom-parser.js'; export class AudioDownloader { + /** + * @param {{japaneseUtil: import('../language/sandbox/japanese-util.js').JapaneseUtil, requestBuilder: RequestBuilder}} details + */ constructor({japaneseUtil, requestBuilder}) { + /** @type {import('../language/sandbox/japanese-util.js').JapaneseUtil} */ this._japaneseUtil = japaneseUtil; + /** @type {RequestBuilder} */ this._requestBuilder = requestBuilder; + /** @type {?JsonSchema} */ this._customAudioListSchema = null; - this._getInfoHandlers = new Map([ + /** @type {Map<import('settings').AudioSourceType, import('audio-downloader').GetInfoHandler>} */ + this._getInfoHandlers = new Map(/** @type {[name: import('settings').AudioSourceType, handler: import('audio-downloader').GetInfoHandler][]} */ ([ ['jpod101', this._getInfoJpod101.bind(this)], ['jpod101-alternate', this._getInfoJpod101Alternate.bind(this)], ['jisho', this._getInfoJisho.bind(this)], @@ -35,9 +43,15 @@ export class AudioDownloader { ['text-to-speech-reading', this._getInfoTextToSpeechReading.bind(this)], ['custom', this._getInfoCustom.bind(this)], ['custom-json', this._getInfoCustomJson.bind(this)] - ]); + ])); } + /** + * @param {import('audio').AudioSourceInfo} source + * @param {string} term + * @param {string} reading + * @returns {Promise<import('audio-downloader').Info[]>} + */ async getTermAudioInfoList(source, term, reading) { const handler = this._getInfoHandlers.get(source.type); if (typeof handler === 'function') { @@ -50,6 +64,14 @@ export class AudioDownloader { return []; } + /** + * @param {import('audio').AudioSourceInfo[]} sources + * @param {?number} preferredAudioIndex + * @param {string} term + * @param {string} reading + * @param {?number} idleTimeout + * @returns {Promise<import('audio-downloader').AudioBinaryBase64>} + */ async downloadTermAudio(sources, preferredAudioIndex, term, reading, idleTimeout) { const errors = []; for (const source of sources) { @@ -70,28 +92,34 @@ export class AudioDownloader { } } - const error = new Error('Could not download audio'); + const error = new ExtensionError('Could not download audio'); error.data = {errors}; throw error; } // Private + /** + * @param {string} url + * @param {string} base + * @returns {string} + */ _normalizeUrl(url, base) { return new URL(url, base).href; } + /** @type {import('audio-downloader').GetInfoHandler} */ async _getInfoJpod101(term, reading) { if (reading === term && this._japaneseUtil.isStringEntirelyKana(term)) { reading = term; - term = null; + term = ''; } const params = new URLSearchParams(); - if (term) { + if (term.length > 0) { params.set('kanji', term); } - if (reading) { + if (reading.length > 0) { params.set('kana', reading); } @@ -99,6 +127,7 @@ export class AudioDownloader { return [{type: 'url', url}]; } + /** @type {import('audio-downloader').GetInfoHandler} */ async _getInfoJpod101Alternate(term, reading) { const fetchUrl = 'https://www.japanesepod101.com/learningcenter/reference/dictionary_post'; const data = new URLSearchParams({ @@ -149,6 +178,7 @@ export class AudioDownloader { throw new Error('Failed to find audio URL'); } + /** @type {import('audio-downloader').GetInfoHandler} */ async _getInfoJisho(term, reading) { const fetchUrl = `https://jisho.org/search/${term}`; const response = await this._requestBuilder.fetchAnonymous(fetchUrl, { @@ -181,26 +211,52 @@ export class AudioDownloader { throw new Error('Failed to find audio URL'); } - async _getInfoTextToSpeech(term, reading, {voice}) { - if (!voice) { - throw new Error('No voice'); + /** @type {import('audio-downloader').GetInfoHandler} */ + async _getInfoTextToSpeech(term, reading, details) { + if (typeof details !== 'object' || details === null) { + throw new Error('Invalid arguments'); + } + const {voice} = details; + if (typeof voice !== 'string') { + throw new Error('Invalid voice'); } return [{type: 'tts', text: term, voice: voice}]; } - async _getInfoTextToSpeechReading(term, reading, {voice}) { - if (!voice) { - throw new Error('No voice'); + /** @type {import('audio-downloader').GetInfoHandler} */ + async _getInfoTextToSpeechReading(term, reading, details) { + if (typeof details !== 'object' || details === null) { + throw new Error('Invalid arguments'); + } + const {voice} = details; + if (typeof voice !== 'string') { + throw new Error('Invalid voice'); } return [{type: 'tts', text: reading, voice: voice}]; } - async _getInfoCustom(term, reading, {url}) { + /** @type {import('audio-downloader').GetInfoHandler} */ + async _getInfoCustom(term, reading, details) { + if (typeof details !== 'object' || details === null) { + throw new Error('Invalid arguments'); + } + let {url} = details; + if (typeof url !== 'string') { + throw new Error('Invalid url'); + } url = this._getCustomUrl(term, reading, url); return [{type: 'url', url}]; } - async _getInfoCustomJson(term, reading, {url}) { + /** @type {import('audio-downloader').GetInfoHandler} */ + async _getInfoCustomJson(term, reading, details) { + if (typeof details !== 'object' || details === null) { + throw new Error('Invalid arguments'); + } + let {url} = details; + if (typeof url !== 'string') { + throw new Error('Invalid url'); + } url = this._getCustomUrl(term, reading, url); const response = await this._requestBuilder.fetchAnonymous(url, { @@ -220,12 +276,14 @@ export class AudioDownloader { if (this._customAudioListSchema === null) { const schema = await this._getCustomAudioListSchema(); - this._customAudioListSchema = new JsonSchema(schema); + this._customAudioListSchema = new JsonSchema(/** @type {import('json-schema').Schema} */ (schema)); } this._customAudioListSchema.validate(responseJson); + /** @type {import('audio-downloader').Info[]} */ const results = []; for (const {url: url2, name} of responseJson.audioSources) { + /** @type {import('audio-downloader').Info1} */ const info = {type: 'url', url: url2}; if (typeof name === 'string') { info.name = name; } results.push(info); @@ -233,17 +291,32 @@ export class AudioDownloader { return results; } + /** + * @param {string} term + * @param {string} reading + * @param {string} url + * @returns {string} + * @throws {Error} + */ _getCustomUrl(term, reading, url) { if (typeof url !== 'string') { throw new Error('No custom URL defined'); } const data = {term, reading}; - return url.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[/** @type {'term'|'reading'} */ (m1)]}` : m0)); } + /** + * @param {string} url + * @param {import('settings').AudioSourceType} sourceType + * @param {?number} idleTimeout + * @returns {Promise<import('audio-downloader').AudioBinaryBase64>} + */ async _downloadAudioFromUrl(url, sourceType, idleTimeout) { let signal; + /** @type {?import('request-builder.js').ProgressCallback} */ let onProgress = null; + /** @type {?import('core').Timeout} */ let idleTimer = null; if (typeof idleTimeout === 'number') { const abortController = new AbortController(); @@ -252,7 +325,9 @@ export class AudioDownloader { abortController.abort('Idle timeout'); }; onProgress = (done) => { - clearTimeout(idleTimer); + if (idleTimer !== null) { + clearTimeout(idleTimer); + } idleTimer = done ? null : setTimeout(onIdleTimeout, idleTimeout); }; idleTimer = setTimeout(onIdleTimeout, idleTimeout); @@ -287,6 +362,11 @@ export class AudioDownloader { return {data, contentType}; } + /** + * @param {ArrayBuffer} arrayBuffer + * @param {import('settings').AudioSourceType} sourceType + * @returns {Promise<boolean>} + */ async _isAudioBinaryValid(arrayBuffer, sourceType) { switch (sourceType) { case 'jpod101': @@ -304,6 +384,10 @@ export class AudioDownloader { } } + /** + * @param {ArrayBuffer} arrayBuffer + * @returns {Promise<string>} + */ async _arrayBufferDigest(arrayBuffer) { const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', new Uint8Array(arrayBuffer))); let digest = ''; @@ -313,6 +397,11 @@ export class AudioDownloader { return digest; } + /** + * @param {string} content + * @returns {import('simple-dom-parser').ISimpleDomParser} + * @throws {Error} + */ _createSimpleDOMParser(content) { if (typeof NativeSimpleDOMParser !== 'undefined' && NativeSimpleDOMParser.isSupported()) { return new NativeSimpleDOMParser(content); @@ -323,6 +412,9 @@ export class AudioDownloader { } } + /** + * @returns {Promise<unknown>} + */ async _getCustomAudioListSchema() { const url = chrome.runtime.getURL('/data/schemas/custom-audio-list-schema.json'); const response = await fetch(url, { diff --git a/ext/js/media/audio-system.js b/ext/js/media/audio-system.js index 55812bec..1e8f1be2 100644 --- a/ext/js/media/audio-system.js +++ b/ext/js/media/audio-system.js @@ -19,12 +19,19 @@ import {EventDispatcher} from '../core.js'; import {TextToSpeechAudio} from './text-to-speech-audio.js'; +/** + * @augments EventDispatcher<import('audio-system').EventType> + */ export class AudioSystem extends EventDispatcher { constructor() { super(); + /** @type {?HTMLAudioElement} */ this._fallbackAudio = null; } + /** + * @returns {void} + */ prepare() { // speechSynthesis.getVoices() will not be populated unless some API call is made. if ( @@ -35,6 +42,9 @@ export class AudioSystem extends EventDispatcher { } } + /** + * @returns {HTMLAudioElement} + */ getFallbackAudio() { if (this._fallbackAudio === null) { this._fallbackAudio = new Audio('/data/audio/button.mp3'); @@ -42,6 +52,11 @@ export class AudioSystem extends EventDispatcher { return this._fallbackAudio; } + /** + * @param {string} url + * @param {import('settings').AudioSourceType} sourceType + * @returns {Promise<HTMLAudioElement>} + */ async createAudio(url, sourceType) { const audio = new Audio(url); await this._waitForData(audio); @@ -51,6 +66,12 @@ export class AudioSystem extends EventDispatcher { return audio; } + /** + * @param {string} text + * @param {string} voiceUri + * @returns {TextToSpeechAudio} + * @throws {Error} + */ createTextToSpeechAudio(text, voiceUri) { const voice = this._getTextToSpeechVoiceFromVoiceUri(voiceUri); if (voice === null) { @@ -61,10 +82,17 @@ export class AudioSystem extends EventDispatcher { // Private - _onVoicesChanged(e) { - this.trigger('voiceschanged', e); + /** + * @param {Event} event + */ + _onVoicesChanged(event) { + this.trigger('voiceschanged', event); } + /** + * @param {HTMLAudioElement} audio + * @returns {Promise<void>} + */ _waitForData(audio) { return new Promise((resolve, reject) => { audio.addEventListener('loadeddata', () => resolve()); @@ -72,6 +100,11 @@ export class AudioSystem extends EventDispatcher { }); } + /** + * @param {HTMLAudioElement} audio + * @param {import('settings').AudioSourceType} sourceType + * @returns {boolean} + */ _isAudioValid(audio, sourceType) { switch (sourceType) { case 'jpod101': @@ -87,6 +120,10 @@ export class AudioSystem extends EventDispatcher { } } + /** + * @param {string} voiceUri + * @returns {?SpeechSynthesisVoice} + */ _getTextToSpeechVoiceFromVoiceUri(voiceUri) { try { for (const voice of speechSynthesis.getVoices()) { diff --git a/ext/js/media/media-util.js b/ext/js/media/media-util.js index 843dc11c..1d70acd3 100644 --- a/ext/js/media/media-util.js +++ b/ext/js/media/media-util.js @@ -103,7 +103,7 @@ export class MediaUtil { /** * Gets the file extension for a corresponding media type. * @param {string} mediaType The media type to use. - * @returns {string} A file extension including the dot for the media type, + * @returns {?string} A file extension including the dot for the media type, * otherwise `null`. */ static getFileExtensionFromAudioMediaType(mediaType) { diff --git a/ext/js/media/text-to-speech-audio.js b/ext/js/media/text-to-speech-audio.js index ae717519..cd1205e5 100644 --- a/ext/js/media/text-to-speech-audio.js +++ b/ext/js/media/text-to-speech-audio.js @@ -17,13 +17,22 @@ */ export class TextToSpeechAudio { + /** + * @param {string} text + * @param {SpeechSynthesisVoice} voice + */ constructor(text, voice) { + /** @type {string} */ this._text = text; + /** @type {SpeechSynthesisVoice} */ this._voice = voice; + /** @type {?SpeechSynthesisUtterance} */ this._utterance = null; + /** @type {number} */ this._volume = 1; } + /** @type {number} */ get currentTime() { return 0; } @@ -32,6 +41,7 @@ export class TextToSpeechAudio { // NOP } + /** @type {number} */ get volume() { return this._volume; } @@ -43,6 +53,9 @@ export class TextToSpeechAudio { } } + /** + * @returns {Promise<void>} + */ async play() { try { if (this._utterance === null) { @@ -59,6 +72,9 @@ export class TextToSpeechAudio { } } + /** + * @returns {void} + */ pause() { try { speechSynthesis.cancel(); |