Class_generator/js/core/settings-manager.js
StillHammer 475006e912 Add Word Discovery game with auto-play TTS and Settings system
- New Word Discovery game with image support and practice phases
- Auto-play TTS on word appearance with speed control (0.7x-1.1x)
- Complete Settings page with TTS controls and debug interface
- Language standardization with BCP 47 codes (en-US, zh-CN, fr-FR)
- Media fallback handling for missing images and audio
- Settings Manager with voice selection and debug tools

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-19 14:39:08 +08:00

397 lines
14 KiB
JavaScript

// === SETTINGS MANAGER FOR TTS AND DEBUG ===
const SettingsManager = {
// TTS Settings
ttsSettings: {
rate: 0.8,
volume: 1.0,
selectedVoice: null
},
availableVoices: [],
debugMessages: [],
init() {
this.loadSettings();
this.initTTSSettings();
this.setupEventListeners();
this.checkBrowserSupport();
this.loadVoices();
this.updateBrowserInfo();
},
// === SETTINGS PERSISTENCE ===
loadSettings() {
const saved = localStorage.getItem('tts-settings');
if (saved) {
try {
this.ttsSettings = { ...this.ttsSettings, ...JSON.parse(saved) };
} catch (e) {
console.warn('Failed to load TTS settings:', e);
}
}
},
saveSettings() {
localStorage.setItem('tts-settings', JSON.stringify(this.ttsSettings));
},
// === TTS SETTINGS INITIALIZATION ===
initTTSSettings() {
const rateSlider = document.getElementById('tts-rate');
const volumeSlider = document.getElementById('tts-volume');
const voiceSelect = document.getElementById('tts-voice');
if (rateSlider) {
rateSlider.value = this.ttsSettings.rate;
document.getElementById('tts-rate-value').textContent = this.ttsSettings.rate;
}
if (volumeSlider) {
volumeSlider.value = this.ttsSettings.volume;
document.getElementById('tts-volume-value').textContent = this.ttsSettings.volume;
}
},
setupEventListeners() {
// Rate slider
const rateSlider = document.getElementById('tts-rate');
if (rateSlider) {
rateSlider.addEventListener('input', (e) => {
this.ttsSettings.rate = parseFloat(e.target.value);
document.getElementById('tts-rate-value').textContent = this.ttsSettings.rate;
this.saveSettings();
});
}
// Volume slider
const volumeSlider = document.getElementById('tts-volume');
if (volumeSlider) {
volumeSlider.addEventListener('input', (e) => {
this.ttsSettings.volume = parseFloat(e.target.value);
document.getElementById('tts-volume-value').textContent = this.ttsSettings.volume;
this.saveSettings();
});
}
// Voice selection
const voiceSelect = document.getElementById('tts-voice');
if (voiceSelect) {
voiceSelect.addEventListener('change', (e) => {
this.ttsSettings.selectedVoice = e.target.value;
this.saveSettings();
});
}
},
// === BROWSER SUPPORT CHECK ===
checkBrowserSupport() {
const checks = [
{ name: 'speechSynthesis', available: 'speechSynthesis' in window },
{ name: 'SpeechSynthesisUtterance', available: 'SpeechSynthesisUtterance' in window },
{ name: 'getVoices', available: speechSynthesis && typeof speechSynthesis.getVoices === 'function' },
{ name: 'speak', available: speechSynthesis && typeof speechSynthesis.speak === 'function' }
];
const support = checks.every(check => check.available);
const supportElement = document.getElementById('browser-support');
if (supportElement) {
supportElement.textContent = support ? '✅ Full Support' : '❌ Limited Support';
supportElement.style.color = support ? '#22C55E' : '#EF4444';
}
return support;
},
// === VOICE MANAGEMENT ===
loadVoices() {
const loadVoicesImpl = () => {
this.availableVoices = speechSynthesis.getVoices();
this.updateVoiceInfo();
this.populateVoiceSelect();
this.displayVoiceList();
};
// Try immediately
loadVoicesImpl();
// Also try after a delay (some browsers load voices asynchronously)
setTimeout(loadVoicesImpl, 100);
// Listen for voice changes
if (speechSynthesis.onvoiceschanged !== undefined) {
speechSynthesis.onvoiceschanged = loadVoicesImpl;
}
},
updateVoiceInfo() {
const voiceCountElement = document.getElementById('voice-count');
const englishVoiceCountElement = document.getElementById('english-voice-count');
if (voiceCountElement) {
voiceCountElement.textContent = this.availableVoices.length;
}
const englishVoices = this.availableVoices.filter(voice => voice.lang.startsWith('en'));
if (englishVoiceCountElement) {
englishVoiceCountElement.textContent = englishVoices.length;
}
},
populateVoiceSelect() {
const voiceSelect = document.getElementById('tts-voice');
if (!voiceSelect) return;
// Clear existing options except the first one
voiceSelect.innerHTML = '<option value="">Auto (System Default)</option>';
const englishVoices = this.availableVoices.filter(voice => voice.lang.startsWith('en'));
englishVoices.forEach(voice => {
const option = document.createElement('option');
option.value = voice.name;
option.textContent = `${voice.name} (${voice.lang})`;
if (voice.name === this.ttsSettings.selectedVoice) {
option.selected = true;
}
voiceSelect.appendChild(option);
});
},
displayVoiceList() {
const voiceListElement = document.getElementById('voice-list');
if (!voiceListElement) return;
if (this.availableVoices.length === 0) {
voiceListElement.innerHTML = '<div style="text-align: center; color: #666;">No voices available</div>';
return;
}
voiceListElement.innerHTML = '';
this.availableVoices.forEach(voice => {
const voiceItem = document.createElement('div');
voiceItem.className = 'voice-item';
voiceItem.innerHTML = `
<div class="voice-name">${voice.name}</div>
<div class="voice-lang">${voice.lang}</div>
<div class="voice-type">${voice.localService ? 'Local' : 'Remote'}</div>
`;
voiceItem.addEventListener('click', () => {
// Test this voice
this.testVoice(voice);
// Update selection visually
document.querySelectorAll('.voice-item').forEach(item => item.classList.remove('selected'));
voiceItem.classList.add('selected');
});
voiceListElement.appendChild(voiceItem);
});
},
// === TTS TESTING FUNCTIONS ===
testVoice(voice) {
try {
const utterance = new SpeechSynthesisUtterance('Hello, this is a voice test');
utterance.voice = voice;
utterance.rate = this.ttsSettings.rate;
utterance.volume = this.ttsSettings.volume;
utterance.onstart = () => {
this.addDebugMessage(`Testing voice: ${voice.name}`, 'info');
};
utterance.onerror = (event) => {
this.addDebugMessage(`Voice test error: ${event.error}`, 'error');
};
speechSynthesis.speak(utterance);
} catch (error) {
this.addDebugMessage(`Voice test failed: ${error.message}`, 'error');
}
},
// === DEBUG FUNCTIONS ===
addDebugMessage(message, type = 'info') {
const timestamp = new Date().toLocaleTimeString();
const logEntry = `[${timestamp}] ${message}`;
this.debugMessages.push({ message: logEntry, type });
this.updateDebugDisplay();
// Also log to console
console.log(`[Settings] ${logEntry}`);
},
updateDebugDisplay() {
const debugLogElement = document.getElementById('debug-log');
if (!debugLogElement) return;
const lastEntries = this.debugMessages.slice(-50); // Keep last 50 entries
debugLogElement.innerHTML = lastEntries
.map(entry => `<span class="${entry.type}">${entry.message}</span>`)
.join('\n');
// Scroll to bottom
debugLogElement.scrollTop = debugLogElement.scrollHeight;
},
clearDebugLog() {
this.debugMessages = [];
this.updateDebugDisplay();
},
// === LANGUAGE DETECTION ===
detectContentLanguage() {
// Try to get language from current content in Word Discovery or other games
if (window.currentGameContent && window.currentGameContent.language) {
this.addDebugMessage(`Auto-detected language: ${window.currentGameContent.language}`, 'info');
return window.currentGameContent.language;
}
// Fallback: try to detect from navigation context
if (window.AppNavigation && window.AppNavigation.scannedContent) {
const currentContentKey = window.AppNavigation.selectedContent;
if (currentContentKey) {
const contentInfo = window.AppNavigation.scannedContent.found.find(
content => content.id === currentContentKey
);
if (contentInfo && contentInfo.language) {
this.addDebugMessage(`Language from navigation: ${contentInfo.language}`, 'info');
return contentInfo.language;
}
}
}
this.addDebugMessage('No content language detected, using en-US default', 'warning');
return 'en-US';
},
// === BROWSER INFO ===
updateBrowserInfo() {
const userAgentElement = document.getElementById('user-agent');
const platformElement = document.getElementById('platform');
const languageElement = document.getElementById('browser-language');
if (userAgentElement) {
userAgentElement.textContent = navigator.userAgent;
}
if (platformElement) {
platformElement.textContent = navigator.platform;
}
if (languageElement) {
languageElement.textContent = navigator.language;
}
},
// === PUBLIC TTS API ===
speak(text, options = {}) {
return new Promise((resolve, reject) => {
try {
if (!('speechSynthesis' in window)) {
reject(new Error('Speech synthesis not supported'));
return;
}
const utterance = new SpeechSynthesisUtterance(text);
// Apply settings
utterance.rate = options.rate || this.ttsSettings.rate;
utterance.volume = options.volume || this.ttsSettings.volume;
// Auto-detect language from content if available
const autoLang = this.detectContentLanguage() || 'en-US';
utterance.lang = options.lang || autoLang;
// Apply selected voice if available
if (this.ttsSettings.selectedVoice) {
const selectedVoice = this.availableVoices.find(
voice => voice.name === this.ttsSettings.selectedVoice
);
if (selectedVoice) {
utterance.voice = selectedVoice;
}
}
utterance.onend = () => {
this.addDebugMessage(`Spoke: "${text}"`, 'success');
resolve();
};
utterance.onerror = (event) => {
this.addDebugMessage(`Speech error: ${event.error}`, 'error');
reject(new Error(event.error));
};
speechSynthesis.speak(utterance);
} catch (error) {
this.addDebugMessage(`Speech failed: ${error.message}`, 'error');
reject(error);
}
});
}
};
// === GLOBAL TTS TEST FUNCTIONS ===
function testBasicTTS() {
SettingsManager.addDebugMessage('Testing basic TTS...', 'info');
SettingsManager.speak('Hello world, this is a basic test')
.then(() => SettingsManager.addDebugMessage('✅ Basic TTS test completed', 'success'))
.catch(error => SettingsManager.addDebugMessage(`❌ Basic TTS test failed: ${error.message}`, 'error'));
}
function testWithCallbacks() {
SettingsManager.addDebugMessage('Testing TTS with detailed callbacks...', 'info');
SettingsManager.speak('Apple, cat, house, car')
.then(() => SettingsManager.addDebugMessage('✅ Callback TTS test completed', 'success'))
.catch(error => SettingsManager.addDebugMessage(`❌ Callback TTS test failed: ${error.message}`, 'error'));
}
function testGameWords() {
SettingsManager.addDebugMessage('Testing game vocabulary words...', 'info');
const words = ['apple', 'cat', 'house', 'car', 'tree', 'book', 'sun', 'dog'];
let index = 0;
function speakNext() {
if (index >= words.length) {
SettingsManager.addDebugMessage('✅ Game words test completed', 'success');
return;
}
const word = words[index++];
SettingsManager.speak(word)
.then(() => {
SettingsManager.addDebugMessage(`✅ Spoke: ${word}`, 'info');
setTimeout(speakNext, 500);
})
.catch(error => {
SettingsManager.addDebugMessage(`❌ Failed to speak ${word}: ${error.message}`, 'error');
setTimeout(speakNext, 500);
});
}
speakNext();
}
function refreshVoices() {
SettingsManager.addDebugMessage('Refreshing voice list...', 'info');
SettingsManager.loadVoices();
SettingsManager.addDebugMessage('✅ Voice list refreshed', 'success');
}
function clearDebugLog() {
SettingsManager.clearDebugLog();
}
// Initialize when page loads
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => SettingsManager.init());
} else {
SettingsManager.init();
}
// Export for use in games
window.SettingsManager = SettingsManager;