/** * Options UI Module * Provides the options UI for the game */ import { BaseModule } from './base-module.js'; import { createUIElement, populateDropdown, registerHandler, createPreferenceBinding } from './ui-helper.js'; class OptionsUIModule extends BaseModule { /** * Create a new options UI module */ constructor() { super('options-ui', 'Options UI'); // Set up dependencies this.dependencies = [ 'persistence-manager', 'localization', 'tts-factory', 'audio-manager' ]; // Modal element this.modal = null; // UI elements this.elements = {}; // Settings that require reload this.reloadRequired = false; // Bind methods this.bindMethods([ 'show', 'hide', 'createModal', 'populateTtsSystems', 'populateVoices', 'populateLanguages', 'loadPreferences', 'showReloadNotice', 'toggle', 'setupEventListeners', 'setupApiUrlFields', 'setupInitialState', 'dispatchApiChangeEvent', 'getPreference', 'updatePreference', 'renderProviderStatuses' ]); } /** * Dispatches an API change event * @param {string} eventType - Event type (e.g. 'api:key:change') * @param {string} provider - Provider name (e.g. 'elevenlabs') * @param {string} valueType - Value type (e.g. 'key', 'url') * @param {string} value - Value to dispatch */ dispatchApiChangeEvent(eventType, provider, valueType, value) { const eventName = `tts:${eventType}`; console.log(`Options UI: Dispatching event ${eventName} for provider ${provider}`); document.dispatchEvent(new CustomEvent(eventName, { detail: { provider, [valueType]: value } })); } /** * Gets a preference from the persistence manager * @param {string} category - Preference category * @param {string} key - Preference key * @param {*} defaultValue - Default value if preference doesn't exist * @returns {*} - Preference value */ getPreference(category, key, defaultValue) { const persistenceManager = this.getModule('persistence-manager'); return persistenceManager.getPreference(category, key, defaultValue); } /** * Updates a preference in the persistence manager * @param {string} category - Preference category * @param {string} key - Preference key * @param {*} value - Value to set */ updatePreference(category, key, value) { const persistenceManager = this.getModule('persistence-manager'); persistenceManager.updatePreference(category, key, value); } /** * Initialize the Options UI module * @returns {Promise} - Promise resolves with initialization success */ async initialize() { console.log('Options UI: Initializing'); // Create DOM elements this.createModal(); // Set up event listeners this.setupEventListeners(); // Set up API URL fields this.setupApiUrlFields(); // Set up initial state await this.setupInitialState(); // Set up automatic bindings using the persistence manager this.setupPreferenceBindings(); this.reportProgress(100, 'Options UI initialized'); return true; } /** * Create the options modal */ createModal() { console.log('Options UI: Creating options modal'); // Create modal container this.modal = document.createElement('div'); this.modal.id = 'options-modal'; this.modal.className = 'modal'; this.modal.style.display = 'none'; // Create modal content const modalContent = document.createElement('div'); modalContent.className = 'modal-content'; // Create header const header = document.createElement('div'); header.className = 'modal-header'; const title = document.createElement('h2'); title.textContent = 'Options'; header.appendChild(title); const closeButton = document.createElement('span'); closeButton.className = 'close'; closeButton.innerHTML = '×'; closeButton.onclick = () => this.hide(); header.appendChild(closeButton); modalContent.appendChild(header); // Create body const body = document.createElement('div'); body.className = 'modal-body'; // Create sections // App Settings Section (Language and Speed) const appSettingsSection = document.createElement('div'); appSettingsSection.className = 'options-section'; const appSettingsTitle = document.createElement('h3'); appSettingsTitle.textContent = 'Application Settings'; appSettingsSection.appendChild(appSettingsTitle); // Language const languageContainer = document.createElement('div'); languageContainer.className = 'option-item'; const languageLabel = document.createElement('label'); languageLabel.textContent = 'Language:'; languageContainer.appendChild(languageLabel); this.elements.language = createUIElement('select', { 'data-pref-bind': 'app.locale' }, null, languageContainer); appSettingsSection.appendChild(languageContainer); // Speed const speedContainer = document.createElement('div'); speedContainer.className = 'option-item'; const speedLabel = document.createElement('label'); speedLabel.textContent = 'Speed:'; speedContainer.appendChild(speedLabel); const speedValue = document.createElement('span'); speedValue.className = 'slider-value'; speedValue.textContent = '100%'; this.elements.ttsSpeedValue = speedValue; speedContainer.appendChild(speedValue); this.elements.ttsSpeed = createUIElement('input', { type: 'range', min: 50, max: 150, value: 100, 'data-pref-bind': 'tts.speed', 'data-pref-transform': 'centered-speed' }, null, speedContainer); // Update displayed value when slider changes this.elements.ttsSpeed.addEventListener('input', () => { this.updateSpeedDisplay(); }); appSettingsSection.appendChild(speedContainer); body.appendChild(appSettingsSection); // TTS Section const ttsSection = document.createElement('div'); ttsSection.className = 'options-section'; const ttsTitle = document.createElement('h3'); ttsTitle.textContent = 'Text-to-Speech'; ttsSection.appendChild(ttsTitle); // TTS Enable const ttsEnableContainer = document.createElement('div'); ttsEnableContainer.className = 'option-item'; const ttsEnableLabel = document.createElement('label'); ttsEnableLabel.textContent = 'Enable TTS:'; ttsEnableContainer.appendChild(ttsEnableLabel); this.elements.ttsEnabled = createUIElement('input', { type: 'checkbox', 'data-pref-bind': 'tts.enabled' }, null, ttsEnableContainer); ttsSection.appendChild(ttsEnableContainer); // TTS System const ttsSystemContainer = document.createElement('div'); ttsSystemContainer.className = 'option-item'; const ttsSystemLabel = document.createElement('label'); ttsSystemLabel.textContent = 'TTS System:'; ttsSystemContainer.appendChild(ttsSystemLabel); this.elements.ttsSystem = createUIElement('select', { 'data-pref-bind': 'tts.preferred_handler' }, null, ttsSystemContainer); ttsSection.appendChild(ttsSystemContainer); const providerStatusContainer = document.createElement('div'); providerStatusContainer.className = 'provider-status-list'; this.elements.providerStatus = providerStatusContainer; ttsSection.appendChild(providerStatusContainer); // TTS Voice const ttsVoiceContainer = document.createElement('div'); ttsVoiceContainer.className = 'option-item'; const ttsVoiceLabel = document.createElement('label'); ttsVoiceLabel.textContent = 'Voice:'; ttsVoiceContainer.appendChild(ttsVoiceLabel); this.elements.ttsVoice = createUIElement('select', { 'data-pref-bind': 'tts.voice' }, null, ttsVoiceContainer); ttsSection.appendChild(ttsVoiceContainer); // Add API Settings const apiSettings = this.createApiSettings(); ttsSection.appendChild(apiSettings); body.appendChild(ttsSection); // Audio Section const audioSection = document.createElement('div'); audioSection.className = 'options-section'; const audioTitle = document.createElement('h3'); audioTitle.textContent = 'Audio'; audioSection.appendChild(audioTitle); // Master Volume const masterVolumeContainer = document.createElement('div'); masterVolumeContainer.className = 'option-item'; const masterVolumeLabel = document.createElement('label'); masterVolumeLabel.textContent = 'Master Volume:'; masterVolumeContainer.appendChild(masterVolumeLabel); const masterVolumeValue = document.createElement('span'); masterVolumeValue.className = 'slider-value'; masterVolumeValue.textContent = '100%'; masterVolumeContainer.appendChild(masterVolumeValue); this.elements.masterVolume = createUIElement('input', { type: 'range', min: 0, max: 100, value: 100, 'data-pref-bind': 'audio.masterVolume', 'data-pref-transform': 'range:0,1' }, null, masterVolumeContainer); // Update displayed value when slider changes this.elements.masterVolume.addEventListener('input', () => { masterVolumeValue.textContent = `${this.elements.masterVolume.value}%`; }); audioSection.appendChild(masterVolumeContainer); // Speech Volume const ttsVolumeContainer = document.createElement('div'); ttsVolumeContainer.className = 'option-item'; const ttsVolumeLabel = document.createElement('label'); ttsVolumeLabel.textContent = 'Speech Volume:'; ttsVolumeContainer.appendChild(ttsVolumeLabel); const ttsVolumeValue = document.createElement('span'); ttsVolumeValue.className = 'slider-value'; ttsVolumeValue.textContent = '100%'; ttsVolumeContainer.appendChild(ttsVolumeValue); this.elements.ttsVolume = createUIElement('input', { type: 'range', min: 0, max: 100, value: 100, 'data-pref-bind': 'audio.ttsVolume', 'data-pref-transform': 'range:0,1' }, null, ttsVolumeContainer); // Update displayed value when slider changes this.elements.ttsVolume.addEventListener('input', () => { ttsVolumeValue.textContent = `${this.elements.ttsVolume.value}%`; }); audioSection.appendChild(ttsVolumeContainer); // Music Volume const musicVolumeContainer = document.createElement('div'); musicVolumeContainer.className = 'option-item'; const musicVolumeLabel = document.createElement('label'); musicVolumeLabel.textContent = 'Music Volume:'; musicVolumeContainer.appendChild(musicVolumeLabel); const musicVolumeValue = document.createElement('span'); musicVolumeValue.className = 'slider-value'; musicVolumeValue.textContent = '100%'; musicVolumeContainer.appendChild(musicVolumeValue); this.elements.musicVolume = createUIElement('input', { type: 'range', min: 0, max: 100, value: 70, 'data-pref-bind': 'audio.musicVolume', 'data-pref-transform': 'range:0,1' }, null, musicVolumeContainer); // Update displayed value when slider changes this.elements.musicVolume.addEventListener('input', () => { musicVolumeValue.textContent = `${this.elements.musicVolume.value}%`; }); audioSection.appendChild(musicVolumeContainer); // SFX Volume const sfxVolumeContainer = document.createElement('div'); sfxVolumeContainer.className = 'option-item'; const sfxVolumeLabel = document.createElement('label'); sfxVolumeLabel.textContent = 'Sound Effects Volume:'; sfxVolumeContainer.appendChild(sfxVolumeLabel); const sfxVolumeValue = document.createElement('span'); sfxVolumeValue.className = 'slider-value'; sfxVolumeValue.textContent = '100%'; sfxVolumeContainer.appendChild(sfxVolumeValue); this.elements.sfxVolume = createUIElement('input', { type: 'range', min: 0, max: 100, value: 100, 'data-pref-bind': 'audio.sfxVolume', 'data-pref-transform': 'range:0,1' }, null, sfxVolumeContainer); // Update displayed value when slider changes this.elements.sfxVolume.addEventListener('input', () => { sfxVolumeValue.textContent = `${this.elements.sfxVolume.value}%`; }); audioSection.appendChild(sfxVolumeContainer); body.appendChild(audioSection); modalContent.appendChild(body); // Create footer const footer = document.createElement('div'); footer.className = 'modal-footer'; const closeModalButton = document.createElement('button'); closeModalButton.textContent = 'Close'; closeModalButton.onclick = () => this.hide(); footer.appendChild(closeModalButton); modalContent.appendChild(footer); // Add modal content to modal this.modal.appendChild(modalContent); // Add modal to document document.body.appendChild(this.modal); } /** * Create API settings controls * @returns {HTMLElement} - API settings element */ createApiSettings() { console.log('Options UI: Creating API settings'); const apiSettings = document.createElement('div'); apiSettings.className = 'options-section'; // ElevenLabs API settings const elevenLabsSettings = document.createElement('div'); elevenLabsSettings.className = 'api-settings elevenlabs-tts-settings'; elevenLabsSettings.style.display = 'none'; const elevenLabsTitle = document.createElement('h3'); elevenLabsTitle.textContent = 'ElevenLabs API Settings'; elevenLabsSettings.appendChild(elevenLabsTitle); // ElevenLabs API Key const elevenLabsApiKeyContainer = document.createElement('div'); elevenLabsApiKeyContainer.className = 'option-item'; const elevenLabsApiKeyLabel = document.createElement('label'); elevenLabsApiKeyLabel.textContent = 'API Key:'; elevenLabsApiKeyContainer.appendChild(elevenLabsApiKeyLabel); this.elements.elevenLabsApiKey = createUIElement('input', { type: 'password', 'data-pref-bind': 'tts.elevenlabs-tts_api_key' }, null, elevenLabsApiKeyContainer); elevenLabsSettings.appendChild(elevenLabsApiKeyContainer); // ElevenLabs API URL const elevenLabsApiUrlContainer = document.createElement('div'); elevenLabsApiUrlContainer.className = 'option-item'; const elevenLabsApiUrlLabel = document.createElement('label'); elevenLabsApiUrlLabel.textContent = 'API URL:'; elevenLabsApiUrlContainer.appendChild(elevenLabsApiUrlLabel); this.elements.elevenLabsApiUrl = createUIElement('input', { type: 'text', 'data-pref-bind': 'tts.elevenlabs-tts_api_url' }, null, elevenLabsApiUrlContainer); elevenLabsSettings.appendChild(elevenLabsApiUrlContainer); // OpenAI API settings const openaiSettings = document.createElement('div'); openaiSettings.className = 'api-settings openai-tts-settings'; openaiSettings.style.display = 'none'; const openaiTitle = document.createElement('h3'); openaiTitle.textContent = 'OpenAI API Settings'; openaiSettings.appendChild(openaiTitle); // OpenAI API Key const openaiApiKeyContainer = document.createElement('div'); openaiApiKeyContainer.className = 'option-item'; const openaiApiKeyLabel = document.createElement('label'); openaiApiKeyLabel.textContent = 'API Key:'; openaiApiKeyContainer.appendChild(openaiApiKeyLabel); this.elements.openaiApiKey = createUIElement('input', { type: 'password', 'data-pref-bind': 'tts.openai-tts_api_key' }, null, openaiApiKeyContainer); openaiSettings.appendChild(openaiApiKeyContainer); // OpenAI API URL const openaiApiUrlContainer = document.createElement('div'); openaiApiUrlContainer.className = 'option-item'; const openaiApiUrlLabel = document.createElement('label'); openaiApiUrlLabel.textContent = 'API URL:'; openaiApiUrlContainer.appendChild(openaiApiUrlLabel); this.elements.openaiApiUrl = createUIElement('input', { type: 'text', 'data-pref-bind': 'tts.openai-tts_api_url' }, null, openaiApiUrlContainer); openaiSettings.appendChild(openaiApiUrlContainer); // Add all API settings to container apiSettings.appendChild(elevenLabsSettings); apiSettings.appendChild(openaiSettings); return apiSettings; } /** * Set up event listeners for options controls */ setupEventListeners() { if (!this.modal) return; // TTS System change if (this.elements.ttsSystem) { this.elements.ttsSystem.addEventListener('change', async (event) => { this.updateApiSettingsVisibility(event.target.value); const ttsFactory = this.getModule('tts-factory'); if (ttsFactory) { await ttsFactory.refreshHandlerStatus(event.target.value); } await this.populateVoices(); this.renderProviderStatuses(); }); } // Close when clicking outside the modal content this.modal.addEventListener('click', (event) => { if (event.target === this.modal) { this.hide(); } }); } /** * Update API settings visibility based on selected TTS system * @param {string} handlerId - Selected TTS system */ updateApiSettingsVisibility(handlerId) { const apiContainers = this.modal.querySelectorAll('.api-settings'); apiContainers.forEach(container => { const shouldShow = container.classList.contains(`${handlerId}-settings`); container.style.display = shouldShow ? 'block' : 'none'; }); } /** * Show the options UI */ show() { if (this.modal) { this.modal.style.display = 'flex'; document.body.classList.add('modal-open'); } } /** * Hide the options UI */ hide() { if (this.modal) { this.modal.style.display = 'none'; document.body.classList.remove('modal-open'); } } /** * Toggle the options UI visibility */ toggle() { if (this.modal) { if (this.modal.style.display === 'flex') { this.hide(); } else { this.show(); } } } /** * Populate the TTS systems dropdown */ async populateTtsSystems() { const ttsFactory = this.getModule('tts-factory'); if (!ttsFactory || !this.elements.ttsSystem) return; // Get available TTS systems const handlers = ttsFactory.getAvailableHandlers(); console.log('Options UI: Available TTS handlers:', handlers); // Format for display const systems = handlers.map(handler => ({ id: handler.id, name: handler.displayName || handler.id })); // Populate dropdown populateDropdown( this.elements.ttsSystem, systems, 'id', 'name', this.getPreference('tts', 'preferred_handler', 'none') ); // Update API settings visibility this.updateApiSettingsVisibility(this.elements.ttsSystem.value); this.renderProviderStatuses(); } /** * Populate the voices dropdown */ async populateVoices() { const ttsFactory = this.getModule('tts-factory'); if (!ttsFactory || !this.elements.ttsVoice) return; const selectedHandler = this.elements.ttsSystem?.value || this.getPreference('tts', 'preferred_handler', 'none'); const voices = typeof ttsFactory.getVoicesForHandler === 'function' ? await ttsFactory.getVoicesForHandler(selectedHandler) || [] : await ttsFactory.getVoices() || []; console.log('Options UI: TTS voices:', voices); // Populate dropdown populateDropdown( this.elements.ttsVoice, voices, 'id', 'name', this.getPreference('tts', `${selectedHandler}_voice`, this.getPreference('tts', 'voice', '')) ); } renderProviderStatuses() { const container = this.elements.providerStatus; const ttsFactory = this.getModule('tts-factory'); if (!container || !ttsFactory || typeof ttsFactory.getHandlerStatuses !== 'function') { return; } container.innerHTML = ''; const statuses = ttsFactory.getHandlerStatuses(); statuses.forEach(status => { const row = document.createElement('div'); row.className = 'provider-status-row'; const name = document.createElement('span'); name.textContent = status.name; row.appendChild(name); const value = document.createElement('span'); value.className = 'provider-status-value'; value.textContent = `${status.ready ? 'ready' : 'not ready'} - ${status.message}`; row.appendChild(value); container.appendChild(row); }); } /** * Populate the languages dropdown */ async populateLanguages() { const localization = this.getModule('localization'); if (!localization || !this.elements.language) return; // Get available languages const languages = localization.getAvailableLocales() || []; console.log('Options UI: Available languages:', languages); // Format languages with their names const languageOptions = languages.map(code => ({ code, name: localization.getLanguageName(code) })); // Populate dropdown populateDropdown( this.elements.language, languageOptions, 'code', 'name', this.getPreference('app', 'locale', 'en-us') ); } /** * Load user preferences from the persistence manager * This is now handled by the persistence manager's setupBindings method */ loadPreferences() { // Update API settings visibility based on current TTS system if (this.elements.ttsSystem) { this.updateApiSettingsVisibility(this.elements.ttsSystem.value); } } /** * Show a reload notice * @param {string} message - Message to show */ showReloadNotice(message) { console.log('Options UI: Reload required -', message); this.reloadRequired = true; } /** * Set up listeners for settings that should save immediately */ setupImmediateSaveListeners() { // Settings are saved immediately with two-way binding via data-pref-bind attributes } /** * Set up API URL fields with default values */ setupApiUrlFields() { // Set up ElevenLabs API URL if (this.elements.elevenLabsApiUrl) { const savedUrl = this.getPreference('tts', 'elevenlabs-tts_api_url'); const defaultUrl = 'https://api.elevenlabs.io/v1'; // If no saved URL, set the default if (!savedUrl) { console.log('Options UI: Setting default ElevenLabs API URL:', defaultUrl); this.updatePreference('tts', 'elevenlabs-tts_api_url', defaultUrl); } } // Set up OpenAI API URL if (this.elements.openaiApiUrl) { const savedUrl = this.getPreference('tts', 'openai-tts_api_url'); const defaultUrl = 'https://api.openai.com/v1'; // If no saved URL, set the default if (!savedUrl) { console.log('Options UI: Setting default OpenAI API URL:', defaultUrl); this.updatePreference('tts', 'openai-tts_api_url', defaultUrl); } } // Make sure API keys are initialized if not already set if (!this.getPreference('tts', 'elevenlabs-tts_api_key')) { this.updatePreference('tts', 'elevenlabs-tts_api_key', ''); } if (!this.getPreference('tts', 'openai-tts_api_key')) { this.updatePreference('tts', 'openai-tts_api_key', ''); } } /** * Set up the initial state of the Options UI */ async setupInitialState() { // Add event listener for toggling options UI document.addEventListener('ui:options:toggle', () => this.toggle()); // Set up key bindings document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.modal && this.modal.style.display === 'flex') { this.hide(); } }); // Populate TTS systems selector await this.populateTtsSystems(); // Populate languages await this.populateLanguages(); // Populate voices based on current TTS system await this.populateVoices(); // Register for TTS events to update voices when they change document.addEventListener('tts:voices:updated', () => { console.log('Options UI: Received tts:voices:updated event, updating voice dropdown'); this.populateVoices(); this.renderProviderStatuses(); }); // Set up language change listener document.addEventListener('locale:changed', async () => { this.updateUIText(); await this.populateLanguages(); }); // Register event listeners for TTS availability and voiceId changes document.addEventListener('tts:engine:change', async (event) => { console.log('Options UI: Received TTS engine change event:', event.detail); await this.populateVoices(); this.updateApiSettingsVisibility(this.elements.ttsSystem.value); this.renderProviderStatuses(); }); document.addEventListener('tts:status:updated', () => { this.renderProviderStatuses(); }); document.addEventListener('tts:enabled:change', async (event) => { if (!event.detail || typeof event.detail.enabled !== 'boolean') { return; } if (this.elements.ttsEnabled) { this.elements.ttsEnabled.checked = event.detail.enabled; } const ttsFactory = this.getModule('tts-factory'); if (!ttsFactory) { return; } if (event.detail.enabled) { const preferredHandler = this.getPreference('tts', 'preferred_handler', 'none'); if (preferredHandler !== 'none') { await ttsFactory.setActiveHandler(preferredHandler); } } else { await ttsFactory.disableAfterCurrentPlayback(); } this.renderProviderStatuses(); }); } /** * Set up two-way bindings for preferences using data attributes */ setupPreferenceBindings() { const persistenceManager = this.getModule('persistence-manager'); if (!persistenceManager || !persistenceManager.setupBindings) { console.error('Options UI: Cannot set up preference bindings, persistence manager not available or missing setupBindings method'); return; } // Setup all bindings in the modal this.bindings = persistenceManager.setupBindings('#options-modal'); console.log('Options UI: Preference bindings set up', this.bindings.length); this.updateSpeedDisplay(); // Add event listeners for side effects when preferences change document.addEventListener('preference-updated', (event) => { const { category, key, value } = event.detail; // Handle audio settings side effects if (category === 'audio') { const audioManager = this.getModule('audio-manager'); if (!audioManager) return; if (key === 'masterVolume') { audioManager.setMasterVolume(value); } else if (key === 'musicVolume') { audioManager.setMusicVolume(value); } else if (key === 'sfxVolume') { audioManager.setSfxVolume(value); } else if (key === 'ttsVolume') { audioManager.setTtsVolume(value); } } // Handle TTS settings side effects if (category === 'tts') { const ttsFactory = this.getModule('tts-factory'); if (!ttsFactory) return; if (key === 'preferred_handler') { const enabled = this.getPreference('tts', 'enabled', false); const activation = enabled && value !== 'none' ? ttsFactory.setActiveHandler(value) : Promise.resolve(ttsFactory.disableAfterCurrentPlayback()); activation.then(() => { this.populateVoices(); this.renderProviderStatuses(); }); this.updateApiSettingsVisibility(value); } else if (key === 'voice') { ttsFactory.configure({ voice: value }); } else if (key === 'speed') { ttsFactory.configure({ speed: value }); } else if (key === 'language') { ttsFactory.configure({ language: value }); } else if (key === 'enabled') { if (!value) { ttsFactory.disableAfterCurrentPlayback(); } else { const preferredHandler = this.getPreference('tts', 'preferred_handler', 'none'); if (preferredHandler !== 'none') { ttsFactory.setActiveHandler(preferredHandler); } } document.dispatchEvent(new CustomEvent('tts:enabled:change', { detail: { enabled: value } })); } else if (key.endsWith('_api_key')) { const provider = key.replace('_api_key', ''); this.dispatchApiChangeEvent('api:keyChanged', provider, 'key', value); ttsFactory.refreshHandlerStatus(provider).then(() => this.renderProviderStatuses()); } else if (key.endsWith('_api_url')) { const provider = key.replace('_api_url', ''); this.dispatchApiChangeEvent('api:urlChanged', provider, 'url', value); ttsFactory.refreshHandlerStatus(provider).then(() => this.renderProviderStatuses()); } if (key === 'speed' && this.elements.ttsSpeed) { this.updateSpeedDisplay(); } } // Handle locale changes if (category === 'app' && key === 'locale') { const localization = this.getModule('localization'); if (localization) { localization.setLocale(value); } const ttsFactory = this.getModule('tts-factory'); if (ttsFactory) { ttsFactory.configure({ language: value }); } this.updatePreference('tts', 'language', value); } }); } updateSpeedDisplay() { if (!this.elements.ttsSpeed || !this.elements.ttsSpeedValue) { return; } this.elements.ttsSpeedValue.textContent = `${this.elements.ttsSpeed.value}%`; } } // Create the singleton instance const OptionsUI = new OptionsUIModule(); // Export the module export { OptionsUI };