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

1304 lines
51 KiB
Plaintext

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