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>
This commit is contained in:
parent
cb0c3b01f8
commit
475006e912
314
css/settings.css
Normal file
314
css/settings.css
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
/* === SETTINGS PAGE STYLES === */
|
||||||
|
|
||||||
|
.settings-container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
|
background: var(--card-background);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section h3 {
|
||||||
|
font-size: 1.4em;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === SETTING GROUPS === */
|
||||||
|
.setting-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group input[type="range"] {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0 15px;
|
||||||
|
accent-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group select {
|
||||||
|
min-width: 200px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group span {
|
||||||
|
min-width: 40px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === DEBUG INFO === */
|
||||||
|
.debug-info {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item .label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item .value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item .value.small {
|
||||||
|
font-size: 0.85em;
|
||||||
|
max-width: 400px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === DEBUG CONTROLS === */
|
||||||
|
.debug-controls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-btn {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-btn:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === DEBUG OUTPUT === */
|
||||||
|
.debug-output {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-output h4 {
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-log {
|
||||||
|
min-height: 120px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
background: #000000;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-log:empty::before {
|
||||||
|
content: "No debug output yet. Click test buttons above.";
|
||||||
|
color: #888;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #333;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn:hover {
|
||||||
|
background: #444;
|
||||||
|
border-color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === VOICE LIST === */
|
||||||
|
.voice-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voice-item {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
transition: var(--transition);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voice-item:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.voice-item.selected {
|
||||||
|
background: var(--primary-light);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.voice-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voice-lang {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voice-type {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: var(--accent-color);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === BROWSER INFO === */
|
||||||
|
.browser-info .info-item {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browser-info .value {
|
||||||
|
text-align: right;
|
||||||
|
max-width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === LOG COLORS === */
|
||||||
|
.debug-log .success {
|
||||||
|
color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-log .error {
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-log .warning {
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-log .info {
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-log .timestamp {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === RESPONSIVE === */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.settings-container {
|
||||||
|
padding: 15px;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group label {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group input[type="range"] {
|
||||||
|
width: 100%;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-group select {
|
||||||
|
width: 100%;
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-controls {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voice-list {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
102
index.html
102
index.html
@ -7,6 +7,7 @@
|
|||||||
<link rel="stylesheet" href="css/main.css">
|
<link rel="stylesheet" href="css/main.css">
|
||||||
<link rel="stylesheet" href="css/navigation.css">
|
<link rel="stylesheet" href="css/navigation.css">
|
||||||
<link rel="stylesheet" href="css/games.css">
|
<link rel="stylesheet" href="css/games.css">
|
||||||
|
<link rel="stylesheet" href="css/settings.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Top Bar with Network Status -->
|
<!-- Top Bar with Network Status -->
|
||||||
@ -45,9 +46,9 @@
|
|||||||
📊 <span>Statistics</span>
|
📊 <span>Statistics</span>
|
||||||
<small>Coming soon</small>
|
<small>Coming soon</small>
|
||||||
</button>
|
</button>
|
||||||
<button class="option-card secondary" onclick="showComingSoon()">
|
<button class="option-card secondary" onclick="navigateTo('settings')">
|
||||||
⚙️ <span>Settings</span>
|
⚙️ <span>Settings</span>
|
||||||
<small>Coming soon</small>
|
<small>Audio & Debug Tools</small>
|
||||||
</button>
|
</button>
|
||||||
<button class="option-card primary" onclick="showContentCreator()">
|
<button class="option-card primary" onclick="showContentCreator()">
|
||||||
🏭 <span>Content Creator</span>
|
🏭 <span>Content Creator</span>
|
||||||
@ -91,12 +92,106 @@
|
|||||||
Score: <span id="current-score">0</span>
|
Score: <span id="current-score">0</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="game-container" id="game-container">
|
<div class="game-container" id="game-container">
|
||||||
<!-- The game will be loaded here dynamically -->
|
<!-- The game will be loaded here dynamically -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings Page -->
|
||||||
|
<div class="page" id="settings-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<button class="back-btn" onclick="goBack()">← Back</button>
|
||||||
|
<h2>⚙️ Settings & Debug Tools</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-container">
|
||||||
|
<!-- Audio Settings Section -->
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3>🔊 Audio Settings</h3>
|
||||||
|
|
||||||
|
<div class="setting-group">
|
||||||
|
<label>TTS Voice Speed:</label>
|
||||||
|
<input type="range" id="tts-rate" min="0.5" max="2" step="0.1" value="0.8">
|
||||||
|
<span id="tts-rate-value">0.8</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-group">
|
||||||
|
<label>TTS Volume:</label>
|
||||||
|
<input type="range" id="tts-volume" min="0" max="1" step="0.1" value="1">
|
||||||
|
<span id="tts-volume-value">1.0</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-group">
|
||||||
|
<label>Preferred Voice:</label>
|
||||||
|
<select id="tts-voice">
|
||||||
|
<option value="">Auto (System Default)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TTS Debug Section -->
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3>🔧 TTS Debug Tools</h3>
|
||||||
|
|
||||||
|
<div class="debug-info" id="tts-status">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Browser Support:</span>
|
||||||
|
<span class="value" id="browser-support">Checking...</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Available Voices:</span>
|
||||||
|
<span class="value" id="voice-count">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">English Voices:</span>
|
||||||
|
<span class="value" id="english-voice-count">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="debug-controls">
|
||||||
|
<button class="debug-btn" onclick="testBasicTTS()">🔊 Test Basic TTS</button>
|
||||||
|
<button class="debug-btn" onclick="testWithCallbacks()">📞 Test with Callbacks</button>
|
||||||
|
<button class="debug-btn" onclick="testGameWords()">🎮 Test Game Words</button>
|
||||||
|
<button class="debug-btn" onclick="refreshVoices()">🔄 Refresh Voices</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="debug-output" id="debug-output">
|
||||||
|
<h4>Debug Output:</h4>
|
||||||
|
<div class="debug-log" id="debug-log"></div>
|
||||||
|
<button class="clear-btn" onclick="clearDebugLog()">Clear Log</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Voice List Section -->
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3>🎤 Available Voices</h3>
|
||||||
|
<div class="voice-list" id="voice-list">
|
||||||
|
Loading voices...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Browser Info Section -->
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3>🌐 Browser Information</h3>
|
||||||
|
<div class="browser-info">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">User Agent:</span>
|
||||||
|
<span class="value small" id="user-agent"></span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Platform:</span>
|
||||||
|
<span class="value" id="platform"></span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Language:</span>
|
||||||
|
<span class="value" id="browser-language"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Modal Coming Soon -->
|
<!-- Modal Coming Soon -->
|
||||||
@ -126,6 +221,7 @@
|
|||||||
<script src="js/core/json-content-loader.js"></script>
|
<script src="js/core/json-content-loader.js"></script>
|
||||||
<script src="js/core/content-game-compatibility.js"></script>
|
<script src="js/core/content-game-compatibility.js"></script>
|
||||||
<script src="js/tools/content-creator.js"></script>
|
<script src="js/tools/content-creator.js"></script>
|
||||||
|
<script src="js/core/settings-manager.js"></script>
|
||||||
<script src="js/core/navigation.js"></script>
|
<script src="js/core/navigation.js"></script>
|
||||||
<script src="js/core/game-loader.js"></script>
|
<script src="js/core/game-loader.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ window.ContentModules.ChineseLongStory = {
|
|||||||
name: "The Dragon's Pearl - 龙珠传说",
|
name: "The Dragon's Pearl - 龙珠传说",
|
||||||
description: "Long story with translation and pronunciation",
|
description: "Long story with translation and pronunciation",
|
||||||
difficulty: "intermediate",
|
difficulty: "intermediate",
|
||||||
|
language: "zh-CN",
|
||||||
totalWords: 1200,
|
totalWords: 1200,
|
||||||
|
|
||||||
vocabulary: {
|
vocabulary: {
|
||||||
|
|||||||
59
js/content/example-minimal.js
Normal file
59
js/content/example-minimal.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// Example content module with minimal data (no images, some missing pronunciation)
|
||||||
|
window.ContentModules = window.ContentModules || {};
|
||||||
|
window.ContentModules.ExampleMinimal = {
|
||||||
|
name: "Minimal Vocabulary Test",
|
||||||
|
description: "Test content with missing images and audio",
|
||||||
|
difficulty: "easy",
|
||||||
|
language: "en-US",
|
||||||
|
|
||||||
|
// Vocabulary with mixed availability of features
|
||||||
|
vocabulary: {
|
||||||
|
"hello": {
|
||||||
|
translation: "bonjour",
|
||||||
|
pronunciation: "həˈloʊ",
|
||||||
|
type: "greeting"
|
||||||
|
// No image
|
||||||
|
},
|
||||||
|
"goodbye": {
|
||||||
|
translation: "au revoir",
|
||||||
|
type: "greeting"
|
||||||
|
// No image, no pronunciation
|
||||||
|
},
|
||||||
|
"water": {
|
||||||
|
translation: "eau",
|
||||||
|
pronunciation: "ˈwɔːtər",
|
||||||
|
type: "noun"
|
||||||
|
// No image
|
||||||
|
},
|
||||||
|
"food": {
|
||||||
|
translation: "nourriture",
|
||||||
|
type: "noun"
|
||||||
|
// No image, no pronunciation
|
||||||
|
},
|
||||||
|
"happy": {
|
||||||
|
translation: "heureux",
|
||||||
|
pronunciation: "ˈhæpi",
|
||||||
|
type: "adjective"
|
||||||
|
// No image
|
||||||
|
},
|
||||||
|
"sad": {
|
||||||
|
translation: "triste",
|
||||||
|
type: "adjective"
|
||||||
|
// No image, no pronunciation
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Backward compatibility for other games
|
||||||
|
sentences: [
|
||||||
|
{
|
||||||
|
english: "Hello, how are you?",
|
||||||
|
chinese: "Bonjour, comment allez-vous?",
|
||||||
|
prononciation: "həˈloʊ haʊ ɑr ju"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
english: "I need some water",
|
||||||
|
chinese: "J'ai besoin d'eau",
|
||||||
|
prononciation: "aɪ nid sʌm ˈwɔːtər"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
81
js/content/example-with-images.js
Normal file
81
js/content/example-with-images.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// Example content module with image support for Word Discovery game
|
||||||
|
window.ContentModules = window.ContentModules || {};
|
||||||
|
window.ContentModules.ExampleWithImages = {
|
||||||
|
name: "Basic Vocabulary with Images",
|
||||||
|
description: "Simple English words with visual support for beginners",
|
||||||
|
difficulty: "easy",
|
||||||
|
language: "en-US",
|
||||||
|
|
||||||
|
// Vocabulary with image support
|
||||||
|
vocabulary: {
|
||||||
|
"apple": {
|
||||||
|
translation: "pomme",
|
||||||
|
pronunciation: "æpəl",
|
||||||
|
type: "noun",
|
||||||
|
image: "assets/images/vocabulary/apple.png",
|
||||||
|
audioFile: "assets/audio/vocabulary/apple.mp3"
|
||||||
|
},
|
||||||
|
"cat": {
|
||||||
|
translation: "chat",
|
||||||
|
pronunciation: "kæt",
|
||||||
|
type: "noun",
|
||||||
|
image: "assets/images/vocabulary/cat.png",
|
||||||
|
audioFile: "assets/audio/vocabulary/cat_broken.mp3" // Broken path to test fallback
|
||||||
|
},
|
||||||
|
"house": {
|
||||||
|
translation: "maison",
|
||||||
|
pronunciation: "haʊs",
|
||||||
|
type: "noun",
|
||||||
|
image: "assets/images/vocabulary/house.png"
|
||||||
|
},
|
||||||
|
"car": {
|
||||||
|
translation: "voiture",
|
||||||
|
pronunciation: "kɑr",
|
||||||
|
type: "noun",
|
||||||
|
image: "assets/images/vocabulary/car.png"
|
||||||
|
},
|
||||||
|
"tree": {
|
||||||
|
translation: "arbre",
|
||||||
|
pronunciation: "tri",
|
||||||
|
type: "noun",
|
||||||
|
image: "assets/images/vocabulary/tree.png"
|
||||||
|
},
|
||||||
|
"book": {
|
||||||
|
translation: "livre",
|
||||||
|
pronunciation: "bʊk",
|
||||||
|
type: "noun",
|
||||||
|
image: "assets/images/vocabulary/book.png"
|
||||||
|
},
|
||||||
|
"sun": {
|
||||||
|
translation: "soleil",
|
||||||
|
pronunciation: "sʌn",
|
||||||
|
type: "noun",
|
||||||
|
image: "assets/images/vocabulary/sun.png"
|
||||||
|
},
|
||||||
|
"dog": {
|
||||||
|
translation: "chien",
|
||||||
|
pronunciation: "dɔg",
|
||||||
|
type: "noun",
|
||||||
|
image: "assets/images/vocabulary/dog.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Backward compatibility for other games
|
||||||
|
sentences: [
|
||||||
|
{
|
||||||
|
english: "The apple is red",
|
||||||
|
chinese: "La pomme est rouge",
|
||||||
|
prononciation: "ðə æpəl ɪz red"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
english: "The cat is sleeping",
|
||||||
|
chinese: "Le chat dort",
|
||||||
|
prononciation: "ðə kæt ɪz slipɪŋ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
english: "I live in a house",
|
||||||
|
chinese: "J'habite dans une maison",
|
||||||
|
prononciation: "aɪ lɪv ɪn ə haʊs"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
@ -6,6 +6,7 @@ window.ContentModules.SBSLevel78New = {
|
|||||||
name: "SBS Level 7-8 New",
|
name: "SBS Level 7-8 New",
|
||||||
description: "Side by Side Level 7-8 vocabulary with language-agnostic format",
|
description: "Side by Side Level 7-8 vocabulary with language-agnostic format",
|
||||||
difficulty: "intermediate",
|
difficulty: "intermediate",
|
||||||
|
language: "en-US",
|
||||||
|
|
||||||
vocabulary: {
|
vocabulary: {
|
||||||
// Housing and Places
|
// Housing and Places
|
||||||
|
|||||||
@ -7,6 +7,7 @@ window.ContentModules.StoryPrototypeOptimized = {
|
|||||||
name: "The Magical Library (Optimized)",
|
name: "The Magical Library (Optimized)",
|
||||||
description: "Adventure story with centralized vocabulary system",
|
description: "Adventure story with centralized vocabulary system",
|
||||||
difficulty: "intermediate",
|
difficulty: "intermediate",
|
||||||
|
language: "en-US",
|
||||||
|
|
||||||
// Centralized vocabulary - defined once, used everywhere
|
// Centralized vocabulary - defined once, used everywhere
|
||||||
vocabulary: {
|
vocabulary: {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ window.ContentModules.TestMinimalContent = {
|
|||||||
name: "Test Minimal (2 mots)",
|
name: "Test Minimal (2 mots)",
|
||||||
description: "Contenu minimal pour tester la compatibilité",
|
description: "Contenu minimal pour tester la compatibilité",
|
||||||
difficulty: "easy",
|
difficulty: "easy",
|
||||||
|
language: "en-US",
|
||||||
|
|
||||||
vocabulary: {
|
vocabulary: {
|
||||||
"hello": "bonjour",
|
"hello": "bonjour",
|
||||||
|
|||||||
@ -7,6 +7,7 @@ window.ContentModules.TestMinimal = {
|
|||||||
name: "Test Minimal (2 mots)",
|
name: "Test Minimal (2 mots)",
|
||||||
description: "Contenu minimal pour tester la compatibilité - seulement 2 mots",
|
description: "Contenu minimal pour tester la compatibilité - seulement 2 mots",
|
||||||
difficulty: "easy",
|
difficulty: "easy",
|
||||||
|
language: "en-US",
|
||||||
|
|
||||||
vocabulary: {
|
vocabulary: {
|
||||||
"hello": "bonjour",
|
"hello": "bonjour",
|
||||||
|
|||||||
@ -7,6 +7,7 @@ window.ContentModules.TestRich = {
|
|||||||
name: "Test Riche (complet)",
|
name: "Test Riche (complet)",
|
||||||
description: "Contenu riche pour tester la compatibilité maximale",
|
description: "Contenu riche pour tester la compatibilité maximale",
|
||||||
difficulty: "medium",
|
difficulty: "medium",
|
||||||
|
language: "en-US",
|
||||||
|
|
||||||
vocabulary: {
|
vocabulary: {
|
||||||
"apple": {
|
"apple": {
|
||||||
|
|||||||
@ -330,6 +330,7 @@ class ContentScanner {
|
|||||||
'english_exemple.json', // Local JSON basic
|
'english_exemple.json', // Local JSON basic
|
||||||
'english_exemple_fixed.json', // Local JSON modular
|
'english_exemple_fixed.json', // Local JSON modular
|
||||||
'english_exemple_ultra_commented.json', // Local JSON ultra-modular
|
'english_exemple_ultra_commented.json', // Local JSON ultra-modular
|
||||||
|
'example-with-images.js', // Local JS with image support for Word Discovery
|
||||||
// AJOUT: Fichiers générés par le système de conversion
|
// AJOUT: Fichiers générés par le système de conversion
|
||||||
'sbs-level-7-8-GENERATED-from-js.json',
|
'sbs-level-7-8-GENERATED-from-js.json',
|
||||||
'english-exemple-commented-GENERATED.json'
|
'english-exemple-commented-GENERATED.json'
|
||||||
|
|||||||
@ -322,7 +322,8 @@ const GameLoader = {
|
|||||||
'adventure-reader': 'AdventureReader',
|
'adventure-reader': 'AdventureReader',
|
||||||
'chinese-study': 'ChineseStudy',
|
'chinese-study': 'ChineseStudy',
|
||||||
'story-reader': 'StoryReader',
|
'story-reader': 'StoryReader',
|
||||||
'word-storm': 'WordStorm'
|
'word-storm': 'WordStorm',
|
||||||
|
'word-discovery': 'WordDiscovery'
|
||||||
};
|
};
|
||||||
return names[gameType] || gameType;
|
return names[gameType] || gameType;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -97,6 +97,12 @@ const AppNavigation = {
|
|||||||
name: 'Word Storm',
|
name: 'Word Storm',
|
||||||
icon: '🌪️',
|
icon: '🌪️',
|
||||||
description: 'Catch falling words before they hit the ground!'
|
description: 'Catch falling words before they hit the ground!'
|
||||||
|
},
|
||||||
|
'word-discovery': {
|
||||||
|
enabled: true,
|
||||||
|
name: 'Word Discovery',
|
||||||
|
icon: '🔍',
|
||||||
|
description: 'Learn new words with images and interactive practice!'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
@ -259,6 +265,9 @@ const AppNavigation = {
|
|||||||
case 'play':
|
case 'play':
|
||||||
this.showGamePage(game, content);
|
this.showGamePage(game, content);
|
||||||
break;
|
break;
|
||||||
|
case 'settings':
|
||||||
|
this.showSettingsPage();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
this.showHomePage();
|
this.showHomePage();
|
||||||
}
|
}
|
||||||
@ -302,6 +311,22 @@ const AppNavigation = {
|
|||||||
this.updateBreadcrumb();
|
this.updateBreadcrumb();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showSettingsPage() {
|
||||||
|
logSh('⚙️ Displaying settings page', 'INFO');
|
||||||
|
this.hideAllPages();
|
||||||
|
document.getElementById('settings-page').classList.add('active');
|
||||||
|
this.currentPage = 'settings';
|
||||||
|
this.updateBreadcrumb();
|
||||||
|
|
||||||
|
// Initialize settings if SettingsManager is available
|
||||||
|
if (window.SettingsManager) {
|
||||||
|
// Ensure SettingsManager is initialized for this page
|
||||||
|
setTimeout(() => {
|
||||||
|
window.SettingsManager.init();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Display games selection page
|
// Display games selection page
|
||||||
async showGamesPage(contentType) {
|
async showGamesPage(contentType) {
|
||||||
logSh(`🎮 Displaying games selection page for content: ${contentType}`, 'INFO');
|
logSh(`🎮 Displaying games selection page for content: ${contentType}`, 'INFO');
|
||||||
|
|||||||
397
js/core/settings-manager.js
Normal file
397
js/core/settings-manager.js
Normal file
@ -0,0 +1,397 @@
|
|||||||
|
// === 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;
|
||||||
1049
js/games/word-discovery.js
Normal file
1049
js/games/word-discovery.js
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user