Spaces:
Running
Running
Update index.html
Browse files- index.html +85 -13
index.html
CHANGED
|
@@ -10,7 +10,7 @@
|
|
| 10 |
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.2/dist/confetti.browser.min.js"></script>
|
| 11 |
<!-- 載入 QR Code 產生器 -->
|
| 12 |
<script src="https://cdn.jsdelivr.net/npm/qrcode-generator/qrcode.js"></script>
|
| 13 |
-
<!--
|
| 14 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js"></script>
|
| 15 |
<style>
|
| 16 |
/* 定義閃卡的翻轉效果 */
|
|
@@ -444,6 +444,7 @@
|
|
| 444 |
|
| 445 |
<audio id="correct-sound" src="correct.mp3" preload="auto"></audio>
|
| 446 |
<audio id="wrong-sound" src="wrong.mp3" preload="auto"></audio>
|
|
|
|
| 447 |
|
| 448 |
<footer class="fixed bottom-4 right-4 text-xs text-gray-500 text-right z-50">
|
| 449 |
<p>遊戲設計者:新竹縣精華國中藍星宇</p>
|
|
@@ -562,6 +563,7 @@
|
|
| 562 |
// 音效元素
|
| 563 |
const correctSound = document.getElementById('correct-sound');
|
| 564 |
const wrongSound = document.getElementById('wrong-sound');
|
|
|
|
| 565 |
|
| 566 |
|
| 567 |
// 應用程式狀態
|
|
@@ -770,7 +772,7 @@
|
|
| 770 |
frontText = '請聽發音';
|
| 771 |
backText = card.english;
|
| 772 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.remove('hidden'));
|
| 773 |
-
speakWord(card.english
|
| 774 |
answerInput.placeholder = "請輸入英文答案...";
|
| 775 |
break;
|
| 776 |
}
|
|
@@ -797,7 +799,7 @@
|
|
| 797 |
case 'listen':
|
| 798 |
frontText = '請聽發音'; backText = card.english;
|
| 799 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.remove('hidden'));
|
| 800 |
-
speakWord(card.english
|
| 801 |
answerInput.placeholder = "請輸入英文答案...";
|
| 802 |
break;
|
| 803 |
}
|
|
@@ -1096,9 +1098,9 @@
|
|
| 1096 |
confetti({ particleCount: 150, spread: 70, origin: { y: 0.6 } });
|
| 1097 |
};
|
| 1098 |
|
| 1099 |
-
const
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
const wordToSpeak = word.split('(')[0].trim();
|
| 1103 |
const utterance = new SpeechSynthesisUtterance(wordToSpeak);
|
| 1104 |
utterance.lang = 'en-US';
|
|
@@ -1106,6 +1108,70 @@
|
|
| 1106 |
window.speechSynthesis.speak(utterance);
|
| 1107 |
}
|
| 1108 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1109 |
|
| 1110 |
const getCurrentWordToSpeak = () => {
|
| 1111 |
if (currentMode === 'speed' && currentSpeedCard) return currentSpeedCard.english;
|
|
@@ -1234,8 +1300,18 @@
|
|
| 1234 |
prevBtn.addEventListener('click', () => { if (currentCardIndex > 0) { currentCardIndex--; displayCard(); }});
|
| 1235 |
nextBtn.addEventListener('click', () => { if (currentCardIndex < wordsForCurrentMode.length - 1) { currentCardIndex++; displayCard(); }});
|
| 1236 |
quizForm.addEventListener('submit', (e) => { e.preventDefault(); checkAnswer(); });
|
| 1237 |
-
|
| 1238 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1239 |
hintBtn.addEventListener('click', () => {
|
| 1240 |
const card = currentMode === 'speed' ? currentSpeedCard : quizQueue[0];
|
| 1241 |
if (!card) return;
|
|
@@ -1310,7 +1386,6 @@
|
|
| 1310 |
generateLinkBtn.disabled = true;
|
| 1311 |
|
| 1312 |
try {
|
| 1313 |
-
// 【已修改】使用 pako 壓縮資料
|
| 1314 |
const wordsString = JSON.stringify(words);
|
| 1315 |
const compressedData = pako.deflate(wordsString);
|
| 1316 |
const base64Words = btoa(String.fromCharCode.apply(null, compressedData));
|
|
@@ -1396,21 +1471,19 @@
|
|
| 1396 |
|
| 1397 |
confirmDeleteBtn.addEventListener('click', () => {
|
| 1398 |
if (indexToDelete > -1) {
|
| 1399 |
-
// 【已修復】建立一個局部常數來保存索引,避免在 setTimeout 執行前被重置
|
| 1400 |
const indexToRemove = indexToDelete;
|
| 1401 |
|
| 1402 |
const itemElement = wordListContainer.querySelector(`.list-item button[data-index="${indexToRemove}"]`)?.closest('.list-item');
|
| 1403 |
if (itemElement) itemElement.classList.add('removing');
|
| 1404 |
|
| 1405 |
setTimeout(() => {
|
| 1406 |
-
// 使用局部常數來執行刪除操作
|
| 1407 |
words.splice(indexToRemove, 1);
|
| 1408 |
saveWordsToStorage();
|
| 1409 |
renderWordList();
|
| 1410 |
}, 300);
|
| 1411 |
}
|
| 1412 |
confirmDeleteModal.classList.add('hidden');
|
| 1413 |
-
indexToDelete = -1;
|
| 1414 |
});
|
| 1415 |
|
| 1416 |
randomQuestionsCheckbox.addEventListener('change', () => {
|
|
@@ -1442,7 +1515,6 @@
|
|
| 1442 |
|
| 1443 |
if (sharedData) {
|
| 1444 |
try {
|
| 1445 |
-
// 【已修改】使用 pako 解壓縮資料
|
| 1446 |
const binaryString = atob(sharedData);
|
| 1447 |
const compressedData = new Uint8Array(binaryString.length).map((_, i) => binaryString.charCodeAt(i));
|
| 1448 |
const decodedWordsString = pako.inflate(compressedData, { to: 'string' });
|
|
|
|
| 10 |
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.2/dist/confetti.browser.min.js"></script>
|
| 11 |
<!-- 載入 QR Code 產生器 -->
|
| 12 |
<script src="https://cdn.jsdelivr.net/npm/qrcode-generator/qrcode.js"></script>
|
| 13 |
+
<!-- 載入 Pako 壓縮函式庫 -->
|
| 14 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js"></script>
|
| 15 |
<style>
|
| 16 |
/* 定義閃卡的翻轉效果 */
|
|
|
|
| 444 |
|
| 445 |
<audio id="correct-sound" src="correct.mp3" preload="auto"></audio>
|
| 446 |
<audio id="wrong-sound" src="wrong.mp3" preload="auto"></audio>
|
| 447 |
+
<audio id="api-audio-player" preload="auto"></audio>
|
| 448 |
|
| 449 |
<footer class="fixed bottom-4 right-4 text-xs text-gray-500 text-right z-50">
|
| 450 |
<p>遊戲設計者:新竹縣精華國中藍星宇</p>
|
|
|
|
| 563 |
// 音效元素
|
| 564 |
const correctSound = document.getElementById('correct-sound');
|
| 565 |
const wrongSound = document.getElementById('wrong-sound');
|
| 566 |
+
const apiAudioPlayer = document.getElementById('api-audio-player');
|
| 567 |
|
| 568 |
|
| 569 |
// 應用程式狀態
|
|
|
|
| 772 |
frontText = '請聽發音';
|
| 773 |
backText = card.english;
|
| 774 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.remove('hidden'));
|
| 775 |
+
speakWord(card.english);
|
| 776 |
answerInput.placeholder = "請輸入英文答案...";
|
| 777 |
break;
|
| 778 |
}
|
|
|
|
| 799 |
case 'listen':
|
| 800 |
frontText = '請聽發音'; backText = card.english;
|
| 801 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.remove('hidden'));
|
| 802 |
+
speakWord(card.english);
|
| 803 |
answerInput.placeholder = "請輸入英文答案...";
|
| 804 |
break;
|
| 805 |
}
|
|
|
|
| 1098 |
confetti({ particleCount: 150, spread: 70, origin: { y: 0.6 } });
|
| 1099 |
};
|
| 1100 |
|
| 1101 |
+
const speakWithBrowserTTS = (word, rate = 1) => {
|
| 1102 |
+
if ('speechSynthesis' in window) {
|
| 1103 |
+
window.speechSynthesis.cancel();
|
| 1104 |
const wordToSpeak = word.split('(')[0].trim();
|
| 1105 |
const utterance = new SpeechSynthesisUtterance(wordToSpeak);
|
| 1106 |
utterance.lang = 'en-US';
|
|
|
|
| 1108 |
window.speechSynthesis.speak(utterance);
|
| 1109 |
}
|
| 1110 |
};
|
| 1111 |
+
|
| 1112 |
+
const speakWord = async (word) => {
|
| 1113 |
+
speakBtn.disabled = true;
|
| 1114 |
+
speakSlowBtn.disabled = true;
|
| 1115 |
+
|
| 1116 |
+
const wordToSpeak = word.split('(')[0].trim();
|
| 1117 |
+
if (!wordToSpeak) {
|
| 1118 |
+
speakBtn.disabled = false;
|
| 1119 |
+
speakSlowBtn.disabled = false;
|
| 1120 |
+
return;
|
| 1121 |
+
}
|
| 1122 |
+
|
| 1123 |
+
const cleanedWord = wordToSpeak.replace(/[^a-zA-Z\s-]/g, '');
|
| 1124 |
+
|
| 1125 |
+
// 【已修改】智慧判斷:如果是片語,直接用瀏覽器語音;如果是單字,才用 API
|
| 1126 |
+
if (cleanedWord.includes(' ')) {
|
| 1127 |
+
speakWithBrowserTTS(cleanedWord, 0.75);
|
| 1128 |
+
speakBtn.disabled = false;
|
| 1129 |
+
speakSlowBtn.disabled = false;
|
| 1130 |
+
return;
|
| 1131 |
+
}
|
| 1132 |
+
|
| 1133 |
+
try {
|
| 1134 |
+
const response = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${cleanedWord}`);
|
| 1135 |
+
if (!response.ok) throw new Error('API request failed');
|
| 1136 |
+
const data = await response.json();
|
| 1137 |
+
|
| 1138 |
+
let audioUrl = '';
|
| 1139 |
+
if (data && data.length > 0) {
|
| 1140 |
+
for (const entry of data) {
|
| 1141 |
+
for (const phonetic of entry.phonetics || []) {
|
| 1142 |
+
if (phonetic.audio) {
|
| 1143 |
+
if (phonetic.audio.includes('-us.mp3')) {
|
| 1144 |
+
audioUrl = phonetic.audio;
|
| 1145 |
+
break;
|
| 1146 |
+
}
|
| 1147 |
+
if (!audioUrl) audioUrl = phonetic.audio;
|
| 1148 |
+
}
|
| 1149 |
+
}
|
| 1150 |
+
if (audioUrl) break;
|
| 1151 |
+
}
|
| 1152 |
+
}
|
| 1153 |
+
|
| 1154 |
+
if (audioUrl) {
|
| 1155 |
+
apiAudioPlayer.src = audioUrl;
|
| 1156 |
+
apiAudioPlayer.play().catch(error => {
|
| 1157 |
+
console.error("Audio playback error:", error);
|
| 1158 |
+
speakWithBrowserTTS(cleanedWord, 0.75);
|
| 1159 |
+
}).finally(() => {
|
| 1160 |
+
speakBtn.disabled = false;
|
| 1161 |
+
speakSlowBtn.disabled = false;
|
| 1162 |
+
});
|
| 1163 |
+
} else {
|
| 1164 |
+
speakWithBrowserTTS(cleanedWord, 0.75);
|
| 1165 |
+
speakBtn.disabled = false;
|
| 1166 |
+
speakSlowBtn.disabled = false;
|
| 1167 |
+
}
|
| 1168 |
+
} catch (error) {
|
| 1169 |
+
console.error("Dictionary API error:", error);
|
| 1170 |
+
speakWithBrowserTTS(cleanedWord, 0.75);
|
| 1171 |
+
speakBtn.disabled = false;
|
| 1172 |
+
speakSlowBtn.disabled = false;
|
| 1173 |
+
}
|
| 1174 |
+
};
|
| 1175 |
|
| 1176 |
const getCurrentWordToSpeak = () => {
|
| 1177 |
if (currentMode === 'speed' && currentSpeedCard) return currentSpeedCard.english;
|
|
|
|
| 1300 |
prevBtn.addEventListener('click', () => { if (currentCardIndex > 0) { currentCardIndex--; displayCard(); }});
|
| 1301 |
nextBtn.addEventListener('click', () => { if (currentCardIndex < wordsForCurrentMode.length - 1) { currentCardIndex++; displayCard(); }});
|
| 1302 |
quizForm.addEventListener('submit', (e) => { e.preventDefault(); checkAnswer(); });
|
| 1303 |
+
|
| 1304 |
+
speakBtn.addEventListener('click', (e) => {
|
| 1305 |
+
e.stopPropagation();
|
| 1306 |
+
const word = getCurrentWordToSpeak();
|
| 1307 |
+
if(word) speakWord(word);
|
| 1308 |
+
});
|
| 1309 |
+
speakSlowBtn.addEventListener('click', (e) => {
|
| 1310 |
+
e.stopPropagation();
|
| 1311 |
+
const word = getCurrentWordToSpeak();
|
| 1312 |
+
if(word) speakWithBrowserTTS(word, 0.2);
|
| 1313 |
+
});
|
| 1314 |
+
|
| 1315 |
hintBtn.addEventListener('click', () => {
|
| 1316 |
const card = currentMode === 'speed' ? currentSpeedCard : quizQueue[0];
|
| 1317 |
if (!card) return;
|
|
|
|
| 1386 |
generateLinkBtn.disabled = true;
|
| 1387 |
|
| 1388 |
try {
|
|
|
|
| 1389 |
const wordsString = JSON.stringify(words);
|
| 1390 |
const compressedData = pako.deflate(wordsString);
|
| 1391 |
const base64Words = btoa(String.fromCharCode.apply(null, compressedData));
|
|
|
|
| 1471 |
|
| 1472 |
confirmDeleteBtn.addEventListener('click', () => {
|
| 1473 |
if (indexToDelete > -1) {
|
|
|
|
| 1474 |
const indexToRemove = indexToDelete;
|
| 1475 |
|
| 1476 |
const itemElement = wordListContainer.querySelector(`.list-item button[data-index="${indexToRemove}"]`)?.closest('.list-item');
|
| 1477 |
if (itemElement) itemElement.classList.add('removing');
|
| 1478 |
|
| 1479 |
setTimeout(() => {
|
|
|
|
| 1480 |
words.splice(indexToRemove, 1);
|
| 1481 |
saveWordsToStorage();
|
| 1482 |
renderWordList();
|
| 1483 |
}, 300);
|
| 1484 |
}
|
| 1485 |
confirmDeleteModal.classList.add('hidden');
|
| 1486 |
+
indexToDelete = -1;
|
| 1487 |
});
|
| 1488 |
|
| 1489 |
randomQuestionsCheckbox.addEventListener('change', () => {
|
|
|
|
| 1515 |
|
| 1516 |
if (sharedData) {
|
| 1517 |
try {
|
|
|
|
| 1518 |
const binaryString = atob(sharedData);
|
| 1519 |
const compressedData = new Uint8Array(binaryString.length).map((_, i) => binaryString.charCodeAt(i));
|
| 1520 |
const decodedWordsString = pako.inflate(compressedData, { to: 'string' });
|