// ==UserScript== // @name 天天看小說-語音朗讀 // @namespace Anong0u0 // @version 2025-02-26 // @description 小說朗讀腳本,支援朗讀控制與句子點擊切換 // @author Anong0u0 // @match https://*.wa01.com/novel/pagea/*.html // @match https://*.ttkan.co/novel/pagea/*.html // @icon https://www.google.com/s2/favicons?sz=64&domain=tw.ttkan.co // @grant GM_setValue // @grant GM_getValue // @license Beerware // @downloadURL none // ==/UserScript== const style = document.createElement("style"); style.textContent = ` .tts-paragraph { cursor: pointer; } .tts-paragraph:hover { text-decoration: underline; } .tts-paragraph.active { color: red; } .tts-control-panel { position: fixed; right: 10px; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 10px; z-index: 1000; } .tts-button { width: 50px; height: 50px; font-size: 24px; border: none; border-radius: 10px; cursor: pointer; } .tts-slider-container { position: absolute; right: 60px; top: 50%; transform: translateY(-50%); padding: 5px; background: rgba(0, 0, 0, 0.7); border-radius: 5px; display: flex; align-items: center; gap: 5px; visibility: hidden; pointer-events: auto; } .tts-slider-container span { color: white; } `; document.body.append(style); document.sc = { speechIndex: 0, speechPaused: false, currentUtterance: null, pElements: [], speechRate: GM_getValue("speechRate", 5), speechVolume: GM_getValue("speechVolume", 0.25), toggleSpeech(selector) { if (!this.pElements.length) { const element = document.querySelector(selector); if (!element) { console.error("Element not found"); return; } const text = element.innerText; const paragraphs = text.split(/\n+/); element.innerHTML = paragraphs.map(para => `
${para}
`).join(''); this.pElements = element.querySelectorAll(".tts-paragraph"); this.pElements.forEach((p, index) => { p.addEventListener("click", () => this.speakSpecific(index)); }); this.speechIndex = 0; this.speechPaused = false; this.speakNext(); } else { if (this.speechPaused) { this.speechPaused = false; this.speakNext(); } else { this.speechPaused = true; speechSynthesis.cancel(); } } }, 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) => { let chapter = Number(window.location.href.match(/\d+(?=\.html$)/)) if (chapter) { chapter = Math.max(1, chapter + offset); const newUrl = window.location.href.replace(/\d+(?=\.html$)/, chapter); window.location.href = newUrl; } } const buttons = [ { icon: "⏮", action: () => navigateChapter(-1) }, { icon: "⏸", action: (e) => { document.sc.toggleSpeech(".content") e.target.innerText = e.target.innerText === "⏸" ? "▶" : "⏸" } }, { icon: "⏭", action: () => navigateChapter(1) }, { icon: "♿", sliderAction: (value) => { document.sc.speechRate = value; GM_setValue("speechRate", value); }, min: 1, max: 10 }, { icon: "🔊", sliderAction: (value) => { document.sc.speechVolume = value / 100; GM_setValue("speechVolume", document.sc.speechVolume); }, min: 0, max: 100 } ]; buttons.forEach(({ icon, action, sliderAction, min, max }) => { const buttonContainer = document.createElement("div"); buttonContainer.style.position = "relative"; const button = document.createElement("button"); button.className = "tts-button"; button.innerText = icon; button.onclick = action; 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); }); document.body.appendChild(controlPanel); setTimeout(() => { speechSynthesis.cancel(); document.sc.toggleSpeech(".content"); }, 200);