947 lines
34 KiB
JavaScript
947 lines
34 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');
|
||
this.persistenceManager = null;
|
||
this.ttsPlayer = null;
|
||
this.audioManager = null;
|
||
this.ttsFactory = null;
|
||
this.modal = null;
|
||
this.isOpen = false;
|
||
|
||
// Configuration
|
||
this.config = {
|
||
modalClass: 'options-modal',
|
||
modalContentClass: 'options-content',
|
||
backdrop: true
|
||
};
|
||
|
||
// Bound event handlers for proper this context
|
||
this.handleTtsSystemChanged = this.handleTtsSystemChanged.bind(this);
|
||
}
|
||
|
||
/**
|
||
* 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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Wait for dependencies to be ready
|
||
* @returns {Promise<boolean>} - Resolves when dependencies are ready
|
||
*/
|
||
async waitForDependencies() {
|
||
try {
|
||
// Wait for the persistence manager if available
|
||
this.persistenceManager = moduleRegistry.getModule('persistence-manager');
|
||
this.ttsPlayer = moduleRegistry.getModule('tts');
|
||
|
||
// These dependencies are optional - UI will adapt if not available
|
||
this.audioManager = moduleRegistry.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
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 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();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Load current preferences into UI
|
||
*/
|
||
loadPreferences() {
|
||
if (!this.persistenceManager || !this.elements) return;
|
||
|
||
const prefs = this.persistenceManager.getAllPreferences();
|
||
|
||
// Animation speed
|
||
const animSpeed = this.persistenceManager.getPreference('animation', 'speed', 50);
|
||
this.elements.animSpeed.value = animSpeed;
|
||
this.elements.animSpeedValue.textContent = `${animSpeed}%`;
|
||
|
||
// 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);
|
||
|
||
// TTS rate slider
|
||
this.elements.speechRate.value = Math.round(ttsRate * 100);
|
||
this.elements.speechRateValue.textContent = `${ttsRate.toFixed(1)}x`;
|
||
|
||
// TTS volume slider
|
||
this.elements.ttsVolume.value = Math.round(ttsVolume * 100);
|
||
this.elements.ttsVolumeValue.textContent = `${Math.round(ttsVolume * 100)}%`;
|
||
|
||
// Audio volumes
|
||
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);
|
||
|
||
this.elements.masterVolume.value = Math.round(masterVolume * 100);
|
||
this.elements.masterVolumeValue.textContent = `${Math.round(masterVolume * 100)}%`;
|
||
|
||
this.elements.musicVolume.value = Math.round(musicVolume * 100);
|
||
this.elements.musicVolumeValue.textContent = `${Math.round(musicVolume * 100)}%`;
|
||
|
||
this.elements.sfxVolume.value = Math.round(sfxVolume * 100);
|
||
this.elements.sfxVolumeValue.textContent = `${Math.round(sfxVolume * 100)}%`;
|
||
|
||
// Accessibility settings
|
||
const highContrast = this.persistenceManager.getPreference('accessibility', 'highContrast', false);
|
||
const largerText = this.persistenceManager.getPreference('accessibility', 'largerText', false);
|
||
|
||
this.elements.highContrast.checked = highContrast;
|
||
this.elements.largerText.checked = largerText;
|
||
}
|
||
|
||
/**
|
||
* Populate TTS systems dropdown
|
||
*/
|
||
populateTtsSystems() {
|
||
if (!this.ttsPlayer || !this.elements) return;
|
||
|
||
const systems = this.ttsPlayer.getAvailableSystems();
|
||
const select = this.elements.ttsSystem;
|
||
|
||
// Clear existing options and listeners
|
||
select.innerHTML = '';
|
||
const newSelect = select.cloneNode(false);
|
||
select.parentNode.replaceChild(newSelect, select);
|
||
this.elements.ttsSystem = newSelect;
|
||
select = newSelect;
|
||
|
||
// Get current TTS info
|
||
const currentInfo = this.ttsPlayer.getTTSInfo();
|
||
const currentId = currentInfo.type || '';
|
||
|
||
// Create an option for each available system
|
||
systems.forEach(id => {
|
||
const option = document.createElement('option');
|
||
option.value = id;
|
||
|
||
switch (id) {
|
||
case 'browser':
|
||
option.textContent = 'Browser Built-in TTS';
|
||
break;
|
||
case 'kokoro':
|
||
option.textContent = 'Kokoro Neural TTS';
|
||
break;
|
||
case 'api':
|
||
option.textContent = 'API-based TTS';
|
||
break;
|
||
default:
|
||
option.textContent = id.charAt(0).toUpperCase() + id.slice(1);
|
||
}
|
||
|
||
if (id === currentId) {
|
||
option.selected = true;
|
||
}
|
||
|
||
select.appendChild(option);
|
||
});
|
||
|
||
// Add change listener
|
||
select.addEventListener('change', () => {
|
||
const selectedSystem = select.value;
|
||
if (this.ttsPlayer) {
|
||
this.ttsPlayer.switchTTS(selectedSystem);
|
||
|
||
// Update persistence
|
||
if (this.persistenceManager) {
|
||
this.persistenceManager.updatePreference('tts', 'provider', selectedSystem);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Populate voices dropdown for current TTS system
|
||
*/
|
||
async populateVoices() {
|
||
if (!this.ttsPlayer || !this.elements || !this.ttsPlayer.getVoices) return;
|
||
|
||
try {
|
||
const voices = await this.ttsPlayer.getVoices();
|
||
const select = this.elements.voiceSelect;
|
||
|
||
// Clear existing options and listeners
|
||
select.innerHTML = '';
|
||
const newSelect = select.cloneNode(false);
|
||
select.parentNode.replaceChild(newSelect, select);
|
||
this.elements.voiceSelect = newSelect;
|
||
select = newSelect;
|
||
|
||
if (!voices || voices.length === 0) {
|
||
const option = document.createElement('option');
|
||
option.value = '';
|
||
option.textContent = 'No voices available';
|
||
select.appendChild(option);
|
||
select.disabled = true;
|
||
return;
|
||
}
|
||
|
||
select.disabled = false;
|
||
|
||
// Get current preference
|
||
let currentVoice = '';
|
||
if (this.persistenceManager) {
|
||
currentVoice = this.persistenceManager.getPreference('tts', 'voice', '');
|
||
}
|
||
|
||
// Add voices to dropdown
|
||
voices.forEach(voice => {
|
||
const option = document.createElement('option');
|
||
option.value = voice.id || voice.name;
|
||
option.textContent = voice.name;
|
||
|
||
if (voice.id === currentVoice || voice.name === currentVoice) {
|
||
option.selected = true;
|
||
}
|
||
|
||
select.appendChild(option);
|
||
});
|
||
|
||
// Add change listener
|
||
select.addEventListener('change', () => {
|
||
const selectedVoice = select.value;
|
||
|
||
// Update TTS
|
||
if (this.ttsPlayer) {
|
||
this.ttsPlayer.setVoice(selectedVoice);
|
||
}
|
||
|
||
// Update persistence
|
||
if (this.persistenceManager) {
|
||
this.persistenceManager.updatePreference('tts', 'voice', selectedVoice);
|
||
}
|
||
});
|
||
|
||
console.log(`Voices populated for current TTS system. Selected: ${select.value}`);
|
||
|
||
} catch (error) {
|
||
console.error("Error populating voices:", error);
|
||
|
||
const select = this.elements.voiceSelect;
|
||
select.innerHTML = '';
|
||
|
||
const option = document.createElement('option');
|
||
option.value = '';
|
||
option.textContent = 'Error loading voices';
|
||
select.appendChild(option);
|
||
select.disabled = true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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.ttsPlayer) {
|
||
// Set TTS system
|
||
if (ttsProvider) {
|
||
this.ttsPlayer.switchTTS(ttsProvider);
|
||
}
|
||
|
||
// Apply voice options
|
||
this.ttsPlayer.setVoiceOptions({
|
||
voice: ttsVoice,
|
||
volume: ttsVolume,
|
||
rate: ttsRate
|
||
});
|
||
}
|
||
|
||
// 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; |