第一版主网站增强

增强第一版主小说网站的功能,详情请看脚本页面的介绍(另含去除广告的说明)

Ekde 2024/04/07. Vidu La ĝisdata versio.

// ==UserScript==
// @name         第一版主网站增强
// @namespace    https://diyibanzhu.org/essence
// @version      0.8
// @description  增强第一版主小说网站的功能,详情请看脚本页面的介绍(另含去除广告的说明)
// @author       Essence
// @match        https://*/*
// @run-at       document-end
// @grant        GM_setValue
// @grant        GM_getValue
// @icon         https://www.google.com/s2/favicons?sz=64&domain=diyibanzhu.org
// @license MIT
// ==/UserScript==

// 站点名。域名经常变动,没有更好的判断方法
const SITE_NAME = "歪歪蒂艾斯"

const TAG = "[第一版主]"

// 持久存储到篡改猴数据中的键
const V_DYBZ_HISTORY = "DYBZ_HISTORY"

// =========================== 公用函数 =========================== //

// 等待
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))

/**
 * 等待指定元素出现后,执行回调
 * @param selector 元素选择器。参考`document.querySelector`
 * @param callback 需要执行的回调。传递的参数为目标元素
 */
const waitElem = (selector, callback) => {
    const observer = new MutationObserver(() => {
        const element = document.querySelector(selector)
        if (element) {
            callback(element)
            observer.disconnect()
        }
    })

    observer.observe(document.body, {
        childList: true,
        subtree: true,
    })
}

// 创建单手操作面板
const createOneHandPanel = () => {
    // 事件
    const gotoNextPage = () => {
        const nextLink = document.querySelector("a.curr")?.nextElementSibling || document.querySelector("a.next")
        if (nextLink) {
            nextLink.click()
        }
    }
    const gotoTop = () => window.scrollTo({top: 0, behavior: "smooth"})

    // 创建按钮
    const bnNextPage = document.createElement('button')
    bnNextPage.textContent = "下一页"
    bnNextPage.addEventListener('click', gotoNextPage)

    const bnTop = document.createElement('button')
    bnTop.textContent = "顶部"
    bnTop.addEventListener('click', gotoTop)

    // 创建扩展面板
    const oneHandPanel = document.createElement('div')
    // 增强面板的样式
    oneHandPanel.style.display = "flex"
    oneHandPanel.style.flexDirection = "row"
    oneHandPanel.style.justifyContent = "space-between"
    oneHandPanel.style.gap = "8px"

    // 向扩展面板添加上面的组件
    oneHandPanel.append(bnNextPage, bnTop)

    return oneHandPanel
}

/**
 * 读取网站存储到 localStorage 的所有值
 * @param excludeRegexp 不读取的键的正则。如 /^Hm_lvt/
 * @return {string} JSON 文本
 */
const readLocalStorageValues = (excludeRegexp = undefined) => {
    const result = {}
    for (let i = 0; i < localStorage.length; i++) {
        const key = localStorage.key(i)
        if (excludeRegexp && excludeRegexp.test(key)) {
            continue
        }

        result[key] = localStorage.getItem(key)
    }

    return JSON.stringify(result)
}

/**
 * 恢复数据到网站的 localStorage
 * @param {string} json
 */
const restoreLocalStorageValues = (json) => {
    const data = JSON.parse(json)

    Object.keys(data).forEach(key => localStorage.setItem(key, data[key]))
}

// =========================== 脚本页面 =========================== //

// 脚本页面:阅读页面,预加载下一页
const nextPage = () => {
    // 依次找到"下一页"的元素:优先“下一页”,其次“下一章”
    const nextLink = document.querySelector("a.curr")?.nextElementSibling || document.querySelector("a.next")

    console.log(TAG, "脚本将预加载下一页", nextLink, nextLink?.href)

    // 存在链接
    if (nextLink && nextLink.href) {
        // 通过"link prefetching"实现预加载下一页
        const link = document.createElement("link")

        // 注意:最后第一版主网站的章中最后的一页的 URL 的 href 是以"javascript:"开头,其它页是正常的 URL
        let href = nextLink.href
        if (href.startsWith("javascript:")) {
            const params = href.match(/\d+/g)
            href = "/" + params.slice(0, -1).join("/") + "_" + params.slice(-1) + ".html"
        }

        link.href = href
        link.rel = "prefetch"
        document.head.appendChild(link)
    }
}

// 脚本页面: 阅读页面,单手操作面板
const oneHand = () => {
    // 操作章节页面元素的父元素,插入扩展面板和章节页面
    const chapterPages = document.querySelector(".chapterPages")
    if (!chapterPages) {
        console.log(TAG, "chapterPages 元素为空")
        return
    }
    // 修正原样式
    chapterPages.style.marginTop = "auto"
    chapterPages.style.lineHeight = "auto"

    const root = chapterPages.parentElement
    if (!root) {
        console.log(TAG, "父元素为空")
        return
    }

    // root 父元素的样式
    root.style.display = "flex"
    root.style.justifyContent = "space-between"
    root.style.flexDirection = "row"

    root.style.marginTop = "30px"

    // 注意 `elem.cloneNode(true)`不能复制`事件`,所以要直接创建
    const oneHandPanel1 = createOneHandPanel()
    const oneHandPanel2 = createOneHandPanel()

    root.append(oneHandPanel1, chapterPages, oneHandPanel2)
}

// 脚本页面: 保存历史阅读记录到篡改猴
// 注释:localstorage 中 bookList 保存书籍 ID列表(以"#"分隔),只保存这个是无效的,还要保存 ID 对应的阅读进度才是完整的历史记录。所以读取、保存整个 localstorage
const saveHistoryBooks = () => {
    const history = readLocalStorageValues(/^Hm_lvt/)
    if (history) {
        // 保存到篡改猴
        GM_setValue(V_DYBZ_HISTORY, history)
    }

    const values = GM_getValue(V_DYBZ_HISTORY)
    console.log(TAG, "已保存的历史阅读记录:", values)
}

// 脚本页面:准许复制文本
const enableCopyText = ()=>{
    document.querySelector("body").oncontextmenu = null;

    const chapterBody = document.querySelector("body.chapter")
    if(chapterBody){
        chapterBody.style.webkitUserSelect = "auto"
    }
}

// 增强作者文本为链接
const makeAuthorLink = (authorNode)=>{
    const author = authorNode.textContent.trim().replace("作者:", "")

    const link = document.createElement("a")
    link.textContent = author
    link.href = `/author/${author}`
    link.classList.add("author")

    const parentElem = authorNode.parentElement
    authorNode.textContent = "作者:"
    parentElem.insertBefore(link, parentElem.childNodes[1])
}

// 脚本页面:自动填充网站人机验证
const verifyPage = () => {
    document.querySelector("input#password").value = "1234"
    document.querySelector("div.login a").click()
}

// 脚本页面:自动填充 CF 人机验证
const cfPage = () => {
    waitElem("div#challenge-stage input[type='checkbox']", (elem) => {
        console.log(TAG, "CF 人机验证的选择框", elem)
        elem.click()
    })
}


(function () {
    'use strict'

    // Your code here...

    // CF 验证页面需要放在最前面。避免因为不是目标网站而跳过

    // 脚本页面:自动填充 CF 人机验证
    // 注意 CF 验证是通过 iframe 嵌入实现的,所以在指定 URL 的 iframe 中运行该函数
    if (location.href.startsWith("https://challenges.cloudflare.com/cdn-cgi/challenge-platform/")) {
        console.log(TAG, "自动填充 CF 人机验证")
        cfPage()
    }

    // 不是目标小说网站
    if (!document.title.includes(SITE_NAME) && !(document.title === "您的阅读足迹" && document.querySelector("h1.page-title").textContent === "您的阅读足迹")) {
        console.log(TAG, "不是目标小说网站,停止运行。", `网站标题:"${document.title}"`, `网站地址:${location.href}`)
        return
    }

    // 脚本页面:自动填充网站人机验证
    if (document.querySelector("div.title")?.textContent?.includes("为防止恶意访问")) {
        console.log(TAG, "自动填充网站人机验证")
        verifyPage()
    }

    // 脚本页面:阅读页面
    if (document.querySelector("h1.page-title")?.textContent?.trim()) {
        console.log(TAG, "预加载下一页")
        nextPage()

        console.log(TAG, "单手操作面板")
        oneHand()

        console.log(TAG, "保存历史阅读记录到篡改猴")
        saveHistoryBooks()

        console.log(TAG, "准许复制文本")
        enableCopyText()
    }

    // 脚本页面:增强作者文本为链接
    if(/作者:.+/.test(document.querySelector("p.info")?.firstChild?.textContent?.trim())){
        console.log(TAG, "增强作者文本为链接")
        makeAuthorLink(document.querySelector("p.info").firstChild)
    }
})()