diff --git a/alacarte-novel-website/reader.html b/alacarte-novel-website/reader.html index b5275bd..9d03108 100644 --- a/alacarte-novel-website/reader.html +++ b/alacarte-novel-website/reader.html @@ -312,6 +312,149 @@ #tocBtn:hover { background: var(--btn-hover); transform: translateY(-2px); } + /* TTS弹出面板 */ + .tts-modal { + display: none; + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.8); + z-index: 2001; + justify-content: center; + align-items: center; + padding: 20px; + } + + .tts-modal.active { display: flex; } + + .tts-panel { + background: rgba(30,30,50,0.95); + backdrop-filter: blur(10px); + border-radius: 16px; + border: 1px solid var(--border-color); + width: 100%; + max-width: 400px; + padding: 24px; + box-shadow: 0 10px 40px rgba(0,0,0,0.3); + } + + [data-theme="light"] .tts-panel { + background: rgba(255,255,255,0.95); + } + + .tts-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 1px solid var(--border-color); + } + + .tts-title { + font-size: 18px; + font-weight: 600; + color: var(--text-primary); + font-family: 'Noto Sans SC', sans-serif; + } + + .tts-close { + width: 32px; height: 32px; + border-radius: 50%; + background: var(--btn-bg); + border: 1px solid var(--border-color); + color: var(--text-primary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + transition: all 0.3s ease; + } + + .tts-close:hover { background: var(--btn-hover); } + + .tts-controls { + display: flex; + justify-content: center; + gap: 16px; + margin-bottom: 20px; + } + + .tts-btn { + width: 56px; height: 56px; + border-radius: 50%; + background: var(--btn-bg); + border: 2px solid var(--border-color); + color: var(--text-primary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + transition: all 0.3s ease; + font-family: 'Noto Sans SC', sans-serif; + } + + .tts-btn:hover { background: var(--btn-hover); transform: scale(1.1); } + .tts-btn.active { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-color: #667eea; + transform: scale(1.1); + } + + .tts-progress { margin-bottom: 20px; } + + .tts-progress-bar { + height: 6px; + background: var(--btn-bg); + border-radius: 3px; + overflow: hidden; + margin-bottom: 8px; + } + + .tts-progress-fill { + height: 100%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + width: 0%; + transition: width 0.1s ease; + } + + .tts-time { + font-size: 13px; + color: var(--text-secondary); + font-family: 'Noto Sans SC', sans-serif; + text-align: center; + } + + .tts-speed { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + } + + .tts-speed-label { + font-size: 14px; + color: var(--text-secondary); + font-family: 'Noto Sans SC', sans-serif; + } + + .tts-speed-select { + padding: 8px 16px; + background: var(--btn-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + color: var(--text-primary); + font-size: 14px; + cursor: pointer; + font-family: 'Noto Sans SC', sans-serif; + } + + .nav-btn.active-tts { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-color: #667eea; + } + .sidebar-title { font-size: 14px; color: var(--text-secondary); @@ -411,6 +554,7 @@
上一章 + 下一章
@@ -426,6 +570,38 @@ + +
+
+
+ 🔊 语音朗读 + +
+
+ + + +
+
+
+
+
+
0 / 0 句
+
+
+ 朗读速度: + +
+
+
+
@@ -668,6 +844,150 @@ e.stopPropagation(); }, { passive: true }); + // ========== TTS语音朗读功能 ========== + const ttsModal = document.getElementById('ttsModal'); + const ttsBtn = document.getElementById('ttsBtn'); + const ttsClose = document.getElementById('ttsClose'); + let ttsSynth = window.speechSynthesis; + let ttsUtterance = null; + let ttsSentences = []; + let ttsCurrentIndex = 0; + let ttsIsPlaying = false; + let ttsIsPaused = false; + let ttsSpeed = 1; + + // 打开TTS面板 + ttsBtn.addEventListener('click', () => { + initTTS(); + ttsModal.classList.add('active'); + }); + + // 关闭TTS面板 + ttsClose.addEventListener('click', () => { + ttsModal.classList.remove('active'); + }); + + // 点击遮罩关闭 + ttsModal.addEventListener('click', (e) => { + if (e.target === ttsModal) { + ttsModal.classList.remove('active'); + } + }); + + // 初始化TTS + function initTTS() { + const contentEl = document.getElementById('chapterContent'); + if (!contentEl) return; + + const text = contentEl.innerText || contentEl.textContent; + ttsSentences = text.match(/[^。!?\n]+[。!?\n]+|[^。!?\n]+$/g) || [text]; + ttsSentences = ttsSentences.filter(s => s.trim().length > 0); + updateTTSProgress(); + } + + // 更新进度 + function updateTTSProgress() { + const total = ttsSentences.length; + const current = ttsCurrentIndex; + document.getElementById('ttsTime').textContent = `${current} / ${total} 句`; + const percent = total > 0 ? (current / total * 100) : 0; + document.getElementById('ttsProgressFill').style.width = `${percent}%`; + } + + // 播放当前句子 + function playCurrentSentence() { + if (ttsCurrentIndex >= ttsSentences.length) { + stopTTS(); + return; + } + + const text = ttsSentences[ttsCurrentIndex].trim(); + ttsUtterance = new SpeechSynthesisUtterance(text); + ttsUtterance.lang = 'zh-CN'; + ttsUtterance.rate = ttsSpeed; + + const voices = ttsSynth.getVoices(); + const zhVoice = voices.find(v => v.lang.includes('zh') || v.lang.includes('CN')); + if (zhVoice) ttsUtterance.voice = zhVoice; + + ttsUtterance.onend = () => { + if (ttsIsPlaying && !ttsIsPaused) { + ttsCurrentIndex++; + updateTTSProgress(); + playCurrentSentence(); + } + }; + + ttsUtterance.onerror = (e) => { + console.error('TTS error:', e); + if (ttsIsPlaying) { + ttsCurrentIndex++; + updateTTSProgress(); + playCurrentSentence(); + } + }; + + ttsSynth.speak(ttsUtterance); + } + + // 播放 + function playTTS() { + if (ttsSentences.length === 0) initTTS(); + + if (ttsIsPaused) { + ttsSynth.resume(); + ttsIsPaused = false; + } else { + ttsIsPlaying = true; + playCurrentSentence(); + } + + document.getElementById('ttsPlay').classList.add('active'); + document.getElementById('ttsPause').classList.remove('active'); + ttsBtn.classList.add('active-tts'); + } + + // 暂停 + function pauseTTS() { + if (ttsIsPlaying) { + ttsSynth.pause(); + ttsIsPaused = true; + document.getElementById('ttsPlay').classList.remove('active'); + document.getElementById('ttsPause').classList.add('active'); + ttsBtn.classList.remove('active-tts'); + } + } + + // 停止 + function stopTTS() { + ttsSynth.cancel(); + ttsIsPlaying = false; + ttsIsPaused = false; + ttsCurrentIndex = 0; + updateTTSProgress(); + document.getElementById('ttsPlay').classList.remove('active'); + document.getElementById('ttsPause').classList.remove('active'); + ttsBtn.classList.remove('active-tts'); + } + + // 设置速度 + function setTTSSpeed(speed) { + ttsSpeed = parseFloat(speed); + if (ttsIsPlaying && !ttsIsPaused) { + ttsSynth.cancel(); + playCurrentSentence(); + } + } + + // 绑定TTS事件 + document.getElementById('ttsPlay').addEventListener('click', playTTS); + document.getElementById('ttsPause').addEventListener('click', pauseTTS); + document.getElementById('ttsStop').addEventListener('click', stopTTS); + document.getElementById('ttsSpeed').addEventListener('change', (e) => setTTSSpeed(e.target.value)); + + // 页面离开时停止 + window.addEventListener('beforeunload', stopTTS); + // 初始化 - 等待app.js加载章节数据 document.addEventListener('DOMContentLoaded', async function() { // 等待章节数据加载完成