Greasy Fork

Greasy Fork is available in English.

小说朗读

自用小说朗读

当前为 2023-02-21 提交的版本,查看 最新版本

// ==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
// ==/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', '新字符串'],
        [/<br><br>/g, '<br>'],
        [/阅读最新章节请下载爱阅小说[\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(/<body[^>]*>([\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, "<span class ='activ'>" + str + "</span>")
    } 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, "<span class ='activ'>" + str + "</span>")
    }
    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(/<body[^>]*>([\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(/<body[^>]*>([\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)
}


})();