1068 lines
38 KiB
JavaScript
1068 lines
38 KiB
JavaScript
/**
|
||
* 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; |