/** * Options UI Module * Provides the options UI for the game */ import { BaseModule } from './base-module.js'; import { moduleRegistry } from './module-registry.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', 'applySettings', 'handleTtsSystemChanged', 'showReloadNotice', 'toggle', 'setupEventListeners', 'saveCurrentSettings', 'setupApiUrlFields', 'setupInitialState', // Helper methods 'createUIElement', 'populateDropdown', 'registerHandler', 'dispatchApiChangeEvent', 'getPreference', 'updatePreference' ]); } /** * Creates a UI element and optionally appends it to a parent * @param {string} type - Element type ('div', 'button', etc.) * @param {string} className - CSS class name * @param {string|Object} textOrProps - Text content or properties object * @param {HTMLElement} parent - Parent element to append to * @returns {HTMLElement} - The created element */ createUIElement(type, className, textOrProps, parent) { const element = document.createElement(type); if (className) element.className = className; if (typeof textOrProps === 'string') { element.textContent = textOrProps; } else if (textOrProps) { Object.assign(element, textOrProps); } if (parent) parent.appendChild(element); return element; } /** * Populates a select dropdown with options * @param {HTMLSelectElement} selectElement - The select element to populate * @param {Array} items - Array of items to add as options * @param {string|Function} valueKey - Property name or function to extract value * @param {string|Function} textKey - Property name or function to extract text * @param {string} selectedValue - Value to select */ populateDropdown(selectElement, items, valueKey, textKey, selectedValue) { if (!selectElement) return; // Clear existing options selectElement.innerHTML = ''; // Add options items.forEach(item => { const option = document.createElement('option'); option.value = typeof valueKey === 'function' ? valueKey(item) : item[valueKey]; option.textContent = typeof textKey === 'function' ? textKey(item) : item[textKey]; selectElement.appendChild(option); }); // Set selected value if provided and exists if (selectedValue && selectElement.querySelector(`option[value="${selectedValue}"]`)) { selectElement.value = selectedValue; } } /** * Registers an event handler on an element * @param {string} elementName - Element name in this.elements * @param {string} eventType - Event type to listen for * @param {Function} handler - Event handler function */ registerHandler(elementName, eventType, handler) { if (this.elements && this.elements[elementName]) { this.elements[elementName].addEventListener(eventType, handler); } } /** * Dispatches an API change event * @param {string} eventType - Event type (e.g. 'tts:api:keyChanged') * @param {string} provider - Provider name (e.g. 'elevenlabs') * @param {string} valueType - Value type (e.g. 'key', 'url') * @param {string} value - The value */ dispatchApiChangeEvent(eventType, provider, valueType, value) { const detail = { provider }; detail[valueType] = value; document.dispatchEvent(new CustomEvent(eventType, { detail })); } /** * Gets a preference from persistence manager * @param {string} category - Preference category * @param {string} key - Preference key * @param {*} defaultValue - Default value if not found * @returns {*} - Preference value */ getPreference(category, key, defaultValue = null) { const persistenceManager = this.getModule('persistence-manager'); return persistenceManager.getPreference(category, key) || defaultValue; } /** * Updates a preference in 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(); // Initialize module this.reportProgress(50, 'Initializing UI'); // Set up initial state await this.setupInitialState(); // Set up API URL fields with correct defaults this.setupApiUrlFields(); // Set up immediate save listeners for all input controls this.setupImmediateSaveListeners(); this.reportProgress(100, 'Options UI initialized'); return true; } /** * Create the options modal */ createModal() { if (this.modal) return; // Create modal container this.modal = document.createElement('div'); this.modal.id = 'options-modal'; this.modal.className = 'options-modal'; this.modal.style.display = 'none'; // Create modal content const content = document.createElement('div'); content.className = 'options-content'; // Create header const header = document.createElement('div'); header.className = 'options-header'; const title = document.createElement('h2'); title.textContent = 'Options'; header.appendChild(title); const closeButton = document.createElement('button'); closeButton.className = 'options-close'; closeButton.innerHTML = '×'; closeButton.addEventListener('click', () => { // Save all current settings when closing this.saveCurrentSettings(); this.hide(); }); header.appendChild(closeButton); content.appendChild(header); // Create settings container const settings = document.createElement('div'); settings.className = 'options-settings'; // TTS Settings const ttsSection = document.createElement('div'); ttsSection.className = 'options-section'; const ttsTitle = document.createElement('h3'); ttsTitle.textContent = 'Text-to-Speech'; ttsSection.appendChild(ttsTitle); // TTS Toggle const ttsSpeechToggleContainer = document.createElement('div'); ttsSpeechToggleContainer.className = 'options-row'; const ttsSpeechToggleLabel = document.createElement('label'); ttsSpeechToggleLabel.textContent = 'Enable Speech:'; ttsSpeechToggleContainer.appendChild(ttsSpeechToggleLabel); const ttsSpeechToggle = document.createElement('input'); ttsSpeechToggle.type = 'checkbox'; ttsSpeechToggle.id = 'tts-speech-toggle'; ttsSpeechToggle.addEventListener('change', (e) => { const persistenceManager = this.getModule('persistence-manager'); if (persistenceManager) { const enabled = e.target.checked; persistenceManager.updatePreference('tts', 'enabled', enabled); // Dispatch event for TTS state change document.dispatchEvent(new CustomEvent('tts:stateChange', { detail: { enabled: enabled } })); } }); ttsSpeechToggleContainer.appendChild(ttsSpeechToggle); ttsSection.appendChild(ttsSpeechToggleContainer); // TTS System const ttsSystemContainer = document.createElement('div'); ttsSystemContainer.className = 'options-row'; const ttsSystemLabel = document.createElement('label'); ttsSystemLabel.textContent = 'TTS System:'; ttsSystemContainer.appendChild(ttsSystemLabel); const ttsSystem = document.createElement('select'); ttsSystem.id = 'tts-system'; ttsSystem.addEventListener('change', (e) => { const persistenceManager = this.getModule('persistence-manager'); const ttsFactory = this.getModule('tts-factory'); if (persistenceManager && ttsFactory) { const provider = e.target.value; persistenceManager.updatePreference('tts', 'provider', provider); ttsFactory.setActiveHandler(provider); // Update TTS enabled state based on provider const enabled = provider !== 'none'; persistenceManager.updatePreference('tts', 'enabled', enabled); // Dispatch event for TTS state change document.dispatchEvent(new CustomEvent('tts:stateChange', { detail: { enabled: enabled } })); this.populateVoices(); } }); ttsSystemContainer.appendChild(ttsSystem); ttsSection.appendChild(ttsSystemContainer); // TTS Voice const ttsVoiceContainer = document.createElement('div'); ttsVoiceContainer.className = 'options-row'; const ttsVoiceLabel = document.createElement('label'); ttsVoiceLabel.textContent = 'Voice:'; ttsVoiceContainer.appendChild(ttsVoiceLabel); const ttsVoice = document.createElement('select'); ttsVoice.id = 'tts-voice'; ttsVoice.addEventListener('change', (e) => { const persistenceManager = this.getModule('persistence-manager'); if (persistenceManager) { persistenceManager.updatePreference('tts', 'voice', e.target.value); } }); ttsVoiceContainer.appendChild(ttsVoice); ttsSection.appendChild(ttsVoiceContainer); // API TTS Provider Settings (ElevenLabs and OpenAI) // Container for API settings that will be shown/hidden based on selected TTS system const apiSettingsContainer = document.createElement('div'); apiSettingsContainer.id = 'api-tts-settings'; apiSettingsContainer.className = 'api-settings-container'; apiSettingsContainer.style.display = 'none'; // ElevenLabs API Key const elevenLabsApiKeyContainer = document.createElement('div'); elevenLabsApiKeyContainer.className = 'options-row elevenlabs-setting'; elevenLabsApiKeyContainer.dataset.provider = 'elevenlabs'; const elevenLabsApiKeyLabel = document.createElement('label'); elevenLabsApiKeyLabel.textContent = 'ElevenLabs API Key:'; elevenLabsApiKeyContainer.appendChild(elevenLabsApiKeyLabel); const elevenLabsApiKey = document.createElement('input'); elevenLabsApiKey.type = 'password'; elevenLabsApiKey.id = 'elevenlabs-api-key'; elevenLabsApiKey.placeholder = 'Enter your ElevenLabs API key'; elevenLabsApiKey.addEventListener('change', (e) => { const persistenceManager = this.getModule('persistence-manager'); if (persistenceManager) { persistenceManager.updatePreference('tts', 'elevenlabs_api_key', e.target.value); // Notify TTS system that API key has changed document.dispatchEvent(new CustomEvent('tts:api:keyChanged', { detail: { provider: 'elevenlabs', key: e.target.value } })); } }); elevenLabsApiKeyContainer.appendChild(elevenLabsApiKey); apiSettingsContainer.appendChild(elevenLabsApiKeyContainer); // ElevenLabs API Base URL const elevenLabsApiUrlContainer = document.createElement('div'); elevenLabsApiUrlContainer.className = 'options-row elevenlabs-setting'; elevenLabsApiUrlContainer.dataset.provider = 'elevenlabs'; const elevenLabsApiUrlLabel = document.createElement('label'); elevenLabsApiUrlLabel.textContent = 'ElevenLabs API URL:'; elevenLabsApiUrlContainer.appendChild(elevenLabsApiUrlLabel); const elevenLabsApiUrl = document.createElement('input'); elevenLabsApiUrl.type = 'text'; elevenLabsApiUrl.id = 'elevenlabs-api-url'; elevenLabsApiUrl.placeholder = 'https://api.elevenlabs.io/v1'; elevenLabsApiUrl.addEventListener('change', (e) => { const persistenceManager = this.getModule('persistence-manager'); if (persistenceManager) { persistenceManager.updatePreference('tts', 'elevenlabs_api_url', e.target.value); // Notify TTS system that API URL has changed document.dispatchEvent(new CustomEvent('tts:api:urlChanged', { detail: { provider: 'elevenlabs', url: e.target.value } })); } }); elevenLabsApiUrlContainer.appendChild(elevenLabsApiUrl); apiSettingsContainer.appendChild(elevenLabsApiUrlContainer); // OpenAI API Key const openaiApiKeyContainer = document.createElement('div'); openaiApiKeyContainer.className = 'options-row openai-setting'; openaiApiKeyContainer.dataset.provider = 'openai'; const openaiApiKeyLabel = document.createElement('label'); openaiApiKeyLabel.textContent = 'OpenAI API Key:'; openaiApiKeyContainer.appendChild(openaiApiKeyLabel); const openaiApiKey = document.createElement('input'); openaiApiKey.type = 'password'; openaiApiKey.id = 'openai-api-key'; openaiApiKey.placeholder = 'Enter your OpenAI API key'; openaiApiKey.addEventListener('change', (e) => { const persistenceManager = this.getModule('persistence-manager'); if (persistenceManager) { persistenceManager.updatePreference('tts', 'openai_api_key', e.target.value); // Notify TTS system that API key has changed document.dispatchEvent(new CustomEvent('tts:api:keyChanged', { detail: { provider: 'openai', key: e.target.value } })); } }); openaiApiKeyContainer.appendChild(openaiApiKey); apiSettingsContainer.appendChild(openaiApiKeyContainer); // OpenAI API Base URL const openaiApiUrlContainer = document.createElement('div'); openaiApiUrlContainer.className = 'options-row openai-setting'; openaiApiUrlContainer.dataset.provider = 'openai'; const openaiApiUrlLabel = document.createElement('label'); openaiApiUrlLabel.textContent = 'OpenAI API URL:'; openaiApiUrlContainer.appendChild(openaiApiUrlLabel); const openaiApiUrl = document.createElement('input'); openaiApiUrl.type = 'text'; openaiApiUrl.id = 'openai-api-url'; openaiApiUrl.placeholder = 'https://api.openai.com/v1'; openaiApiUrl.addEventListener('change', (e) => { const persistenceManager = this.getModule('persistence-manager'); if (persistenceManager) { persistenceManager.updatePreference('tts', 'openai_api_url', e.target.value); // Notify TTS system that API URL has changed document.dispatchEvent(new CustomEvent('tts:api:urlChanged', { detail: { provider: 'openai', url: e.target.value } })); } }); openaiApiUrlContainer.appendChild(openaiApiUrl); apiSettingsContainer.appendChild(openaiApiUrlContainer); ttsSection.appendChild(apiSettingsContainer); // Speed controls const speedContainer = document.createElement('div'); speedContainer.className = 'options-row'; const speedLabel = document.createElement('label'); speedLabel.textContent = 'Speed:'; speedContainer.appendChild(speedLabel); const speedSlider = document.createElement('input'); speedSlider.type = 'range'; speedSlider.min = '0'; speedSlider.max = '100'; speedSlider.value = '50'; // Default to 0.5 speed (50 out of 100) speedSlider.id = 'speech-rate'; speedSlider.addEventListener('input', (e) => { const persistenceManager = this.getModule('persistence-manager'); const ttsFactory = this.getModule('tts-factory'); if (persistenceManager && ttsFactory) { // Convert to normalized speed (0-1 range) const speed = parseInt(e.target.value) / 100; // Update persistence manager persistenceManager.updatePreference('tts', 'speed', speed); // Configure the TTS factory ttsFactory.configure({ speed: speed }); // Broadcast the speed change event for other components document.dispatchEvent(new CustomEvent('tts:speed:change', { detail: { speed: speed } })); } }); speedContainer.appendChild(speedSlider); ttsSection.appendChild(speedContainer); // Language const languageContainer = document.createElement('div'); languageContainer.className = 'options-row'; const languageLabel = document.createElement('label'); languageLabel.textContent = 'Language:'; languageContainer.appendChild(languageLabel); const language = document.createElement('select'); language.id = 'language'; language.addEventListener('change', (e) => { const persistenceManager = this.getModule('persistence-manager'); const localization = this.getModule('localization'); if (persistenceManager && localization) { persistenceManager.updatePreference('app', 'locale', e.target.value); persistenceManager.updatePreference('tts', 'language', e.target.value); localization.setLocale(e.target.value); this.showReloadNotice(); } }); languageContainer.appendChild(language); ttsSection.appendChild(languageContainer); // Text Speed const textSpeedContainer = document.createElement('div'); textSpeedContainer.className = 'options-row'; const textSpeedLabel = document.createElement('label'); textSpeedLabel.textContent = 'Text Speed:'; textSpeedContainer.appendChild(textSpeedLabel); const textSpeed = document.createElement('input'); textSpeed.type = 'range'; textSpeed.min = '0'; textSpeed.max = '100'; textSpeed.value = '50'; textSpeed.id = 'text-speed'; textSpeed.addEventListener('input', (e) => { const persistenceManager = this.getModule('persistence-manager'); if (persistenceManager) { persistenceManager.updatePreference('animation', 'speed', parseInt(e.target.value)); } }); textSpeedContainer.appendChild(textSpeed); ttsSection.appendChild(textSpeedContainer); settings.appendChild(ttsSection); // Audio Settings 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 = 'options-row'; const masterVolumeLabel = document.createElement('label'); masterVolumeLabel.textContent = 'Master Volume:'; masterVolumeContainer.appendChild(masterVolumeLabel); const masterVolume = document.createElement('input'); masterVolume.type = 'range'; masterVolume.min = '0'; masterVolume.max = '100'; masterVolume.value = '100'; masterVolume.id = 'master-volume'; masterVolume.addEventListener('input', (e) => { const persistenceManager = this.getModule('persistence-manager'); const audioManager = this.getModule('audio-manager'); if (persistenceManager && audioManager) { const volume = parseInt(e.target.value) / 100; persistenceManager.updatePreference('audio', 'masterVolume', volume); audioManager.setMasterVolume(volume); } }); masterVolumeContainer.appendChild(masterVolume); audioSection.appendChild(masterVolumeContainer); // Speech Volume const speechVolumeContainer = document.createElement('div'); speechVolumeContainer.className = 'options-row'; const speechVolumeLabel = document.createElement('label'); speechVolumeLabel.textContent = 'Speech Volume:'; speechVolumeContainer.appendChild(speechVolumeLabel); const speechVolume = document.createElement('input'); speechVolume.type = 'range'; speechVolume.min = '0'; speechVolume.max = '100'; speechVolume.value = '100'; speechVolume.id = 'speech-volume'; speechVolume.addEventListener('input', (e) => { const persistenceManager = this.getModule('persistence-manager'); if (persistenceManager) { const volume = parseInt(e.target.value) / 100; persistenceManager.updatePreference('tts', 'volume', volume); } }); speechVolumeContainer.appendChild(speechVolume); audioSection.appendChild(speechVolumeContainer); // Music Volume const musicVolumeContainer = document.createElement('div'); musicVolumeContainer.className = 'options-row'; const musicVolumeLabel = document.createElement('label'); musicVolumeLabel.textContent = 'Music Volume:'; musicVolumeContainer.appendChild(musicVolumeLabel); const musicVolume = document.createElement('input'); musicVolume.type = 'range'; musicVolume.min = '0'; musicVolume.max = '100'; musicVolume.value = '70'; musicVolume.id = 'music-volume'; musicVolume.addEventListener('input', (e) => { const persistenceManager = this.getModule('persistence-manager'); const audioManager = this.getModule('audio-manager'); if (persistenceManager && audioManager) { const volume = parseInt(e.target.value) / 100; persistenceManager.updatePreference('audio', 'musicVolume', volume); audioManager.setMusicVolume(volume); } }); musicVolumeContainer.appendChild(musicVolume); audioSection.appendChild(musicVolumeContainer); // Effects Volume const effectsVolumeContainer = document.createElement('div'); effectsVolumeContainer.className = 'options-row'; const effectsVolumeLabel = document.createElement('label'); effectsVolumeLabel.textContent = 'Effects Volume:'; effectsVolumeContainer.appendChild(effectsVolumeLabel); const effectsVolume = document.createElement('input'); effectsVolume.type = 'range'; effectsVolume.min = '0'; effectsVolume.max = '100'; effectsVolume.value = '100'; effectsVolume.id = 'effects-volume'; effectsVolume.addEventListener('input', (e) => { const persistenceManager = this.getModule('persistence-manager'); const audioManager = this.getModule('audio-manager'); if (persistenceManager && audioManager) { const volume = parseInt(e.target.value) / 100; persistenceManager.updatePreference('audio', 'sfxVolume', volume); audioManager.setSfxVolume(volume); } }); effectsVolumeContainer.appendChild(effectsVolume); audioSection.appendChild(effectsVolumeContainer); settings.appendChild(audioSection); // Reload notice const reloadNotice = document.createElement('div'); reloadNotice.id = 'reload-notice'; reloadNotice.className = 'reload-notice'; reloadNotice.style.display = 'none'; reloadNotice.innerHTML = '* Changes to language or speech system require a page reload to take full effect.'; settings.appendChild(reloadNotice); content.appendChild(settings); this.modal.appendChild(content); document.body.appendChild(this.modal); // Store references to elements this.elements = { ttsSystem: ttsSystem, ttsVoice: ttsVoice, language: language, textSpeed: textSpeed, masterVolume: masterVolume, speechVolume: speechVolume, musicVolume: musicVolume, effectsVolume: effectsVolume, reloadNotice: reloadNotice, speechRate: speedSlider, ttsSpeechToggle: ttsSpeechToggle, apiSettingsContainer: apiSettingsContainer, elevenLabsApiKey: elevenLabsApiKey, elevenLabsApiUrl: elevenLabsApiUrl, openaiApiKey: openaiApiKey, openaiApiUrl: openaiApiUrl }; } /** * Show the options modal */ show() { // Show modal if (this.modal) { this.modal.style.display = 'flex'; // Refresh TTS dropdown this.populateTtsSystems(); // Make sure the UI reflects the current voice this.populateVoices(); // Update API settings visibility based on the current selection this.updateApiSettingsVisibility(); } } /** * Hide the options modal */ hide() { if (!this.modal) return; this.modal.style.display = 'none'; } /** * Toggle the options modal */ toggle() { if (this.modal.style.display === 'flex') { this.hide(); } else { this.show(); } } /** * Populate TTS systems dropdown */ populateTtsSystems() { if (!this.elements.ttsSystem) return; const ttsFactory = this.getModule('tts-factory'); // Get available TTS handlers const handlers = ttsFactory.getAvailableHandlers(); // Format for dropdown const items = handlers.map(handler => ({ id: handler.id, name: this.getTtsSystemName(handler.id) })); // Get current handler const currentHandler = ttsFactory.getActiveHandler(); const currentHandlerId = currentHandler ? currentHandler.id : 'none'; // Populate dropdown this.populateDropdown( this.elements.ttsSystem, items, 'id', 'name', currentHandlerId ); // Update API settings visibility this.updateApiSettingsVisibility(); } /** * Update visibility of API settings based on selected TTS system */ updateApiSettingsVisibility() { if (!this.elements.ttsSystem || !this.elements.apiSettingsContainer) return; const selectedSystem = this.elements.ttsSystem.value; const isApiSystem = selectedSystem === 'elevenlabs' || selectedSystem === 'openai'; // Show or hide API settings this.elements.apiSettingsContainer.style.display = isApiSystem ? 'block' : 'none'; // Show/hide specific provider settings if (this.elements.elevenLabsApiKey && this.elements.elevenLabsApiUrl) { const isElevenLabs = selectedSystem === 'elevenlabs'; this.elements.elevenLabsApiKey.parentNode.style.display = isElevenLabs ? 'block' : 'none'; this.elements.elevenLabsApiUrl.parentNode.style.display = isElevenLabs ? 'block' : 'none'; } if (this.elements.openaiApiKey && this.elements.openaiApiUrl) { const isOpenAI = selectedSystem === 'openai'; this.elements.openaiApiKey.parentNode.style.display = isOpenAI ? 'block' : 'none'; this.elements.openaiApiUrl.parentNode.style.display = isOpenAI ? 'block' : 'none'; } } /** * Get a user-friendly name for a TTS system * @param {string} id - TTS system ID * @returns {string} - User-friendly name */ getTtsSystemName(id) { switch (id) { case 'browser': return 'Web Browser'; case 'elevenlabs': return 'ElevenLabs'; case 'openai': return 'OpenAI'; case 'kokoro': return 'Kokoro (Local) '; default: return id.charAt(0).toUpperCase() + id.slice(1); } } /** * Populate voices dropdown for the current TTS system */ populateVoices() { if (!this.elements.ttsVoice) return; const ttsFactory = this.getModule('tts-factory'); const localization = this.getModule('localization'); // Get current handler and voices const currentHandler = ttsFactory.getActiveHandler(); const currentLocale = localization.getLocale() || 'en-US'; // If we have a handler with voices, populate the dropdown if (currentHandler && currentHandler.getVoices) { const voices = currentHandler.getVoices(currentLocale); if (voices && voices.length > 0) { // Format for dropdown const items = voices.map(voice => ({ id: voice.id || voice.name, name: voice.name })); // Get current voice const currentVoice = this.getPreference('tts', 'voice'); // Populate dropdown this.populateDropdown( this.elements.ttsVoice, items, 'id', 'name', currentVoice ); } else { // No voices available, add a placeholder this.elements.ttsVoice.innerHTML = ''; const option = document.createElement('option'); option.value = ''; option.textContent = `No voices available for ${currentLocale}`; option.disabled = true; this.elements.ttsVoice.appendChild(option); console.log(`Options UI: No voices available for ${currentLocale}, added placeholder option`); } } } /** * Populate languages dropdown */ populateLanguages() { if (!this.elements.language) return; const localization = this.getModule('localization'); // Get available locales const availableLocales = localization.getAvailableLocales(); // Format for dropdown const items = availableLocales.map(localeCode => ({ id: localeCode, name: localization.getLanguageName(localeCode) })); // Get current locale const currentLocale = localization.getLocale(); // Populate dropdown this.populateDropdown( this.elements.language, items, 'id', 'name', currentLocale ); } /** * Load current preferences into the UI */ loadPreferences() { console.log('Options UI: Loading preferences'); // Get current preferences const ttsSpeechEnabled = this.getPreference('tts', 'enabled', false); const ttsSpeed = this.getPreference('tts', 'speed', 0.5); const currentLocale = this.getPreference('app', 'locale', 'en-US'); // Set TTS speech toggle if (this.elements.ttsSpeechToggle) { this.elements.ttsSpeechToggle.checked = ttsSpeechEnabled; } // Set speech rate if (this.elements.speechRate) { // Convert from 0-1 to 0-100 for slider this.elements.speechRate.value = Math.round(ttsSpeed * 100); } // Set language if (this.elements.language) { // Check if the locale is available in the dropdown if (this.elements.language.querySelector(`option[value="${currentLocale}"]`)) { this.elements.language.value = currentLocale; } } // Set API keys if (this.elements.elevenLabsApiKey) { const elevenLabsApiKey = this.getPreference('tts', 'elevenlabs_api_key', ''); this.elements.elevenLabsApiKey.value = elevenLabsApiKey; } if (this.elements.openaiApiKey) { const openaiApiKey = this.getPreference('tts', 'openai_api_key', ''); this.elements.openaiApiKey.value = openaiApiKey; } // Set API URLs - these are handled in setupApiUrlFields } /** * Apply settings to the game */ applySettings() { if (!this.persistenceManager) return; const prefs = this.persistenceManager.getAllPreferences(); // Apply TTS settings const ttsFactory = this.getModule('tts-factory'); if (ttsFactory) { // Set active handler const provider = this.elements.ttsSystem.value; ttsFactory.setActiveHandler(provider); // Update TTS state const enabled = provider !== 'none'; document.dispatchEvent(new CustomEvent('tts:stateChange', { detail: { enabled: enabled } })); // Update persistence this.persistenceManager.updatePreference('tts', 'provider', provider); this.persistenceManager.updatePreference('tts', 'enabled', enabled); } // Apply language settings const localization = this.getModule('localization'); if (localization && this.elements.language) { const currentLocale = localization.getLocale(); // Update the UI to match the current locale if (currentLocale && this.elements.language.value !== currentLocale) { this.elements.language.value = currentLocale; } } // Apply audio settings const audioManager = this.getModule('audio-manager'); if (audioManager) { audioManager.setMasterVolume(prefs.audio.masterVolume); audioManager.setMusicVolume(prefs.audio.musicVolume); audioManager.setSfxVolume(prefs.audio.sfxVolume); } } /** * Handle TTS system changed event */ handleTtsSystemChanged() { const ttsFactory = this.getModule('tts-factory'); if (!ttsFactory) return; // Repopulate the systems dropdown to reflect the current state this.populateTtsSystems(); // Populate voices for the new system this.populateVoices(); } /** * Show reload notice */ showReloadNotice() { if (!this.elements || !this.elements.reloadNotice) return; this.elements.reloadNotice.style.display = 'block'; this.reloadRequired = true; } /** * Save current settings */ saveCurrentSettings() { if (!this.persistenceManager) return; // Save TTS settings const ttsFactory = this.getModule('tts-factory'); if (ttsFactory) { const provider = this.elements.ttsSystem.value; const voice = this.elements.ttsVoice.value; const speed = parseInt(this.elements.speechRate.value) / 100; const enabled = this.elements.ttsSpeechToggle.checked; this.persistenceManager.updatePreference('tts', 'provider', provider); this.persistenceManager.updatePreference('tts', 'voice', voice); this.persistenceManager.updatePreference('tts', 'speed', speed); this.persistenceManager.updatePreference('tts', 'enabled', enabled); } // Save language settings const localization = this.getModule('localization'); if (localization && this.elements.language) { const locale = this.elements.language.value; this.persistenceManager.updatePreference('app', 'locale', locale); this.persistenceManager.updatePreference('tts', 'language', locale); } // Save audio settings const audioManager = this.getModule('audio-manager'); if (audioManager) { const masterVolume = parseInt(this.elements.masterVolume.value) / 100; const musicVolume = parseInt(this.elements.musicVolume.value) / 100; const sfxVolume = parseInt(this.elements.effectsVolume.value) / 100; const speechVolume = parseInt(this.elements.speechVolume.value) / 100; this.persistenceManager.updatePreference('audio', 'masterVolume', masterVolume); this.persistenceManager.updatePreference('audio', 'musicVolume', musicVolume); this.persistenceManager.updatePreference('audio', 'sfxVolume', sfxVolume); this.persistenceManager.updatePreference('tts', 'volume', speechVolume); } // Save text speed setting const textSpeed = parseInt(this.elements.textSpeed.value); this.persistenceManager.updatePreference('animation', 'speed', textSpeed); // Save ElevenLabs API Key const elevenLabsApiKey = this.elements.elevenLabsApiKey.value; this.persistenceManager.updatePreference('tts', 'elevenlabs_api_key', elevenLabsApiKey); // Save ElevenLabs API URL const elevenLabsApiUrl = this.elements.elevenLabsApiUrl.value; this.persistenceManager.updatePreference('tts', 'elevenlabs_api_url', elevenLabsApiUrl); // Save OpenAI API Key const openaiApiKey = this.elements.openaiApiKey.value; this.persistenceManager.updatePreference('tts', 'openai_api_key', openaiApiKey); // Save OpenAI API URL const openaiApiUrl = this.elements.openaiApiUrl.value; this.persistenceManager.updatePreference('tts', 'openai_api_url', openaiApiUrl); } setupEventListeners() { try { // Listen for language change events document.addEventListener('localization:languageChanged', () => { try { this.populateLanguages(); this.populateVoices(); } catch (error) { console.error('Options UI: Error handling language change event', error); } }); // Listen for TTS state changes document.addEventListener('tts:stateChange', (event) => { try { if (this.elements && this.elements.ttsSpeechToggle) { this.elements.ttsSpeechToggle.checked = event.detail.enabled; // Save preference immediately if (this.persistenceManager) { this.persistenceManager.updatePreference('tts', 'enabled', event.detail.enabled); } } } catch (error) { console.error('Options UI: Error handling TTS state change event', error); } }); // Listen for TTS handler changes document.addEventListener('tts:handler:changed', (event) => { try { if (event.detail && event.detail.handler) { console.log(`Options UI: TTS handler changed to ${event.detail.handler}`); this.handleTtsSystemChanged(); // Save preference immediately if (this.persistenceManager) { this.persistenceManager.updatePreference('tts', 'preferred_handler', event.detail.handler); } } } catch (error) { console.error('Options UI: Error handling TTS handler change event', error); } }); // Listen for TTS availability changes document.addEventListener('tts:availability', (event) => { try { if (event.detail && typeof event.detail.available === 'boolean' && this.elements) { const available = event.detail.available; console.log(`Options UI: TTS availability changed to ${available}`); // Update UI to reflect TTS availability if (this.elements.ttsSection) { this.elements.ttsSection.classList.toggle('tts-unavailable', !available); // Add status message if not available if (!available && !this.elements.ttsUnavailableMessage) { const statusDiv = document.createElement('div'); statusDiv.className = 'tts-status-message'; statusDiv.innerHTML = 'TTS Unavailable: Check logs for details. You can still configure API keys below.'; statusDiv.style.color = '#ca3c3c'; statusDiv.style.padding = '5px 0'; statusDiv.style.marginBottom = '10px'; this.elements.ttsUnavailableMessage = statusDiv; // Insert at the top of the TTS section this.elements.ttsSection.insertBefore(statusDiv, this.elements.ttsSection.firstChild); } else if (available && this.elements.ttsUnavailableMessage) { // Remove the message if TTS becomes available this.elements.ttsUnavailableMessage.remove(); this.elements.ttsUnavailableMessage = null; } } } // Update the TTS system dropdown this.populateTtsSystems(); } catch (error) { console.error('Options UI: Error handling TTS availability event', error); } }); // Listen for Kokoro voice updates document.addEventListener('kokoro:voices-updated', () => { try { // Repopulate the voices dropdown when Kokoro voices become available this.populateVoices(); } catch (error) { console.error('Options UI: Error handling Kokoro voices update event', error); } }); // Browser window resize event window.addEventListener('resize', () => { try { // Update modal positioning if (this.modal && this.modal.style.display === 'block') { this.positionModal(); } } catch (error) { console.error('Options UI: Error handling window resize event', error); } }); console.log('Options UI: Event listeners set up successfully'); } catch (error) { console.error('Options UI: Error setting up event listeners', error); } } /** * Set up immediate save listeners for all input elements */ setupImmediateSaveListeners() { try { if (!this.elements || !this.persistenceManager) { console.warn('Options UI: Cannot set up immediate save listeners - elements or persistence manager missing'); return; } // Ensure we have the required elements before setting up listeners const elementsToSetup = [ 'ttsSystem', 'ttsVoice', 'language', 'ttsSpeechToggle', 'speechRate', 'elevenLabsApiKey', 'elevenLabsApiUrl', 'openaiApiKey', 'openaiApiUrl' ]; // Add change listeners for immediate save on each input elementsToSetup.forEach(elementName => { this.registerHandler(elementName, 'change', () => { console.log(`Options UI: Change detected on ${elementName}, saving settings`); this.saveCurrentSettings(); }); }); // For range inputs, also add input event to update during dragging this.registerHandler('speechRate', 'input', () => { // Update TTS speech rate immediately on slider movement if (this.elements.speechRate) { const ttsFactory = this.getModule('tts-factory'); const speechRate = parseFloat(this.elements.speechRate.value); ttsFactory.setSpeed(speechRate); } }); } catch (error) { console.error('Options UI: Error setting up immediate save listeners', error); } } setupApiUrlFields() { if (!this.elements) return; // Set up ElevenLabs API URL if (this.elements.elevenLabsApiUrl) { const savedUrl = this.getPreference('tts', 'elevenlabs_api_url'); const defaultUrl = 'https://api.elevenlabs.io/v1'; // Always set the input value to the saved or default URL this.elements.elevenLabsApiUrl.value = savedUrl || defaultUrl; // Save default to persistence if not already set if (!savedUrl) { console.log('Options UI: Setting default ElevenLabs API URL:', defaultUrl); this.updatePreference('tts', 'elevenlabs_api_url', defaultUrl); } } // Set up OpenAI API URL if (this.elements.openaiApiUrl) { const savedUrl = this.getPreference('tts', 'openai_api_url'); const defaultUrl = 'https://api.openai.com/v1'; // Always set the input value to the saved or default URL this.elements.openaiApiUrl.value = savedUrl || defaultUrl; // Save default to persistence only if not already set if (!savedUrl) { console.log('Options UI: Setting default OpenAI API URL:', defaultUrl); this.updatePreference('tts', 'openai_api_url', defaultUrl); } } // Make sure API keys are initialized if not already set if (!this.getPreference('tts', 'elevenlabs_api_key')) { this.updatePreference('tts', 'elevenlabs_api_key', ''); } if (!this.getPreference('tts', 'openai_api_key')) { this.updatePreference('tts', 'openai_api_key', ''); } } /** * Set up the initial state of the Options UI * @returns {Promise} - Promise resolves when setup is complete */ async setupInitialState() { try { console.log('Options UI: Setting up initial state'); // 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.saveCurrentSettings(); this.hide(); } }); // Populate TTS systems await this.populateTtsSystems(); // Populate languages await this.populateLanguages(); // Populate voices based on current TTS system await this.populateVoices(); // Load current preferences this.loadPreferences(); // 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(); }); // 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(); await this.populateLanguages(); this.updateApiSettingsVisibility(); }); console.log('Options UI: Initial state setup complete'); return true; } catch (error) { console.error('Options UI: Error setting up initial state', error); return false; } } } // Create the singleton instance const OptionsUI = new OptionsUIModule(); // Register with the module registry moduleRegistry.register(OptionsUI); // Export the module export { OptionsUI };