Greasy Fork

来自缓存

Greasy Fork is available in English.

手机百度贴吧自动展开楼层

有时候用手机的浏览器打开百度贴吧,只想看一眼就走,并不想打开APP,这个脚本用于帮助用户自动展开楼层。注意:只支持手机浏览器,测试环境为Iceraven+Tampermonkey

当前为 2022-06-07 提交的版本,查看 最新版本

// ==UserScript==
// @name         手机百度贴吧自动展开楼层
// @namespace    http://tampermonkey.net/
// @homepage     http://greasyfork.icu/scripts/445657
// @version      2.0
// @description  有时候用手机的浏览器打开百度贴吧,只想看一眼就走,并不想打开APP,这个脚本用于帮助用户自动展开楼层。注意:只支持手机浏览器,测试环境为Iceraven+Tampermonkey
// @author       voeoc
// @match        https://tieba.baidu.com/p/*
// @match        https://jump2.bdimg.com/p/*
// @match        https://tiebac.baidu.com/p/*
// @connect      https://tieba.baidu.com/mg/o/getFloorData
// @connect      https://jump2.bdimg.com/mg/o/getFloorData
// @connect      https://tiebac.baidu.com/mg/o/getFloorData
// @icon         https://tieba.baidu.com/favicon.ico
// @grant        unsafeWindow
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @license MIT
// ==/UserScript==

(function () {
    'use strict';
    const DEBUGLOG_SWITCH = false; // 调试信息开关
    const isFixedTitle = false; // 是否将标题置顶

    const STR_NEWOPENLZLTEXT = "展开评论"; // 打开楼中楼按钮的文本
    const STR_VOEOCMARK = "VOEOCMARK"; // 临时标记

    const REG_URL_POSTPAGE = RegExp(`postPage\?(?=.*tid\=)(?=.*postAuthorId\=)(?=.*forumId\=)`, 'i'); // 评论页url正则
    const REG_URL_FLOORREPLAYPAGE = RegExp(`lzlPage\?(?=.*floor\=)(?=.*pid\=)`, 'i'); // 楼中楼页url正则
    const REG_URL_JSON_FLOORDATA = RegExp(`getFloorData\?(?=.*pn\=)(?=.*rn\=)(?=.*tid\=)(?=.*pid\=)`, 'i'); // 楼中楼json数据
    const REG_URL_PBDATA = RegExp(`/getPbData\?(?=.*pn\=)(?=.*rn\=)(?=.*only_post\=)(?=.*kz\=)`, 'i'); // 评论页数据url正则

    const PAGE_TYPE = { // 页面类型
        UNKNOW: -1, // 未知
        MAINPAGE: 0, // 主页
        POSTPAGE: 1, // 评论页
        FLOORREPLAYPAGE: 2, // 楼中楼页
    };

    let tie = undefined; // 一楼
    let tiebaName = undefined; // 吧名
    let lzId = ""; // 楼主id

    let currentHash = unsafeWindow.location.hash; // 存储当前页的Hash
    let oldHash = currentHash; // 存储上一链接的Hash
    let scrollPos = undefined; // 存储当前滚动位置

    let floorDataList = {} // 存储所有楼层数据以便搜索,索引为楼层数字符串,值为抓取的json。主要信息为pid,获取路径floorDataList[floor].id
    let currentUrlInfo = { // 截取当前url
        host: "tieba.baidu.com",
        tid: "",
        postAuthorId: "",
        forumId: "",
    }

    function DEBUGLOG(msg, label = "") {
        if (!DEBUGLOG_SWITCH) {
            return;
        }
        let outputFunc = console.log;
        if (label == "error") {
            outputFunc = console.error;
        }
        outputFunc(`voeoc(DEBUG)<${label}>: ${msg}`);
    }

    function waitElementLoaded(selector, func, TIME_OUT = 30) {
        //const TIME_OUT = 30; // 找n次没有找到就放弃
        let findTimeNum = 0; // 记录查找的次数
        let timer = setInterval(() => {
            let element = document.querySelector(selector);
            DEBUGLOG(`${selector}=${element}`, "waitElementLoaded");
            if (element != null) {
                // 清除定时器
                clearInterval(timer);
                func(element);
            } else {
                findTimeNum++;
                if (TIME_OUT < findTimeNum) {
                    // 清除定时器
                    clearInterval(timer);
                }
            }
        }, 200);
    }

    // 获取url参数
    function getUrlAttr(url, attrName) {
        return RegExp(`${attrName}=([^&]*)&?`, 'i').exec(url)[1].trim();
    }

    // 简单判断当前页面的类型
    function getPageType(hash = unsafeWindow.location.hash) {
        if (hash === "" || hash === "#/") {
            return PAGE_TYPE.MAINPAGE;
        } else if (REG_URL_POSTPAGE.test(hash)) {
            return PAGE_TYPE.POSTPAGE;
        } else if (REG_URL_FLOORREPLAYPAGE.test(hash)) {
            return PAGE_TYPE.FLOORREPLAYPAGE;
        }
        return PAGE_TYPE.UNKNOW;
    }

    // 展开对应楼中楼
    function openLzlPage(floorNum, floorElement, expandBtn) {
        // 另一种打开楼中楼的方法,但是需要页面切换
        //let newHash = `#/lzlPage?tid=${currentUrlInfo.tid}&pid=${pid}&floor=${floorNum}&postAuthorId=${currentUrlInfo.postAuthorId}&forumId=${currentUrlInfo.forumId}`;
        //DEBUGLOG(newHash, "newHash");
        //unsafeWindow.location.hash = newHash;

        const page_size = 20; // 单个页面评论数量,至少为10
        let pid = floorDataList[floorNum].id; // 楼层id
        let current_page = parseInt(expandBtn.getAttribute("current_page")); // 当前页数

        // 按钮动画
        expandBtn.disabled = true;
        expandBtn.classList.add("loading");
        function recoverExpandBtn() {
            expandBtn.disabled = false;
            expandBtn.classList.remove("loading");
        }

        let url = `${unsafeWindow.origin}/mg/o/getFloorData?pn=${current_page}&rn=${page_size}&tid=${currentUrlInfo.tid}&pid=${pid}`;
        DEBUGLOG(url, "GM_xmlhttpRequest");
        // 使用GM_xmlhttpRequest前需要添加对应url的@connect标记
        GM_xmlhttpRequest({
            method: "get",
            url: url,
            onload: function(details){
                try {
                    // 爬取解析楼中楼评论数据
                    let floorData = JSON.parse(details.responseText);
                    let pageinfo = floorData.data.page; // 楼中楼信息,包括楼层数、页面大小、页面数量

                    let pageSelector = undefined;

                    let subpostlist = floorData.data.sub_post_list; // 评论列表
                    // 加载楼中楼
                    let lzlParent = floorElement.querySelector("div.lzl-post");
                    let originItemList = lzlParent.getElementsByClassName("lzl-post-item");
                    let sampleItem = originItemList[0].cloneNode(true);
                    // 获取dataset数据
                    let dvlist = []
                    for(let dv in originItemList[0].querySelector(".thread-text").dataset){
                        dvlist.push(`data-${dv}`);
                    }
                    const CONTENT_TYPE = { // 页面类型
                        TEXT: 0, // 文本
                        EMOJI: 2, // 表情
                        USERNAME: 4, // 用户名,一般用作回复
                    };
                    //lzlParent.innerHTML = ""; // 删除原有
                    // 去掉前两个评论
                    if(current_page == 1) {
                        subpostlist.shift();
                        subpostlist.shift();
                    }
                    subpostlist.forEach(function(subpost){
                        let allContent = "";
                        let userid = subpost.author.id;
                        subpost.content.forEach(function(content){
                            let item = "";
                            switch(content.type) {
                                case CONTENT_TYPE.EMOJI:
                                    item = `<img ${dvlist[0]}="" src="${content.src}" alt="${content.text}"class="emotion-img">`
                                    break;
                                case CONTENT_TYPE.USERNAME:
                                    item = `<span ${dvlist[0]}="" class="user rich-link-disabled"> ${content.text} </span>`
                                    break;
                                case CONTENT_TYPE.TEXT:
                                default:
                                    // 如有其他的类型暂时用文本代替
                                    item = `<span ${dvlist[0]}="" class="text-content">${content.text}</span>`;
                                    break;
                            }
                            allContent += item;
                        })
                        let newItem = sampleItem.cloneNode(true);
                        newItem.querySelector(".username").innerHTML = `${subpost.author.show_nickname} ${lzId == subpost.author.id ?
                            `<svg ${dvlist[1]}="" class="landlord"><use xlink:href="#icon_landlord"></use></svg>`: ""}:`;
                        newItem.querySelector(".thread-text").innerHTML = allContent;
                        lzlParent.insertBefore(newItem, expandBtn.parentNode);

                        if(parseInt(pageinfo.total_page) > current_page) { // 仍有剩余评论未展开
                            expandBtn.setAttribute("current_page", current_page + 1);
                        } else { // 所有评论已展开
                            expandBtn.parentNode.style.display = "none";
                        }
                    });
                } finally {
                     recoverExpandBtn();
                }

            },
            onerror: function(details) {
                recoverExpandBtn();
                console.error(`无法加载评论区,爬取的url为${url}`);
            },
            onabort: onerror,
            ontimeout: onerror,
        });

    }

    // 检测URL Hash变化
    function checkUrlHashChange() {
        let newHash = unsafeWindow.location.hash;
        if(currentHash != newHash) {
            currentHash = newHash;
        } else {
            return false;
        }

        let pageType = getPageType();
        if (pageType == PAGE_TYPE.POSTPAGE) {
            // 收集url数据
            currentUrlInfo = {
                host: unsafeWindow.location.hostname,
                tid: getUrlAttr(currentHash, "tid"),
                postAuthorId: getUrlAttr(currentHash, "postAuthorId"),
                forumId: getUrlAttr(currentHash, "forumId"),
            }

            // 当页面变动时,刷新展开楼层的按钮
            waitElementLoaded(".post-page-list", (postpagelist) => { // 等待页面加载完成
                // 监听页面改变事件的回调
                function onNewFloorAdded(newFloor){
                    if(newFloor.classList.contains(STR_VOEOCMARK)) { // 已被打上标记
                        return;
                    }
                    newFloor.classList.add(STR_VOEOCMARK);

                    let expandBtnParent = newFloor.querySelector(".open-app-guide"); // 楼中楼展开按钮
                    if(!expandBtnParent) { // 原来的展开楼中楼在App查看按钮不存在
                        return;
                    }
                    let floorinfo = newFloor.querySelector(".floor-info"); // 楼层数
                    if(!floorinfo) { // 楼层数获取失败
                        return;
                    }

                    // 爬取当前的楼层数
                    let floorNum = RegExp(`第([0-9]+)楼`, 'i').exec(floorinfo.innerHTML)[1].trim();
                    // 创建新按钮
                    let expandBtn = document.createElement( "span");
                    expandBtn.className = "open-app-text-real";
                    expandBtn.innerHTML = STR_NEWOPENLZLTEXT;
                    expandBtn.setAttribute("current_page", 1);
                    expandBtn.onclick = function() {
                        openLzlPage(floorNum, newFloor, expandBtn);
                    };
                    // 替换新按钮
                    expandBtnParent.insertBefore(expandBtn, expandBtnParent.children[0]);
                };

                // 使用新按钮刷新楼层
                function searchAndUpdatePostPage() {
                    // 遍历所有楼层元素
                    let dlist = postpagelist.querySelectorAll(`div.post-item:not(.${STR_VOEOCMARK})`);
                    dlist.forEach(onNewFloorAdded);
                }

                // 注册楼层元素添加事件
                let config = {
                    attributes: false,
                    childList: true,
                    characterData: false,
                    subtree: false,
                };
                let observer = new MutationObserver(function(mutationList) {
                    searchAndUpdatePostPage();
                });
                observer.observe(postpagelist, config);
                searchAndUpdatePostPage();
            }, 10);

            // 恢复一楼显示
            restore();

            // 页面切换后恢复滚动位置
            scrollTo(scrollPos);
        } else if(pageType == PAGE_TYPE.MAINPAGE) {
            if(tie) {
                // 将剪切走的一楼复制回来
                waitElementLoaded("#replySwitch", (splitline) => { // 等待页面加载完成
                    splitline.parentNode.insertBefore(tie, splitline);
                }, 10);
                scrollTo(0);
            }
        }
        return true;
    }

    // 滚动到指定y坐标(如果当前楼层数比较大,只能滚动到贴末尾的最大加载位置)
    function scrollTo(yPos) {
        waitElementLoaded(".post-page", (postpage) => { // 等待页面加载完成
            document.documentElement.scrollTop = yPos;
            // 在一定时间内维持滚动位置
            setTimeout(function () {
                document.documentElement.scrollTop = yPos;
                DEBUGLOG(yPos, "scrollTo");
            }, 200);
        });
    }

    // 显示一楼的内容
    function restore() {
        DEBUGLOG("restore")
        waitElementLoaded(".text", (titletext) => { // 等待标题位置加载
            // 显示贴吧名
            try {
                let tiebaNameclone = tiebaName.cloneNode(true);
                titletext.parentNode.replaceChild(tiebaNameclone, titletext);
                // 关联点击贴吧名的事件
                tiebaNameclone.onclick = function () {
                    tiebaName.click();
                }
            } catch (e) { console.error(e); }

            // 显示楼主发帖层
            try {
                // 复原样式丢失
                tie.style.cssText = `
                    margin-left: 0.12rem;
                    margin-right: 0.12rem;
                    margin-bottom: 0.25rem;
                `
                // 尝试找回楼主丢失的头像
                try {
                    let lzavatar = tie.querySelector(".avatar");
                    lzavatar.style.backgroundImage = `url("${lzavatar.getAttribute("data-src")}")`
                } catch (e) { console.error(e); }

                // 尝试复原发帖内容的字体样式
                try {
                    let textContent = tie.querySelector(".thread-text");
                    textContent.style.cssText = `
                            margin-top: 0.18rem;
                            font-size: 0.16rem;
                            line-height: 0.28rem;
                        `
                } catch (e) { console.error(e); }

                let replySwitch = document.querySelector("#replySwitch"); // 标题下方的分割
                replySwitch.parentNode.insertBefore(tie, replySwitch);
            } catch (e) { console.error(e); }

            // 尝试复原标题样式
            try {
                let threadtitle = document.querySelector(".thread-title");
                threadtitle.style.cssText = `
                    margin-bottom: 0.13rem;
                    font-size: 0.22rem;
                    font-weight: 700;
                    line-height: 0.33rem;
                `

                // 置顶标题显示
                if (isFixedTitle) {
                    let threadtitleclone = threadtitle.cloneNode(true)
                    threadtitle.style.visibility = "hidden";
                    threadtitleclone.style.cssText += `
                        position: fixed !important;
                        z-index: 99 !important;
                        opacity: 0.8 !important;
                        background-color: #FFFFFF !important;
                    `
                    threadtitle.parentNode.insertBefore(threadtitleclone, threadtitle)
                }
            } catch (e) { console.error(e); }
        })
    }

    (function main() {
        // 删减多余的打开APP的按钮,减少遮挡
        GM_addStyle(`
            .comment-box, .only-lz, .nav-bar-bottom, .open-app, .more-image-desc {
                display: none !important;
            }
        `);

        // 隐藏原有的打开楼中楼App按钮
        GM_addStyle(`
            .open-app-text {
                display: none !important;
            }
            .open-app-text-real {
                display: block !important;
                -webkit-box-flex: 0;
                -webkit-flex: none;
                -ms-flex: none;
                flex: none;
                font-size: .13rem;
                color: #614ec2;
            }
            @keyframes rotate {
                0%{-webkit-transform:rotate3d(1, 0, 0, 0deg);}
                25%{-webkit-transform:rotate3d(1, 0, 0, 90deg);}
                50%{-webkit-transform:rotate3d(1, 0, 0, 180deg);}
                75%{-webkit-transform:rotate3d(1, 0, 0, 270deg);}
                100%{-webkit-transform:rotate3d(1, 0, 0, 360deg);}
            }
            .open-app-text-real.loading { animation: rotate 0.5s linear infinite; }

        `);

        (function() {
            let oldXHR = unsafeWindow.XMLHttpRequest;
            // 监听楼层加载的网络事件
            unsafeWindow.XMLHttpRequest = function() {
                let realXHR = new oldXHR();
                realXHR.addEventListener('readystatechange', function() {
                    if(REG_URL_PBDATA.test(realXHR.responseURL) && realXHR.response != "") {
                        let data = JSON.parse(realXHR.response).data;
                        let post_list = data.post_list;
                        // 获取楼主id
                        try {
                            lzId = post_list[0].author.id;
                        } catch(e){console.error(e)}

                        // 获取楼层信息
                        for (let i = 0; i < post_list.length; i++) {
                            let d = post_list[i];
                            floorDataList[d.floor] = d;
                        }
                    }
                }, false);
                return realXHR;
            }
        })();

        unsafeWindow.onscroll = function () {
            checkUrlHashChange();

            if (getPageType() == PAGE_TYPE.POSTPAGE) {
                if(unsafeWindow.pageYOffset != 0) {
                    // 记录当前滚动位置
                    scrollPos = unsafeWindow.pageYOffset;
                    //DEBUGLOG(scrollPos, "scrollPos");
                }
            }
        }

        unsafeWindow.onhashchange = function(e) {
            checkUrlHashChange();
        }

        switch (getPageType()) {
            case PAGE_TYPE.MAINPAGE:
                waitElementLoaded(".post-page-entry-btn", (postbtn) => {
                    tiebaName = document.querySelector(".forum-block"); // 获取吧名
                    tie = document.querySelector(".main-thread-content"); // 获取楼主发帖内容

                    // 点击展开按钮
                    postbtn.click();

                    // 手动触发页面刷新检测
                    checkUrlHashChange();
                })
                break;
            case PAGE_TYPE.POSTPAGE:
                // 如果当前刷新加载的是评论页,则需要先打开主页面获取数据
                unsafeWindow.location.hash = "";
                unsafeWindow.location.reload();
                break;
            case PAGE_TYPE.FLOORREPLAYPAGE:
                break;
            default:
                break;
        }

    })()
})();