B站分P搜索

在B站的多P稿件和合集中添加搜索框,进行内容搜索

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         B站分P搜索
// @namespace    https://github.com/LianTianYou
// @version      1.0.2
// @description  在B站的多P稿件和合集中添加搜索框,进行内容搜索
// @author       LianTianYou
// @license      MIT
// @match        https://www.bilibili.com/video/*
// @icon         https://www.bilibili.com/favicon.ico
// @grant        none
// ==/UserScript==

(() => {

    const clearBox = document.createElement("div");
    const isDebug = false;

    function debug(...msg) {
        const isDebug = false;

        if (isDebug) {
            console.log(...msg);
        };
    }

    /**
     * 清空筛选结果
     */
    function clearFilter(data) {
        if (data == null) return;
        if (data.parts == null || data.parts.length === 0) return;
        if (data.pages == null || data.pages.length === 0) return;

        data.parts.forEach((part, index) => {
            data.pages[index].style.display = "flex";
            part.innerHTML = part.textContent;
        });


        /* 定位到正在播放的分P */
        // 分P的容器
        const listContainer = document.querySelector('.video-pod__body');
        // 当前选择的分P
        const selectedItem = document.querySelector('.video-pod__item.active');

        if (listContainer && selectedItem) {
            // const offsetTop = selectedItem.offsetTop - listContainer.offsetTop;
            listContainer.scrollTo({ top: selectedItem.offsetTop, behavior: 'instant' });
        }
    }

    /**
     * 根据关键字进行筛选
     */
    function toggleFilter(data, keyword) {
        if (keyword == null || data == null) return;
        if (data.parts == null || data.parts.length === 0) return;
        if (data.pages == null || data.pages.length === 0) return;

        const regex = new RegExp(`(${keyword})`, "ig");
        debug(keyword);
        debug(regex);
        data.parts.forEach((part, index) => {
            const page = data.pages[index];
            let value = part.textContent;
            if(value.search(regex) != -1) {
                // 匹配到关键词
                page.style.display = "flex";
                part.innerHTML = value.replaceAll(regex, '<em class="keyword">$1</em>');
            } else {
                // 未匹配项
                page.style.display = "none";
            }
        });
    }

    /**
     * 根据搜索框的结果进行处理
     */
    function searchFilter(data, keyword = "") {
        keyword = keyword.trim();
        if (!keyword) {
            // 关键词为空
            clearFilter(data);
        } else {
            // 有关键词
            toggleFilter(data, keyword);
        }
    }

    /**
     * 改变清空按钮的显示状态
     */
    function changeClearBtn(value) {
        if (!value || value.trim() === "") {
            clearBox.style.display = "none";
        } else if(clearBox.style.display !== "block") {
            clearBox.style.display = "block";
        }
    }

    /**
     * 添加 style 标签
     */
    function addStyle() {
        let styleTag = document.createElement("style");
        styleTag.type = "text/css";
        styleTag.id = "bili-filter";
        let styleCode = `
            .search-box {
                margin: 5px auto 0 auto;
                /* padding: 0 10px; */
                background: #F1F2F3;
                height: 44px;
                display: flex;
                align-items: center;
            }
            .search-box > input.search {
                height: 34px;
                width: 100%;
                padding: 0 10px;
                font-size: 14px;
                outline: none;
                border: 1px solid #e3e5e7;
                border-radius: 5px;
                caret-color: #5e5e5e;
            }
            .search-box > input.search:focus {
                border: 1px solid #00aeec;
            }
            .keyword {
                color: #f25d8e;
                font-style: normal;
            }
            .clear-box {
                display: none;
                position: absolute;
                right: 16px;
            }
            .clear-btn {
                width: 20px;
                height: 20px;
                cursor: pointer;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            .clear-btn > svg {
                height: 8px;
                width: 8px;
                fill: #61666d;
            }
        `;
        styleTag.appendChild(document.createTextNode(styleCode));
        document.querySelector("head").appendChild(styleTag);
    }

    /**
     * 创建自定义的 DOM 元素,并返回容器的节点
     */
    function createElement(data) {
        // 插入 style 标签
        addStyle();

        // 容器
        const searchBox = document.createElement("div");
        searchBox.className = "search-box";

        // 搜索框
        const search = document.createElement("input");
        search.type = "search";
        search.className = "search";
        search.placeholder = "搜索分P...";
        // search.addEventListener("change", function(e) {
        //     searchFilter(data, e.target.value);
        // });
        search.addEventListener("input", function(e) {
            searchFilter(data, e.target.value);
            changeClearBtn(e.target.value);
        });
        searchBox.append(search);

        // 清空按钮
        clearBox.className = "clear-box";
        const clearBtn = document.createElement("div");
        clearBtn.className = "clear-btn";
        clearBtn.innerHTML = '<svg t="1706028151814" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9157" width="200" height="200"><path d="M632.117978 513.833356l361.805812 361.735298a85.462608 85.462608 0 1 1-121.001515 120.789974L511.116463 634.552816 146.913186 998.756094a86.026718 86.026718 0 0 1-121.706652-121.706652L389.480325 512.775651 27.674513 150.969839A85.392095 85.392095 0 0 1 148.393973 30.250379L510.199785 392.056191l366.671258-366.671258a86.026718 86.026718 0 0 1 121.706652 121.706652z" p-id="9158"></path></svg>';
        clearBtn.addEventListener("click", function() {
            search.value = "";
            searchFilter(data);
            changeClearBtn("");
        });
        clearBox.append(clearBtn);
        searchBox.append(clearBox);

        return searchBox;
    }

    /**
     * 在普通分P中添加元素
     */
    function insertToPages() {
        const data = {
            pages: document.querySelectorAll(".video-pod__body .video-pod__item.normal"),
            parts: document.querySelectorAll(".video-pod__body .video-pod__item.normal div.title-txt")
        };

        if (data.pages == null || data.pages.length === 0 || data.parts == null || data.parts.length === 0) {
            return;
        }
        // 将元素插入到网页中
        const searchBox = createElement(data);
        const pageList = document.querySelector(".video-pod__header .header-top");     // 分P列表
        if (pageList) {
            pageList.after(searchBox);
        }
    }

    /**
     * 在合集中添加元素
     */
    function insertToSections() {
        const data = {
            pages: document.querySelectorAll(".video-episode-card"),
            parts: document.querySelectorAll(".video-episode-card .video-episode-card__info-title")
        };

        // 将元素插入到网页中
        const searchBox = createElement(data);
        // const headCon = document.querySelector(".video-sections-head");
        // headCon.after(searchBox);
        // document.querySelector(".video-section-list").style.height = "auto";
    }

    /** 等待元素出现 */
    function waitExist(selecter, timeout = 5, check = null) {
        let interval = 50;
        let count = 0;

        const p = new Promise((resolve, reject) => {
            const timer = setInterval(() => {
                const ele = document.querySelector(selecter);

                if (interval * count > timeout * 1000) {
                    clearInterval(timer);
                    // debug("等待超时");
                    reject(new Error("等待超时"));
                }
                count++;

                debug(ele);

                if (!ele) return;
                if (check && !check(ele)) return;

                clearInterval(timer);
                resolve(ele);
            }, interval);
        });
        return p;
    }

    /**
     * 入口函数
     */
    async function main() {
        try {
            // 等待弹幕列表出现
            await waitExist("#danmukuBox", 5);
            debug("#danmukuBox 出现");

            // 等待分P列表出现
            await waitExist(".video-pod__body", 5);
            debug(".video-pod__body 出现");
            // 等待分P列表顶部操作栏出现
            await waitExist(".video-pod__header > .header-top", 5);
            debug(".header-top 出现");
            await waitExist(".video-pod__item.active", 5);
            // 等待弹幕列表的标题出现
            await waitExist("#danmukuBox .bui-dropdown-name", 5);
            debug(".bui-dropdown-name 出现");
            insertToPages();

//             if (document.querySelector(".video-pod__body")) {
//                 // 等待分P列表出现
//                 debug(".video-pod__body 出现");
//                 // 等待分P列表顶部操作栏出现
//                 await waitExist(".video-pod__header > .header-top", 5);
//                 debug(".header-top 出现");
//                 insertToPages();
//             } else if(document.querySelector(".base-video-sections-v1")) {
//                 debug("sections 出现");

//                 const result = await waitExist(".video-episode-card__info-playing .cur-play-icon", 5, (e) => e.style.display !== "none");
//                 debug("cur-play-icon 出现");
//                 debug(result);
//                 await new Promise((resolve) => requestAnimationFrame(resolve));
//                 insertToSections();
//                 requestIdleCallback(() => {
//                     debug("浏览器空闲");
//                     // debugger;
//                 });
//             }
        } catch(err) {
            debug(err);
        }

        debug("执行完毕");
    }

    window.onload = () => {
        debug("onloaded");
        main();
    };
})();