Greasy Fork

Greasy Fork is available in English.

笔趣阁外观优化

专注阅读

当前为 2021-11-16 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         笔趣阁外观优化
// @namespace    https://gitee.com/linhq1999/OhMyScript
// @version      3.1
// @description  专注阅读
// @author       LinHQ
// @match        http*://www.shuquge.com/*.html
// @exclude      http*://www.shuquge.com/*index.html
// @match        http*://www.sywx8.com/*.html
// @match        http*://www.biqugetv.com/*.html
// @match        http*://www.bqxs520.com/*.html
// @match        http*://www.mht99.com/*.html
// @grant        GM_addStyle
// @grant        GM_openInTab
// @grant        GM_xmlhttpRequest
// @inject-into  auto
// @license MIT
// ==/UserScript==
'use strict';
/** 配置示例
* "sites": [
*   {
*       "desc": "shuquge", 网站链接关键字
*       "url": "https://....", 网站首页链接
*       "main": "div.reader", 主要部分选择器
*       "title": ".reader h1", 标题选择器
*       "txt": "#content", 文字部分选择器
*       "toc": "dd a", 目录链接选择器
*       "tocJump": 12, 跳过前面多少章
*       "filter": ["div.header", "div.nav", "div.link"], 带有此选择器的元素将被删除
*       "txtfilter": ["shuqu"] 带有此关键字的行将被删除
*       "funcFilter": () => void, 自定义过滤器
*       search?: (keywords: string, baseurl:string) => Promise<Link[]> 搜索行为
*   }
* ]
*/
(() => {
    // 缺省值,一般不用修改
    const lineHeight = 1.3, defaultFont = "楷体";
    let C = {
        "sites": [
            {
                "desc": "shuquge",
                "url": "https://www.shuquge.com/",
                "main": "div.reader",
                "title": ".reader h1",
                "txt": "#content",
                "toc": "dd a",
                "tocJump": 12,
                "filter": [
                    "div.header", "div.nav", "div.link", "img",
                    "#coupletleft", "#coupletright", "#HMRichBox"
                ],
                "txtfilter": ["shuqu"],
                "funcFilter": () => { var _a, _b; return (_b = (_a = document.querySelector("#content")) === null || _a === void 0 ? void 0 : _a.previousSibling) === null || _b === void 0 ? void 0 : _b.remove(); }
            },
            {
                "desc": "sywx",
                "url": "https://www.sywx8.com/",
                "main": "div#container",
                "title": "div>h1",
                "toc": "li a",
                "tocJump": 0,
                "txt": "div#BookText",
                "filter": ["div.top", ".link.xb", "#footer"],
                "txtfilter": ["最快更新", "松语"]
                // javascript 不支持 gbk 的 uri 编码,所以无法实现
            },
            {
                "desc": "bqxs",
                "url": "http://www.bqxs520.com/",
                "main": ".box_con",
                "title": "div.content_read h1",
                "toc": "#list dd a",
                "tocJump": 9,
                "txt": "#content",
                "filter": [".ywtop", ".header", ".nav", ".bottem1", ".lm", "#page_set", ".bookname~.box_con"],
                "txtfilter": [],
                "search": async (keywords, baseurl) => {
                    let links = [];
                    let form = new URLSearchParams();
                    form.append("key", keywords);
                    let doc = await search({
                        "method": "POST",
                        "headers": { "Content-Type": "application/x-www-form-urlencoded" },
                        "url": encodeURI(`http://www.bqxs520.com/case.php?m=search`),
                        "data": form.toString(),
                        "responseType": "document",
                    });
                    for (let a of doc.querySelectorAll(".l .s2 a")) {
                        links.push({ "title": `(bqxs) ${a.textContent}`, "href": concatURL(baseurl, attr(a, "href")) });
                    }
                    return links;
                }
            },
            {
                "desc": "biqugetv",
                "url": "https://www.biqugetv.com/",
                "main": ".box_con",
                "title": "div.content_read h1",
                "toc": "#list dd a",
                "tocJump": 0,
                "txt": "#content",
                "filter": [".ywtop", ".header", ".nav", ".bottem1", ".lm", "#page_set"],
                "txtfilter": [],
                "search": async (keywords, baseurl) => {
                    let links = [];
                    let doc = await search({
                        "url": encodeURI(`https://www.biqugetv.com/search.php?keyword=${keywords}`),
                        "responseType": "document",
                    });
                    for (let a of doc.querySelectorAll("h3 a")) {
                        links.push({ "title": `(biqugetv) ${a.textContent}`, "href": concatURL(baseurl, attr(a, "href")) });
                    }
                    return links;
                }
            },
            {
                "desc": "mht99",
                "url": "http://www.mht99.com/",
                "main": ".box_con",
                "title": "div.content_read h1",
                "toc": "#list dd a",
                "tocJump": 9,
                "txt": "#content",
                "filter": [".ywtop", ".header", ".nav", ".bottem1", ".lm", "#page_set", ".bookname~.box_con"],
                "txtfilter": [],
                "search": async (keywords, baseurl) => {
                    let links = [];
                    let form = new URLSearchParams();
                    form.append("key", keywords);
                    let doc = await search({
                        "method": "POST",
                        "headers": { "Content-Type": "application/x-www-form-urlencoded" },
                        "url": `http://www.mht99.com/case.php?m=search`,
                        "data": form.toString(),
                        "responseType": "document",
                    });
                    for (let a of doc.querySelectorAll(".l .s2 a")) {
                        links.push({ "title": `(mht) ${a.textContent}`, "href": concatURL(baseurl, attr(a, "href")) });
                    }
                    return links;
                }
            }
        ],
        "states": {
            "fontSize": 16,
            "lineHeight": 16 * lineHeight,
            "toc": false
        },
        "style": `
            body {
                background-color: #EAEAEF !important;
            }

            .bqg.inject.win {
                width: 55vw !important;
                min-width: 600px;
                border: 2px double gray !important;
                border-radius: 8px;
            }

            .bqg.inject.txt {
                font-family: Calibri,'${defaultFont}',serif!important;
                background-color: #EAEAEF !important;
                padding: 0.5em 1em !important;
                margin: 0.5em auto !important;
                width: auto !important;
            }

            .bqg.inject.title {
                color: black;
                background-color: #EAEAEF;
                font-family: Calibri,'${defaultFont}',serif!important;
                cursor: pointer !important;
            }

            .bqg.inject.title:hover {
                color: #0258d8 !important;
            }
            
            .hq.inject.toc {
                font-family: Calibri,sans-serif;
                width: 275px;
                position: fixed;
                top: 30px;
                padding: 5px;
                display: flex;
                flex-flow: column;
                transition: left 0.5s cubic-bezier(0.35, 1.06, 0.83, 0.99);
                background: rgb(246 246 246 / 60%);
                border-radius: 8px;
            }

            .hq.inject ul {
                height: 280px;
                width: 100%;
                /*offsetTop 计算需要*/
                position:relative;
                overflow: auto;
            }

            .hq.inject ul li {
                cursor: pointer;
                margin: 2px;
                width: 95%;
                padding: 1px 4px;
                font-size: 12px;
                border-radius: 4px;
            }

            .hq.inject ul li:hover {
                background: #0258d8;
                color: #f6f6f6;
            }

            .hq.inject.toc>h3 {
                font-size: 1.1rem;
                font-weight: bold;
                border-radius: 2px;
                align-self: center;
                cursor: pointer;
                margin: 4px 0 8px 0;
            }

            .hq.inject.toc>h3:hover {
                color: #ffa631 !important;
            }

            .hq.inject.search {
                font-family: Calibri,sans-serif;
                width: 275px;
                position: fixed;
                top: 30px;
                padding: 5px;
                display: flex;
                flex-flow: column;
                transition: right 0.5s cubic-bezier(0.35, 1.06, 0.83, 0.99);
                background: rgb(246 246 246 / 60%);
                border-radius: 8px;
            }

            .hq.inject.search input {
                margin: 8px auto;
                width: 95%;
            }
            `
    };
    // 查询已经保存的字体信息
    let savedStates = localStorage.getItem("bqg_cfg");
    // 检查是否存在已有设置且和当前版本相符
    let states;
    if (savedStates === null) {
        states = C.states;
        console.warn("当前状态已保存");
    }
    else {
        let cfg = JSON.parse(savedStates);
        let defaultStates = Object.keys(C.states);
        let cfg_ = Object.keys(cfg);
        let useSaved = true;
        // 检查键是否匹配
        if (defaultStates.length == cfg_.length) {
            for (let key of Object.keys(cfg)) {
                if (!defaultStates.includes(key)) {
                    useSaved = false;
                    break;
                }
            }
        }
        else {
            useSaved = false;
        }
        if (useSaved) {
            states = cfg;
        }
        else {
            states = C.states;
            console.warn("检测到版本变化,状态已重置");
        }
    }
    // 检测当前的网址,应用对应的设置
    let tmp = C.sites.filter(site => document.URL.includes(site.desc));
    if (tmp.length == 0) {
        console.warn("没有匹配的设置,脚本已终止!");
        return;
    }
    let currentSite = tmp[0];
    // 完成样式注入
    GM_addStyle(C.style);
    let saveStates = () => {
        localStorage.setItem("bqg_cfg", JSON.stringify(states));
    };
    // 上一章
    let prevChapter = () => {
        var _a;
        let prevs = document.querySelectorAll("a");
        for (const prev of prevs) {
            if ((_a = prev.textContent) === null || _a === void 0 ? void 0 : _a.includes("上一")) {
                prev.click();
                break;
            }
        }
    };
    // 下一章
    let nextChapter = () => {
        var _a;
        let nexts = document.querySelectorAll("a");
        for (const next of nexts) {
            if ((_a = next.textContent) === null || _a === void 0 ? void 0 : _a.includes("下一")) {
                next.click();
                break;
            }
        }
    };
    // 目录开关
    let toggleToc = () => {
        let toc = document.querySelector(".hq.inject.toc");
        if (parseInt(toc.style.left) < 0) {
            toc.style.left = "8px";
            states.toc = true;
        }
        else {
            toc.style.left = "-300px";
            states.toc = false;
        }
        // 每一次触发目录操作都保存一次状态
        saveStates();
    };
    // 对可变部分产生影响
    let doInject = function () {
        var _a, _b;
        // 执行元素过滤
        currentSite.filter.forEach(filter => { var _a; return (_a = document.querySelectorAll(filter)) === null || _a === void 0 ? void 0 : _a.forEach(ele => ele.remove()); });
        // 执行自定义过滤
        if (currentSite.funcFilter) {
            currentSite.funcFilter();
        }
        // 应用已经保存的状态
        let textWin = document.querySelector(currentSite.txt);
        textWin.setAttribute("style", `font-size:${states.fontSize}px;line-height:${states.lineHeight}px`);
        textWin.classList.add("bqg", "inject", "txt");
        // 执行文字过滤
        if (currentSite.txtfilter !== undefined) {
            textWin.innerText = (_b = (_a = textWin.innerText) === null || _a === void 0 ? void 0 : _a.split("\n\n")) === null || _b === void 0 ? void 0 : _b.filter(line => {
                for (const key of currentSite.txtfilter) {
                    if (line.includes(key)) {
                        return false;
                    }
                }
                return true;
            }).join("\n\n");
        }
        let mainWin = document.querySelector(currentSite.main);
        mainWin.classList.add("bqg", "inject", "win");
        let title = document.querySelector(currentSite.title);
        title.title = "点击显示目录";
        title.classList.add("bqg", "inject", "title");
        title.onclick = (ev) => {
            toggleToc();
            // 避免跳到上一章
            // 比下面的更为具体,所以有效。
            ev.stopPropagation();
        };
        // 阻止双击事件被捕获(双击会回到顶部)
        document.body.ondblclick = (ev) => ev.stopImmediatePropagation();
        document.body.onclick = (ev) => {
            let root = document.documentElement;
            let winHeight = window.innerHeight;
            // 下半屏单击下滚,反之上滚
            if (ev.clientY > root.clientHeight / 2) {
                if (root.scrollTop + winHeight >= root.scrollHeight) {
                    nextChapter();
                }
                window.scrollBy({ top: (window.innerHeight - lineHeight) * 1 });
            }
            else {
                if (root.scrollTop === 0) {
                    prevChapter();
                }
                window.scrollBy({ top: (window.innerHeight - lineHeight) * -1 });
            }
        };
        document.body.onkeydown = (ev) => {
            switch (ev.key) {
                case "-":
                    states.fontSize -= 2;
                    textWin.style.fontSize = `${states.fontSize}px`;
                    states.lineHeight = states.fontSize * lineHeight;
                    textWin.style.lineHeight = `${states.lineHeight}px`;
                    saveStates();
                    break;
                case "=":
                    states.fontSize += 2;
                    textWin.style.fontSize = `${states.fontSize}px`;
                    states.lineHeight = states.fontSize * lineHeight;
                    textWin.style.lineHeight = `${states.lineHeight}px`;
                    saveStates();
                    break;
                case "j":
                    window.scrollBy({ top: window.innerHeight - states.lineHeight });
                    break;
                case "k":
                    window.scrollBy({ top: -1 * (window.innerHeight - states.lineHeight) });
                    break;
                case "h":
                    prevChapter();
                    break;
                case "l":
                    nextChapter();
                    break;
                case "t":
                    toggleToc();
                    break;
                case "s":
                    toggleSearch();
                    break;
                default:
                    break;
            }
        };
    };
    // 先调用一次,后面是有变化时才会触发,避免有时无法起作用
    doInject();
    // 强力覆盖
    new MutationObserver((_, ob) => {
        doInject();
    }).observe(document.body, { childList: true });
    // 添加目录
    let toc = document.createElement("div");
    toc.onclick = ev => ev.stopPropagation();
    toc.className = "hq inject toc";
    // 已保存状态读取
    toc.style.left = (states.toc) ? "8px" : "-300px";
    document.body.append(toc);
    // 目录状态指示灯
    let pointer = document.createElement("h3");
    let pointerColors = { "loaded": "#afdd22", "loading": "#ffa631", "unload": "#ed5736" };
    pointer.title = "点击以重新加载目录";
    pointer.innerHTML = "目<span style='display: inline-block;width: 1em'></span>录";
    pointer.style.color = pointerColors.unload;
    toc.append(pointer);
    // 目录列表
    let ul = document.createElement("ul");
    toc.append(ul);
    let renderToc = function (toc, ul) {
        // 清空旧内容
        ul.innerHTML = "";
        let current = null;
        // 进度计数器
        let counter = 1;
        for (let lnk of toc) {
            let li = document.createElement("li");
            li.textContent = lnk.title;
            if (current == null && document.URL == lnk.href) {
                li.innerHTML = `${lnk.title}<span style="flex: 1;"></span>${(counter / toc.length * 100).toFixed(1)}%`;
                current = li;
            }
            li.onclick = (ev) => {
                document.location.href = lnk.href;
                ev.stopPropagation();
            };
            ul.append(li);
            counter++;
        }
        // 渲染完修改指示灯状态
        pointer.style.color = pointerColors.loaded;
        // 滚动到当前位置,并高亮
        if (current !== null) {
            current.setAttribute("style", "display:flex;font-weight:bold;background: #0258d8;color: #f6f6f6;");
            ul.scrollTo({ top: current.offsetTop - 130 });
        }
    };
    // fetchTOC 获取目录信息并重新渲染
    let fetchTOC = function (currentBookLink, pointer) {
        // 修改指示灯状态
        pointer.style.color = pointerColors.loading;
        GM_xmlhttpRequest({
            url: currentBookLink,
            // 直接返回 dom
            responseType: "document",
            onload: resp => {
                var _a;
                let doc = resp.response;
                let tocs = doc.querySelectorAll(currentSite.toc);
                let data = [];
                // 序列化存储准备
                for (let link of tocs) {
                    data.push({ "title": (_a = link.textContent) !== null && _a !== void 0 ? _a : "", "href": link.href });
                }
                if (currentSite.tocJump)
                    data = data.slice(currentSite.tocJump + 1);
                // 缓存目录信息
                sessionStorage.setItem(currentBookLink, JSON.stringify(data));
                renderToc(data, ul);
            },
            onerror: (_) => pointer.style.color = pointerColors.unload
        });
    };
    let source = document.URL.split("/");
    source.pop();
    // 最后加斜杠保险
    let currentBook = source.join("/") + "/";
    let currentBookToc = sessionStorage.getItem(currentBook);
    if (currentBookToc === null) {
        fetchTOC(currentBook, pointer);
    }
    else {
        renderToc(JSON.parse(currentBookToc), ul);
    }
    // 单击指示灯刷新目录缓存
    pointer.onclick = () => fetchTOC(currentBook, pointer);
    // 添加聚合搜索
    let searchBox = document.createElement("div");
    searchBox.onclick = ev => ev.stopPropagation();
    searchBox.onkeydown = ev => ev.stopPropagation();
    searchBox.className = "hq inject search";
    searchBox.style.right = "-300px";
    searchBox.innerHTML = `
    <input id="insearch" type="search" placeholder="至少输入两个字"/>
    `;
    document.body.append(searchBox);
    let inputBox = document.querySelector("#insearch");
    let search_ul = document.createElement("ul");
    searchBox.append(search_ul);
    // debounce 一下不然顶不住
    let timer = null;
    inputBox.oninput = ev => {
        if (timer !== null)
            clearTimeout(timer);
        if ((inputBox === null || inputBox === void 0 ? void 0 : inputBox.value.length) < 2)
            return;
        timer = setTimeout(() => {
            let requests = [];
            let others = [{ "title": "没有搜索结果,也可以看看:", "href": "#" }];
            for (let s of C.sites) {
                if (s.search !== undefined) {
                    requests.push(s.search(inputBox === null || inputBox === void 0 ? void 0 : inputBox.value, s.url));
                }
                else {
                    others.push({ "title": s.desc, "href": s.url });
                }
            }
            let count = 0;
            Promise.allSettled(requests).then(list => {
                search_ul.innerHTML = "";
                for (let site of list) {
                    if (site.status === "fulfilled") {
                        for (let lnk of site.value) {
                            let li = document.createElement("li");
                            li.textContent = lnk.title.trim();
                            li.onclick = ev => GM_openInTab(lnk.href, { active: true });
                            search_ul.append(li);
                            count++;
                        }
                    }
                }
                // 处理一下没有结果的情况
                if (count === 0) {
                    for (let o of others) {
                        let li = document.createElement("li");
                        li.textContent = o.title;
                        li.onclick = ev => GM_openInTab(o.href, { active: true });
                        search_ul.append(li);
                    }
                }
            });
        }, 1000);
    };
    // 搜索框开关
    function toggleSearch() {
        if (parseInt(searchBox.style.right) < 0) {
            searchBox.style.right = "8px";
            // states.searchBox = true;
        }
        else {
            searchBox.style.right = "-300px";
            // states.searchBox = false;
        }
        // 每一次触发目录操作都保存一次状态
        // saveStates();
    }
    // 封装一下搜索
    async function search(details) {
        return new Promise((res, rej) => {
            details.onload = resp => res(resp.response);
            details.onerror = rej;
            GM_xmlhttpRequest(details);
        });
    }
    // 拼接 URL
    function concatURL(host, path) {
        let url = new URL(host);
        url.pathname = path;
        return url.toString();
    }
    /**
     * 获取属性,如果是 a 标签,且想要获取字面上的 href,不可以用 a.href
     */
    function attr(ele, attr) {
        var _a;
        return (_a = ele.getAttribute(attr)) !== null && _a !== void 0 ? _a : "";
    }
    //TODO: 瀑布流
})();