Files
ai.interactive.fiction/public/js/options-ui.js
T

1204 lines
47 KiB
JavaScript

/**
* 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<boolean>} - 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 = '&times;';
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 = '<span>* Changes to language or speech system require a page reload to take full effect.</span>';
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 };