/** * 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'); // Modal element this.modal = null; // UI elements this.elements = null; // 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' ]); } /** * Initialize the options UI * @returns {Promise} - Resolves with success status */ async initialize() { try { console.log('Initializing Options UI Module'); // Set up dependencies this.dependencies = [ 'persistence-manager', 'localization', 'tts-factory', 'audio-manager' ]; // Create the options modal this.createModal(); // Set up event listeners this.setupEventListeners(); // Add event listener for showing options UI document.addEventListener('ui:showOptions', () => this.show()); // Add event listener for toggling options UI document.addEventListener('ui:options:toggle', () => this.toggle()); // Wait for dependencies and populate UI with delay to ensure TTS handlers are registered this.waitForDependencies().then(() => { console.log('Options UI: Dependencies loaded, initializing UI with delay'); // Add a delay to ensure all TTS handlers are registered and initialized setTimeout(() => { // Populate TTS systems this.populateTtsSystems(); // Populate languages this.populateLanguages(); // Load current preferences this.loadPreferences(); // Apply settings this.applySettings(); // Setup API URLs with default values if needed this.setupApiUrlFields(); console.log('Options UI: Initialization complete'); }, 1000); // 1 second delay }); // 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 key bindings document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.modal.style.display === 'flex') { this.saveCurrentSettings(); this.hide(); } }); return true; } catch (error) { console.error("Options UI: Error initializing", error); return false; } } /** * Wait for dependencies to be available * @returns {Promise} - Resolves when dependencies are available */ waitForDependencies() { return new Promise((resolve) => { const checkDependencies = () => { const persistenceManager = this.getModule('persistence-manager'); const localization = this.getModule('localization'); const ttsFactory = this.getModule('tts-factory'); const audioManager = this.getModule('audio-manager'); if (persistenceManager && localization && ttsFactory && audioManager) { this.persistenceManager = persistenceManager; this.localization = localization; this.ttsFactory = ttsFactory; this.audioManager = audioManager; resolve(); } else { setTimeout(checkDependencies, 100); } }; checkDependencies(); }); } /** * 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, ttsVoice, language, textSpeed, masterVolume, speechVolume, musicVolume, effectsVolume, reloadNotice, speechRate: speedSlider, ttsSpeechToggle, apiSettingsContainer, elevenLabsApiKey, elevenLabsApiUrl, openaiApiKey, openaiApiUrl }; } /** * Show the options modal */ show() { if (!this.modal) return; // Reload preferences before showing this.loadPreferences(); // Show modal this.modal.style.display = 'flex'; } /** * 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 || !this.elements.ttsSystem) return; const ttsFactory = this.getModule('tts-factory'); if (!ttsFactory) return; // Clear existing options this.elements.ttsSystem.innerHTML = ''; // Add 'None' option const noneOption = document.createElement('option'); noneOption.value = 'none'; noneOption.textContent = 'None'; this.elements.ttsSystem.appendChild(noneOption); // Get available TTS handlers const handlers = ttsFactory.getAvailableHandlers(); console.log('Options UI: Available TTS handlers:', handlers.map(h => h.id).join(', ')); // Add options for each handler for (const handler of handlers) { const option = document.createElement('option'); option.value = handler.id; option.textContent = this.getTtsSystemName(handler.id); this.elements.ttsSystem.appendChild(option); } // Set the current active handler const activeHandler = ttsFactory.getActiveHandler(); console.log('Options UI: Active TTS handler:', activeHandler ? (activeHandler.getId ? activeHandler.getId() : activeHandler.id) : 'none'); if (activeHandler) { if (typeof activeHandler.getId === 'function') { // Use getId() if available this.elements.ttsSystem.value = activeHandler.getId(); } else if (activeHandler.id) { // Otherwise try to use the id property this.elements.ttsSystem.value = activeHandler.id; } else { // If no id is available, default to 'none' this.elements.ttsSystem.value = 'none'; console.warn('Options UI: Active TTS handler has no ID'); } } else { this.elements.ttsSystem.value = 'none'; } // Show/hide API settings based on selected TTS system this.updateApiSettingsVisibility(); // Add change event to show/hide API settings this.elements.ttsSystem.addEventListener('change', () => { this.updateApiSettingsVisibility(); }); } /** * Update visibility of API settings based on selected TTS system */ updateApiSettingsVisibility() { if (!this.elements || !this.elements.apiSettingsContainer) return; const selectedProvider = this.elements.ttsSystem.value; // Show/hide API settings container based on whether an API provider is selected if (selectedProvider === 'elevenlabs' || selectedProvider === 'openai') { this.elements.apiSettingsContainer.style.display = 'block'; // Show/hide provider-specific settings const elevenLabsSettings = document.querySelectorAll('.elevenlabs-setting'); const openaiSettings = document.querySelectorAll('.openai-setting'); elevenLabsSettings.forEach(element => { element.style.display = selectedProvider === 'elevenlabs' ? 'flex' : 'none'; }); openaiSettings.forEach(element => { element.style.display = selectedProvider === 'openai' ? 'flex' : 'none'; }); } else { this.elements.apiSettingsContainer.style.display = '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 'Browser TTS'; case 'api': return 'API TTS'; case 'kokoro': return 'Kokoro TTS'; default: return id; } } /** * Populate voices dropdown for the current TTS system */ populateVoices() { if (!this.elements || !this.elements.ttsVoice) { console.log('Options UI: Cannot populate voices - elements not initialized'); return; } const ttsFactory = this.getModule('tts-factory'); const localization = this.getModule('localization'); if (!ttsFactory || !localization) { console.log('Options UI: Cannot populate voices - required modules not available'); return; } // Clear existing options this.elements.ttsVoice.innerHTML = ''; // Get current locale const currentLocale = localization.getLocale(); console.log(`Options UI: Current locale from localization module: ${currentLocale}`); // Get active TTS handler const activeHandler = ttsFactory.getActiveHandler(); const handlerId = activeHandler ? activeHandler.getId() : 'none'; console.log(`Options UI: Populating voices for locale: ${currentLocale}, handler: ${handlerId}`); // Get voices from active handler const voices = ttsFactory.getVoices(); console.log(`Options UI: Got ${voices ? voices.length : 0} voices from TTS factory`); // Add available voices to dropdown if (voices && voices.length > 0) { // Add options for each voice voices.forEach(voice => { const option = document.createElement('option'); option.value = voice.id || voice.name; option.textContent = voice.name; if (voice.lang) { option.textContent += ` (${voice.lang})`; } this.elements.ttsVoice.appendChild(option); }); console.log(`Options UI: Added ${voices.length} voice options to the dropdown`); } else { // No voices available 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 || !this.elements.language) return; const localization = this.getModule('localization'); if (!localization) return; // Clear existing options this.elements.language.innerHTML = ''; // Get available locales from the localization module const availableLocales = localization.getAvailableLocales(); // Add options for each language availableLocales.forEach(localeCode => { const option = document.createElement('option'); option.value = localeCode; option.textContent = localization.getLanguageName(localeCode); this.elements.language.appendChild(option); }); // Set current locale as selected const currentLocale = localization.getLocale(); if (currentLocale && this.elements.language.querySelector(`option[value="${currentLocale}"]`)) { this.elements.language.value = currentLocale; } } /** * Load current preferences into the UI */ loadPreferences() { if (!this.persistenceManager || !this.elements) return; this.waitForDependencies().then(() => { const prefs = this.persistenceManager.getAllPreferences(); // TTS System if (this.elements.ttsSystem) { const provider = prefs.tts.provider; if (provider) { // Check if the option exists const option = Array.from(this.elements.ttsSystem.options).find(opt => opt.value === provider); if (option) { this.elements.ttsSystem.value = provider; } } } // TTS Voice if (this.elements.ttsVoice) { const voice = prefs.tts.voice; if (voice) { // Check if the option exists const option = Array.from(this.elements.ttsVoice.options).find(opt => opt.value === voice); if (option) { this.elements.ttsVoice.value = voice; } } } // Language if (this.elements.language) { const locale = prefs.app.locale; if (locale) { // Check if the option exists const option = Array.from(this.elements.language.options).find(opt => opt.value === locale); if (option) { this.elements.language.value = locale; } } } // Text Speed if (this.elements.textSpeed) { this.elements.textSpeed.value = prefs.animation.speed; } // Master Volume if (this.elements.masterVolume) { this.elements.masterVolume.value = Math.round(prefs.audio.masterVolume * 100); } // Speech Volume if (this.elements.speechVolume) { this.elements.speechVolume.value = Math.round(prefs.tts.volume * 100); } // Music Volume if (this.elements.musicVolume) { this.elements.musicVolume.value = Math.round(prefs.audio.musicVolume * 100); } // Effects Volume if (this.elements.effectsVolume) { this.elements.effectsVolume.value = Math.round(prefs.audio.sfxVolume * 100); } // Speech Rate if (this.elements.speechRate) { this.elements.speechRate.value = Math.round(prefs.tts.speed * 100); } // TTS Speech Toggle if (this.elements.ttsSpeechToggle) { this.elements.ttsSpeechToggle.checked = prefs.tts.enabled; } // ElevenLabs API Key if (this.elements.elevenLabsApiKey) { this.elements.elevenLabsApiKey.value = prefs.tts.elevenlabs_api_key; } // ElevenLabs API Base URL if (this.elements.elevenLabsApiUrl) { this.elements.elevenLabsApiUrl.value = prefs.tts.elevenlabs_api_url; } // OpenAI API Key if (this.elements.openaiApiKey) { this.elements.openaiApiKey.value = prefs.tts.openai_api_key; } // OpenAI API Base URL if (this.elements.openaiApiUrl) { this.elements.openaiApiUrl.value = prefs.tts.openai_api_url; } }); } /** * Apply settings to the game */ applySettings() { if (!this.persistenceManager) return; this.waitForDependencies().then(() => { 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() { 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() { // Listen for language change events document.addEventListener('localization:languageChanged', () => { this.populateLanguages(); this.populateVoices(); }); // Listen for TTS state changes document.addEventListener('tts:stateChange', (event) => { if (this.elements && this.elements.ttsSpeechToggle) { this.elements.ttsSpeechToggle.checked = event.detail.enabled; // Update persistence manager const persistenceManager = this.getModule('persistence-manager'); if (persistenceManager) { persistenceManager.updatePreference('tts', 'enabled', event.detail.enabled); } } }); // Listen for TTS handler changes document.addEventListener('tts:handlerChanged', (event) => { if (this.elements && this.elements.ttsSystem) { // Update the dropdown to match the active handler const handlerId = event.detail.handlerId; if (handlerId && handlerId !== 'none') { this.elements.ttsSystem.value = handlerId; // Update persistence manager const persistenceManager = this.getModule('persistence-manager'); if (persistenceManager) { persistenceManager.updatePreference('tts', 'provider', handlerId); } } // Refresh voices when handler changes this.populateVoices(); } }); // Listen for TTS availability events document.addEventListener('tts:availability', (event) => { if (!this.elements) return; const available = event.detail?.available || false; // Update the TTS options visibility if (this.elements.ttsSection) { this.elements.ttsSection.style.display = available ? 'block' : 'none'; } // Update the TTS system dropdown this.populateTtsSystems(); }); // Listen for Kokoro voice updates document.addEventListener('kokoro:voices-updated', () => { // Repopulate the voices dropdown when Kokoro voices become available this.populateVoices(); }); // Browser window resize event window.addEventListener('resize', () => { // Update modal positioning if (this.modal && this.modal.style.display === 'block') { this.positionModal(); } }); } setupApiUrlFields() { if (!this.elements) return; const persistenceManager = this.getModule('persistence-manager'); if (!persistenceManager) return; // Set up ElevenLabs API URL if (this.elements.elevenLabsApiUrl) { const savedUrl = persistenceManager.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); persistenceManager.updatePreference('tts', 'elevenlabs_api_url', defaultUrl); } } // Set up OpenAI API URL if (this.elements.openaiApiUrl) { const savedUrl = persistenceManager.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); persistenceManager.updatePreference('tts', 'openai_api_url', defaultUrl); } } // Make sure API keys are initialized if not already set if (!persistenceManager.getPreference('tts', 'elevenlabs_api_key')) { persistenceManager.updatePreference('tts', 'elevenlabs_api_key', ''); } if (!persistenceManager.getPreference('tts', 'openai_api_key')) { persistenceManager.updatePreference('tts', 'openai_api_key', ''); } } } // Create the singleton instance const OptionsUI = new OptionsUIModule(); // Register with the module registry moduleRegistry.register(OptionsUI); // Export the module export { OptionsUI };