/** * Options UI Module * Provides the options UI for the game */ import { BaseModule } from './base-module.js'; import { createUIElement, populateDropdown, registerHandler, createPreferenceBinding } from './ui-helper.js'; class OptionsUIModule extends BaseModule { /** * Create a new options UI module */ constructor() { super('options-ui', 'Options UI'); // Set up dependencies this.dependencies = [ 'persistence-manager', 'localization', 'tts-factory', 'audio-manager' ]; // Modal element this.modal = null; // UI elements this.elements = {}; // Settings that require reload this.reloadRequired = false; // Bind methods this.bindMethods([ 'show', 'hide', 'createModal', 'populateTtsSystems', 'populateVoices', 'populateLanguages', 'loadPreferences', 'applySettings', 'handleTtsSystemChanged', 'showReloadNotice', 'toggle', 'setupEventListeners', 'saveCurrentSettings', 'setupApiUrlFields', 'setupInitialState', 'dispatchApiChangeEvent', 'getPreference', 'updatePreference' ]); } /** * Dispatches an API change event * @param {string} eventType - Event type (e.g. 'api:key:change') * @param {string} provider - Provider name (e.g. 'elevenlabs') * @param {string} valueType - Value type (e.g. 'key', 'url') * @param {string} value - Value to dispatch */ dispatchApiChangeEvent(eventType, provider, valueType, value) { const eventName = `tts:${eventType}`; console.log(`Options UI: Dispatching event ${eventName} for provider ${provider}`); document.dispatchEvent(new CustomEvent(eventName, { detail: { provider, [valueType]: value } })); } /** * Gets a preference from the persistence manager * @param {string} category - Preference category * @param {string} key - Preference key * @param {*} defaultValue - Default value if preference doesn't exist * @returns {*} - Preference value */ getPreference(category, key, defaultValue) { const persistenceManager = this.getModule('persistence-manager'); return persistenceManager.getPreference(category, key, defaultValue); } /** * Updates a preference in the persistence manager * @param {string} category - Preference category * @param {string} key - Preference key * @param {*} value - Value to set */ updatePreference(category, key, value) { const persistenceManager = this.getModule('persistence-manager'); persistenceManager.updatePreference(category, key, value); } /** * Initialize the Options UI module * @returns {Promise} - Promise resolves with initialization success */ async initialize() { console.log('Options UI: Initializing'); // Create DOM elements this.createModal(); // Set up event listeners this.setupEventListeners(); // Set up API URL fields this.setupApiUrlFields(); // Set up initial state await this.setupInitialState(); // Set up immediate save listeners this.setupImmediateSaveListeners(); this.reportProgress(100, 'Options UI initialized'); return true; } /** * Create the options modal */ createModal() { if (this.modal) return; const body = document.body; // Create modal container this.modal = createUIElement('div', { className: 'options-modal', id: 'options-modal' }, null, body); // Create modal content const modalContent = createUIElement('div', { className: 'options-content' }, null, this.modal); // Create header const header = createUIElement('div', { className: 'options-header' }, null, modalContent); createUIElement('h2', {}, 'Options', header); this.elements.closeButton = createUIElement('button', { className: 'options-close', 'aria-label': 'Close' }, '×', header); // Create settings container const settings = createUIElement('div', { className: 'options-settings' }, null, modalContent); // Language Section const languageSection = createUIElement('div', { className: 'options-section' }, null, settings); createUIElement('h3', {}, 'Language Settings', languageSection); // Language selection const languageContainer = createUIElement('div', { className: 'options-row' }, null, languageSection); createUIElement('label', {}, 'Language:', languageContainer); this.elements.language = createUIElement('select', { id: 'app-language' }, null, languageContainer); // TTS Settings const ttsSection = createUIElement('div', { className: 'options-section' }, null, settings); createUIElement('h3', {}, 'Text-to-Speech', ttsSection); // TTS Toggle const ttsSpeechToggleContainer = createUIElement('div', { className: 'options-row' }, null, ttsSection); createUIElement('label', {}, 'Enable Text-to-Speech:', ttsSpeechToggleContainer); this.elements.ttsEnabled = createUIElement('input', { type: 'checkbox', id: 'tts-enabled' }, null, ttsSpeechToggleContainer); // TTS System const ttsSystemContainer = createUIElement('div', { className: 'options-row' }, null, ttsSection); createUIElement('label', {}, 'TTS System:', ttsSystemContainer); this.elements.ttsSystem = createUIElement('select', { id: 'tts-system' }, null, ttsSystemContainer); // TTS Voice const ttsVoiceContainer = createUIElement('div', { className: 'options-row' }, null, ttsSection); createUIElement('label', {}, 'Voice:', ttsVoiceContainer); this.elements.ttsVoice = createUIElement('select', { id: 'tts-voice' }, null, ttsVoiceContainer); // TTS Speed const speedContainer = createUIElement('div', { className: 'options-row' }, null, ttsSection); createUIElement('label', {}, 'TTS Speed:', speedContainer); this.elements.ttsSpeed = createUIElement('input', { type: 'range', id: 'tts-speed', min: '0', max: '100' }, null, speedContainer); // Create API settings for each provider const apiSettings = this.createApiSettings(ttsSection); // Audio Settings Section const audioSection = createUIElement('div', { className: 'options-section' }, null, settings); createUIElement('h3', {}, 'Audio', audioSection); // Master Volume const masterVolumeContainer = createUIElement('div', { className: 'options-row' }, null, audioSection); createUIElement('label', {}, 'Master Volume:', masterVolumeContainer); this.elements.masterVolume = createUIElement('input', { type: 'range', id: 'master-volume', min: '0', max: '100' }, null, masterVolumeContainer); // Music Volume const musicVolumeContainer = createUIElement('div', { className: 'options-row' }, null, audioSection); createUIElement('label', {}, 'Music Volume:', musicVolumeContainer); this.elements.musicVolume = createUIElement('input', { type: 'range', id: 'music-volume', min: '0', max: '100' }, null, musicVolumeContainer); // SFX Volume const sfxVolumeContainer = createUIElement('div', { className: 'options-row' }, null, audioSection); createUIElement('label', {}, 'Sound Effects Volume:', sfxVolumeContainer); this.elements.sfxVolume = createUIElement('input', { type: 'range', id: 'sfx-volume', min: '0', max: '100' }, null, sfxVolumeContainer); // Ambience Volume const ambienceVolumeContainer = createUIElement('div', { className: 'options-row' }, null, audioSection); createUIElement('label', {}, 'Ambience Volume:', ambienceVolumeContainer); this.elements.ambienceVolume = createUIElement('input', { type: 'range', id: 'ambience-volume', min: '0', max: '100' }, null, ambienceVolumeContainer); // Initialize with display: none this.modal.style.display = 'none'; // Add event handlers this.elements.closeButton.addEventListener('click', () => { this.saveCurrentSettings(); this.hide(); }); } /** * Create API settings for TTS providers * @param {HTMLElement} parentSection - Parent section for API settings * @returns {Object} - Object with API settings elements */ createApiSettings(parentSection) { // ElevenLabs settings // API Key const elevenLabsApiKeyContainer = createUIElement('div', { className: 'options-row elevenlabs-tts-setting', 'data-provider': 'elevenlabs-tts' }, null, parentSection); createUIElement('label', {}, 'ElevenLabs API Key:', elevenLabsApiKeyContainer); this.elements.elevenLabsApiKey = createUIElement('input', { type: 'password', placeholder: 'Enter your ElevenLabs API key' }, null, elevenLabsApiKeyContainer); // API URL const elevenLabsApiUrlContainer = createUIElement('div', { className: 'options-row elevenlabs-tts-setting', 'data-provider': 'elevenlabs-tts' }, null, parentSection); createUIElement('label', {}, 'ElevenLabs API URL:', elevenLabsApiUrlContainer); this.elements.elevenLabsApiUrl = createUIElement('input', { type: 'text', placeholder: 'https://api.elevenlabs.io/v1' }, null, elevenLabsApiUrlContainer); // OpenAI settings // API Key const openaiApiKeyContainer = createUIElement('div', { className: 'options-row openai-tts-setting', 'data-provider': 'openai-tts' }, null, parentSection); createUIElement('label', {}, 'OpenAI API Key:', openaiApiKeyContainer); this.elements.openaiApiKey = createUIElement('input', { type: 'password', placeholder: 'Enter your OpenAI API key' }, null, openaiApiKeyContainer); // API URL const openaiApiUrlContainer = createUIElement('div', { className: 'options-row openai-tts-setting', 'data-provider': 'openai-tts' }, null, parentSection); createUIElement('label', {}, 'OpenAI API URL:', openaiApiUrlContainer); this.elements.openaiApiUrl = createUIElement('input', { type: 'text', placeholder: 'https://api.openai.com/v1' }, null, openaiApiUrlContainer); // Initially hide API settings const apiSettings = document.querySelectorAll('.elevenlabs-tts-setting, .openai-tts-setting'); apiSettings.forEach(setting => { setting.style.display = 'none'; }); return { elevenLabsApiKeyContainer, elevenLabsApiUrlContainer, openaiApiKeyContainer, openaiApiUrlContainer }; } /** * Set up event listeners for UI elements */ setupEventListeners() { // TTS System change event if (this.elements.ttsSystem) { this.elements.ttsSystem.addEventListener('change', this.handleTtsSystemChanged); } // TTS Enable toggle event if (this.elements.ttsEnabled) { this.elements.ttsEnabled.addEventListener('change', (event) => { const enabled = event.target.checked; console.log('Options UI: TTS enabled changed to', enabled); // Save setting this.updatePreference('tts', 'enabled', enabled); // Update TTS Factory const ttsFactory = this.getModule('tts-factory'); if (ttsFactory) { ttsFactory.configure({ enabled }); } }); } // Voice change event if (this.elements.ttsVoice) { this.elements.ttsVoice.addEventListener('change', (event) => { const voice = event.target.value; console.log('Options UI: TTS voice changed to', voice); // Save setting this.updatePreference('tts', 'voice', voice); // Update TTS Factory const ttsFactory = this.getModule('tts-factory'); if (ttsFactory) { ttsFactory.configure({ voice }); } }); } // TTS Speed change event if (this.elements.ttsSpeed) { this.elements.ttsSpeed.addEventListener('input', (event) => { const speed = parseInt(event.target.value) / 100; console.log('Options UI: TTS speed changed to', speed); // Save setting this.updatePreference('tts', 'speed', speed); // Update TTS Factory const ttsFactory = this.getModule('tts-factory'); if (ttsFactory) { ttsFactory.configure({ speed }); } }); } // Language change event if (this.elements.language) { this.elements.language.addEventListener('change', (event) => { const locale = event.target.value; console.log('Options UI: Language changed to', locale); // Save settings this.updatePreference('app', 'locale', locale); // Update Localization module const localization = this.getModule('localization'); if (localization) { localization.setLocale(locale); } // Show reload notice this.showReloadNotice(); }); } // Audio Settings // Master Volume if (this.elements.masterVolume) { this.elements.masterVolume.addEventListener('input', (event) => { const volume = parseInt(event.target.value) / 100; console.log('Options UI: Master volume changed to', volume); // Save setting this.updatePreference('audio', 'masterVolume', volume); // Update Audio Manager const audioManager = this.getModule('audio-manager'); if (audioManager) { audioManager.setMasterVolume(volume); } }); } // Music Volume if (this.elements.musicVolume) { this.elements.musicVolume.addEventListener('input', (event) => { const volume = parseInt(event.target.value) / 100; console.log('Options UI: Music volume changed to', volume); // Save setting this.updatePreference('audio', 'musicVolume', volume); // Update Audio Manager const audioManager = this.getModule('audio-manager'); if (audioManager) { audioManager.setMusicVolume(volume); } }); } // SFX Volume if (this.elements.sfxVolume) { this.elements.sfxVolume.addEventListener('input', (event) => { const volume = parseInt(event.target.value) / 100; console.log('Options UI: SFX volume changed to', volume); // Save setting this.updatePreference('audio', 'sfxVolume', volume); // Update Audio Manager const audioManager = this.getModule('audio-manager'); if (audioManager) { audioManager.setSfxVolume(volume); } }); } } /** * Handle TTS system change * @param {Event} event - Change event */ async handleTtsSystemChanged(event) { const selectedSystem = event.target.value; console.log('Options UI: TTS system changed to', selectedSystem); // Update API settings visibility this.updateApiSettingsVisibility(selectedSystem); // Save setting this.updatePreference('tts', 'preferred_handler', selectedSystem); // Notify TTSFactory of handler change const ttsFactory = this.getModule('tts-factory'); if (ttsFactory) { await ttsFactory.setActiveHandler(selectedSystem); // Now that the handler has changed, update voices for the selected system await this.populateVoices(); } } /** * Update API settings visibility based on selected TTS system * @param {string} selectedSystem - Selected TTS system */ updateApiSettingsVisibility(selectedSystem) { const elevenLabsSettings = document.querySelectorAll('.elevenlabs-tts-setting'); const openaiSettings = document.querySelectorAll('.openai-tts-setting'); elevenLabsSettings.forEach(setting => { setting.style.display = selectedSystem === 'elevenlabs-tts' ? 'flex' : 'none'; }); openaiSettings.forEach(setting => { setting.style.display = selectedSystem === 'openai-tts' ? 'flex' : 'none'; }); } /** * Show the options UI */ show() { if (this.modal) { this.modal.style.display = 'flex'; document.body.classList.add('modal-open'); } } /** * Hide the options UI */ hide() { if (this.modal) { this.modal.style.display = 'none'; document.body.classList.remove('modal-open'); } } /** * Toggle the options UI visibility */ toggle() { if (this.modal) { if (this.modal.style.display === 'flex') { this.hide(); } else { this.show(); } } } /** * Populate the TTS systems dropdown */ async populateTtsSystems() { const ttsFactory = this.getModule('tts-factory'); if (!ttsFactory || !this.elements.ttsSystem) return; // Get available TTS systems const handlers = ttsFactory.getAvailableHandlers(); console.log('Options UI: Available TTS handlers:', handlers); // Format for display const systems = handlers.map(handler => ({ id: handler.id, name: handler.displayName || handler.id })); // Populate dropdown populateDropdown( this.elements.ttsSystem, systems, 'id', 'name', this.getPreference('tts', 'preferred_handler', 'none') ); // Update API settings visibility this.updateApiSettingsVisibility(this.elements.ttsSystem.value); } /** * Populate the voices dropdown */ async populateVoices() { const ttsFactory = this.getModule('tts-factory'); if (!ttsFactory || !this.elements.ttsVoice) return; // Get voices for current TTS system const voices = await ttsFactory.getVoices() || []; console.log('Options UI: TTS voices:', voices); // Populate dropdown populateDropdown( this.elements.ttsVoice, voices, 'id', 'name', this.getPreference('tts', 'voice', '') ); } /** * Populate the languages dropdown */ async populateLanguages() { const localization = this.getModule('localization'); if (!localization || !this.elements.language) return; // Get available languages const languages = localization.getAvailableLocales() || []; console.log('Options UI: Available languages:', languages); // Format languages with their names const languageOptions = languages.map(code => ({ code, name: localization.getLanguageName(code) })); // Populate dropdown populateDropdown( this.elements.language, languageOptions, 'code', 'name', this.getPreference('app', 'locale', 'en-us') ); } /** * Load user preferences from the persistence manager */ loadPreferences() { const persistenceManager = this.getModule('persistence-manager'); if (!persistenceManager) return; console.log('Options UI: Loading preferences'); // TTS Settings // TTS Enable if (this.elements.ttsEnabled) { this.elements.ttsEnabled.checked = this.getPreference('tts', 'enabled', true); } // TTS System if (this.elements.ttsSystem) { const preferredHandler = this.getPreference('tts', 'preferred_handler', 'none'); if (this.elements.ttsSystem.querySelector(`option[value="${preferredHandler}"]`)) { this.elements.ttsSystem.value = preferredHandler; } } // TTS Speed if (this.elements.ttsSpeed) { const speed = this.getPreference('tts', 'speed', 1); this.elements.ttsSpeed.value = Math.round(speed * 100); } // API Keys and URLs // ElevenLabs API Key if (this.elements.elevenLabsApiKey) { this.elements.elevenLabsApiKey.value = this.getPreference('tts', 'elevenlabs-tts_api_key', ''); } // ElevenLabs API URL if (this.elements.elevenLabsApiUrl) { this.elements.elevenLabsApiUrl.value = this.getPreference('tts', 'elevenlabs-tts_api_url', 'https://api.elevenlabs.io/v1'); } // OpenAI API Key if (this.elements.openaiApiKey) { this.elements.openaiApiKey.value = this.getPreference('tts', 'openai-tts_api_key', ''); } // OpenAI API URL if (this.elements.openaiApiUrl) { this.elements.openaiApiUrl.value = this.getPreference('tts', 'openai-tts_api_url', 'https://api.openai.com/v1'); } // Audio Settings // Master Volume if (this.elements.masterVolume) { const masterVolume = this.getPreference('audio', 'masterVolume', 1); this.elements.masterVolume.value = Math.round(masterVolume * 100); } // Music Volume if (this.elements.musicVolume) { const musicVolume = this.getPreference('audio', 'musicVolume', 1); this.elements.musicVolume.value = Math.round(musicVolume * 100); } // SFX Volume if (this.elements.sfxVolume) { const sfxVolume = this.getPreference('audio', 'sfxVolume', 1); this.elements.sfxVolume.value = Math.round(sfxVolume * 100); } // Ambience Volume if (this.elements.ambienceVolume) { const ambienceVolume = this.getPreference('audio', 'ambienceVolume', 1); this.elements.ambienceVolume.value = Math.round(ambienceVolume * 100); } // Language if (this.elements.language) { const locale = this.getPreference('app', 'locale', 'en'); if (this.elements.language.querySelector(`option[value="${locale}"]`)) { this.elements.language.value = locale; } } // Update API settings visibility if (this.elements.ttsSystem) { this.updateApiSettingsVisibility(this.elements.ttsSystem.value); } } /** * Set up two-way binding for TTS Enabled * @param {HTMLElement} element - UI element * @param {Object} persistenceManager - Persistence Manager module * @param {string} category - Preference category * @param {string} key - Preference key * @param {*} defaultValue - Default value if preference doesn't exist * @param {Function} [transform] - Optional transform function */ setupTtsEnabledBinding(element, persistenceManager, category, key, defaultValue, transform) { createPreferenceBinding( element, persistenceManager, category, key, defaultValue, transform ); } /** * Set up two-way binding for TTS Voice * @param {HTMLElement} element - UI element * @param {Object} persistenceManager - Persistence Manager module * @param {string} category - Preference category * @param {string} key - Preference key * @param {*} defaultValue - Default value if preference doesn't exist * @param {Function} [transform] - Optional transform function */ setupTtsVoiceBinding(element, persistenceManager, category, key, defaultValue, transform) { createPreferenceBinding( element, persistenceManager, category, key, defaultValue, transform ); } /** * Set up two-way binding for App Language * @param {HTMLElement} element - UI element * @param {Object} persistenceManager - Persistence Manager module * @param {string} category - Preference category * @param {string} key - Preference key * @param {*} defaultValue - Default value if preference doesn't exist * @param {Function} [transform] - Optional transform function */ setupLanguageBinding(element, persistenceManager, category, key, defaultValue, transform) { createPreferenceBinding( element, persistenceManager, category, key, defaultValue, transform ); } /** * Set up two-way binding for API settings * @param {Object} persistenceManager - Persistence Manager module */ setupApiPreferenceBindings(persistenceManager) { // ElevenLabs API Key createPreferenceBinding( this.elements.elevenLabsApiKey, persistenceManager, 'tts', 'elevenlabs-tts_api_key', null, (value) => { this.dispatchApiChangeEvent('api:keyChanged', 'elevenlabs-tts', 'key', value); return value; } ); // ElevenLabs API URL createPreferenceBinding( this.elements.elevenLabsApiUrl, persistenceManager, 'tts', 'elevenlabs-tts_api_url', null, (value) => { this.dispatchApiChangeEvent('api:urlChanged', 'elevenlabs-tts', 'url', value); return value; } ); // OpenAI API Key createPreferenceBinding( this.elements.openaiApiKey, persistenceManager, 'tts', 'openai-tts_api_key', null, (value) => { this.dispatchApiChangeEvent('api:keyChanged', 'openai-tts', 'key', value); return value; } ); // OpenAI API URL createPreferenceBinding( this.elements.openaiApiUrl, persistenceManager, 'tts', 'openai-tts_api_url', null, (value) => { this.dispatchApiChangeEvent('api:urlChanged', 'openai-tts', 'url', value); return value; } ); } /** * Save current settings */ saveCurrentSettings() { // With two-way binding, settings are saved automatically as they change console.log('Options UI: Settings saved'); } /** * Apply settings */ applySettings() { const ttsFactory = this.getModule('tts-factory'); if (ttsFactory) { // Apply TTS settings const enabled = this.getPreference('tts', 'enabled', false); const preferredHandler = this.getPreference('tts', 'preferred_handler', 'none'); ttsFactory.configure({ enabled }); ttsFactory.setActiveHandler(preferredHandler); } } /** * Show a reload notice * @param {string} message - Message to show */ showReloadNotice(message) { console.log('Options UI: Reload required -', message); this.reloadRequired = true; } /** * Set up listeners for settings that should save immediately */ setupImmediateSaveListeners() { // Settings are saved immediately with two-way binding } /** * Update UI text based on current language */ updateUIText() { // Update UI text based on current language const localization = this.getModule('localization'); if (!localization) return; // Update modal title const modalTitle = this.modal.querySelector('h2'); if (modalTitle) { modalTitle.textContent = localization.translate('options.title', 'Options'); } // Update section titles const ttsSectionTitle = this.modal.querySelector('.options-section h3:first-child'); if (ttsSectionTitle) { ttsSectionTitle.textContent = localization.translate('options.tts.title', 'Text-to-Speech'); } const langSectionTitle = this.modal.querySelector('.options-section:nth-child(2) h3'); if (langSectionTitle) { langSectionTitle.textContent = localization.translate('options.language.title', 'Language Settings'); } } /** * Set up API URL fields with default values */ setupApiUrlFields() { // Set up ElevenLabs API URL if (this.elements.elevenLabsApiUrl) { const savedUrl = this.getPreference('tts', 'elevenlabs-tts_api_url'); const defaultUrl = 'https://api.elevenlabs.io/v1'; // If no saved URL, set the default if (!savedUrl) { console.log('Options UI: Setting default ElevenLabs API URL:', defaultUrl); this.updatePreference('tts', 'elevenlabs-tts_api_url', defaultUrl); } } // Set up OpenAI API URL if (this.elements.openaiApiUrl) { const savedUrl = this.getPreference('tts', 'openai-tts_api_url'); const defaultUrl = 'https://api.openai.com/v1'; // If no saved URL, set the default if (!savedUrl) { console.log('Options UI: Setting default OpenAI API URL:', defaultUrl); this.updatePreference('tts', 'openai-tts_api_url', defaultUrl); } } // Make sure API keys are initialized if not already set if (!this.getPreference('tts', 'elevenlabs-tts_api_key')) { this.updatePreference('tts', 'elevenlabs-tts_api_key', ''); } if (!this.getPreference('tts', 'openai-tts_api_key')) { this.updatePreference('tts', 'openai-tts_api_key', ''); } } /** * Set up the initial state of the Options UI * @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(); this.updateApiSettingsVisibility(this.elements.ttsSystem.value); }); 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 };