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/pages/settings/audio-controller.js | |
| parent | fd6bba8a2a869eaf2b2c1fa49001f933fce3c618 (diff) | |
| parent | 23e6fb76319c9ed7c9bcdc3efba39bc5dd38f288 (diff) | |
Merge pull request #339 from toasted-nutbread/type-annotations
Type annotations
Diffstat (limited to 'ext/js/pages/settings/audio-controller.js')
| -rw-r--r-- | ext/js/pages/settings/audio-controller.js | 181 | 
1 files changed, 159 insertions, 22 deletions
| diff --git a/ext/js/pages/settings/audio-controller.js b/ext/js/pages/settings/audio-controller.js index fb54ee6b..0a3f9454 100644 --- a/ext/js/pages/settings/audio-controller.js +++ b/ext/js/pages/settings/audio-controller.js @@ -19,48 +19,71 @@  import {EventDispatcher, EventListenerCollection} from '../../core.js';  import {AudioSystem} from '../../media/audio-system.js'; +/** + * @augments EventDispatcher<import('audio-controller').EventType> + */  export class AudioController extends EventDispatcher { +    /** +     * @param {import('./settings-controller.js').SettingsController} settingsController +     * @param {import('./modal-controller.js').ModalController} modalController +     */      constructor(settingsController, modalController) {          super(); +        /** @type {import('./settings-controller.js').SettingsController} */          this._settingsController = settingsController; +        /** @type {import('./modal-controller.js').ModalController} */          this._modalController = modalController; +        /** @type {AudioSystem} */          this._audioSystem = new AudioSystem(); +        /** @type {?HTMLElement} */          this._audioSourceContainer = null; +        /** @type {?HTMLButtonElement} */          this._audioSourceAddButton = null; +        /** @type {AudioSourceEntry[]} */          this._audioSourceEntries = []; +        /** @type {?HTMLInputElement} */          this._voiceTestTextInput = null; +        /** @type {import('audio-controller').VoiceInfo[]} */          this._voices = [];      } +    /** @type {import('./settings-controller.js').SettingsController} */      get settingsController() {          return this._settingsController;      } +    /** @type {import('./modal-controller.js').ModalController} */      get modalController() {          return this._modalController;      } +    /** */      async prepare() {          this._audioSystem.prepare(); -        this._voiceTestTextInput = document.querySelector('#text-to-speech-voice-test-text'); -        this._audioSourceContainer = document.querySelector('#audio-source-list'); -        this._audioSourceAddButton = document.querySelector('#audio-source-add'); +        this._voiceTestTextInput = /** @type {HTMLInputElement} */ (document.querySelector('#text-to-speech-voice-test-text')); +        this._audioSourceContainer = /** @type {HTMLElement} */ (document.querySelector('#audio-source-list')); +        this._audioSourceAddButton = /** @type {HTMLButtonElement} */ (document.querySelector('#audio-source-add'));          this._audioSourceContainer.textContent = ''; +        const testButton = /** @type {HTMLButtonElement} */ (document.querySelector('#text-to-speech-voice-test'));          this._audioSourceAddButton.addEventListener('click', this._onAddAudioSource.bind(this), false); -        this._audioSystem.on('voiceschanged', this._updateTextToSpeechVoices.bind(this), false); +        this._audioSystem.on('voiceschanged', this._updateTextToSpeechVoices.bind(this));          this._updateTextToSpeechVoices(); -        document.querySelector('#text-to-speech-voice-test').addEventListener('click', this._onTestTextToSpeech.bind(this), false); +        testButton.addEventListener('click', this._onTestTextToSpeech.bind(this), false);          this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this));          const options = await this._settingsController.getOptions(); -        this._onOptionsChanged({options}); +        const optionsContext = this._settingsController.getOptionsContext(); +        this._onOptionsChanged({options, optionsContext});      } +    /** +     * @param {AudioSourceEntry} entry +     */      async removeSource(entry) {          const {index} = entry;          this._audioSourceEntries.splice(index, 1); @@ -78,16 +101,25 @@ export class AudioController extends EventDispatcher {          }]);      } +    /** +     * @returns {import('audio-controller').VoiceInfo[]} +     */      getVoices() {          return this._voices;      } +    /** +     * @param {string} voice +     */      setTestVoice(voice) { -        this._voiceTestTextInput.dataset.voice = voice; +        /** @type {HTMLInputElement} */ (this._voiceTestTextInput).dataset.voice = voice;      }      // Private +    /** +     * @param {import('settings-controller').OptionsChangedEvent} details +     */      _onOptionsChanged({options}) {          for (const entry of this._audioSourceEntries) {              entry.cleanup(); @@ -100,15 +132,18 @@ export class AudioController extends EventDispatcher {          }      } +    /** */      _onAddAudioSource() {          this._addAudioSource();      } +    /** */      _onTestTextToSpeech() {          try { -            const text = this._voiceTestTextInput.value || ''; -            const voiceUri = this._voiceTestTextInput.dataset.voice; -            const audio = this._audioSystem.createTextToSpeechAudio(text, voiceUri); +            const input = /** @type {HTMLInputElement} */ (this._voiceTestTextInput); +            const text = input.value || ''; +            const voiceUri = input.dataset.voice; +            const audio = this._audioSystem.createTextToSpeechAudio(text, typeof voiceUri === 'string' ? voiceUri : '');              audio.volume = 1.0;              audio.play();          } catch (e) { @@ -116,6 +151,7 @@ export class AudioController extends EventDispatcher {          }      } +    /** */      _updateTextToSpeechVoices() {          const voices = (              typeof speechSynthesis !== 'undefined' ? @@ -131,6 +167,11 @@ export class AudioController extends EventDispatcher {          this.trigger('voicesUpdated');      } +    /** +     * @param {import('audio-controller').VoiceInfo} a +     * @param {import('audio-controller').VoiceInfo} b +     * @returns {number} +     */      _textToSpeechVoiceCompare(a, b) {          if (a.isJapanese) {              if (!b.isJapanese) { return -1; } @@ -147,6 +188,10 @@ export class AudioController extends EventDispatcher {          return a.index - b.index;      } +    /** +     * @param {string} languageTag +     * @returns {boolean} +     */      _languageTagIsJapanese(languageTag) {          return (              languageTag.startsWith('ja_') || @@ -155,15 +200,23 @@ export class AudioController extends EventDispatcher {          );      } +    /** +     * @param {number} index +     * @param {import('settings').AudioSourceOptions} source +     */      _createAudioSourceEntry(index, source) { -        const node = this._settingsController.instantiateTemplate('audio-source'); +        const node = /** @type {HTMLElement} */ (this._settingsController.instantiateTemplate('audio-source'));          const entry = new AudioSourceEntry(this, index, source, node);          this._audioSourceEntries.push(entry); -        this._audioSourceContainer.appendChild(node); +        /** @type {HTMLElement} */ (this._audioSourceContainer).appendChild(node);          entry.prepare();      } +    /** +     * @returns {import('settings').AudioSourceType} +     */      _getUnusedAudioSourceType() { +        /** @type {import('settings').AudioSourceType[]} */          const typesAvailable = [              'jpod101',              'jpod101-alternate', @@ -178,8 +231,10 @@ export class AudioController extends EventDispatcher {          return typesAvailable[0];      } +    /** */      async _addAudioSource() {          const type = this._getUnusedAudioSourceType(); +        /** @type {import('settings').AudioSourceOptions} */          const source = {type, url: '', voice: ''};          const index = this._audioSourceEntries.length;          this._createAudioSourceEntry(index, source); @@ -194,19 +249,36 @@ export class AudioController extends EventDispatcher {  }  class AudioSourceEntry { +    /** +     * @param {AudioController} parent +     * @param {number} index +     * @param {import('settings').AudioSourceOptions} source +     * @param {HTMLElement} node +     */      constructor(parent, index, source, node) { +        /** @type {AudioController} */          this._parent = parent; +        /** @type {number} */          this._index = index; +        /** @type {import('settings').AudioSourceType} */          this._type = source.type; +        /** @type {string} */          this._url = source.url; +        /** @type {string} */          this._voice = source.voice; +        /** @type {HTMLElement} */          this._node = node; +        /** @type {EventListenerCollection} */          this._eventListeners = new EventListenerCollection(); +        /** @type {?HTMLSelectElement} */          this._typeSelect = null; +        /** @type {?HTMLInputElement} */          this._urlInput = null; +        /** @type {?HTMLSelectElement} */          this._voiceSelect = null;      } +    /** @type {number} */      get index() {          return this._index;      } @@ -215,17 +287,19 @@ class AudioSourceEntry {          this._index = value;      } +    /** @type {import('settings').AudioSourceType} */      get type() {          return this._type;      } +    /** */      prepare() {          this._updateTypeParameter(); -        const menuButton = this._node.querySelector('.audio-source-menu-button'); -        this._typeSelect = this._node.querySelector('.audio-source-type-select'); -        this._urlInput = this._node.querySelector('.audio-source-parameter-container[data-field=url] .audio-source-parameter'); -        this._voiceSelect = this._node.querySelector('.audio-source-parameter-container[data-field=voice] .audio-source-parameter'); +        const menuButton = /** @type {HTMLButtonElement} */ (this._node.querySelector('.audio-source-menu-button')); +        this._typeSelect = /** @type {HTMLSelectElement} */ (this._node.querySelector('.audio-source-type-select')); +        this._urlInput = /** @type {HTMLInputElement} */ (this._node.querySelector('.audio-source-parameter-container[data-field=url] .audio-source-parameter')); +        this._voiceSelect = /** @type {HTMLSelectElement} */ (this._node.querySelector('.audio-source-parameter-container[data-field=voice] .audio-source-parameter'));          this._typeSelect.value = this._type;          this._urlInput.value = this._url; @@ -239,6 +313,7 @@ class AudioSourceEntry {          this._onVoicesUpdated();      } +    /** */      cleanup() {          if (this._node.parentNode !== null) {              this._node.parentNode.removeChild(this._node); @@ -248,7 +323,9 @@ class AudioSourceEntry {      // Private +    /** */      _onVoicesUpdated() { +        if (this._voiceSelect === null) { return; }          const voices = this._parent.getVoices();          const fragment = document.createDocumentFragment(); @@ -270,18 +347,35 @@ class AudioSourceEntry {          this._voiceSelect.value = this._voice;      } +    /** +     * @param {Event} e +     */      _onTypeSelectChange(e) { -        this._setType(e.currentTarget.value); +        const element = /** @type {HTMLSelectElement} */ (e.currentTarget); +        const value = this._normalizeAudioSourceType(element.value); +        if (value === null) { return; } +        this._setType(value);      } +    /** +     * @param {Event} e +     */      _onUrlInputChange(e) { -        this._setUrl(e.currentTarget.value); +        const element = /** @type {HTMLInputElement} */ (e.currentTarget); +        this._setUrl(element.value);      } +    /** +     * @param {Event} e +     */      _onVoiceSelectChange(e) { -        this._setVoice(e.currentTarget.value); +        const element = /** @type {HTMLSelectElement} */ (e.currentTarget); +        this._setVoice(element.value);      } +    /** +     * @param {import('popup-menu').MenuOpenEvent} e +     */      _onMenuOpen(e) {          const {menu} = e.detail; @@ -295,9 +389,15 @@ class AudioSourceEntry {                  break;          } -        menu.bodyNode.querySelector('.popup-menu-item[data-menu-action=help]').hidden = !hasHelp; +        const helpNode = /** @type {?HTMLElement} */ (menu.bodyNode.querySelector('.popup-menu-item[data-menu-action=help]')); +        if (helpNode !== null) { +            helpNode.hidden = !hasHelp; +        }      } +    /** +     * @param {import('popup-menu').MenuCloseEvent} e +     */      _onMenuClose(e) {          switch (e.detail.action) {              case 'help': @@ -309,22 +409,32 @@ class AudioSourceEntry {          }      } +    /** +     * @param {import('settings').AudioSourceType} value +     */      async _setType(value) {          this._type = value;          this._updateTypeParameter();          await this._parent.settingsController.setProfileSetting(`audio.sources[${this._index}].type`, value);      } +    /** +     * @param {string} value +     */      async _setUrl(value) {          this._url = value;          await this._parent.settingsController.setProfileSetting(`audio.sources[${this._index}].url`, value);      } +    /** +     * @param {string} value +     */      async _setVoice(value) {          this._voice = value;          await this._parent.settingsController.setProfileSetting(`audio.sources[${this._index}].voice`, value);      } +    /** */      _updateTypeParameter() {          let field = null;          switch (this._type) { @@ -337,11 +447,14 @@ class AudioSourceEntry {                  field = 'voice';                  break;          } -        for (const node of this._node.querySelectorAll('.audio-source-parameter-container')) { +        for (const node of /** @type {NodeListOf<HTMLElement>} */ (this._node.querySelectorAll('.audio-source-parameter-container'))) {              node.hidden = (field === null || node.dataset.field !== field);          }      } +    /** +     * @param {import('settings').AudioSourceType} type +     */      _showHelp(type) {          switch (type) {              case 'custom': @@ -358,7 +471,31 @@ class AudioSourceEntry {          }      } +    /** +     * @param {string} name +     */      _showModal(name) { -        this._parent.modalController.getModal(name).setVisible(true); +        const modal = this._parent.modalController.getModal(name); +        if (modal === null) { return; } +        modal.setVisible(true); +    } + +    /** +     * @param {string} value +     * @returns {?import('settings').AudioSourceType} +     */ +    _normalizeAudioSourceType(value) { +        switch (value) { +            case 'jpod101': +            case 'jpod101-alternate': +            case 'jisho': +            case 'text-to-speech': +            case 'text-to-speech-reading': +            case 'custom': +            case 'custom-json': +                return value; +            default: +                return null; +        }      }  } |