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:
2025-04-04 19:15:28 +00:00
parent 02c7b9ef28
commit 49a5af252c
33 changed files with 7227 additions and 4060 deletions
+331 -210
View File
@@ -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