/** * Options UI Module for AI Interactive Fiction * Provides a user interface for adjusting game settings, TTS options, etc. */ import { BaseModule } from './base-module.js'; import { moduleRegistry } from './module-registry.js'; class OptionsUIModule extends BaseModule { /** * Create new options UI */ constructor() { super('options-ui', 'Options UI'); this.persistenceManager = null; this.ttsPlayer = null; this.audioManager = null; this.ttsFactory = null; this.modal = null; this.isOpen = false; // Configuration this.config = { modalClass: 'options-modal', modalContentClass: 'options-content', backdrop: true }; // Bound event handlers for proper this context this.handleTtsSystemChanged = this.handleTtsSystemChanged.bind(this); } /** * Initialize the module * @returns {Promise} - Resolves with success status */ async initialize() { try { // Set up event listeners window.addEventListener('tts-system-changed', this.handleTtsSystemChanged); // The option modal will be created on demand this.reportProgress(100, "Options UI ready"); return true; } catch (error) { console.error("Error initializing options UI:", error); return false; } } /** * Handle TTS system changes * @param {CustomEvent} event - The event containing TTS system change details */ handleTtsSystemChanged(event) { console.log("TTS system changed:", event.detail); if (this.isOpen) { // Refresh the voices list if the options UI is currently open this.populateVoices(); } } /** * Wait for dependencies to be ready * @returns {Promise} - Resolves when dependencies are ready */ async waitForDependencies() { try { // Wait for the persistence manager if available this.persistenceManager = moduleRegistry.getModule('persistence-manager'); this.ttsPlayer = moduleRegistry.getModule('tts'); // These dependencies are optional - UI will adapt if not available this.audioManager = moduleRegistry.getModule('audio-manager'); return true; } catch (error) { console.error("Error waiting for options UI dependencies:", error); return true; // Non-critical, can continue } } /** * Create the options UI elements */ createModal() { if (this.modal) return; // Create modal container this.modal = document.createElement('div'); this.modal.className = this.config.modalClass; this.modal.style.display = 'none'; // Create backdrop if enabled if (this.config.backdrop) { this.backdrop = document.createElement('div'); this.backdrop.className = 'modal-backdrop'; this.backdrop.addEventListener('click', () => this.hide()); this.modal.appendChild(this.backdrop); } // Create content container const content = document.createElement('div'); content.className = this.config.modalContentClass; // Add header with title and close button const header = document.createElement('div'); header.className = 'options-header'; const title = document.createElement('h2'); title.textContent = 'Options'; header.appendChild(title); const closeBtn = document.createElement('button'); closeBtn.className = 'close-button'; closeBtn.textContent = '×'; closeBtn.setAttribute('aria-label', 'Close options'); closeBtn.addEventListener('click', () => this.hide()); header.appendChild(closeBtn); content.appendChild(header); // Create tabs const tabContainer = document.createElement('div'); tabContainer.className = 'tabs-container'; const tabs = document.createElement('div'); tabs.className = 'tabs'; const tabGeneral = document.createElement('button'); tabGeneral.className = 'tab active'; tabGeneral.textContent = 'General'; tabGeneral.dataset.tab = 'general'; const tabVoice = document.createElement('button'); tabVoice.className = 'tab'; tabVoice.textContent = 'Voice'; tabVoice.dataset.tab = 'voice'; const tabAudio = document.createElement('button'); tabAudio.className = 'tab'; tabAudio.textContent = 'Audio'; tabAudio.dataset.tab = 'audio'; const tabAccess = document.createElement('button'); tabAccess.className = 'tab'; tabAccess.textContent = 'Accessibility'; tabAccess.dataset.tab = 'accessibility'; tabs.appendChild(tabGeneral); tabs.appendChild(tabVoice); tabs.appendChild(tabAudio); tabs.appendChild(tabAccess); tabContainer.appendChild(tabs); content.appendChild(tabContainer); // Create tab content sections const tabContent = document.createElement('div'); tabContent.className = 'tab-content'; // General tab content const generalContent = document.createElement('div'); generalContent.className = 'tab-pane active'; generalContent.dataset.tab = 'general'; const animSpeedSection = document.createElement('div'); animSpeedSection.className = 'option-section'; const animSpeedLabel = document.createElement('label'); animSpeedLabel.textContent = 'Animation Speed'; animSpeedLabel.htmlFor = 'option-anim-speed'; const animSpeedSlider = document.createElement('input'); animSpeedSlider.type = 'range'; animSpeedSlider.id = 'option-anim-speed'; animSpeedSlider.min = '0'; animSpeedSlider.max = '100'; animSpeedSlider.value = '50'; // Will be updated from preferences const animSpeedValue = document.createElement('span'); animSpeedValue.className = 'range-value'; animSpeedValue.textContent = '50%'; animSpeedSlider.addEventListener('input', () => { const val = animSpeedSlider.value; animSpeedValue.textContent = `${val}%`; if (this.persistenceManager) { this.persistenceManager.updatePreference('animation', 'speed', parseInt(val, 10)); } // Update animation queue speed if available const animQueue = moduleRegistry.getModule('animation-queue'); if (animQueue) { const speed = Math.pow(100.0 - val, 3) / 10000 * 10 + 0.01; animQueue.setSpeed(speed); } }); animSpeedSection.appendChild(animSpeedLabel); animSpeedSection.appendChild(animSpeedSlider); animSpeedSection.appendChild(animSpeedValue); generalContent.appendChild(animSpeedSection); // Voice tab content const voiceContent = document.createElement('div'); voiceContent.className = 'tab-pane'; voiceContent.dataset.tab = 'voice'; const ttsSysSection = document.createElement('div'); ttsSysSection.className = 'option-section'; const ttsSysLabel = document.createElement('label'); ttsSysLabel.textContent = 'TTS System'; ttsSysLabel.htmlFor = 'option-tts-system'; const ttsSysSelect = document.createElement('select'); ttsSysSelect.id = 'option-tts-system'; // Will populate systems dynamically later ttsSysSection.appendChild(ttsSysLabel); ttsSysSection.appendChild(ttsSysSelect); voiceContent.appendChild(ttsSysSection); // Voice selection section const voiceSection = document.createElement('div'); voiceSection.className = 'option-section'; const voiceLabel = document.createElement('label'); voiceLabel.textContent = 'Voice'; voiceLabel.htmlFor = 'option-voice'; const voiceSelect = document.createElement('select'); voiceSelect.id = 'option-voice'; // Will populate voices dynamically later voiceSection.appendChild(voiceLabel); voiceSection.appendChild(voiceSelect); voiceContent.appendChild(voiceSection); // Voice rate section const rateSection = document.createElement('div'); rateSection.className = 'option-section'; const rateLabel = document.createElement('label'); rateLabel.textContent = 'Speech Rate'; rateLabel.htmlFor = 'option-speech-rate'; const rateSlider = document.createElement('input'); rateSlider.type = 'range'; rateSlider.id = 'option-speech-rate'; rateSlider.min = '50'; rateSlider.max = '200'; rateSlider.value = '100'; // Will be updated from preferences const rateValue = document.createElement('span'); rateValue.className = 'range-value'; rateValue.textContent = '1.0x'; rateSlider.addEventListener('input', () => { const val = rateSlider.value; const rate = val / 100; rateValue.textContent = `${rate.toFixed(1)}x`; if (this.ttsPlayer) { this.ttsPlayer.setSpeed(rate); } if (this.persistenceManager) { this.persistenceManager.updatePreference('tts', 'rate', rate); } }); rateSection.appendChild(rateLabel); rateSection.appendChild(rateSlider); rateSection.appendChild(rateValue); voiceContent.appendChild(rateSection); // Audio tab content const audioContent = document.createElement('div'); audioContent.className = 'tab-pane'; audioContent.dataset.tab = 'audio'; // Master volume section const masterVolSection = document.createElement('div'); masterVolSection.className = 'option-section'; const masterVolLabel = document.createElement('label'); masterVolLabel.textContent = 'Master Volume'; masterVolLabel.htmlFor = 'option-master-vol'; const masterVolSlider = document.createElement('input'); masterVolSlider.type = 'range'; masterVolSlider.id = 'option-master-vol'; masterVolSlider.min = '0'; masterVolSlider.max = '100'; masterVolSlider.value = '100'; // Will be updated from preferences const masterVolValue = document.createElement('span'); masterVolValue.className = 'range-value'; masterVolValue.textContent = '100%'; masterVolSlider.addEventListener('input', () => { const val = masterVolSlider.value; masterVolValue.textContent = `${val}%`; if (this.audioManager) { this.audioManager.setMasterVolume(val / 100); } if (this.persistenceManager) { this.persistenceManager.updatePreference('audio', 'masterVolume', val / 100); } }); masterVolSection.appendChild(masterVolLabel); masterVolSection.appendChild(masterVolSlider); masterVolSection.appendChild(masterVolValue); audioContent.appendChild(masterVolSection); // TTS volume section const ttsVolSection = document.createElement('div'); ttsVolSection.className = 'option-section'; const ttsVolLabel = document.createElement('label'); ttsVolLabel.textContent = 'Speech Volume'; ttsVolLabel.htmlFor = 'option-tts-vol'; const ttsVolSlider = document.createElement('input'); ttsVolSlider.type = 'range'; ttsVolSlider.id = 'option-tts-vol'; ttsVolSlider.min = '0'; ttsVolSlider.max = '100'; ttsVolSlider.value = '100'; // Will be updated from preferences const ttsVolValue = document.createElement('span'); ttsVolValue.className = 'range-value'; ttsVolValue.textContent = '100%'; ttsVolSlider.addEventListener('input', () => { const val = ttsVolSlider.value; ttsVolValue.textContent = `${val}%`; if (this.ttsPlayer) { this.ttsPlayer.setVolume(val / 100); } if (this.persistenceManager) { this.persistenceManager.updatePreference('tts', 'volume', val / 100); } }); ttsVolSection.appendChild(ttsVolLabel); ttsVolSection.appendChild(ttsVolSlider); ttsVolSection.appendChild(ttsVolValue); audioContent.appendChild(ttsVolSection); // Music volume section (for future use) const musicVolSection = document.createElement('div'); musicVolSection.className = 'option-section'; const musicVolLabel = document.createElement('label'); musicVolLabel.textContent = 'Music Volume'; musicVolLabel.htmlFor = 'option-music-vol'; const musicVolSlider = document.createElement('input'); musicVolSlider.type = 'range'; musicVolSlider.id = 'option-music-vol'; musicVolSlider.min = '0'; musicVolSlider.max = '100'; musicVolSlider.value = '70'; // Will be updated from preferences const musicVolValue = document.createElement('span'); musicVolValue.className = 'range-value'; musicVolValue.textContent = '70%'; musicVolSlider.addEventListener('input', () => { const val = musicVolSlider.value; musicVolValue.textContent = `${val}%`; if (this.audioManager) { this.audioManager.setMusicVolume(val / 100); } if (this.persistenceManager) { this.persistenceManager.updatePreference('audio', 'musicVolume', val / 100); } }); musicVolSection.appendChild(musicVolLabel); musicVolSection.appendChild(musicVolSlider); musicVolSection.appendChild(musicVolValue); audioContent.appendChild(musicVolSection); // SFX volume section (for future use) const sfxVolSection = document.createElement('div'); sfxVolSection.className = 'option-section'; const sfxVolLabel = document.createElement('label'); sfxVolLabel.textContent = 'Effects Volume'; sfxVolLabel.htmlFor = 'option-sfx-vol'; const sfxVolSlider = document.createElement('input'); sfxVolSlider.type = 'range'; sfxVolSlider.id = 'option-sfx-vol'; sfxVolSlider.min = '0'; sfxVolSlider.max = '100'; sfxVolSlider.value = '100'; // Will be updated from preferences const sfxVolValue = document.createElement('span'); sfxVolValue.className = 'range-value'; sfxVolValue.textContent = '100%'; sfxVolSlider.addEventListener('input', () => { const val = sfxVolSlider.value; sfxVolValue.textContent = `${val}%`; if (this.audioManager) { this.audioManager.setSfxVolume(val / 100); } if (this.persistenceManager) { this.persistenceManager.updatePreference('audio', 'sfxVolume', val / 100); } }); sfxVolSection.appendChild(sfxVolLabel); sfxVolSection.appendChild(sfxVolSlider); sfxVolSection.appendChild(sfxVolValue); audioContent.appendChild(sfxVolSection); // Accessibility tab content const accessContent = document.createElement('div'); accessContent.className = 'tab-pane'; accessContent.dataset.tab = 'accessibility'; // High contrast toggle const contrastSection = document.createElement('div'); contrastSection.className = 'option-section checkbox-section'; const contrastCheckbox = document.createElement('input'); contrastCheckbox.type = 'checkbox'; contrastCheckbox.id = 'option-high-contrast'; const contrastLabel = document.createElement('label'); contrastLabel.textContent = 'High Contrast Mode'; contrastLabel.htmlFor = 'option-high-contrast'; contrastCheckbox.addEventListener('change', () => { const isEnabled = contrastCheckbox.checked; // Apply high contrast class to body if (isEnabled) { document.body.classList.add('high-contrast'); } else { document.body.classList.remove('high-contrast'); } if (this.persistenceManager) { this.persistenceManager.updatePreference('accessibility', 'highContrast', isEnabled); } }); contrastSection.appendChild(contrastCheckbox); contrastSection.appendChild(contrastLabel); accessContent.appendChild(contrastSection); // Larger text toggle const largerTextSection = document.createElement('div'); largerTextSection.className = 'option-section checkbox-section'; const largerTextCheckbox = document.createElement('input'); largerTextCheckbox.type = 'checkbox'; largerTextCheckbox.id = 'option-larger-text'; const largerTextLabel = document.createElement('label'); largerTextLabel.textContent = 'Larger Text'; largerTextLabel.htmlFor = 'option-larger-text'; largerTextCheckbox.addEventListener('change', () => { const isEnabled = largerTextCheckbox.checked; // Apply larger text class to body if (isEnabled) { document.body.classList.add('larger-text'); } else { document.body.classList.remove('larger-text'); } if (this.persistenceManager) { this.persistenceManager.updatePreference('accessibility', 'largerText', isEnabled); } }); largerTextSection.appendChild(largerTextCheckbox); largerTextSection.appendChild(largerTextLabel); accessContent.appendChild(largerTextSection); // Add tab content to container tabContent.appendChild(generalContent); tabContent.appendChild(voiceContent); tabContent.appendChild(audioContent); tabContent.appendChild(accessContent); content.appendChild(tabContent); // Add buttons at the bottom const buttons = document.createElement('div'); buttons.className = 'options-buttons'; const resetButton = document.createElement('button'); resetButton.textContent = 'Reset to Defaults'; resetButton.className = 'reset-button'; resetButton.addEventListener('click', () => this.resetToDefaults()); const saveButton = document.createElement('button'); saveButton.textContent = 'Save & Close'; saveButton.className = 'save-button'; saveButton.addEventListener('click', () => this.saveAndClose()); buttons.appendChild(resetButton); buttons.appendChild(saveButton); content.appendChild(buttons); // Set up tab switching tabs.addEventListener('click', (e) => { if (e.target.classList.contains('tab')) { // Deactivate all tabs and tab panes Array.from(tabs.querySelectorAll('.tab')).forEach(tab => { tab.classList.remove('active'); }); Array.from(tabContent.querySelectorAll('.tab-pane')).forEach(pane => { pane.classList.remove('active'); }); // Activate clicked tab and corresponding pane e.target.classList.add('active'); const tabName = e.target.dataset.tab; const pane = tabContent.querySelector(`.tab-pane[data-tab="${tabName}"]`); if (pane) { pane.classList.add('active'); } // If switching to voice tab, ensure voices are updated if (tabName === 'voice') { this.populateTtsSystems(); this.populateVoices(); } } }); this.modal.appendChild(content); document.body.appendChild(this.modal); // Store references to UI elements for later use this.elements = { animSpeed: animSpeedSlider, animSpeedValue: animSpeedValue, ttsSystem: ttsSysSelect, voiceSelect: voiceSelect, speechRate: rateSlider, speechRateValue: rateValue, masterVolume: masterVolSlider, masterVolumeValue: masterVolValue, ttsVolume: ttsVolSlider, ttsVolumeValue: ttsVolValue, musicVolume: musicVolSlider, musicVolumeValue: musicVolValue, sfxVolume: sfxVolSlider, sfxVolumeValue: sfxVolValue, highContrast: contrastCheckbox, largerText: largerTextCheckbox }; } /** * Show the options UI */ show() { if (!this.modal) { this.createModal(); } // Load current preferences this.loadPreferences(); // Populate TTS systems and voices this.populateTtsSystems(); this.populateVoices(); // Show the modal this.modal.style.display = 'flex'; this.isOpen = true; } /** * Hide the options UI */ hide() { if (this.modal) { this.modal.style.display = 'none'; this.isOpen = false; } } /** * Toggle the options UI visibility */ toggle() { if (this.isOpen) { this.hide(); } else { this.show(); } } /** * Load current preferences into UI */ loadPreferences() { if (!this.persistenceManager || !this.elements) return; const prefs = this.persistenceManager.getAllPreferences(); // Animation speed const animSpeed = this.persistenceManager.getPreference('animation', 'speed', 50); this.elements.animSpeed.value = animSpeed; this.elements.animSpeedValue.textContent = `${animSpeed}%`; // TTS settings const ttsEnabled = this.persistenceManager.getPreference('tts', 'enabled', false); const ttsProvider = this.persistenceManager.getPreference('tts', 'provider', 'browser'); const ttsVoice = this.persistenceManager.getPreference('tts', 'voice', ''); const ttsVolume = this.persistenceManager.getPreference('tts', 'volume', 1.0); const ttsRate = this.persistenceManager.getPreference('tts', 'rate', 1.0); // TTS rate slider this.elements.speechRate.value = Math.round(ttsRate * 100); this.elements.speechRateValue.textContent = `${ttsRate.toFixed(1)}x`; // TTS volume slider this.elements.ttsVolume.value = Math.round(ttsVolume * 100); this.elements.ttsVolumeValue.textContent = `${Math.round(ttsVolume * 100)}%`; // Audio volumes const masterVolume = this.persistenceManager.getPreference('audio', 'masterVolume', 1.0); const musicVolume = this.persistenceManager.getPreference('audio', 'musicVolume', 0.7); const sfxVolume = this.persistenceManager.getPreference('audio', 'sfxVolume', 1.0); this.elements.masterVolume.value = Math.round(masterVolume * 100); this.elements.masterVolumeValue.textContent = `${Math.round(masterVolume * 100)}%`; this.elements.musicVolume.value = Math.round(musicVolume * 100); this.elements.musicVolumeValue.textContent = `${Math.round(musicVolume * 100)}%`; this.elements.sfxVolume.value = Math.round(sfxVolume * 100); this.elements.sfxVolumeValue.textContent = `${Math.round(sfxVolume * 100)}%`; // Accessibility settings const highContrast = this.persistenceManager.getPreference('accessibility', 'highContrast', false); const largerText = this.persistenceManager.getPreference('accessibility', 'largerText', false); this.elements.highContrast.checked = highContrast; this.elements.largerText.checked = largerText; } /** * Populate TTS systems dropdown */ populateTtsSystems() { if (!this.ttsPlayer || !this.elements) return; const systems = this.ttsPlayer.getAvailableSystems(); const select = this.elements.ttsSystem; // Clear existing options and listeners select.innerHTML = ''; const newSelect = select.cloneNode(false); select.parentNode.replaceChild(newSelect, select); this.elements.ttsSystem = newSelect; select = newSelect; // Get current TTS info const currentInfo = this.ttsPlayer.getTTSInfo(); const currentId = currentInfo.type || ''; // Create an option for each available system systems.forEach(id => { const option = document.createElement('option'); option.value = id; switch (id) { case 'browser': option.textContent = 'Browser Built-in TTS'; break; case 'kokoro': option.textContent = 'Kokoro Neural TTS'; break; case 'api': option.textContent = 'API-based TTS'; break; default: option.textContent = id.charAt(0).toUpperCase() + id.slice(1); } if (id === currentId) { option.selected = true; } select.appendChild(option); }); // Add change listener select.addEventListener('change', () => { const selectedSystem = select.value; if (this.ttsPlayer) { this.ttsPlayer.switchTTS(selectedSystem); // Update persistence if (this.persistenceManager) { this.persistenceManager.updatePreference('tts', 'provider', selectedSystem); } } }); } /** * Populate voices dropdown for current TTS system */ async populateVoices() { if (!this.ttsPlayer || !this.elements || !this.ttsPlayer.getVoices) return; try { const voices = await this.ttsPlayer.getVoices(); const select = this.elements.voiceSelect; // Clear existing options and listeners select.innerHTML = ''; const newSelect = select.cloneNode(false); select.parentNode.replaceChild(newSelect, select); this.elements.voiceSelect = newSelect; select = newSelect; if (!voices || voices.length === 0) { const option = document.createElement('option'); option.value = ''; option.textContent = 'No voices available'; select.appendChild(option); select.disabled = true; return; } select.disabled = false; // Get current preference let currentVoice = ''; if (this.persistenceManager) { currentVoice = this.persistenceManager.getPreference('tts', 'voice', ''); } // Add voices to dropdown voices.forEach(voice => { const option = document.createElement('option'); option.value = voice.id || voice.name; option.textContent = voice.name; if (voice.id === currentVoice || voice.name === currentVoice) { option.selected = true; } select.appendChild(option); }); // Add change listener select.addEventListener('change', () => { const selectedVoice = select.value; // Update TTS if (this.ttsPlayer) { this.ttsPlayer.setVoice(selectedVoice); } // Update persistence if (this.persistenceManager) { this.persistenceManager.updatePreference('tts', 'voice', selectedVoice); } }); console.log(`Voices populated for current TTS system. Selected: ${select.value}`); } catch (error) { console.error("Error populating voices:", error); const select = this.elements.voiceSelect; select.innerHTML = ''; const option = document.createElement('option'); option.value = ''; option.textContent = 'Error loading voices'; select.appendChild(option); select.disabled = true; } } /** * Reset all options to defaults */ resetToDefaults() { if (!this.persistenceManager) return; const confirmed = confirm('Reset all options to default values?'); if (confirmed) { // Reset preferences this.persistenceManager.resetPreferences(); // Update UI this.loadPreferences(); // Apply changes this.applySettings(); // Refresh voice list this.populateVoices(); } } /** * Save settings and close modal */ saveAndClose() { if (this.persistenceManager && this.elements) { // Save preferences - already saved as they change // Apply settings this.applySettings(); } this.hide(); } /** * Apply current settings to the app */ applySettings() { if (!this.persistenceManager) return; // Apply animation speed const animSpeed = this.persistenceManager.getPreference('animation', 'speed', 50); const animQueue = moduleRegistry.getModule('animation-queue'); if (animQueue) { const speed = Math.pow(100.0 - animSpeed, 3) / 10000 * 10 + 0.01; animQueue.setSpeed(speed); } // Apply TTS settings const ttsEnabled = this.persistenceManager.getPreference('tts', 'enabled', false); const ttsProvider = this.persistenceManager.getPreference('tts', 'provider', 'browser'); const ttsVoice = this.persistenceManager.getPreference('tts', 'voice', ''); const ttsVolume = this.persistenceManager.getPreference('tts', 'volume', 1.0); const ttsRate = this.persistenceManager.getPreference('tts', 'rate', 1.0); if (this.ttsPlayer) { // Set TTS system if (ttsProvider) { this.ttsPlayer.switchTTS(ttsProvider); } // Apply voice options this.ttsPlayer.setVoiceOptions({ voice: ttsVoice, volume: ttsVolume, rate: ttsRate }); } // Apply audio volume settings const masterVolume = this.persistenceManager.getPreference('audio', 'masterVolume', 1.0); const musicVolume = this.persistenceManager.getPreference('audio', 'musicVolume', 0.7); const sfxVolume = this.persistenceManager.getPreference('audio', 'sfxVolume', 1.0); if (this.audioManager) { this.audioManager.setMasterVolume(masterVolume); this.audioManager.setMusicVolume(musicVolume); this.audioManager.setSfxVolume(sfxVolume); } // Apply accessibility settings const highContrast = this.persistenceManager.getPreference('accessibility', 'highContrast', false); const largerText = this.persistenceManager.getPreference('accessibility', 'largerText', false); if (highContrast) { document.body.classList.add('high-contrast'); } else { document.body.classList.remove('high-contrast'); } if (largerText) { document.body.classList.add('larger-text'); } else { document.body.classList.remove('larger-text'); } } /** * Set the TTS factory reference * @param {Object} factory - The TTS factory instance */ setTtsFactory(factory) { this.ttsFactory = factory; } /** * Update available TTS systems info * @param {Object} systemsInfo - Information about available TTS systems */ updateAvailableSystems(systemsInfo) { // Will repopulate next time UI is opened console.log("TTS systems info updated:", systemsInfo); // If the options UI is currently open, update it if (this.isOpen) { this.populateTtsSystems(); this.populateVoices(); } } /** * Clean up when module is disposed */ dispose() { // Remove event listeners window.removeEventListener('tts-system-changed', this.handleTtsSystemChanged); } } // Create the singleton instance const OptionsUI = new OptionsUIModule(); // Register with the module registry moduleRegistry.register(OptionsUI); // Export the module export { OptionsUI }; // Keep a reference in window for loader system window.OptionsUI = OptionsUI;