${para}
`).join(''); this.pElements = element.querySelectorAll("p"); } this.pElements.forEach((p, index) => { p.classList.add("tts-paragraph"); p.addEventListener("click", () => { if (this.speechPaused) this.toggleSpeech(); this.speakSpecific(index); }); }); document.addEventListener("keydown", (e) => { if (e.code === "ArrowLeft" && this.speechIndex > 0) { this.speakSpecific(this.speechIndex - 1); } else if (e.code === "ArrowRight" && this.speechIndex < this.pElements.length - 1) { this.speakSpecific(this.speechIndex + 1); } else if (e.code === "Space") { e.preventDefault(); this.toggleSpeech(); } }); this.speechIndex = 0; this.speechPaused = false; this.speakNext(); } else { if (this.speechPaused) { this.speechPaused = false; this.speakNext(); toggleBtn.innerText ="⏸"; toggleBtn.style = "background-color: lightcoral"; } else { this.speechPaused = true; speechSynthesis.cancel(); toggleBtn.innerText = "▶"; toggleBtn.style = "background-color: lightgreen"; } } }, speakNext() { if (this.speechIndex >= this.pElements.length || this.speechPaused) return; this.speakSpecific(this.speechIndex); }, speakSpecific(index) { if (index >= this.pElements.length) return; if (this.currentUtterance) { speechSynthesis.cancel(); } this.speechIndex = index; const currentParagraph = this.pElements[this.speechIndex]; this.currentUtterance = new SpeechSynthesisUtterance(currentParagraph.innerText); this.currentUtterance.lang = "zh-TW"; this.currentUtterance.rate = this.speechRate; this.currentUtterance.volume = this.speechVolume; this.pElements.forEach(p => p.classList.remove("active")); currentParagraph.classList.add("active"); currentParagraph.scrollIntoView({ behavior: "smooth", block: "center" }); this.currentUtterance.onend = () => { currentParagraph.classList.remove("active"); this.speechIndex++; this.speakNext(); }; speechSynthesis.speak(this.currentUtterance); } }; const controlPanel = document.createElement("div"); controlPanel.className = "tts-control-panel"; const navigateChapter = (offset) => { const chapter = Number(location.href.match(/\d+(?=[^\d]*$)/)) if (chapter) { location.href = location.href.replace(/\d+(?=[^\d]*$)/, Math.max(1, chapter + offset)); } } const prevChapter = () => { if (typeof ReadParams != "undefined" && ReadParams?.url_previous) location.href = ReadParams.url_previous; const t = document.querySelector(":is(a.prev-chapter, .mlfy_page>a:nth-child(1))"); if (t) { t.click(); return; } navigateChapter(-1); } const nextChapter = () => { if (typeof ReadParams != "undefined" && ReadParams?.url_next) location.href = ReadParams.url_next; const t = document.querySelector(":is(a.next-chapter, .mlfy_page>a:nth-child(5))"); if (t) { t.click(); return; } navigateChapter(1); } const buttons = [ { icon: "⏮", action: () => prevChapter(), tip: "上一章" }, { icon: "▲", action: () => { if (document.sc.speechIndex > 0) { document.sc.speakSpecific(document.sc.speechIndex - 1); } }, tip: "上一句" }, { icon: "⏸", action: (e) => document.sc.toggleSpeech(), tip: "播放/暫停" }, { icon: "▼", action: () => { if (document.sc.speechIndex < document.sc.pElements.length - 1) { document.sc.speakSpecific(document.sc.speechIndex + 1); } }, tip: "下一句" }, { icon: "⏭", action: () => nextChapter(1), tip: "下一章" }, { icon: "♿", sliderAction: (value) => { document.sc.speechRate = value; GM_setValue("speechRate", value); }, min: 1, max: 10, tip: "語速" }, { icon: "🔊", sliderAction: (value) => { document.sc.speechVolume = value / 100; GM_setValue("speechVolume", document.sc.speechVolume); }, min: 0, max: 100, tip: "音量" } ]; buttons.forEach(({ icon, action, sliderAction, min, max, tip }) => { const buttonContainer = document.createElement("div"); buttonContainer.style.position = "relative"; const button = document.createElement("button"); button.className = "tts-button"; button.innerText = icon; button.onclick = action; button.title = tip; if(icon === "⏸") button.style = "background-color: lightcoral"; buttonContainer.appendChild(button); if (sliderAction) { const sliderContainer = document.createElement("div"); sliderContainer.className = "tts-slider-container"; const slider = document.createElement("input"); slider.type = "range"; slider.min = min.toString(); slider.max = max.toString(); slider.value = (icon === "🔊" ? document.sc.speechVolume * 100 : document.sc.speechRate).toString(); slider.oninput = (event) => { sliderValue.innerText = event.target.value; sliderAction(Number(event.target.value)); document.sc.toggleSpeech() document.sc.toggleSpeech() }; const sliderValue = document.createElement("span"); sliderValue.innerText = slider.value; sliderContainer.appendChild(slider); sliderContainer.appendChild(sliderValue); buttonContainer.addEventListener("mouseenter", () => sliderContainer.style.visibility = "visible"); buttonContainer.addEventListener("mouseleave", () => setTimeout(() => { if (!sliderContainer.matches(":hover")) { sliderContainer.style.visibility = "hidden"; } }, 300)); buttonContainer.appendChild(sliderContainer); } controlPanel.appendChild(buttonContainer); }); toggleBtn = controlPanel.querySelector("[title='播放/暫停']"); document.body.appendChild(controlPanel); const selector = location.host.match(/\w+\.\w+$/); delay(100) .then(() => waitElementLoad(siteMap[selector], 1, -1, 200)) .then(() => { speechSynthesis.cancel(); document.sc.toggleSpeech(siteMap[selector]); })