107 lines
3.4 KiB
JavaScript
107 lines
3.4 KiB
JavaScript
import { state } from '../core/state.js';
|
|
import { logTts, prefetchAudio, speak, stopAllAudio } from '../services/tts.js';
|
|
|
|
let autoPlayActive = false;
|
|
let autoPlayTimer = null;
|
|
|
|
export function toggleAutoPlay() {
|
|
if (autoPlayActive) stopAutoPlay();
|
|
else startAutoPlay();
|
|
}
|
|
|
|
export function startAutoPlay() {
|
|
const rows = document.querySelectorAll('.word-table tbody tr[data-word-id]');
|
|
const wordIds = Array.from(rows).map(row => Number.parseInt(row.dataset.wordId, 10));
|
|
if (wordIds.length === 0) return;
|
|
|
|
autoPlayActive = true;
|
|
updateAutoPlayBtn();
|
|
prefetchWordListAudio(wordIds);
|
|
playWordSequence(wordIds, 0);
|
|
}
|
|
|
|
export function prefetchWordListAudio(wordIds) {
|
|
const texts = wordIds
|
|
.map(id => state.words.find(word => word.id === id)?.english)
|
|
.filter(Boolean);
|
|
if (!texts.length) return;
|
|
void prefetchAudio(texts, 0.92, { allowCloud: true })
|
|
.catch(error => logTts('word list audio prefetch failed', error));
|
|
}
|
|
|
|
export function playWordSequence(wordIds, index) {
|
|
if (!autoPlayActive || index >= wordIds.length) {
|
|
stopAutoPlay();
|
|
return;
|
|
}
|
|
|
|
const word = state.words.find(item => item.id === wordIds[index]);
|
|
if (!word) {
|
|
playWordSequence(wordIds, index + 1);
|
|
return;
|
|
}
|
|
|
|
document.querySelectorAll('.word-table tbody tr').forEach(row => row.classList.remove('is-playing'));
|
|
const row = document.querySelector(`tr[data-word-id="${word.id}"]`);
|
|
if (row) {
|
|
row.classList.add('is-playing');
|
|
let shouldScroll = true;
|
|
if (state.autoPlayScrollMode === 'visible') {
|
|
const rect = row.getBoundingClientRect();
|
|
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
const header = document.getElementById('mobile-header');
|
|
const topBound = header ? header.getBoundingClientRect().height : 0;
|
|
shouldScroll = rect.top < topBound || rect.bottom > viewportHeight;
|
|
}
|
|
if (shouldScroll) {
|
|
row.scrollIntoView({
|
|
behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth',
|
|
block: 'center',
|
|
inline: 'nearest'
|
|
});
|
|
}
|
|
}
|
|
|
|
const repeatCount = Math.max(1, state.autoPlayRepeat || 1);
|
|
const repeatDelay = state.autoPlayRepeatDelay || 1000;
|
|
const wordInterval = state.autoPlayInterval || 2500;
|
|
let spoken = 0;
|
|
|
|
async function playCurrentWord() {
|
|
if (!autoPlayActive) return;
|
|
await speak(word.english);
|
|
if (!autoPlayActive) return;
|
|
spoken++;
|
|
if (spoken < repeatCount) {
|
|
autoPlayTimer = setTimeout(playCurrentWord, repeatDelay);
|
|
} else {
|
|
autoPlayTimer = setTimeout(() => playWordSequence(wordIds, index + 1), wordInterval);
|
|
}
|
|
}
|
|
|
|
void playCurrentWord();
|
|
}
|
|
|
|
export function stopAutoPlay() {
|
|
autoPlayActive = false;
|
|
if (autoPlayTimer) {
|
|
clearTimeout(autoPlayTimer);
|
|
autoPlayTimer = null;
|
|
}
|
|
stopAllAudio();
|
|
document.querySelectorAll('.word-table tbody tr').forEach(row => row.classList.remove('is-playing'));
|
|
updateAutoPlayBtn();
|
|
}
|
|
|
|
export function updateAutoPlayBtn() {
|
|
const button = document.getElementById('auto-play-btn');
|
|
if (!button) return;
|
|
if (autoPlayActive) {
|
|
button.innerHTML = '<i class="fas fa-stop"></i> 停止播放';
|
|
button.className = 'btn btn-error';
|
|
} else {
|
|
button.innerHTML = '<i class="fas fa-play"></i> 自动播放';
|
|
button.className = 'btn btn-secondary';
|
|
}
|
|
}
|