Split everything up into dynamically loaded modules.
This commit is contained in:
@@ -0,0 +1,947 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user