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

1068 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Options UI Module for AI Interactive Fiction
* Provides a user interface for adjusting game settings, TTS options, etc.
*/
import { BaseModule } from './base-module.js';
import { moduleRegistry } from './module-registry.js';
class OptionsUIModule extends BaseModule {
/**
* Create new options UI
*/
constructor() {
super('options-ui', 'Options UI');
// Dependencies
this.dependencies = ['persistence-manager', 'localization'];
this.persistenceManager = null;
this.ttsPlayer = null;
this.audioManager = null;
this.ttsFactory = null;
this.localization = null;
this.modal = null;
this.isOpen = false;
// Configuration
this.config = {
modalClass: 'options-modal',
modalContentClass: 'options-content',
backdrop: true
};
// Elements reference
this.elements = null;
// Bound event handlers for proper this context
this.bindMethods([
'handleTtsSystemChanged',
'loadPreferences',
'populateTtsSystems',
'populateVoices',
'resetToDefaults',
'saveAndClose',
'applySettings'
]);
}
/**
* Initialize the module
* @returns {Promise<boolean>} - Resolves with success status
*/
async initialize() {
try {
// Set up event listeners
window.addEventListener('tts-system-changed', this.handleTtsSystemChanged);
// The option modal will be created on demand
this.reportProgress(100, "Options UI ready");
return true;
} catch (error) {
console.error("Error initializing options UI:", error);
return false;
}
}
/**
* Wait for dependencies to be ready
* @returns {Promise<boolean>} - Resolves when dependencies are ready
*/
async waitForDependencies() {
try {
// Get required modules
this.persistenceManager = this.getModule('persistence-manager');
if (!this.persistenceManager) {
console.warn("Options UI: Persistence Manager not found");
}
this.localization = this.getModule('localization');
if (!this.localization) {
console.warn("Options UI: Localization module not found");
}
// These dependencies are optional - UI will adapt if not available
this.ttsFactory = this.getModule('tts-factory');
this.ttsPlayer = this.getModule('tts');
this.audioManager = this.getModule('audio-manager');
return true;
} catch (error) {
console.error("Error waiting for options UI dependencies:", error);
return true; // Non-critical, can continue
}
}
/**
* Create the options UI elements
*/
createModal() {
if (this.modal) return;
// Create modal container
this.modal = document.createElement('div');
this.modal.className = this.config.modalClass;
this.modal.style.display = 'none';
// Create backdrop if enabled
if (this.config.backdrop) {
this.backdrop = document.createElement('div');
this.backdrop.className = 'modal-backdrop';
this.backdrop.addEventListener('click', () => this.hide());
this.modal.appendChild(this.backdrop);
}
// Create content container
const content = document.createElement('div');
content.className = this.config.modalContentClass;
// Add header with title and close button
const header = document.createElement('div');
header.className = 'options-header';
const title = document.createElement('h2');
title.textContent = 'Options';
header.appendChild(title);
const closeBtn = document.createElement('button');
closeBtn.className = 'close-button';
closeBtn.textContent = '×';
closeBtn.setAttribute('aria-label', 'Close options');
closeBtn.addEventListener('click', () => this.hide());
header.appendChild(closeBtn);
content.appendChild(header);
// Create tabs
const tabContainer = document.createElement('div');
tabContainer.className = 'tabs-container';
const tabs = document.createElement('div');
tabs.className = 'tabs';
const tabGeneral = document.createElement('button');
tabGeneral.className = 'tab active';
tabGeneral.textContent = 'General';
tabGeneral.dataset.tab = 'general';
const tabVoice = document.createElement('button');
tabVoice.className = 'tab';
tabVoice.textContent = 'Voice';
tabVoice.dataset.tab = 'voice';
const tabAudio = document.createElement('button');
tabAudio.className = 'tab';
tabAudio.textContent = 'Audio';
tabAudio.dataset.tab = 'audio';
const tabAccess = document.createElement('button');
tabAccess.className = 'tab';
tabAccess.textContent = 'Accessibility';
tabAccess.dataset.tab = 'accessibility';
tabs.appendChild(tabGeneral);
tabs.appendChild(tabVoice);
tabs.appendChild(tabAudio);
tabs.appendChild(tabAccess);
tabContainer.appendChild(tabs);
content.appendChild(tabContainer);
// Create tab content sections
const tabContent = document.createElement('div');
tabContent.className = 'tab-content';
// General tab content
const generalContent = document.createElement('div');
generalContent.className = 'tab-pane active';
generalContent.dataset.tab = 'general';
const animSpeedSection = document.createElement('div');
animSpeedSection.className = 'option-section';
const animSpeedLabel = document.createElement('label');
animSpeedLabel.textContent = 'Animation Speed';
animSpeedLabel.htmlFor = 'option-anim-speed';
const animSpeedSlider = document.createElement('input');
animSpeedSlider.type = 'range';
animSpeedSlider.id = 'option-anim-speed';
animSpeedSlider.min = '0';
animSpeedSlider.max = '100';
animSpeedSlider.value = '50'; // Will be updated from preferences
const animSpeedValue = document.createElement('span');
animSpeedValue.className = 'range-value';
animSpeedValue.textContent = '50%';
animSpeedSlider.addEventListener('input', () => {
const val = animSpeedSlider.value;
animSpeedValue.textContent = `${val}%`;
if (this.persistenceManager) {
this.persistenceManager.updatePreference('animation', 'speed', parseInt(val, 10));
}
// Update animation queue speed if available
const animQueue = moduleRegistry.getModule('animation-queue');
if (animQueue) {
const speed = Math.pow(100.0 - val, 3) / 10000 * 10 + 0.01;
animQueue.setSpeed(speed);
}
});
animSpeedSection.appendChild(animSpeedLabel);
animSpeedSection.appendChild(animSpeedSlider);
animSpeedSection.appendChild(animSpeedValue);
generalContent.appendChild(animSpeedSection);
// Voice tab content
const voiceContent = document.createElement('div');
voiceContent.className = 'tab-pane';
voiceContent.dataset.tab = 'voice';
const ttsSysSection = document.createElement('div');
ttsSysSection.className = 'option-section';
const ttsSysLabel = document.createElement('label');
ttsSysLabel.textContent = 'TTS System';
ttsSysLabel.htmlFor = 'option-tts-system';
const ttsSysSelect = document.createElement('select');
ttsSysSelect.id = 'option-tts-system';
// Will populate systems dynamically later
ttsSysSection.appendChild(ttsSysLabel);
ttsSysSection.appendChild(ttsSysSelect);
voiceContent.appendChild(ttsSysSection);
// Voice selection section
const voiceSection = document.createElement('div');
voiceSection.className = 'option-section';
const voiceLabel = document.createElement('label');
voiceLabel.textContent = 'Voice';
voiceLabel.htmlFor = 'option-voice';
const voiceSelect = document.createElement('select');
voiceSelect.id = 'option-voice';
// Will populate voices dynamically later
voiceSection.appendChild(voiceLabel);
voiceSection.appendChild(voiceSelect);
voiceContent.appendChild(voiceSection);
// Voice rate section
const rateSection = document.createElement('div');
rateSection.className = 'option-section';
const rateLabel = document.createElement('label');
rateLabel.textContent = 'Speech Rate';
rateLabel.htmlFor = 'option-speech-rate';
const rateSlider = document.createElement('input');
rateSlider.type = 'range';
rateSlider.id = 'option-speech-rate';
rateSlider.min = '50';
rateSlider.max = '200';
rateSlider.value = '100'; // Will be updated from preferences
const rateValue = document.createElement('span');
rateValue.className = 'range-value';
rateValue.textContent = '1.0x';
rateSlider.addEventListener('input', () => {
const val = rateSlider.value;
const rate = val / 100;
rateValue.textContent = `${rate.toFixed(1)}x`;
if (this.ttsPlayer) {
this.ttsPlayer.setSpeed(rate);
}
if (this.persistenceManager) {
this.persistenceManager.updatePreference('tts', 'rate', rate);
}
});
rateSection.appendChild(rateLabel);
rateSection.appendChild(rateSlider);
rateSection.appendChild(rateValue);
voiceContent.appendChild(rateSection);
// Audio tab content
const audioContent = document.createElement('div');
audioContent.className = 'tab-pane';
audioContent.dataset.tab = 'audio';
// Master volume section
const masterVolSection = document.createElement('div');
masterVolSection.className = 'option-section';
const masterVolLabel = document.createElement('label');
masterVolLabel.textContent = 'Master Volume';
masterVolLabel.htmlFor = 'option-master-vol';
const masterVolSlider = document.createElement('input');
masterVolSlider.type = 'range';
masterVolSlider.id = 'option-master-vol';
masterVolSlider.min = '0';
masterVolSlider.max = '100';
masterVolSlider.value = '100'; // Will be updated from preferences
const masterVolValue = document.createElement('span');
masterVolValue.className = 'range-value';
masterVolValue.textContent = '100%';
masterVolSlider.addEventListener('input', () => {
const val = masterVolSlider.value;
masterVolValue.textContent = `${val}%`;
if (this.audioManager) {
this.audioManager.setMasterVolume(val / 100);
}
if (this.persistenceManager) {
this.persistenceManager.updatePreference('audio', 'masterVolume', val / 100);
}
});
masterVolSection.appendChild(masterVolLabel);
masterVolSection.appendChild(masterVolSlider);
masterVolSection.appendChild(masterVolValue);
audioContent.appendChild(masterVolSection);
// TTS volume section
const ttsVolSection = document.createElement('div');
ttsVolSection.className = 'option-section';
const ttsVolLabel = document.createElement('label');
ttsVolLabel.textContent = 'Speech Volume';
ttsVolLabel.htmlFor = 'option-tts-vol';
const ttsVolSlider = document.createElement('input');
ttsVolSlider.type = 'range';
ttsVolSlider.id = 'option-tts-vol';
ttsVolSlider.min = '0';
ttsVolSlider.max = '100';
ttsVolSlider.value = '100'; // Will be updated from preferences
const ttsVolValue = document.createElement('span');
ttsVolValue.className = 'range-value';
ttsVolValue.textContent = '100%';
ttsVolSlider.addEventListener('input', () => {
const val = ttsVolSlider.value;
ttsVolValue.textContent = `${val}%`;
if (this.ttsPlayer) {
this.ttsPlayer.setVolume(val / 100);
}
if (this.persistenceManager) {
this.persistenceManager.updatePreference('tts', 'volume', val / 100);
}
});
ttsVolSection.appendChild(ttsVolLabel);
ttsVolSection.appendChild(ttsVolSlider);
ttsVolSection.appendChild(ttsVolValue);
audioContent.appendChild(ttsVolSection);
// Music volume section (for future use)
const musicVolSection = document.createElement('div');
musicVolSection.className = 'option-section';
const musicVolLabel = document.createElement('label');
musicVolLabel.textContent = 'Music Volume';
musicVolLabel.htmlFor = 'option-music-vol';
const musicVolSlider = document.createElement('input');
musicVolSlider.type = 'range';
musicVolSlider.id = 'option-music-vol';
musicVolSlider.min = '0';
musicVolSlider.max = '100';
musicVolSlider.value = '70'; // Will be updated from preferences
const musicVolValue = document.createElement('span');
musicVolValue.className = 'range-value';
musicVolValue.textContent = '70%';
musicVolSlider.addEventListener('input', () => {
const val = musicVolSlider.value;
musicVolValue.textContent = `${val}%`;
if (this.audioManager) {
this.audioManager.setMusicVolume(val / 100);
}
if (this.persistenceManager) {
this.persistenceManager.updatePreference('audio', 'musicVolume', val / 100);
}
});
musicVolSection.appendChild(musicVolLabel);
musicVolSection.appendChild(musicVolSlider);
musicVolSection.appendChild(musicVolValue);
audioContent.appendChild(musicVolSection);
// SFX volume section (for future use)
const sfxVolSection = document.createElement('div');
sfxVolSection.className = 'option-section';
const sfxVolLabel = document.createElement('label');
sfxVolLabel.textContent = 'Effects Volume';
sfxVolLabel.htmlFor = 'option-sfx-vol';
const sfxVolSlider = document.createElement('input');
sfxVolSlider.type = 'range';
sfxVolSlider.id = 'option-sfx-vol';
sfxVolSlider.min = '0';
sfxVolSlider.max = '100';
sfxVolSlider.value = '100'; // Will be updated from preferences
const sfxVolValue = document.createElement('span');
sfxVolValue.className = 'range-value';
sfxVolValue.textContent = '100%';
sfxVolSlider.addEventListener('input', () => {
const val = sfxVolSlider.value;
sfxVolValue.textContent = `${val}%`;
if (this.audioManager) {
this.audioManager.setSfxVolume(val / 100);
}
if (this.persistenceManager) {
this.persistenceManager.updatePreference('audio', 'sfxVolume', val / 100);
}
});
sfxVolSection.appendChild(sfxVolLabel);
sfxVolSection.appendChild(sfxVolSlider);
sfxVolSection.appendChild(sfxVolValue);
audioContent.appendChild(sfxVolSection);
// Accessibility tab content
const accessContent = document.createElement('div');
accessContent.className = 'tab-pane';
accessContent.dataset.tab = 'accessibility';
// High contrast toggle
const contrastSection = document.createElement('div');
contrastSection.className = 'option-section checkbox-section';
const contrastCheckbox = document.createElement('input');
contrastCheckbox.type = 'checkbox';
contrastCheckbox.id = 'option-high-contrast';
const contrastLabel = document.createElement('label');
contrastLabel.textContent = 'High Contrast Mode';
contrastLabel.htmlFor = 'option-high-contrast';
contrastCheckbox.addEventListener('change', () => {
const isEnabled = contrastCheckbox.checked;
// Apply high contrast class to body
if (isEnabled) {
document.body.classList.add('high-contrast');
} else {
document.body.classList.remove('high-contrast');
}
if (this.persistenceManager) {
this.persistenceManager.updatePreference('accessibility', 'highContrast', isEnabled);
}
});
contrastSection.appendChild(contrastCheckbox);
contrastSection.appendChild(contrastLabel);
accessContent.appendChild(contrastSection);
// Larger text toggle
const largerTextSection = document.createElement('div');
largerTextSection.className = 'option-section checkbox-section';
const largerTextCheckbox = document.createElement('input');
largerTextCheckbox.type = 'checkbox';
largerTextCheckbox.id = 'option-larger-text';
const largerTextLabel = document.createElement('label');
largerTextLabel.textContent = 'Larger Text';
largerTextLabel.htmlFor = 'option-larger-text';
largerTextCheckbox.addEventListener('change', () => {
const isEnabled = largerTextCheckbox.checked;
// Apply larger text class to body
if (isEnabled) {
document.body.classList.add('larger-text');
} else {
document.body.classList.remove('larger-text');
}
if (this.persistenceManager) {
this.persistenceManager.updatePreference('accessibility', 'largerText', isEnabled);
}
});
largerTextSection.appendChild(largerTextCheckbox);
largerTextSection.appendChild(largerTextLabel);
accessContent.appendChild(largerTextSection);
// Add tab content to container
tabContent.appendChild(generalContent);
tabContent.appendChild(voiceContent);
tabContent.appendChild(audioContent);
tabContent.appendChild(accessContent);
content.appendChild(tabContent);
// Add buttons at the bottom
const buttons = document.createElement('div');
buttons.className = 'options-buttons';
const resetButton = document.createElement('button');
resetButton.textContent = 'Reset to Defaults';
resetButton.className = 'reset-button';
resetButton.addEventListener('click', () => this.resetToDefaults());
const saveButton = document.createElement('button');
saveButton.textContent = 'Save & Close';
saveButton.className = 'save-button';
saveButton.addEventListener('click', () => this.saveAndClose());
buttons.appendChild(resetButton);
buttons.appendChild(saveButton);
content.appendChild(buttons);
// Set up tab switching
tabs.addEventListener('click', (e) => {
if (e.target.classList.contains('tab')) {
// Deactivate all tabs and tab panes
Array.from(tabs.querySelectorAll('.tab')).forEach(tab => {
tab.classList.remove('active');
});
Array.from(tabContent.querySelectorAll('.tab-pane')).forEach(pane => {
pane.classList.remove('active');
});
// Activate clicked tab and corresponding pane
e.target.classList.add('active');
const tabName = e.target.dataset.tab;
const pane = tabContent.querySelector(`.tab-pane[data-tab="${tabName}"]`);
if (pane) {
pane.classList.add('active');
}
// If switching to voice tab, ensure voices are updated
if (tabName === 'voice') {
this.populateTtsSystems();
this.populateVoices();
}
}
});
this.modal.appendChild(content);
document.body.appendChild(this.modal);
// Store references to UI elements for later use
this.elements = {
animSpeed: animSpeedSlider,
animSpeedValue: animSpeedValue,
ttsSystem: ttsSysSelect,
voiceSelect: voiceSelect,
speechRate: rateSlider,
speechRateValue: rateValue,
masterVolume: masterVolSlider,
masterVolumeValue: masterVolValue,
ttsVolume: ttsVolSlider,
ttsVolumeValue: ttsVolValue,
musicVolume: musicVolSlider,
musicVolumeValue: musicVolValue,
sfxVolume: sfxVolSlider,
sfxVolumeValue: sfxVolValue,
highContrast: contrastCheckbox,
largerText: largerTextCheckbox
};
}
/**
* Load current preferences into UI
*/
loadPreferences() {
if (!this.persistenceManager || !this.elements) return;
// Wait for dependencies
this.waitForDependencies().then(() => {
// Get current preferences
const prefs = this.persistenceManager.getAllPreferences();
// Animation speed
if (this.elements.animationSpeed) {
this.elements.animationSpeed.value = prefs.animation.speed;
this.elements.animationSpeedValue.textContent = prefs.animation.speed;
}
// TTS enabled
if (this.elements.ttsEnabled) {
this.elements.ttsEnabled.checked = prefs.tts.enabled;
// Show/hide TTS options based on enabled state
const ttsOptionsContainer = document.querySelector('.tts-options-container');
if (ttsOptionsContainer) {
ttsOptionsContainer.style.display = prefs.tts.enabled ? 'block' : 'none';
}
}
// TTS system
this.populateTtsSystems();
// TTS volume
if (this.elements.ttsVolume) {
this.elements.ttsVolume.value = prefs.tts.volume * 100;
this.elements.ttsVolumeValue.textContent = Math.round(prefs.tts.volume * 100);
}
// TTS rate
if (this.elements.ttsRate) {
this.elements.ttsRate.value = prefs.tts.rate * 100;
this.elements.ttsRateValue.textContent = Math.round(prefs.tts.rate * 100);
}
// Language selection
if (this.elements.language && this.localization) {
const currentLocale = this.localization.getLocale();
const availableLocales = this.localization.getAvailableLocales();
// Clear existing options
this.elements.language.innerHTML = '';
// Add options for each available locale
availableLocales.forEach(locale => {
const option = document.createElement('option');
option.value = locale;
option.textContent = this.localization.getLanguageName(locale);
option.selected = locale === currentLocale;
this.elements.language.appendChild(option);
});
}
// Audio volumes
if (this.elements.masterVolume) {
this.elements.masterVolume.value = prefs.audio.masterVolume * 100;
this.elements.masterVolumeValue.textContent = Math.round(prefs.audio.masterVolume * 100);
}
if (this.elements.musicVolume) {
this.elements.musicVolume.value = prefs.audio.musicVolume * 100;
this.elements.musicVolumeValue.textContent = Math.round(prefs.audio.musicVolume * 100);
}
if (this.elements.sfxVolume) {
this.elements.sfxVolume.value = prefs.audio.sfxVolume * 100;
this.elements.sfxVolumeValue.textContent = Math.round(prefs.audio.sfxVolume * 100);
}
// Accessibility options
if (this.elements.highContrast) {
this.elements.highContrast.checked = prefs.accessibility.highContrast;
}
if (this.elements.largerText) {
this.elements.largerText.checked = prefs.accessibility.largerText;
}
});
}
/**
* Populate TTS systems dropdown
*/
populateTtsSystems() {
if (!this.elements || !this.elements.ttsSystem) return;
// Clear existing options
this.elements.ttsSystem.innerHTML = '';
// Get current TTS preferences
const currentProvider = this.persistenceManager.getPreference('tts', 'provider', 'browser');
// Get available handlers from TTS factory
let availableHandlers = {};
if (this.ttsFactory) {
availableHandlers = this.ttsFactory.getAvailableHandlers();
} else {
// Fallback if TTS factory not available
availableHandlers = {
browser: true, // Assume browser TTS is available
api: false, // Assume API TTS is not available
kokoro: false // Assume Kokoro is not available
};
}
// Add option for each handler
const handlers = [
{ id: 'browser', name: 'Browser TTS', description: 'Uses your browser\'s built-in speech synthesis' },
{ id: 'api', name: 'API TTS', description: 'Uses a remote API for higher quality voices' },
{ id: 'kokoro', name: 'Kokoro TTS', description: 'Uses local AI-powered speech synthesis' }
];
handlers.forEach(handler => {
const option = document.createElement('option');
option.value = handler.id;
// Check if handler is available
const isAvailable = availableHandlers[handler.id] === true;
// Format option text
option.textContent = `${handler.name}${isAvailable ? '' : ' (unavailable)'}`;
option.title = handler.description;
// Disable option if handler is not available
option.disabled = !isAvailable;
// Select if this is the current provider
option.selected = handler.id === currentProvider;
this.elements.ttsSystem.appendChild(option);
});
// Populate voices for the selected system
this.populateVoices();
}
/**
* Populate voices dropdown for current TTS system
*/
populateVoices() {
if (!this.elements || !this.elements.ttsVoice) return;
// Clear existing options
this.elements.ttsVoice.innerHTML = '';
// Get current preferences
const currentVoice = this.persistenceManager.getPreference('tts', 'voice', '');
const currentProvider = this.persistenceManager.getPreference('tts', 'provider', 'browser');
// Get current locale
const currentLocale = this.localization ? this.localization.getLocale() : 'en-us';
// Get voices from TTS factory
let voices = [];
if (this.ttsFactory) {
// Get active handler
const activeHandler = this.ttsFactory.getActiveHandler();
if (activeHandler) {
voices = activeHandler.getVoices();
}
}
// If no voices available, add a placeholder
if (voices.length === 0) {
const option = document.createElement('option');
option.value = '';
option.textContent = 'No voices available';
this.elements.ttsVoice.appendChild(option);
return;
}
// Sort voices by language and name
voices.sort((a, b) => {
// First sort by matching current locale
const aMatchesLocale = a.lang && a.lang.toLowerCase().startsWith(currentLocale.split('-')[0]);
const bMatchesLocale = b.lang && b.lang.toLowerCase().startsWith(currentLocale.split('-')[0]);
if (aMatchesLocale && !bMatchesLocale) return -1;
if (!aMatchesLocale && bMatchesLocale) return 1;
// Then sort by language name
const aLang = this.getLanguageNameFromCode(a.lang);
const bLang = this.getLanguageNameFromCode(b.lang);
if (aLang !== bLang) {
return aLang.localeCompare(bLang);
}
// Finally sort by voice name
return a.name.localeCompare(b.name);
});
// Group voices by language
const voicesByLang = {};
voices.forEach(voice => {
const langCode = voice.lang || 'unknown';
const langName = this.getLanguageNameFromCode(langCode);
if (!voicesByLang[langName]) {
voicesByLang[langName] = [];
}
voicesByLang[langName].push(voice);
});
// Add voices grouped by language
Object.keys(voicesByLang).sort().forEach(langName => {
// Create optgroup for language
const optgroup = document.createElement('optgroup');
optgroup.label = langName;
// Add voices for this language
voicesByLang[langName].forEach(voice => {
const option = document.createElement('option');
option.value = voice.name || voice.id;
option.textContent = voice.name;
option.selected = voice.name === currentVoice || voice.id === currentVoice;
optgroup.appendChild(option);
});
this.elements.ttsVoice.appendChild(optgroup);
});
}
/**
* Get language name from language code
* @param {string} code - Language code (e.g., 'en', 'de')
* @returns {string} - Language name
*/
getLanguageNameFromCode(code) {
// Use localization module if available
if (this.localization && typeof this.localization.getLanguageName === 'function') {
return this.localization.getLanguageName(code);
}
// Fallback language names
const languageNames = {
'en': 'English',
'de': 'German',
'fr': 'French',
'es': 'Spanish',
'it': 'Italian',
'ja': 'Japanese',
'ko': 'Korean',
'zh': 'Chinese',
'ru': 'Russian',
'ar': 'Arabic',
'hi': 'Hindi',
'pt': 'Portuguese',
'nl': 'Dutch',
'pl': 'Polish',
'sv': 'Swedish',
'tr': 'Turkish',
'uk': 'Ukrainian'
};
return languageNames[code] || code.toUpperCase();
}
/**
* Show the options UI
*/
show() {
if (!this.modal) {
this.createModal();
}
// Load current preferences
this.loadPreferences();
// Populate TTS systems and voices
this.populateTtsSystems();
this.populateVoices();
// Show the modal
this.modal.style.display = 'flex';
this.isOpen = true;
}
/**
* Hide the options UI
*/
hide() {
if (this.modal) {
this.modal.style.display = 'none';
this.isOpen = false;
}
}
/**
* Toggle the options UI visibility
*/
toggle() {
if (this.isOpen) {
this.hide();
} else {
this.show();
}
}
/**
* Handle TTS system changes
* @param {CustomEvent} event - The event containing TTS system change details
*/
handleTtsSystemChanged(event) {
console.log("TTS system changed:", event.detail);
if (this.isOpen) {
// Refresh the voices list if the options UI is currently open
this.populateVoices();
}
}
/**
* Reset all options to defaults
*/
resetToDefaults() {
if (!this.persistenceManager) return;
const confirmed = confirm('Reset all options to default values?');
if (confirmed) {
// Reset preferences
this.persistenceManager.resetPreferences();
// Update UI
this.loadPreferences();
// Apply changes
this.applySettings();
// Refresh voice list
this.populateVoices();
}
}
/**
* Save settings and close modal
*/
saveAndClose() {
if (this.persistenceManager && this.elements) {
// Save preferences - already saved as they change
// Apply settings
this.applySettings();
}
this.hide();
}
/**
* Apply current settings to the app
*/
applySettings() {
if (!this.persistenceManager) return;
// Apply animation speed
const animSpeed = this.persistenceManager.getPreference('animation', 'speed', 50);
const animQueue = moduleRegistry.getModule('animation-queue');
if (animQueue) {
const speed = Math.pow(100.0 - animSpeed, 3) / 10000 * 10 + 0.01;
animQueue.setSpeed(speed);
}
// Apply TTS settings
const ttsEnabled = this.persistenceManager.getPreference('tts', 'enabled', false);
const ttsProvider = this.persistenceManager.getPreference('tts', 'provider', 'browser');
const ttsVoice = this.persistenceManager.getPreference('tts', 'voice', '');
const ttsVolume = this.persistenceManager.getPreference('tts', 'volume', 1.0);
const ttsRate = this.persistenceManager.getPreference('tts', 'rate', 1.0);
if (this.ttsFactory) {
// Set TTS provider if it's available
const availableHandlers = this.ttsFactory.getAvailableHandlers();
if (ttsProvider && availableHandlers[ttsProvider]) {
this.ttsFactory.setActiveHandler(ttsProvider);
}
// Get the active handler
const activeHandler = this.ttsFactory.getActiveHandler();
if (activeHandler) {
// Set voice if specified
if (ttsVoice) {
activeHandler.setVoice(ttsVoice);
}
// Set options
activeHandler.setOptions({
volume: ttsVolume,
rate: ttsRate
});
}
}
// Apply language settings
if (this.localization && this.elements && this.elements.language) {
const selectedLocale = this.elements.language.value;
if (selectedLocale && selectedLocale !== this.localization.getLocale()) {
this.localization.setLocale(selectedLocale);
}
}
// Apply audio volume settings
const masterVolume = this.persistenceManager.getPreference('audio', 'masterVolume', 1.0);
const musicVolume = this.persistenceManager.getPreference('audio', 'musicVolume', 0.7);
const sfxVolume = this.persistenceManager.getPreference('audio', 'sfxVolume', 1.0);
if (this.audioManager) {
this.audioManager.setMasterVolume(masterVolume);
this.audioManager.setMusicVolume(musicVolume);
this.audioManager.setSfxVolume(sfxVolume);
}
// Apply accessibility settings
const highContrast = this.persistenceManager.getPreference('accessibility', 'highContrast', false);
const largerText = this.persistenceManager.getPreference('accessibility', 'largerText', false);
if (highContrast) {
document.body.classList.add('high-contrast');
} else {
document.body.classList.remove('high-contrast');
}
if (largerText) {
document.body.classList.add('larger-text');
} else {
document.body.classList.remove('larger-text');
}
}
/**
* Set the TTS factory reference
* @param {Object} factory - The TTS factory instance
*/
setTtsFactory(factory) {
this.ttsFactory = factory;
}
/**
* Update available TTS systems info
* @param {Object} systemsInfo - Information about available TTS systems
*/
updateAvailableSystems(systemsInfo) {
// Will repopulate next time UI is opened
console.log("TTS systems info updated:", systemsInfo);
// If the options UI is currently open, update it
if (this.isOpen) {
this.populateTtsSystems();
this.populateVoices();
}
}
/**
* Clean up when module is disposed
*/
dispose() {
// Remove event listeners
window.removeEventListener('tts-system-changed', this.handleTtsSystemChanged);
}
}
// Create the singleton instance
const OptionsUI = new OptionsUIModule();
// Register with the module registry
moduleRegistry.register(OptionsUI);
// Export the module
export { OptionsUI };
// Keep a reference in window for loader system
window.OptionsUI = OptionsUI;