Fix TTS module initialization and dependency issues. Update module IDs for consistency, improve circular dependency detection, and fix UI Controller event handling.
This commit is contained in:
+331
-210
@@ -11,10 +11,15 @@ class OptionsUIModule extends BaseModule {
|
||||
*/
|
||||
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;
|
||||
|
||||
@@ -25,8 +30,19 @@ class OptionsUIModule extends BaseModule {
|
||||
backdrop: true
|
||||
};
|
||||
|
||||
// Elements reference
|
||||
this.elements = null;
|
||||
|
||||
// Bound event handlers for proper this context
|
||||
this.handleTtsSystemChanged = this.handleTtsSystemChanged.bind(this);
|
||||
this.bindMethods([
|
||||
'handleTtsSystemChanged',
|
||||
'loadPreferences',
|
||||
'populateTtsSystems',
|
||||
'populateVoices',
|
||||
'resetToDefaults',
|
||||
'saveAndClose',
|
||||
'applySettings'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,31 +63,27 @@ class OptionsUIModule extends BaseModule {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
// 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.audioManager = moduleRegistry.getModule('audio-manager');
|
||||
this.ttsFactory = this.getModule('tts-factory');
|
||||
this.ttsPlayer = this.getModule('tts');
|
||||
this.audioManager = this.getModule('audio-manager');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -574,6 +586,273 @@ class OptionsUIModule extends BaseModule {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@@ -616,190 +895,15 @@ class OptionsUIModule extends BaseModule {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load current preferences into UI
|
||||
* Handle TTS system changes
|
||||
* @param {CustomEvent} event - The event containing TTS system change details
|
||||
*/
|
||||
loadPreferences() {
|
||||
if (!this.persistenceManager || !this.elements) return;
|
||||
handleTtsSystemChanged(event) {
|
||||
console.log("TTS system changed:", event.detail);
|
||||
|
||||
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;
|
||||
if (this.isOpen) {
|
||||
// Refresh the voices list if the options UI is currently open
|
||||
this.populateVoices();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -860,18 +964,35 @@ class OptionsUIModule extends BaseModule {
|
||||
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);
|
||||
if (this.ttsFactory) {
|
||||
// Set TTS provider if it's available
|
||||
const availableHandlers = this.ttsFactory.getAvailableHandlers();
|
||||
if (ttsProvider && availableHandlers[ttsProvider]) {
|
||||
this.ttsFactory.setActiveHandler(ttsProvider);
|
||||
}
|
||||
|
||||
// Apply voice options
|
||||
this.ttsPlayer.setVoiceOptions({
|
||||
voice: ttsVoice,
|
||||
volume: ttsVolume,
|
||||
rate: ttsRate
|
||||
});
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user