/** * 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'); // Dependencies this.dependencies = ['persistence-manager', 'localization']; this.persistenceManager = null; this.ttsPlayer = null; this.audioManager = null; this.ttsFactory = null; this.localization = null; this.modal = null; this.isOpen = false; // Configuration this.config = { modalClass: 'options-modal', modalContentClass: 'options-content', backdrop: true }; // Elements reference this.elements = null; // Bound event handlers for proper this context this.bindMethods([ 'handleTtsSystemChanged', 'loadPreferences', 'populateTtsSystems', 'populateVoices', 'resetToDefaults', 'saveAndClose', 'applySettings' ]); } /** * 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; } } /** * Wait for dependencies to be ready * @returns {Promise} - Resolves when dependencies are ready */ async waitForDependencies() { try { // Get required modules this.persistenceManager = this.getModule('persistence-manager'); if (!this.persistenceManager) { console.warn("Options UI: Persistence Manager not found"); } this.localization = this.getModule('localization'); if (!this.localization) { console.warn("Options UI: Localization module not found"); } // These dependencies are optional - UI will adapt if not available this.ttsFactory = this.getModule('tts-factory'); this.ttsPlayer = this.getModule('tts'); this.audioManager = this.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 }; } /** * Load current preferences into UI */ loadPreferences() { if (!this.persistenceManager || !this.elements) return; // Wait for dependencies this.waitForDependencies().then(() => { // Get current preferences const prefs = this.persistenceManager.getAllPreferences(); // Animation speed if (this.elements.animationSpeed) { this.elements.animationSpeed.value = prefs.animation.speed; this.elements.animationSpeedValue.textContent = prefs.animation.speed; } // TTS enabled if (this.elements.ttsEnabled) { this.elements.ttsEnabled.checked = prefs.tts.enabled; // Show/hide TTS options based on enabled state const ttsOptionsContainer = document.querySelector('.tts-options-container'); if (ttsOptionsContainer) { ttsOptionsContainer.style.display = prefs.tts.enabled ? 'block' : 'none'; } } // TTS system this.populateTtsSystems(); // TTS volume if (this.elements.ttsVolume) { this.elements.ttsVolume.value = prefs.tts.volume * 100; this.elements.ttsVolumeValue.textContent = Math.round(prefs.tts.volume * 100); } // TTS rate if (this.elements.ttsRate) { this.elements.ttsRate.value = prefs.tts.rate * 100; this.elements.ttsRateValue.textContent = Math.round(prefs.tts.rate * 100); } // Language selection if (this.elements.language && this.localization) { const currentLocale = this.localization.getLocale(); const availableLocales = this.localization.getAvailableLocales(); // Clear existing options this.elements.language.innerHTML = ''; // Add options for each available locale availableLocales.forEach(locale => { const option = document.createElement('option'); option.value = locale; option.textContent = this.localization.getLanguageName(locale); option.selected = locale === currentLocale; this.elements.language.appendChild(option); }); } // Audio volumes if (this.elements.masterVolume) { this.elements.masterVolume.value = prefs.audio.masterVolume * 100; this.elements.masterVolumeValue.textContent = Math.round(prefs.audio.masterVolume * 100); } if (this.elements.musicVolume) { this.elements.musicVolume.value = prefs.audio.musicVolume * 100; this.elements.musicVolumeValue.textContent = Math.round(prefs.audio.musicVolume * 100); } if (this.elements.sfxVolume) { this.elements.sfxVolume.value = prefs.audio.sfxVolume * 100; this.elements.sfxVolumeValue.textContent = Math.round(prefs.audio.sfxVolume * 100); } // Accessibility options if (this.elements.highContrast) { this.elements.highContrast.checked = prefs.accessibility.highContrast; } if (this.elements.largerText) { this.elements.largerText.checked = prefs.accessibility.largerText; } }); } /** * Populate TTS systems dropdown */ populateTtsSystems() { if (!this.elements || !this.elements.ttsSystem) return; // Clear existing options this.elements.ttsSystem.innerHTML = ''; // Get current TTS preferences const currentProvider = this.persistenceManager.getPreference('tts', 'provider', 'browser'); // Get available handlers from TTS factory let availableHandlers = {}; if (this.ttsFactory) { availableHandlers = this.ttsFactory.getAvailableHandlers(); } else { // Fallback if TTS factory not available availableHandlers = { browser: true, // Assume browser TTS is available api: false, // Assume API TTS is not available kokoro: false // Assume Kokoro is not available }; } // Add option for each handler const handlers = [ { id: 'browser', name: 'Browser TTS', description: 'Uses your browser\'s built-in speech synthesis' }, { id: 'api', name: 'API TTS', description: 'Uses a remote API for higher quality voices' }, { id: 'kokoro', name: 'Kokoro TTS', description: 'Uses local AI-powered speech synthesis' } ]; handlers.forEach(handler => { const option = document.createElement('option'); option.value = handler.id; // Check if handler is available const isAvailable = availableHandlers[handler.id] === true; // Format option text option.textContent = `${handler.name}${isAvailable ? '' : ' (unavailable)'}`; option.title = handler.description; // Disable option if handler is not available option.disabled = !isAvailable; // Select if this is the current provider option.selected = handler.id === currentProvider; this.elements.ttsSystem.appendChild(option); }); // Populate voices for the selected system this.populateVoices(); } /** * Populate voices dropdown for current TTS system */ populateVoices() { if (!this.elements || !this.elements.ttsVoice) return; // Clear existing options this.elements.ttsVoice.innerHTML = ''; // Get current preferences const currentVoice = this.persistenceManager.getPreference('tts', 'voice', ''); const currentProvider = this.persistenceManager.getPreference('tts', 'provider', 'browser'); // Get current locale const currentLocale = this.localization ? this.localization.getLocale() : 'en-us'; // Get voices from TTS factory let voices = []; if (this.ttsFactory) { // Get active handler const activeHandler = this.ttsFactory.getActiveHandler(); if (activeHandler) { voices = activeHandler.getVoices(); } } // If no voices available, add a placeholder if (voices.length === 0) { const option = document.createElement('option'); option.value = ''; option.textContent = 'No voices available'; this.elements.ttsVoice.appendChild(option); return; } // Sort voices by language and name voices.sort((a, b) => { // First sort by matching current locale const aMatchesLocale = a.lang && a.lang.toLowerCase().startsWith(currentLocale.split('-')[0]); const bMatchesLocale = b.lang && b.lang.toLowerCase().startsWith(currentLocale.split('-')[0]); if (aMatchesLocale && !bMatchesLocale) return -1; if (!aMatchesLocale && bMatchesLocale) return 1; // Then sort by language name const aLang = this.getLanguageNameFromCode(a.lang); const bLang = this.getLanguageNameFromCode(b.lang); if (aLang !== bLang) { return aLang.localeCompare(bLang); } // Finally sort by voice name return a.name.localeCompare(b.name); }); // Group voices by language const voicesByLang = {}; voices.forEach(voice => { const langCode = voice.lang || 'unknown'; const langName = this.getLanguageNameFromCode(langCode); if (!voicesByLang[langName]) { voicesByLang[langName] = []; } voicesByLang[langName].push(voice); }); // Add voices grouped by language Object.keys(voicesByLang).sort().forEach(langName => { // Create optgroup for language const optgroup = document.createElement('optgroup'); optgroup.label = langName; // Add voices for this language voicesByLang[langName].forEach(voice => { const option = document.createElement('option'); option.value = voice.name || voice.id; option.textContent = voice.name; option.selected = voice.name === currentVoice || voice.id === currentVoice; optgroup.appendChild(option); }); this.elements.ttsVoice.appendChild(optgroup); }); } /** * Get language name from language code * @param {string} code - Language code (e.g., 'en', 'de') * @returns {string} - Language name */ getLanguageNameFromCode(code) { // Use localization module if available if (this.localization && typeof this.localization.getLanguageName === 'function') { return this.localization.getLanguageName(code); } // Fallback language names const languageNames = { 'en': 'English', 'de': 'German', 'fr': 'French', 'es': 'Spanish', 'it': 'Italian', 'ja': 'Japanese', 'ko': 'Korean', 'zh': 'Chinese', 'ru': 'Russian', 'ar': 'Arabic', 'hi': 'Hindi', 'pt': 'Portuguese', 'nl': 'Dutch', 'pl': 'Polish', 'sv': 'Swedish', 'tr': 'Turkish', 'uk': 'Ukrainian' }; return languageNames[code] || code.toUpperCase(); } /** * 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(); } } /** * 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(); } } /** * 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.ttsFactory) { // Set TTS provider if it's available const availableHandlers = this.ttsFactory.getAvailableHandlers(); if (ttsProvider && availableHandlers[ttsProvider]) { this.ttsFactory.setActiveHandler(ttsProvider); } // Get the active handler const activeHandler = this.ttsFactory.getActiveHandler(); if (activeHandler) { // Set voice if specified if (ttsVoice) { activeHandler.setVoice(ttsVoice); } // Set options activeHandler.setOptions({ volume: ttsVolume, rate: ttsRate }); } } // Apply language settings if (this.localization && this.elements && this.elements.language) { const selectedLocale = this.elements.language.value; if (selectedLocale && selectedLocale !== this.localization.getLocale()) { this.localization.setLocale(selectedLocale); } } // 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;