小说朗读

小说阅读,朗读

Zainstaluj skrypt?
Skrypt zaproponowany przez autora

Może Ci się również spodobać. My Novel Reader

Zainstaluj skrypt
// ==UserScript==
// @name         小说朗读
// @namespace    http://tampermonkey.net/
// @version      2.1.1
// @description  小说阅读,朗读
// @author       FHT
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        GM_addElement
// @grant        GM_addStyle
// @grant        unsafeWindow
// @license MIT
// @exclude      https://www.hujuge.com/*/index.html
// @match        https://www.hujuge.com/*/*html
// @match        https://www.qidian.com/chapter/*/*/
// @match        https://www.douyinxs.com/bqg/*/*.html
// ==/UserScript==

(function() {
    'use strict';
    const rule = [
        {
            url: '域名',
            nextSelector: '下一页',
            prevSelector: '上一页',
            indexSelector: '目录',
            titleSelector: '章节名',
            bookTitleSelector: '书名',
            contentSelector: '正文',
            contentReplace: [['去广告正则', ""],
                             []
                            ]
        },{
            url: 'www.hujuge.com',
            nextSelector: '#next_url',
            prevSelector: '#prev_url',
            indexSelector: '#info_url',
            titleSelector: '.title',
            bookTitleSelector: '.layout-tit>a:nth-of-type(2)',
            contentSelector: '#content',
            contentReplace: [
                ['/\n/g', ' '],
                [/章节错误.*/g,'']]
        }, {
            url: 'www.douyinxs.com',
            nextSelector: '#next',
            prevSelector: '#prev',
            indexSelector: '.bottem1 a:nth-child(2)',
            titleSelector: '.bookname h1',
            bookTitleSelector: '.con_top>a:nth-of-type(3)',
            contentSelector: '#content',
            contentReplace: [['/\n/g', ' ']]
        },{
            url: 'www.qidian.com',
            nextSelector: '.nav-btn-group>a:nth-last-of-type(1)',
            prevSelector: '.nav-btn-group>a:nth-of-type(1)',
            indexSelector: '.nav-btn-group>a:nth-of-type(2)',
            titleSelector: '.chapter-wrapper h1',
            bookTitleSelector: '#r-breadcrumbs>a:nth-of-type(4)',
            contentSelector: '.content',
            contentReplace: [[/\s\s/g, '\n'],[/<span.class=.review-count.>\d+<\/span>/g, '']
                            ]
        },
    ]

    GM_addElement('script', {
        src: 'https://cdn.staticfile.net/vue/3.3.4/vue.global.min.js',
        type: 'text/javascript'
    });
    GM_addElement('script', {
        src: 'https://cdn.staticfile.net/axios/1.6.5/axios.min.js',
        type: 'text/javascript'
    });
    window.onload = () => {
        let body = document.querySelector('body')
        let htmlData = body.innerHTML
        const synth = window.speechSynthesis;
        synth.cancel()
        const utterThis = new SpeechSynthesisUtterance();
        body.innerHTML = ''
        GM_addElement(body, 'div', {
            id:'app'
        });
        const { createApp } = Vue
        createApp({
            data() {
                return {
                    bookData: {
                        bookName: null,             //书名
                        chapter: [],                //章节
                        content: [],                //正文
                        next: null,                 //下一章url
                        prev: null,                 //目录url
                        menu: null,                 //上一章url
                    },
                    voicesData: {
                        voicesList: [],             //语音列表
                        voicesIndex: 5,             //默认语音
                    },
                    readData: {
                        readIndex: 0,               //当前章节下标
                    },
                    speakData: {
                        speakState: 0,              //语音是否存在
                        speakingState: 1,           //语音暂停播放
                        speakIndex: 0,              //语音播放的章节下标
                        focusText: null             //是否有选中文字
                    },
                    data: '',                       //隐藏div数据
                    state: 1,                       //监听防抖
                }
            },
            created() {
                this.data = htmlData
            },
            mounted() {
                this.setData()
                //获取朗读引擎
                const _this = this
                synth.onvoiceschanged = function () {
                    _this.voicesData.voicesList = []
                    let arr = synth.getVoices()
                    for (let i = 0; i < arr.length; i++) {
                        if (arr[i].lang == 'zh-CN') {
                            _this.voicesData.voicesList.push(arr[i])
                        }
                    }
                }
                utterThis.onend = (event) => {
                    this.speakData.focusText = null
                    let tag = document.querySelector('.right_' + this.speakData.speakIndex)
                    tag.innerHTML = tag.innerHTML.replace(/<\/?span.*?>/g, '')
                    this.speakData.speakIndex++
                    if (this.speakData.speakIndex < this.bookData.chapter.length) {
                        tag = document.querySelector('.right_' + this.speakData.speakIndex)
                        tag.scrollIntoView(true)
                        utterThis.voice = this.voicesData.voicesList[this.voicesData.voicesIndex]
                        utterThis.text = tag.innerText
                        synth.speak(utterThis)
                    } else {
                        axios({
                            url: this.bookData.next,
                            method: 'get'
                        }).then(res => {
                            this.data = /<body[^>]*>([\s\S]*)<\/body>/.exec(res.data)[1]
                            this.setData()
                            setTimeout(() => {
                                tag = document.querySelector('.right_' + _this.speakData.speakIndex)
                                tag.scrollIntoView(true)
                                utterThis.voice = _this.voicesData.voicesList[_this.voicesData.voicesIndex]
                                utterThis.text = tag.innerText
                                synth.speak(utterThis)
                            }, 1);
                        })
                    }
                };
                utterThis.onboundary = (event) => {
                    let div = document.querySelector('.right_' + this.speakData.speakIndex)
                    let str
                    let read_text = []
                    if (this.speakData.focusText) {
                        let t1 = div.innerText.split(this.speakData.focusText)[0]
                        let t2 = this.speakData.focusText + div.innerText.split(this.speakData.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.substr(0, event.charIndex)
                        read_text[1] = div.innerText.substr(event.charIndex)
                        div.innerHTML = read_text[0] + read_text[1].replace(str, "<span class ='activ'>" + str + "</span>")
                    }
                    if (document.querySelector('.activ').offsetTop > document.querySelector('.right').scrollTop + document.querySelector('.right').clientHeight) {
                        document.querySelector('.right').scrollTop = document.querySelector('.activ').offsetTop - 50
                    }
                }
                //监听滚动条
                const right = document.querySelector('.right')
                right.addEventListener('scroll', () => {
                    if (right.scrollHeight - right.scrollTop - right.clientHeight <= 400) {
                        this.state++
                        if (this.state == 2) {
                            this.getData({ url: this.bookData.next, method: 'get' })
                        }
                    }
                    //判断当前页面在第几章
                    switch (document.querySelectorAll('.right>div').length) {
                        case 1:
                            this.readData.readIndex = 0;
                            break;
                        case 2:
                            right.scrollTop >= document.querySelectorAll('.right>div')[1].offsetTop ? this.readData.readIndex = 1 : this.readData.readIndex = 0
                            break;
                        default:
                            for (let index = 0; index < document.querySelectorAll('.right>div').length - 1; index++) {
                                if (document.querySelectorAll('.right>div')[index].offsetTop <= right.scrollTop && right.scrollTop < document.querySelectorAll('.right>div')[index + 1].offsetTop) {
                                    this.readData.readIndex = index
                                    break
                                } else {
                                    right.scrollTop < document.querySelectorAll('.right>div')[1].offsetTop ? this.readData.readIndex = 0 : this.readData.readIndex = index + 1
                                }
                            }
                            break;
                    }
                })
            },
            methods: {
                selectChange() {
                    this.voicesData.voicesIndex = document.querySelector('#voiceSelect').value
                },
                // 语音功能按钮
                play() {
                    this.speakData.speakIndex = this.readData.readIndex
                    utterThis.voice = this.voicesData.voicesList[this.voicesData.voicesIndex]
                    if (window.getSelection().toString()) {
                        this.speakData.focusText = window.getSelection().toString()
                        utterThis.text = this.speakData.focusText + document.querySelector('.right_' + this.speakData.speakIndex).innerText.split(this.speakData.focusText)[1]
                    } else {
                        utterThis.text = document.querySelector('.right_' + this.speakData.speakIndex).innerText
                    }
                    synth.speak(utterThis)
                    this.speakData.speakState = synth.speaking
                },
                del() {
                    synth.cancel()
                    this.speakData.speakState = synth.speaking
                    this.speakData.speakingState = !synth.paused

                },
                suspend() {
                    if (synth.speaking) {
                        synth.pause()
                        this.speakData.speakingState = synth.paused
                    } else (
                        alert('没有播放文本')
                    )
                },
                recovery() {
                    if (synth.speaking) {
                        synth.resume()
                        this.speakData.speakingState = !synth.paused
                    }
                },
                // 设置数据
                setData() {
                    rule.forEach(element => {
                        if (window.location.host == element.url) {
                            this.$nextTick(() => {
                                const name = document.querySelector(element.bookTitleSelector)
                                const chapter = document.querySelector(element.titleSelector)
                                const menu = document.querySelector(element.indexSelector)
                                const prev = document.querySelector(element.prevSelector)
                                const next = document.querySelector(element.nextSelector)
                                const content = document.querySelector(element.contentSelector)
                                element.contentReplace.forEach(e => {
                                    content.innerHTML = content.innerHTML.replace(e[0], e[1])
                                })
                                this.bookData.bookName = name ? name.innerHTML : '未找到书名'
                                this.bookData.chapter.push({ msg: chapter ? chapter.innerText : '未找到章节名', src: this.bookData.next || window.location.href })
                                this.bookData.menu = menu ? menu.href : '未找到'
                                this.bookData.prev = this.bookData.prev ? this.bookData.prev : prev.href
                                this.bookData.next = next ? next.href : '未找到'
                                this.bookData.content.push(content ? content.innerText : '未找到内容')
                                this.data = ''
                            })
                        }
                    });
                },
                // 请求数据
                getData(params) {
                    axios({
                        url: params.url,
                        method: params.method
                    }).then(res => {
                        this.state = 1
                        this.data = /<body[^>]*>([\s\S]*)<\/body>/.exec(res.data)[1]
                        this.setData()
                    })
                },
                //点击章节列表
                click_left_chapter(params) {
                    this.readData.readIndex = params
                    const tag = '.right > div:nth-of-type(' + (params + 1) + ')'
                    document.querySelector(tag).scrollIntoView(true)
                },
                //根据标签内容选择标签
                tagContains(params) {
                    const arr = document.querySelectorAll(params.tag)
                    for (let index = 0; index < arr.length; index++) {
                        const element = arr[index];
                        if (element.innerText == params.msg) {
                            return element
                        }
                    }
                }
            },
            watch: {
                'readData.readIndex'() {
                    window.history.replaceState('', '', this.bookData.chapter[this.readData.readIndex].src)
                }
            },
            template: `
            <div v-show="0" v-html="data"></div>
            <div class="left">
                <div class="left_bookname">{{bookData.bookName}}</div>
                <div class="left_select">
                    <a :href = 'bookData.prev'>上一章</a>
                    <a :href = 'bookData.menu'>目录</a>
                    <a :href = 'bookData.next'>下一章</a>
                </div>
                <div class="left_chapter">
                    <div v-for="i,j in bookData.chapter"
                         :class = '[{ li_activ: j==readData.readIndex },"left_"+j]'
                         @click='click_left_chapter(j)'>{{ i.msg }}</div>
                </div>
            </div>
            <div class="right">
                <div v-for="i,j in bookData.content">
                    <div class="tit">{{ bookData.chapter[j].msg }}</div>
                    <div v-html="i" :class = '"right_"+j'></div>
                </div>
            </div>
            <div class="speak">
                <select name="" id="voiceSelect"  v-if="!speakData.speakState" @change = selectChange() >
                    <option v-for="i,j in voicesData.voicesList" :value=j :selected = 'voicesData.voicesIndex==j?1:0'>{{ i.name }}</option>
                </select>
                <button v-if="speakData.speakState" @click = del()>终止</button>
                <button v-else  @click = play()>播放</button>
                <button v-if="speakData.speakingState" @click = suspend()>暂停</button>
                <button v-else @click = recovery()>恢复</button>
            </div>
            `,
        }).mount('#app')
    }
    // css
    const css = ` * {
            margin: 0;
            padding: 0;
        }
        #app {
            width: 100vw;
            height: 100vh;
            display: flex;
            overflow: hidden;
            background: url(https://qidian.gtimg.com/qd/images/read.qidian.com/theme/theme_1_bg_2x.0.3.png);
        }
        .left {
            width: 250px;
            height: 100%;
            overflow: hidden;
            background: rgb(70, 70, 70);
            color: white;
            font-size: 14px;
            cursor: pointer;
            padding: 8px;

        }
        .left a{

            color: white;
        }
        .left_bookname {
            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;
        }
        .left_select {
            display: flex;
            justify-content: space-evenly;
            padding: 10px;
            border-bottom: 1px solid black;
        }
        .left_chapter {
            overflow: auto;
            height: 85%;
        }
        .left_chapter div {
            padding: 10px;
            border-bottom: 1px solid black;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        .right {
            overflow: auto;
            width: 100%;
            height: 100%;
            min-width: 60%;
            font-size: 26px;
            line-height: 50px;
            white-space: pre-line !important;
            user-select: text !important;
        }
        .tit {
            text-align: center;
            margin: 10px 80px;
            padding: 10px;
            border-bottom: solid 1px rgb(134, 124, 124);
            font-size: 32px;
        }
        .right div {
            margin: 0 100px;
            letter-spacing: 4px;
        }
        .speak {
            position: fixed;
            top: 10px;
            right: 30px;
        }
        .li_activ {
            background: #000;
        }

        .activ {
            color: red;
        } `
    GM_addStyle(css)
})();