// ==UserScript==
// @name 小说朗读
// @namespace http://tampermonkey.net/
// @version 1.1
// @description 自用小说朗读
// @author FHT
// @icon https://www.google.com/s2/favicons?sz=64&domain=csdn.net
// @grant GM_addStyle
// @grant unsafeWindow
// @license MIT
// @require https://code.jquery.com/jquery-3.6.3.min.js
// @match https://www.scjld.net/*/*.html
// @match https://www.douyinxs.com/bqg/*/*.html
// @downloadURL none
// ==/UserScript==
(function() {
'use strict';
// Your code here...
let css = `
*{
margin: 0;
padding: 0;
}
#FHT_main {
width: 100vw;
height: 100vh;
display: flex;
overflow: hidden;
background: url(http://qidian.gtimg.com/qd/images/read.qidian.com/theme/theme_1_bg_2x.0.3.png);
}
#FHT_catalogue {
width: 250px;
height: 100%;
overflow: hidden;
background: rgb(70,70,70);
color: white;
font-size: 14px;
cursor: pointer;
padding :8px;
}
#FHT_name {
text-align: center;
font-size: 24px;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding :10px 10px 15px ;
border-bottom: 2px solid black;
}
#FHT_bottem {
display: flex;
justify-content:space-evenly;
padding :10px;
border-bottom: 1px solid black;
}
#FHT_chapters{
overflow: auto;
height: 85%;
}
#FHT_chapters li {
padding :10px;
border-bottom: 1px solid black;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#FHT_contents {
overflow: auto;
width: 100%;
height: 100%;
min-width: 60%;
font-size: 26px;
line-height: 50px;
white-space:pre-line;
}
.FHT_Tit {
text-align: center;
margin: 10px 80px;
padding :10px;
border-bottom: solid 1px rgb(134, 124, 124);
font-size: 32px;
}
.FHT_content {
margin: 0 200px;
letter-spacing: 4px;
}
#FHT_read {
position: fixed;
top: 10px;
right: 30px;
}
.li_activ{
background: #000;
}
.activ {
color: red;
}
.remove {
display: none;
}
a{
color: white;
}
a:visited{
color: white;
}`
GM_addStyle(css)
//自定义规则
const DIYRule = [{
url: '域名',
nextSelector: '下一页(jq选择器)',
prevSelector: '上一页',
indexSelector: '目录',
titleSelector: '章节名',
bookTitleSelector: '书名',
contentSelector: '正文',
contentReplace: '/去广告/'
}]
//通用规则
let Rule = {
nextSelectors: ["a:contains('下一页')", "a:contains('下一章')", "a:contains('下一节')", "a:contains('下页')"],
prevSelectors: ["a:contains('书首页')", "a:contains('上一页')", "a:contains('上一章')", "a:contains('上一节')", "a:contains('上页')"],
indexSelectors: ["a:contains('返回书目')", "a:contains('章节目录')", "a:contains('章节列表')", "a:contains('最新章节')", "a:contains('回目录')", "a:contains('回书目')", "a:contains('目 录')", "a:contains('目录')"],
titleSelectors: ['h1'],
bookTitleSelectors: ['.con_top > a:last-of-type'],
contentSelectors: ["#pagecontent", "#contentbox", "#bmsy_content", "#bookpartinfo", "#htmlContent", "#text_area", "#chapter_content", "#chapterContent", "#chaptercontent", "#partbody", "#BookContent", "#read-content", "#article_content", "#BookTextRead", "#booktext", "#book_text", "#BookText", "#BookTextt", "#readtext", "#readcon", "#read", "#TextContent", "#txtContent", "#text_c", "#txt_td", "#TXT", "#txt", "#zjneirong", ".novel_content", ".readmain_inner", ".noveltext", ".booktext", ".yd_text2", "#contentTxt", "#oldtext", "#a_content", "#contents", "#content2", "#contentts", "#content1", "#content", ".content"],
contentReplace: [
['/需要替换的字符串/g', '新字符串'],
[/
/g, '
'],
[/阅读最新章节请下载爱阅小说[\s\S]+/g, '']
]
}
let body = $('body')[0]
let BookData = { chapters: [], }
const utterThis = new SpeechSynthesisUtterance();
let delBtn = false
let focusText = 0
let state = 0 //网络获取的状态,防抖
let PageIndex = 0
const synth = window.speechSynthesis;
synth.cancel() //初始化语音
let Voiceslist
initialization()
//语音朗读状态函数
utterThis.onstart = () => {
FHT_del.classList.remove("remove");
voiceSelect.classList.add("remove");
FHT_play.classList.add("remove");
}
utterThis.onend = () => {
focusText = null
$(`#FHT_Chapter_${BookData.readIndex}>.FHT_content`)[0].innerHTML = $(`#FHT_Chapter_${BookData.readIndex}>.FHT_content`)[0].innerText
if (delBtn) {
FHT_play.classList.remove("remove");
voiceSelect.classList.remove("remove");
FHT_del.classList.add("remove");
delBtn = false
return
}
else if (BookData.readIndex < BookData.chapters.length - 1) {
BookData.readIndex += 1
speaking($(`#FHT_Chapter_${BookData.readIndex}>.FHT_content`)[0].innerText)
window.history.replaceState('', '', BookData.chapters[BookData.readIndex].next)
FHT_contents.scrollTop = $(`#FHT_Chapter_${BookData.readIndex}`)[0].offsetTop
}
else {
getAjax(BookData.chapters[BookData.readIndex].next).then((res) => {
getData(/
]*>([\s\S]*)<\/body>/.exec(res)[1])
BookData.readIndex += 1
speaking($(`#FHT_Chapter_${BookData.readIndex}>.FHT_content`)[0].innerText)
FHT_contents.scrollTop = $(`#FHT_Chapter_${BookData.readIndex}`)[0].offsetTop
})
}
};
utterThis.onboundary = (event) => {
let div = $(`#FHT_Chapter_${BookData.readIndex}>.FHT_content`)[0]
let str
let read_text = []
if (focusText) {
let t1 = div.innerText.split(focusText)[0]
let t2 = focusText + div.innerText.split(focusText)[1]
str = t2.substr(event.charIndex, event.charLength)
read_text[0] = t2.slice(0, event.charIndex)
read_text[1] = t2.slice(event.charIndex)
div.innerHTML = t1 + read_text[0] + read_text[1].replace(str, "" + str + "")
} else {
str = div.innerText.substr(event.charIndex, event.charLength)
read_text[0] = div.innerText.slice(0, event.charIndex)
read_text[1] = div.innerText.slice(event.charIndex)
div.innerHTML = read_text[0] + read_text[1].replace(str, "" + str + "")
}
if ($('.activ')[0].offsetTop > FHT_contents.scrollTop + FHT_contents.clientHeight) {
FHT_contents.scrollTop = $('.activ')[0].offsetTop - 50
}
}
//创建章节page
function addPage() {
let i = BookData.chapters.length - 1
let data = BookData.chapters[i]
let url
addTag(FHT_chapters, { name: 'li', key: 'id', value: 'FHT_Chapters_' + i, text: data.title })
addTag(FHT_contents, { name: 'div', key: 'id', value: 'FHT_Chapter_' + i })
let FHT_Chapter = $(`#FHT_Chapter_${i}`)[0]
let li = $(`#FHT_Chapters_${i}`)[0]
if (i == 0) { url = window.location.href; } else { url = BookData.chapters[i - 1].next }
BookData.chapters[i].index = url //当前页url
li.onclick = function () {
FHT_contents.scrollTop = FHT_Chapter.offsetTop
window.history.replaceState('', '', url)
}
addTag(FHT_Chapter, { name: 'div', key: 'class', value: 'FHT_Tit', text: data.title })
addTag(FHT_Chapter, { name: 'div', key: 'class', value: 'FHT_content', text: data.content.innerHTML })
//判断开始时是否有滚动条
if (FHT_contents.clientHeight >= FHT_contents.scrollHeight) {
getAjax(BookData.chapters[i].next.href).then((res) => {
getData(/]*>([\s\S]*)<\/body>/.exec(res)[1])
})
}
window.history.replaceState('', '', url)
// console.log(BookData);
}
//获取dom指定的内容,参数DOM.innerHTM
function getData(domData) {
addTag(body, { name: 'div', key: 'id', value: 'data_none', text: domData });
DIYRule.forEach(element => {
if (element.url == window.location.host) {
setData(element)
} else {
setData(Rule)
}
});
data_none.remove();
addPage()
//规则函数
function setData(rule) {
let data = {}
// 目录
rule.indexSelectors.forEach(element => {
if ($("#data_none " + element).length) {
BookData.meun = $("#data_none " + element)[0].href
}
})
// 下一页
rule.nextSelectors.forEach(element => {
if ($("#data_none " + element).length) {
data.next = $("#data_none " + element)[0].href
}
})
// 上一页
rule.prevSelectors.forEach(element => {
if ($("#data_none " + element).length) {
data.prev = $("#data_none " + element)[0].href
}
})
// 章节名
rule.titleSelectors.forEach(element => {
if ($("#data_none " + element).length) {
data.title = $("#data_none " + element)[0].innerText
}
})
// 书名
rule.bookTitleSelectors.forEach(element => {
if ($("#data_none " + element).length) {
BookData.bookName = $("#data_none " + element)[0].innerText
}
})
// 正文
rule.contentSelectors.forEach(element => {
if ($("#data_none " + element).length) {
data.content = $("#data_none " + element)[0]
}
})
//去广告
rule.contentReplace.forEach(element => {
if (data.content) {
data.content.innerHTML = data.content.innerHTML.replace(element[0], element[1])
}
});
BookData.chapters.push(data)
}
}
//初始化
function initialization() {
let data = body.innerHTML
body.innerHTML = ''
addTag(body, { name: 'div', key: 'id', value: 'FHT_main' })
addTag(FHT_main, { name: 'div', key: 'id', value: 'FHT_catalogue' })
addTag(FHT_main, { name: 'div', key: 'id', value: 'FHT_contents' })
addTag(FHT_main, { name: 'div', key: 'id', value: 'FHT_read' })
addTag(FHT_catalogue, { name: 'div', key: 'id', value: 'FHT_name', })
addTag(FHT_catalogue, { name: 'div', key: 'id', value: 'FHT_bottem' })
addTag(FHT_catalogue, { name: 'ul', key: 'id', value: 'FHT_chapters' })
addTag(FHT_read, { name: 'select', key: 'id', value: 'voiceSelect' })
addTag(FHT_read, { name: 'button', key: 'id', value: 'FHT_play', text: '播放' })
addTag(FHT_read, { name: 'button', key: 'id', value: 'FHT_del', text: '终止' }); FHT_del.classList.add("remove");
addTag(FHT_read, { name: 'button', key: 'id', value: 'FHT_suspend', text: '暂停' })
addTag(FHT_read, { name: 'button', key: 'id', value: 'FHT_recovery', text: '恢复' }); FHT_recovery.classList.add("remove");
readBtn()
getData(data)
FHT_name.innerText = BookData.bookName
addTag(FHT_bottem, { name: 'a', key: 'id', value: 'FHT_prev', text: '上一章' }); FHT_prev.href = BookData.chapters[BookData.chapters.length - 1].prev
addTag(FHT_bottem, { name: 'a', key: 'id', value: 'FHT_index', text: '目 录' }); FHT_index.href = BookData.meun
addTag(FHT_bottem, { name: 'a', key: 'id', value: 'FHT_next', text: '下一章' }); FHT_next.href = BookData.chapters[BookData.chapters.length - 1].next
}
//read按钮函数
function readBtn() {
FHT_play.onclick = function () {
BookData.readIndex = BookData.PageIndex
let text = $(`#FHT_Chapter_${BookData.readIndex}>.FHT_content`)[0].innerText
//判断是否有选中文字
if (window.getSelection().toString()) {
focusText = window.getSelection().toString()
text = focusText + text.split(focusText)[1]
}
speaking(text)
}
FHT_del.onclick = function () {
delBtn = true
synth.cancel()
}
FHT_suspend.onclick = function () {
synth.pause()
FHT_recovery.classList.remove("remove");
FHT_suspend.classList.add("remove");
}
FHT_recovery.onclick = function () {
synth.resume()
FHT_suspend.classList.remove("remove");
FHT_recovery.classList.add("remove");
}
}
//监听函数
FHT_contents.addEventListener("scroll", () => {
//监听正文滚动到400
if (FHT_contents.scrollHeight - FHT_contents.scrollTop - FHT_contents.clientHeight <= 400) {
state += 1
if (state == 1) {
getAjax(BookData.chapters[BookData.chapters.length - 1].next).then((res) => {
state = 0
getData(/]*>([\s\S]*)<\/body>/.exec(res)[1])
})
}
}
let array = $('#FHT_contents>div')
//判断当前页面在第几章
switch (array.length) {
case 1:
BookData.PageIndex = 0;
break;
case 2:
FHT_contents.scrollTop >= array[1].offsetTop ? BookData.PageIndex = 1 : BookData.PageIndex = 0
break;
default:
for (let index = 0; index < array.length - 1; index++) {
if (array[index].offsetTop <= FHT_contents.scrollTop && FHT_contents.scrollTop < array[index + 1].offsetTop) {
BookData.PageIndex = index
break
} else {
FHT_contents.scrollTop < array[1].offsetTop ? BookData.PageIndex = 0 : BookData.PageIndex = index + 1
}
}
break;
}
}, true);
Object.defineProperty(BookData, 'PageIndex', {
get() {
return PageIndex
},
set(value) {
let a = $('#FHT_chapters>li')
PageIndex = value;
Array.from(a).forEach((e, i) => {
if (i == value) {
e.classList.add("li_activ");
} else {
e.classList.remove("li_activ");
}
});
}
})
//AJAX
function getAjax(url) {
let xhr = new XMLHttpRequest();
xhr.open("GET", url);
let promise = new Promise(function (resolve, reject) {
xhr.onreadystatechange = function () {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
xhr.send();
});
return promise;
};
//创建标签
function addTag(fatherTag, sonTag) {
let tag = document.createElement(sonTag.name)
tag.setAttribute(sonTag.key, sonTag.value)
tag.innerHTML = sonTag.text || ''
fatherTag.appendChild(tag)
}
//获取朗读引擎
function getVoiceList() {
voiceSelect.innerHTML = ''
Voiceslist = [];
let voices = synth.getVoices()
for (let i = 0; i < voices.length; i++) {
if (voices[i].lang == 'zh-CN') {
Voiceslist.push(voices[i])
let option = document.createElement('option');
option.textContent = voices[i].name + ' (' + voices[i].lang + ')';
if (voices[i].default) {
option.textContent += ' -- DEFAULT';
}
option.setAttribute('data-lang', voices[i].lang);
option.setAttribute('data-name', voices[i].name);
voiceSelect.appendChild(option);
}
}
voiceSelect.selectedIndex = 5 //默认语音
}
if (speechSynthesis.onvoiceschanged !== undefined) {
speechSynthesis.onvoiceschanged = getVoiceList;
}
//语音播放函数
function speaking(text) {
utterThis.voice = Voiceslist[voiceSelect.selectedIndex]
utterThis.text = text
synth.speak(utterThis)
}
})();