biquge365.net阅读背景调整

改善笔趣阁的阅读体验

// ==UserScript==
// @name         biquge365.net阅读背景调整
// @namespace    https://github.com/yoophoon
// @copyright    2024, yoophoon (https://github.com/yoophoon)
// @version      2024-02-25
// @description  改善笔趣阁的阅读体验
// @icon         https://www.biquge365.net/style/favicon.ico
// @grant        unsafeWindow
// @grant        GM_addStyle
// @author       yoophoon
// @homepage     https://github.com/yoophoon
// @match        https://www.biquge365.net/chapter/*
// @run-at       document-end
// @license      MIT
// ==/UserScript==


(function () {
    //GM_addStyle对脚本无效单纯插入一段CSS验证该函数的功能性
    //GM_addStyle(".uselessStyle,.usefulStyle{height:150px !important}")
    setUserCSS('userCSS')
    let webSelfScript = document.head.querySelectorAll('script')
    for (let i = 0; i < webSelfScript.length; i++) {
        webSelfScript[i].remove()
    }
    document.body.style.backgroundColor = '#999999'
    document.body.style.color = '#cc9966'
    let showContent = document.createElement('div')
    showContent.setAttribute('id', 'contentPanel')


    //here
    let chapterID = document.location.href.split('/')[5].split('.')[0]
    showContent.appendChild(setContent(chapterID, document.body))
    document.body.appendChild(showContent)


    //创建侧栏显示最一定数量的最新章节和当前章节的前后章节
    let sideBar = document.createElement('ul')
    sideBar.classList.add('sideBarPanel')
    // sideBar.style.cssText = 'display:none;'
    // sideBar.style.cssText = 'display:block;width:250px;position:fixed;top:20%;left:calc(50% - 750px);box-sizing:border-box;overflow:hidden;'
    //设置鼠标进入退出效果
    sideBar.onmouseenter = function () {
        sideBar.style.backgroundColor = '#666666'
        sideBar.style.border = '2px solid #999999'
    }
    sideBar.onmouseleave = function () {
        sideBar.style.backgroundColor = ''
    }

    let bookID = document.location.href.split('/')[4]
    let allChaptersInfo = getAllChaptersInfo(bookID)
    document.body.appendChild(sideBar)
    //加载侧栏章节信息
    setChapterPanel(sideBar, chapterID, allChaptersInfo, 5)
    //动态加载章节内容
    preloadNextChapters(sideBar, showContent, allChaptersInfo, 5)

})();


/**
 * 
 * @param {string} userCSS 用户样式表
 * @link https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/insertRule#function_to_add_a_stylesheet_rule
 * @description 将用户样式表插入到DOM对象中通过sheet属性生效,无法直接在开发者工具->元素(HTML)中直接查看
 */
function setUserCSS(userCSS) {
    let styleEl = document.createElement('style')
    document.head.appendChild(styleEl)
    //类似documen.style.cssText
    //userCSS.sheet.addRule('.userCSS::after', 'color:green')
    styleEl.sheet.insertRule(`.splitInChapterPanel{
        border: none;
        border-top: 2px double #cc9966;
        color:#333;
        overflow: visible;
        text-align: center;
        height: 2px;
        margin-top:11px;
        margin-bottom:8px;
    }`)
    styleEl.sheet.insertRule(`.splitInChapterPanel::after {color:#666666;background-color: #cc9966;content:'最新章节↓§↑最近章节';padding:0 4px;position:relative;font-size:12px;top:-13px;border-radius:6px}`)

    styleEl.sheet.insertRule(`.chapterInfo{
        width:230px;
        border-left: 2px solid #666666;
        color: #cc9966;
        padding-left: 10px;
        margin: 5px 0px;
        text-overflow:ellipsis;
        overflow:hidden;
        white-space:nowrap;}
    `)

    styleEl.sheet.insertRule(`.chapterInfo:hover {
        margin-left: 10px;}
    `)

    styleEl.sheet.insertRule(`.currentChapterInfo {
        margin:5px 0px 5px 25px;
        border-bottom: 2px solid #cc9966;
        color:#cc9966}
    `)

    styleEl.sheet.insertRule(`.currentChapterInfo:hover {
        margin-left: 35px;}
    `)

    styleEl.sheet.insertRule(`.currentChapterInfo::before {
        content: ">";
        font-size: 30px;
        display: block;
        position: relative;
        top: 10px;
        right: 20px;
        color: #666666;
        line-height:0px;}
    `)
    styleEl.sheet.insertRule(`#contentPanel{
        height:100vh;
        background-color: #666666;
        margin-top: 0;
        padding-top: 0;
        margin: 0 auto;
        overflow: scroll;
        scrollbar-width:none;}
    `)
    styleEl.sheet.insertRule(`#contentPanel::-webkit-scrollbar{
        display:none;
    }`)
    styleEl.sheet.insertRule(`.sideBarPanel{
        display:none;
    }`)
    styleEl.sheet.insertRule(`@media(min-width:1500px){
        ul.sideBarPanel{
            display:block;
            width:250px;
            position:fixed;
            top:20%;
            left:calc(50% - 750px);
            box-sizing:border-box;
            overflow:hidden;
        }
    }`)
    styleEl.sheet.insertRule(`@media(min-width:1000px){
        div#contentPanel{
            width:1000px;
            height:100vh;
            background-color: #666666;
            margin-top: 0;
            padding-top: 0;
            margin: 0 auto;
            overflow: scroll;
    }`)
}

/**
 * 
 * @param {*} id 章节ID
 * @param {*} node 传入的包含章节内容的DOM
 * @returns 放回仅包含标题和正文的DOM
 */
function setContent(chapterID, node) {
    //章节标题样式
    const styleTitle = `
    margin: 0;
    padding:50px 0;
    text-align: center;`
    //章节正文样式
    const styleContent = `
    font-family: "microsoft yahei";
    font-size: 18px;
    line-height: 50px;
    box-sizing: border-box;
    padding: 0px 50px;
    `
    let currentChapter = document.createElement('div')
    currentChapter.setAttribute('id', chapterID)
    currentChapter.setAttribute('class', 'content')
    //window.location.href = window.location.href.split('#')[0] + '#' + chapterID
    let currentContent = node.querySelector('#txt')
    let currentTitle = node.querySelector('h1')
    currentTitle.style.cssText = styleTitle
    currentContent.style.cssText = styleContent
    currentContent.removeAttribute('id')
    currentContent.removeAttribute('class')
    currentContent.removeChild(currentContent.firstElementChild)//文字广告
    currentContent.removeChild(currentContent.firstElementChild)//第一个换行
    node.innerHTML = ''
    currentChapter.appendChild(currentTitle)
    currentChapter.appendChild(currentContent)
    return currentChapter
}

/**
 * 
 * @param {*} chapterPanel 侧栏显示章节信息容器
 * @param {*} contentPanel 显示章节正文信息容器
 * @param {*} allChaptersInfo 所有的章节信息
 * @param {*} nodeNum 最新章节数量=nodeNum、前后章节数量=parseInt(nodeNum)、预加载数量=parseInt(nodeNum)
 * @description 简单版无限阅读
 */
function preloadNextChapters(chapterPanel, contentPanel, allChaptersInfo, nodeNum) {
    const parser = new DOMParser()
    contentPanel.addEventListener("scrollend", (event) => {
        allChaptersInfo.then(data => {
            let indexOfCurrentChapter = parseInt(window.location.href.split('#')[2])
            for (let i = 1; i <= parseInt(nodeNum / 2); i++) {
                let tempIndex = indexOfCurrentChapter + i
                if (tempIndex >= data.length) {
                    break
                }
                if (data[tempIndex][2] == 1) {
                    fetch(data[indexOfCurrentChapter + i][1])
                        .then(response => response.text())
                        .then(resText => {
                            let chapterPage = parser.parseFromString(resText, 'text/html')
                            data[indexOfCurrentChapter + i][2] = setContent(data[indexOfCurrentChapter + i][1].split('/')[5].split('.')[0], chapterPage)
                            //contentPanel.appendChild(data[indexOfCurrentChapter + i][2])
                        })
                }
            }
        })
        //TODO 需要优化加载速度和加载完成检测
        allChaptersInfo.then(data => {
            let theFirstChapterInChapterPanel = contentPanel.querySelector('.content')
            //let allChaptersInChapterPanel = contentPanel.querySelectorAll('.content')
            let indexOfNextChapter = parseInt(window.location.href.split('#')[2]) + 1
            //let currentChapterUrl = `https://www.biquge365.net/chapter/${window.location.href.split('/')[4]}/${theFirstChapterInChapterPanel.getAttribute('id')}.html`
            for (let i = indexOfNextChapter; i < (indexOfNextChapter - 1 + parseInt(nodeNum / 2) < data.length ? indexOfNextChapter - 1 + parseInt(nodeNum / 2) : data.length); i++) {
                if (data[indexOfNextChapter][3] == false && data[indexOfNextChapter][2] != 1) {
                    contentPanel.appendChild(data[indexOfNextChapter][2])
                    data[indexOfNextChapter][3] = true
                }
                if (data[indexOfNextChapter][3] == true) break
            }
            if (contentPanel.scrollTop - theFirstChapterInChapterPanel.scrollHeight > 0) {
                if (indexOfNextChapter >= data.length) return
                //console.log(data)
                let chapterID = data[indexOfNextChapter][1].split('/')[5].split('.')[0]
                document.head.querySelector('title').innerText = `${document.head.querySelector('title').innerText.split('_')[0]}_${data[indexOfNextChapter][0]}`
                window.location.href = `${window.location.href.split('#')[0]}#${chapterID}#${indexOfNextChapter}`
                setChapterPanel(chapterPanel, chapterID, allChaptersInfo, nodeNum)
                theFirstChapterInChapterPanel.remove()
            }
        })
    });
}

/**
 * 
 * @param {*} bookID 书籍ID
 * @returns 所有章节标题及链接
 */
async function getAllChaptersInfo(bookID) {
    const chaptersURL = `https://www.biquge365.net/newbook/${bookID}/`
    const parser = new DOMParser()
    let allChaptersInfo = new Array()

    await fetch(chaptersURL)
        .then(response => response.text())
        .then(resText => {
            let chapterPage = parser.parseFromString(resText, 'text/html')
            let chapterLi = chapterPage.querySelectorAll(" ul.info > li> a")
            for (i = 0; i < chapterLi.length; i++) {
                let tempInfo = new Array(4)
                tempInfo[0] = chapterLi[i].getAttribute('title')
                tempInfo[1] = 'https://www.biquge365.net' + chapterLi[i].getAttribute('href')
                tempInfo[2] = 1
                tempInfo[3] = false
                allChaptersInfo.push(tempInfo)
            }
        })
    return allChaptersInfo
}

/**
 * 
 * @param {*} chapterPanel 侧栏章节信息挂载对象
 * @param {*} currentChapterID 当前章节ID用于生成前后章节信息
 * @param {*} allChaptersInfo 所有章节信息
 * @param {*} nodeNum 显示章节数量
 */
function setChapterPanel(chapterPanel, currentChapterID, allChaptersInfo, nodeNum) {
    allChaptersInfo.then(data => {
        //清空侧栏目录内容
        chapterPanel.innerHTML = ''
        //设置侧栏目录
        let directory = document.createElement('a')
        directory.style.cssText = 'color:#cc9966;font-size:25px;padding-bottom:5px;display:block;text-align:center'
        directory.href = `https://www.biquge365.net/newbook/${window.location.href.split('/')[4]}/`
        directory.innerText = '全部章节'
        chapterPanel.appendChild(directory)
        //处理前后章节信息
        //外循环定位
        let indexOfCurrentChapter, j
        for (let i = data.length - 1; i >= 0; i--) {
            //如果href有当前章节的index信息就直接用index信息加速
            if (window.location.href.split('#')[2] != undefined) {
                i = i - parseInt(window.location.href.split('#')[2]) > nodeNum ? parseInt(window.location.href.split('#')[2]) : i
            }
            if (currentChapterID == data[i][1].split('/')[5].split('.')[0]) {
                indexOfCurrentChapter = i
                if (window.location.href.split('#')[1] != undefined) {
                    window.location.href = `${window.location.href.split('#')[0]}#${window.location.href.split('#')[1]}#${indexOfCurrentChapter}`
                } else {
                    window.location.href += `#${currentChapterID}#${indexOfCurrentChapter}`
                }
                //内循环生成元素
                for (j = (0 > (i - parseInt(nodeNum / 2)) ? 0 : (i - parseInt(nodeNum / 2)));
                    j <= ((data.length - 1) > (i + parseInt(nodeNum / 2)) ? (i + parseInt(nodeNum / 2)) : (data.length - 1)); j++) {
                    let chapterInfo = document.createElement('a')
                    chapterInfo.setAttribute('class', 'chapterInfo')
                    if (i == j) {
                        chapterInfo.setAttribute('class', 'chapterInfo currentChapterInfo')
                    }
                    chapterInfo.innerText = data[j][0]
                    chapterInfo.href = data[j][1]
                    chapterPanel.appendChild(chapterInfo)
                }
                break
            }
        }
        //插入分割线
        setSplitLine(chapterPanel)
        //处理最新章节信息
        for (i = (indexOfCurrentChapter + parseInt(nodeNum / 2) + 1 < data.length - nodeNum ? data.length - nodeNum : indexOfCurrentChapter + parseInt(nodeNum / 2) + 1); i < data.length; i++) {
            let chapterInfo = document.createElement('a')
            chapterInfo.setAttribute('class', 'chapterInfo')
            chapterInfo.innerText = data[i][0]
            chapterInfo.href = data[i][1]
            chapterPanel.appendChild(chapterInfo)
        }
    })
}

/**
 * 
 * @param {*} parentNode 分割线挂载对象
 */
function setSplitLine(parentNode) {
    let hrIndirectory = document.createElement('hr')
    hrIndirectory.setAttribute('class', 'splitInChapterPanel')
    parentNode.appendChild(hrIndirectory)
}