Greasy Fork

Greasy Fork is available in English.

Aerfaying Explore - 阿儿法营/稽木世界社区优化插件

提供优化、补丁及小功能提升社区内的探索效率和用户体验

目前为 2022-11-05 提交的版本,查看 最新版本

// ==UserScript==
// @name         Aerfaying Explore - 阿儿法营/稽木世界社区优化插件
// @namespace    waterblock79.github.io
// @version      1.9.2
// @description  提供优化、补丁及小功能提升社区内的探索效率和用户体验
// @author       waterblock79
// @match        http://gitblock.cn/*
// @match        https://gitblock.cn/*
// @match        http://aerfaying.com/*
// @match        https://aerfaying.com/*
// @match        http://3eworld.cn/*
// @match        https://3eworld.cn/*
// @icon         https://gitblock.cn/Content/logo.ico
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        unsafeWindow
// @license      MIT
// ==/UserScript==
/*
   aerfaying-explore 是一个非官方的、针对阿儿法营/稽木世界社区的开源优化插件
   https://github.com/waterblock79/aerfaying-explore
*/

(function () {
    'use strict';
    // 初始化信息
    var window = unsafeWindow || window;
    const version = '1.9.2';

    // 判断 GM_setValue、GM_getValue 是否可用(貌似不存在的话,获取就报错,不能像 foo == undefined 那样获取它是否存在)
    try {
        if (GM_getValue && GM_setValue) {
            window.GMAvailable = true;
        } else {
            window.GMAvailable = false;
        }
    } catch (e) {
        window.GMAvailable = false;
    }

    //  $(selector)
    //  即 document.querySelectorAll(selector)
    const $ = (selector) => document.querySelectorAll(selector);


    //  addSelectorEvent(selector, event, callback)
    //  为全部符合 selector 选择器的元素自动添加 event 事件,若该事件被触发就会执行 callback 回调
    let eventElement = [];
    const addSelectorEvent = (selector, event, callback) => {
        if (Array.isArray(event)) {
            for (let i in event) {
                addSelectorEvent(selector, event[i], callback);
            }
            return;
        }
        eventElement.push({
            selector: selector,
            event: event,
            callback: callback,
            handledElements: []
        })
    }
    window.addSelectorEvent = addSelectorEvent;


    //  addFindElement(selector, callback)
    //  当选择器发现新的符合 selector 的元素就执行 callback,callback 会传入该元素。
    let findElement = [];
    const addFindElement = (selector, callback) => {
        findElement.push({
            selector: selector,
            callback: callback,
            handledElements: []
        })
        // 此处返回该任务在 findElement 中的 index,方便后续删除该任务。
        return findElement.length - 1;
    };
    window.addFindElement = addFindElement;

    // addHrefChangeEvent(callback) 
    // 当页面 location.href 改变触发该事件
    let lastHref = null;
    let hrefChangeEvent = [];
    const addHrefChangeEvent = (callback) => {
        hrefChangeEvent.push({
            callback: callback,
        });
    };

    //  →_→
    //  通过 setInterval 实现 addFindElement 和 addSelectorEvent。
    setInterval(() => {
        // addFindElement
        findElement.forEach((item) => {
            $(item.selector).forEach((element) => {
                if (!item.handledElements.find(e => e == element)) {
                    item.handledElements.push(element);
                    (async () => { item.callback(element) })();
                }
            })
        })
        // addSelectorEvent
        eventElement.forEach((item) => {
            $(item.selector).forEach((element) => {
                if (!item.handledElements.find(e => e == element)) {
                    element.addEventListener(item.event, item.callback);
                    item.handledElements.push(element);
                }
            })
        });
        // addHrefChangeEvent
        if (lastHref != location.href) {
            hrefChangeEvent.forEach((item) => {
                (async () => { item.callback(location.href) })();
            });
        }
        lastHref = location.href;
    }, 16);


    //  addStyle(css)
    //  将 CSS 塞到 <style> 标签里然后添加到页面中
    const addStyle = (css) => {
        const style = document.createElement('style');
        style.innerHTML = css;
        document.head.appendChild(style);
    };


    //  insertBefore(newElement, targetElement)
    //  把 newElement 插入到 targetElement 前面
    const insertBefore = (newElement, targetElement) => {
        targetElement.parentNode.insertBefore(newElement, targetElement);
    };

    // encodeHTML(str)
    // 转义字符串中的 HTML 字符
    const encodeHTML = (str) => {
        let textNode = document.createTextNode(str);
        let div = document.createElement('div');
        div.append(textNode);
        return div.innerHTML;
    };


    // 监听请求(这里用的是 jQuery 的 $)
    window.$(document).ajaxSuccess(function (event, xhr, settings, response) {
        if (settings.url.search(/WebApi\/Projects\/[0-9]+\/Get/) == 1) { // /WebApi/Projects/*/Get 获取作品信息
            projectThumbId = response.project.thumbId; // 在变量里保存获取到的作品封面
        }
        if (settings.url == '/WebApi/Comment/GetPage') { // /WebApi/Comment/GetPage 评论
            response.replies.forEach((comment) => {
                commentData[comment.id] = comment;
            });
            response.pagedThreads.items.forEach((comment) => {
                commentData[comment.id] = comment;
            });
        }
        if (settings.url == '/WebApi/Comment/Post') { // 匹配用户发送评论 /WebApi/Comment/Post
            commentData[response.comment.id] = response.comment;
        }
    });

    //  自动 HTTPS
    if (localStorage['explore:https'] == 'true') {
        if (location.toString().startsWith("http://")) {
            location = location.toString().replace("http://", "https://", 1);
        }
    }

    //  替换原不可用的 asset.mozhua.org:444 的资源地址
    addFindElement('img[src*="asset.mozhua.org:444"]', (element) => {
        element.src = element.src.replace('https://asset.mozhua.org:444/Media?name=', 'https://cdn.gitblock.cn/Media?name=');
    });


    //  添加控制台的提示
    if (window.top == window.self) {
        console.log(
            `%cAerfaying-Explore %c\n当前版本:${version}\n本插件开源于 Github:\nhttps://github.com/waterblock79/aerfaying-explore/`,
            'font-size: 1.5em; color: dodgerblue;',
            'font-size: 1em; color: black;'
        );
    }


    //  插件设置
    let settings = [{
        tag: 'explore:loading',
        text: '加载中所显示的提示设置',
        select: [
            '保持原状',
            '在导航栏显示“加载中”的文字和动画(最小)',
            '在左下角显示不影响浏览的加载中动画(经典)'
        ],
        type: 'radio',
        default: 1,
        desp: `
            <a target="_blank" href="/AboutLoading">详见这里</a>
        `
    }, {
        tag: 'explore:https',
        text: '自动 HTTPS',
        type: 'check',
        default: true,
    }, {
        tag: 'explore:hoverId',
        text: '仅当鼠标悬停在评论上时显示评论 ID',
        type: 'check',
        default: false,
    }, {
        tag: 'explore:noMaxHeight',
        text: '禁用个人简介的最大高度限制',
        type: 'check',
        default: true,
    }, {
        tag: 'explore:lessRecommendProject',
        text: '单行显示推荐的精华作品',
        type: 'check',
        default: false,
    }, {
        tag: 'explore:copyLink',
        text: '鼠标悬停页面右下角时显示复制页面 Markdown 链接的按钮',
        type: 'check',
        default: false,
        disabled: !navigator.clipboard
    }, {
        tag: 'explore:tiebaEmoji',
        text: '在评论时添加贴吧表情',
        type: 'check',
        default: false,
        desp: '实验性功能,仍在完善中'
    }, {
        tag: 'explore:fullscreenDisableScroll',
        text: '作品全屏时禁用鼠标滚轮滚动',
        type: 'check',
        default: true
    }, {
        tag: 'explore:previewReply',
        text: '在消息页面预览回复的内容',
        type: 'check',
        default: false,
        desp: '实验性功能,请谨慎使用'
    }, {
        tag: 'explore:previewCommentMarkdown',
        text: '在发表评论时预览评论 Markdown',
        type: 'check',
        default: false,
    }, {
        tag: 'explore:localSearch',
        text: '快速搜索',
        type: 'check',
        default: true,
        desp: `存储 ${localStorage['explore:searchDb'] ? JSON.parse(localStorage['explore:searchDb']).length : 0} 条数据,共 ${localStorage['explore:searchDb'] ? (localStorage['explore:searchDb'].length / 1024).toFixed(2) : 0} KB,<a href="/AboutLocalSearch" target="_blank">详细</a>`
    }
    ];
    // 设置默认值
    settings.forEach((item) => {
        if (!localStorage[item.tag]) {
            localStorage[item.tag] = item.default;
        }
    })
    // 创建设置摁钮
    let settingsButton = document.createElement('li');
    settingsButton.innerHTML = '<a id="nav-explore-setting"><span>插件设置</span></a>';
    addStyle(`
        .explore-settings-label {
            display: inline-table;
            font-size: 14px;
            font-weight: unset;
            line-height: unset;
            margin-bottom: 0px !important;
            color: #575e75;
            user-select: none;
        }
    `)
    settingsButton.addEventListener('click', () => {
        let html = '';
        // 设置项标题
        html += `
            <b style="margin: 0 .3em">
                设置
            </b>
        `
        // 每项的设置
        settings.forEach((item) => {
            if (item.show == false) {
                return;
            }
            html += `
                <div style="
                    margin: .6em .5em;
                    display: flex;
                    justify-content: space-between;
                ">
            `;
            // 设置名称,如果是 check 类型的设置项,就用 span 包裹,否则就用 b 包裹
            html += `
                        <label 
                            class="explore-settings-label" 
                            for="${item.tag}"
                        >
                            <span>${item.text}</span>
                            ${item.desp ? `
                                <br/>
                                <small>${item.desp}</small>
                            ` : ''}
                        </label>
                    `;
            // Check 类型设置项的勾选控件
            if (item.type == 'check') {
                html += `
                    <input
                        type="checkbox"
                        name="${item.tag}"
                        id="${item.tag}"
                        ${localStorage[item.tag] == 'true' ? 'checked' : ''}
                        onchange="localStorage['${item.tag}'] = this.checked"
                        style="margin-left: 0.8em; margin-left: 0.05em;"
                        ${item.disabled ? 'disabled' : ''}
                    />
                `;
            }
            // Radio 类型设置项的设置选项
            if (item.type == 'radio') {
                // 设置选项
                html += `<div style="margin-left: 0.8em;">`;
                item.select.forEach((selectItem, index) => {
                    html += `
                        <input
                            type="radio"
                            name="${item.tag}"
                            value="${index}"
                            id="${item.tag}-${index}"
                            ${index == localStorage[item.tag] ? 'checked' : ''}
                            onchange="localStorage['${item.tag}'] = ${index}"
                        />
                        <label
                            class="explore-settings-label"
                            style="display: inline;"
                            for="${item.tag}-${index}"
                        >
                            ${selectItem}
                        </label>
                        <br/>
                    `;
                });
                html += `</div>`;
            }
            html += '</div>';
        });
        // 自动跳转设置
        html += `
            <div style="
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin: 1em 0;
            ">
                <div style="margin: 0.3em 0">
                    <b style="display: block">自动跳转</b>
                    <small>${window.GMAvailable ? '若不理解该选项的用途,请勿修改' : '似乎不支持该功能?'}</small>
                </div>
                <select 
                    style="height: 2em"
                    id="explore-redirect-selector"
                    onchange="SetRedirect(document.querySelector('#explore-redirect-selector').value)"
                    ${window.GMAvailable ? '' : 'disabled'}
                >
                    <option value="none">不自动跳转</option>
                    <option value="aerfaying">自动跳转 aerfaying.com</option>
                    <option value="gitblock">自动跳转 gitblock.cn</option>
                    <option value="3eworld">自动跳转 3eworld.cn</option>
                </select>
            </div>
        `;
        // 设置的尾部显示开源地址、版本
        html += `<hr/>`;
        html += `
            <div style="text-align:center">
                <a href="https://waterblock79.github.io/aerfaying-explore/" style="font-weight:600">插件官方页面</a>
                |
                <a href="https://github.com/waterblock79/aerfaying-explore" style="font-weight:600">开源仓库</a>
            </div>`;
        html += `<span style="display:block;text-align:center;margin-top:0.2em;font-size:85%;"> 插件版本 ${version} </span>`;
        html += `<br/>`;
        // 显示提示框
        Blockey.Utils.confirm('插件设置', html);
        // 移除掉“确定”按钮左边的“取消”按钮,并把“确定”摁钮中的文字替换为“关闭”
        $('button.ok-button')[0].parentNode.childNodes[0].remove();
        $('button.ok-button')[0].innerHTML = '关闭';
        $('button.ok-button')[0].addEventListener('click', () => { location.reload(); });
    });
    // 设置自动跳转选项的初始值
    addFindElement('select#explore-redirect-selector', (element) => {
        if (!window.GMAvailable) {
            element.value = 'none';
            return;
        }
        element.value = GM_getValue('explore:autoRedirect') || 'none';
    })
    // 插入设置按钮
    if (location.pathname.match(/\S+\/Editor/) == null && $('#nav-settings').length > 0) {// 当前页面不是作品编辑器页面时,并且已经登陆(#nav-settings 存在)
        insertBefore(settingsButton, $('#nav-settings')[0]);
    } else { // 如果现在没有插入这个元素,那就静待良机,等这个条件成立了以后再插入元素
        let waitInsertSettingsButtonInterval = setInterval(() => {
            if ($('#nav-settings').length > 0 && location.pathname.match(/\S+\/Editor/) == null) {
                insertBefore(settingsButton, $('#nav-settings')[0]);
                clearInterval(waitInsertSettingsButtonInterval);
            }
        }, 1000)
    }

    // 对于加载提示的介绍
    if (location.pathname == '/AboutLoading') {
        $('title')[0].innerHTML = `关于加载中的提示 - Aerfaying Explore`;
        $('.container')[1].innerHTML = `
            <img class="explore-about-loading" src="https://fastly.jsdelivr.net/gh/waterblock79/aerfaying-explore@main/assets/%E5%8A%A0%E8%BD%BD%E6%8F%90%E7%A4%BA.svg">
        `;
        $('.container')[1].classList.add('content-container');
        addStyle(`
            .content-container {
                text-align: center;
            }
            .explore-about-loading {
                max-width: 85%;
                max-height: 30em;
                background: white;
                padding: 1em;
                border-radius: 12px;
                margin: 0.5em 0 2em 0;
                box-shadow: 2px 2px 15px rgb(0 0 0 / 5%);
            }
        `)
    }

    // 使弹出框(如评论详细信息、原创声明)中的内容可以被复制
    addStyle(`
    .modal_modal-content_3brCX {
        -webkit-user-select: auto !important;
        -moz-user-select: auto !important;
        -ms-user-select: auto !important;
        user-select: auto !important;
    }
    .item-attached-thin-modal-body_wrapper_3KdPz { user-select: none; }
    `);


    // 不文明用语“警告!!!”的不再提示
    addFindElement('div.modal_header-item_1WbOm.modal_header-item-title_1N2BE', (element) => {
        // 如果这个弹出框的标题是“警告!!!”
        if (element.innerHTML == '警告!!!') {
            // 如果已经标记不再提示了那就直接帮忙点一下确定键就好了
            if (sessionStorage.blockedAlert) {
                $('.footer>.ok-button')[0].click();
                return;
            }
            // 给真的确定摁钮加一个标记
            $('.footer>.ok-button')[0].classList.add("real");
            // 创建“不再提示”按钮
            let blockAlert = document.createElement('button');
            blockAlert.classList.add("ok-button");
            blockAlert.style.background = "coral";
            blockAlert.innerHTML = '不再提示';
            blockAlert.addEventListener('click', () => {
                $('.footer>.ok-button.real')[0].click(); // 点击真·确定按钮
                sessionStorage.blockedAlert = true;
            })
            // 插入摁钮
            insertBefore(blockAlert, $('.footer>.ok-button')[0]);
            $('.footer')[0].style.marginTop = '0.5em';
        }
    });


    // 替换掉原先全屏的加载遮盖
    let projectThumbId = 'E0D08BE45041CB909364CE99790E7249.png'; // 在加载作品时候需要用到的作品封面 assets ID
    addFindElement('.menu-bar_right-bar_3dIRQ', (element) => {
        // 如果其设置为“保持原状”,那就直接退出
        if (localStorage['explore:loading'] == 0) return;
        // 先隐藏了原先的加载遮盖
        addStyle(`
            .loader_background_1-Rwn { display: none !important }
        `);
        // 方案 1:在顶部导航栏中显示“加载中”图标及文字
        if (localStorage['explore:loading'] == 1) {
            // 创建并插入“加载中”文字
            let text = document.createElement('span');
            text.classList.add('explore-loading-text');
            text.innerText = '加载中';
            element.insertBefore(text, element.firstChild);
            // 创建并插入加载动画
            let loading = document.createElement('div');
            loading.classList.add('explore-loading');
            element.insertBefore(loading, element.firstChild);
            // CSS
            addStyle(`
            /* 加载动画和加载文字的 CSS */
            .explore-loading {
                border: 2.5px solid #f3f3f3b0;
                border-top: 2.5px solid #fff;
                border-radius: 100%;
                min-width: 1em;
                min-height: 1em;
                display: inline-block;
                animation: spin 2s linear infinite;
                margin: 0 0.3em;
            }
            .explore-loading-text {
                margin: 0 1.25em 0 0;
                min-width: 2em;
            }
            @keyframes spin {
                0% { transform: rotate(0deg); }
                100% { transform: rotate(360deg); }
            }
            /* 顶部通知图标稍微有一点歪,和这个加载提示在一起有点难看,要修正下 */
            i.notification {
                margin-bottom: 3px;
            }
            /* 若屏幕过窄就不显示加载文字 */
            @media screen and (max-width: 380px) {
                .explore-loading-text {
                    display: none !important;
                    width: 0 !important;
                }
            }
            `);
            // 默认隐藏
            $('.explore-loading')[0].style.display = 'none';
            try { $('.explore-loading-text')[0].style.display = 'none'; } catch (e) { }
        }
        // 方案 2:在左下角显示不影响浏览的加载提示(原方案)
        else {
            // 添加左下角加载提示
            let loadingElement = document.createElement('div');
            loadingElement.style = "width: 5em; height: 5em; position: fixed; background-color: #4c97ff; right: 5%; opacity: 0.8; bottom: 5%; border-radius: 8px;";
            loadingElement.classList.add("explore-loading");
            loadingElement.innerHTML = '<div class="loader_block-animation_2EgCQ" style="height: 3em;margin: 1em 1em 1em 1.25em;"><img class="loader_top-block_1-yuR" src="https://cdn.gitblock.cn/static/images/209cd016f099f4515cf80dff81e6e0f7.svg" style="margin: 0;"><img class="loader_middle-block_2ma0T" src="https://cdn.gitblock.cn/static/images/ab844ae9647bd855ed2f15b22c6b9337.svg" style="margin: 0;"><img class="loader_bottom-block_ABwSu" src="https://cdn.gitblock.cn/static/images/ee4f8261355c8d3b6fd7228a386c62be.svg" style="margin: 0;"></div>';
            document.body.appendChild(loadingElement)
            $('.explore-loading')[0].style.display = 'none';
        }
        // 如果发现了原先的加载遮盖,就显示新的加载提示
        addFindElement('.loader_background_1-Rwn', (element) => {
            $('.explore-loading')[0].style.display = 'block';
            try { $('.explore-loading-text')[0].style.display = 'block'; } catch (e) { }
            // 轮询直到原先的加载遮盖消失
            let interval = setInterval(() => {
                if (!$('.loader_background_1-Rwn')[0]) {
                    $('.explore-loading')[0].style.display = 'none';
                    try { $('.explore-loading-text')[0].style.display = 'none'; } catch (e) { }
                    // 作品加载完了就得删掉作品的加载动画了,并且恢复作品的大绿旗摁钮、恢复鼠标事件、删除作品封面背景
                    if ($('.explore-project-loading')[0]) {
                        // 删掉加载动画
                        $('.explore-project-loading')[0].remove();
                        // 恢复大绿旗摁钮
                        $('.stage_green-flag-overlay_219KT')[0].style.display = 'flex';
                        // 恢复鼠标事件
                        $('.controls_controls-container_3ZRI_')[0].style = '';
                        $('.stage_green-flag-overlay-wrapper_3bCO-')[0].style = '';
                        // 删除作品封面背景
                        try { $('.explore-project-cover')[0].remove(); } catch (e) { }
                        clearInterval(interval);
                    }
                }
            }, 50);
        });
        // 如果还发现了只有作品加载的时候会出现的“加载消息”,那就得给作品也加上一个加载的小动画+提示
        addFindElement('div.loader_message-container-outer_oYjTv', (element) => {
            // 如果现在是在编辑器页面,那就不用添加这个小动画和提示了
            if (location.pathname.match(/\S+\/Editor/) != null) return;
            // 创建加载的小动画和提示
            let projectLoad = document.createElement('div');
            projectLoad.classList.add('explore-project-loading');
            projectLoad.innerHTML = `
                <div class="loader_block-animation_2EgCQ">
                    <img class="loader_top-block_1-yuR" src="https://cdn.gitblock.cn/static/gui/static/assets/bbbd98ae6a34eac772e34a57aaa5f977.svg">
                    <img class="loader_middle-block_2ma0T" src="https://cdn.gitblock.cn/static/gui/static/assets/f9dce53613d5f85b311ce9f84423c08b.svg">
                    <img class="loader_bottom-block_ABwSu" src="https://cdn.gitblock.cn/static/gui/static/assets/ce5820b006d753e4133f46ae776f4d96.svg">
                </div>
                <div class="loader_title_28GDz" style="
                    color: white;
                ">
                    <span>载入项目</span
                ></div>
            `;
            $('div.stage_green-flag-overlay-wrapper_3bCO-.box_box_tWy-0')[0].appendChild(projectLoad);
            // 隐藏作品的大绿旗摁钮
            $('.stage_green-flag-overlay_219KT')[0].style.display = 'none';
            // 禁止鼠标事件(别加载着一半就点绿旗开始运行了)
            $('.controls_controls-container_3ZRI_')[0].style = 'pointer-events: none;';
            $('.stage_green-flag-overlay-wrapper_3bCO-')[0].style = 'pointer-events: none;';
            // 用这个“...canvas-wrapper-mobile_2WJLy”是否存在判断是否为手机端布局,不是手机端布局就加上作品封面背景
            let projectImage = document.createElement('img');
            projectImage.src = `https://cdn.gitblock.cn/Media?name=${projectThumbId}`;
            projectImage.classList.add('explore-project-cover');
            addStyle(`
                .explore-project-cover {
                    position: absolute;
                    top: 0;
                    width: 100%;
                    filter: blur(6px);
                    overflow: hidden;
                    transform: scale(1.05);
                }
                /* 因为这个封面有模糊效果,它可能会超出边界,所以要给最外层的这个设置一个 overflow: hidden;,
                    再设置一个 border-radius: 0.5rem; 修一下边 */
                div.stage-wrapper_stage-canvas-wrapper_n2Q5r.box_box_tWy-0 {
                    border-radius: 0.5rem;
                    overflow: hidden;
                }
                div.stage-wrapper_stage-canvas-wrapper-mobile_2WJLy.box_box_tWy-0 {
                    border-radius: 0.5rem;
                    overflow: hidden;
                }
            `);
            insertBefore(projectImage, $('div.stage_green-flag-overlay-wrapper_3bCO-.box_box_tWy-0')[0]);
        });
    })

    // 让手机端布局的用户主页也能显示用户 ID、金币、比特石
    addStyle(`
        @media (max-width: 768px) {
            .profile-head_bitStones_1GFkj, .profile-head_goldCoins_TxdJM {
                display: inline-flex !important;
            }
        }
    `);

    // 在用户主页显示被邀请的信息、显示邀请的用户的入口
    addHrefChangeEvent((url) => {
        if (url.match(/\/Users\/([0-9]+\/?)/g) != location.pathname) return; // 如果这个页面不是个用户的主页就退出掉(不匹配 /Users/NUMBER/ 或 /Users/NUMBER)
        let userId = url.match(/[0-9]+/); // 从 URL 匹配用户 ID
        window.$.ajax({
            method: 'POST',
            url: `/WebApi/Users/${userId}/GetPagedInvitedUsers`,
            data: {
                pageIndex: 1, pageSize: 10
            },
            success: (data) => {
                let length = data.invitorPath.length; // 邀请链深度
                // 若该用户不是在邀请链的第一层上,那就是被邀请的用户
                if (data.invitorPath.length != 1) {
                    let userId = data.invitorPath[length - 2].id,
                        userName = data.invitorPath[length - 2].username;
                    let showInvitingUser = addFindElement('.profile-head_join_HPHzg>small', (element) => {
                        element.innerHTML += ` · 由<a href="/Users/${encodeHTML(userId)}">${encodeHTML(userName)}</a>邀请`;
                        delete findElement[showInvitingUser];
                    });
                }
            }
        })

        // 在关注、粉丝、下面添加一个“显示邀请的用户”的入口
        let showInvitedUsers = addFindElement('div.grid-2fr1.grid-gap-xl', (element) => {
            // 生成查看该用户邀请过的用户的链接
            let targetUrl = location.pathname;
            if (targetUrl.slice(-1) == '/') targetUrl = targetUrl.slice(0, -1);
            targetUrl += '/My/InvitedUsers'
            // 找到“关注”、“粉丝”的父级元素
            let parent = element.childNodes[1];
            // 生成“邀请”栏的元素
            let newElement = document.createElement('div');
            newElement.className = 'panel2_wrapper_3UZFE panel-border-bottom';
            newElement.innerHTML = `
                <div class="panel2_panel_1hPqt">
                    <div class="panel2_panelHead_1Bn6y panel-head">
                        <h2>
                            <span class="panel2_border_2Slyp" style="background-color: rgb(77, 151, 255);"></span>邀请
                        </h2>
                        <a class="more" href="${encodeURI(targetUrl)}">查看»</a>
                    </div>
                </div>
            `;
            // 将此元素放到“关注”、“粉丝”后面
            if (window.innerWidth <= 768) { // 如果是手机端布局,那么关注、邀请后面还会有个评论,这个时候就需要特判一下,让邀请栏放在评论前面
                parent.insertBefore(newElement, parent.childNodes[2]);
            } else {
                parent.appendChild(newElement);
            }
            delete findElement[showInvitedUsers];
        });
    })

    // 修复作品“继续加载”的预览图尺寸问题
    addFindElement('.img-responsive', (element) => {
        element.style.width = '100%';
    })

    // 评论显示评论 ID
    let commentData = {};
    addStyle(`
        .explore-comment-info-icon {
            margin-right: .4em;
        }
    `);
    // 自动隐藏评论 ID,鼠标 hover 时再显示
    if (localStorage['explore:hoverId'] == 'true') {
        addStyle(`
            .explore-comment-id {
                display: none;
            }
            .comment_base_info:hover .explore-comment-id {
                display: inline-block;
            }
        `);
    }
    addStyle(`
        .explore-comment-id {
            color: #888;
            font-size: 12px;
            margin-left: .5em;
        }
        .comment_ipregion_11bpP {
            margin-left: .5em;
        }
    `);
    addFindElement('.comment_comment_P_hgY', (element) => {
        // 如果没获取到评论 ID(比如是奥灰推荐位等),就直接退出了
        if (element.id == '')
            return;
        // 给评论时间父级 div 评论信息添加 comment_base_info 类,以便控制显示隐藏
        element.querySelector('.comment_time_3A6Cg').parentNode.classList.add('comment_base_info');
        // 创建评论 ID
        let newElement = document.createElement('span');
        newElement.classList.add('explore-comment-id');
        newElement.classList.add(`explore-comment-id-${element.id}`);
        newElement.innerText = `#${element.id}`;
        // 创建评论 ID 被点击事件
        newElement.addEventListener('click', () => {
            if (!commentData[element.id]) {
                window.Blockey.Utils.Alerter.info('🚧 找不到这条评论的数据');
            } else {
                let linkToComment = (location.href.includes('#') ? location.href.split('#')[0] : location.href) + '#commentId=' + element.id;
                window.Blockey.Utils.confirm(
                    "评论",
                    `
                        <span class="glyphicon glyphicon-time explore-comment-info-icon"></span><b>评论时间</b>
                        <br/>
                        <span>
                            ${(new Date(commentData[element.id].createTime)).toLocaleString()}
                        </span>
                        <br/><br/>
                        <span class="glyphicon glyphicon-link explore-comment-info-icon"></span><b>评论链接</b>
                        <br/>
                        <a href="${linkToComment}">${linkToComment}</a>
                        <br/><br/>
                        <pre>${encodeHTML(commentData[element.id].content)}</pre>
                    `
                );
            }
        });
        // 在评论时间的右边、IP 属地的左边插入评论 ID
        if (element.querySelector('.comment_info_2Sjc0 > .comment_base_info > .comment_ipregion_11bpP') != null)
            insertBefore(newElement, element.querySelector('.comment_ipregion_11bpP'));
        else // 适配无 IP 属地评论
            element.querySelector('.comment_base_info').appendChild(newElement)
    })

    // 给用户主页用户名右边真人认证的图标的位置进行一个矫正
    addFindElement('.profile-head_name_3PNBk>i', (element) => {
        element.style.marginLeft = '0.2em';
        element.style.height = '1em';
    });

    // 用户备注功能
    // 给用户添加备注
    if (!localStorage['explore:remark'])
        localStorage['explore:remark'] = JSON.stringify({});
    addFindElement('.profile-head_name_3PNBk>span', (element) => {
        element.addEventListener('click', () => {
            if (Blockey.Utils.getContext().target.id === Blockey.INIT_DATA.loggedInUser.id) { // 不能给自己添加备注
                Blockey.Utils.Alerter.info('不能给自己添加备注');
                return;
            }
            window.Blockey.Utils.prompt('更新给 TA 的备注')
                .then((data) => {
                    let remark = JSON.parse(localStorage['explore:remark']);
                    remark[Blockey.Utils.getContext().target.id] = data == '' ? undefined : data;
                    localStorage['explore:remark'] = JSON.stringify(remark);
                    location.reload();
                })
        })
    })
    // 如果给自己备注过,那就删除这个备注
    if (JSON.parse(localStorage['explore:remark'])[Blockey.INIT_DATA.loggedInUser ? Blockey.INIT_DATA.loggedInUser.id : '']) {
        let remark = JSON.parse(localStorage['explore:remark']);
        delete remark[Blockey.INIT_DATA.loggedInUser.id];
        localStorage['explore:remark'] = JSON.stringify(remark);
    }
    // 在所有用户名后面添加备注
    let handleUserName = (element) => {
        let remark = JSON.parse(localStorage['explore:remark']);
        let usrId = element.nodeName == 'SPAN' ? Blockey.Utils.getContext().target.id : element.href.split('/')[element.href.split('/').length - 1];
        if (usrId in remark) {
            let newElement = document.createElement('small');
            newElement.style.fontSize = '50%';
            newElement.innerHTML = `(${encodeHTML(remark[usrId])})`
            element.appendChild(newElement);
        }
    };
    addFindElement('a.comment_name_2ZnFZ', handleUserName)
    addFindElement('a.user-info_wrapper_2acbL:not(.event-component_info_2c3Jo > a)', handleUserName)
    addFindElement('.profile-head_name_3PNBk>span:first-child', handleUserName)

    // 去除 maxHeight 限制
    if (localStorage['explore:noMaxHeight'] == 'true') {
        addStyle(`
            .user-home_userInfo_2szc4 { max-height: none !important }
        `);
    }


    // 只显示一行推荐的精华作品
    if (localStorage['explore:lessRecommendProject'] == 'true') {
        addStyle(`
            .home_wrapper_2gKE7 > div:first-child div.home_padding_2Bomd li:nth-child(-n+6) {
                display:none;
            }
        `);
        // nth-child:https://developer.mozilla.org/zh-CN/docs/Web/CSS/:nth-child
        // CSS 选择器挺复杂但是也挺有意思的,值得研究
    }

    // 提示卸载旧版
    if (localStorage['explore:multiVersionAlert'] != 'blocked') {
        setTimeout(() => {
            if (document.querySelectorAll('#nav-explore-setting').length >= 2) { // 如果发现了菜单中有两个插件设置那就说明安装了旧版或多个版本
                window.Blockey.Utils.confirm(`提示`, `
                <b>您似乎安装了旧版本或多个版本的插件?这可能会出现冲突问题,建议卸载较旧版本的插件。</b>
                <br/>
                <small>
                    在控制台输入 <code>localStorage['explore:multiVersionAlert'] = 'blocked'</code> 以禁用该警告(不推荐)
                </small>
                <img
                    src="https://asset.gitblock.cn/Media?name=4D19BB71482063DD3FB4187575A408E2.png"
                    width="80%"
                    style="margin: .5em; border: 1px solid #ccc"
                />
        `);
            }
        }, 1000)
    }

    // 输入框长度自适应输入的文字行数
    const autoHeight = (e) => {
        if (
            e.parentNode.parentNode.parentNode.classList.contains('project-view_descp_IZ1eH') ||
            e.parentNode.parentNode.parentNode.classList.contains('forum-post-add_wrapper_2IFFJ') ||
            e.parentNode.parentNode.parentNode.classList.contains('studio-home_studioCard_2r8EZ')
        ) { // 若为作品简介、帖子、工作室简介编辑则不自动调整
            return;
        }
        e.style.minHeight = '75px'
        if (e.value.length <= 512) {
            e.style.height = 'auto';
            e.style.height = e.scrollHeight <= 75 ? '75px' : (e.scrollHeight + 4) + `px`;
        }
    };
    addSelectorEvent('textarea.form-control', ['input', 'focus'], (e) => autoHeight(e.target));
    addFindElement('textarea.form-control', (element) => autoHeight(element));
    // 发送消息后自动复位
    addFindElement('textarea.form-control', (element) => {
        element.parentElement.parentElement.parentElement.querySelector('.btn.btn-submit.btn-sm')?.addEventListener('click', () => {
            element.style.height = '75px';
        })
    });

    // 复制页面链接按键
    if (localStorage['explore:copyLink'] == 'true') {
        // 创建元素
        let copyBtn = document.createElement('button');
        copyBtn.classList.add('explore-copy');
        copyBtn.addEventListener('click', () => {
            let title = document.title;
            let link = location.pathname + location.search + location.hash;
            if (location.pathname.search(/Studios\/[0-9]+\/Forum\/PostView/) == 1) { // 论坛帖子的网页标题都是“论坛 - 稽木世界”,这里给它加上帖子标题
                title = $('.title')[0].innerText + ' - ' + title
            }
            navigator.clipboard.writeText(`[${title}](${link})`);
            window.Blockey.Utils.Alerter.info('已复制到剪贴板');
        });
        copyBtn.innerHTML = `
            <i class="lg share color-gray"></i>
        `;
        // 添加元素到页面
        let addCopyButtonToDocument = () => {
            try {
                $('.container')[1].appendChild(copyBtn);
            } catch (e) {
                setTimeout(addCopyButtonToDocument, 200);
            }
        };
        addCopyButtonToDocument();
        // 创建样式
        addStyle(`
            .explore-copy {
                width: 3em;
                height: 3em;
                right: 0;
                bottom: 0;
                background: white;
                position: fixed;
                display: flex;
                align-items: center;
                justify-content: center;
                margin: 1em;
                border-radius: 50%;
                box-shadow: 1px 1px 15px rgb(0,0,0,0.15);
                transition: opacity 0.15s ease-in-out;
                border: none;
            }

            .explore-copy:hover {
                opacity: 0.99;
            }

            .explore-copy {
                opacity: 0;
            }

            .explore-copy > i {
                padding-left: 0.2em; /* 和自带的 padding-right 中和一下 */
                font-size: 1.6em;
                line-height: initial;
            }
        `);
    }

    // 贴吧表情
    if (localStorage['explore:tiebaEmoji'] == 'true') {
        addFindElement('.control-group', (element) => {
            // 创建表情选择器元素
            let emojiSelector = document.createElement('div');
            let selectorId = (Math.random() * 10 ^ 8).toFixed().toString(16);
            emojiSelector.classList.add('explore-emoji-selector-' + selectorId);
            emojiSelector.classList.add('explore-emoji-selector');
            emojiSelector.style.display = 'none';
            // 创建表情元素
            for (let i = 1; i <= 50; i++) {
                // 使表情 ID 始终为两位
                if (i < 10) {
                    i = '0' + i;
                }
                // 创建元素并设置 URL 和点击后在输入框添加对应 Markdown
                let emoji = document.createElement('img');
                emoji.src = `https://tb2.bdstatic.com/tb/editor/images/face/i_f${i}.png?t=20140803`;
                emoji.addEventListener('click', (e) => {
                    let textarea = e.target.parentNode.parentNode.parentNode.parentNode.querySelector('textarea');
                    // value +=
                    textarea.value += `![贴吧表情](${e.target.src})`;
                    // 关闭并 focus 到输入框
                    emojiSelector.style.display = 'none';
                    textarea.focus();
                    // 通过修改 value 的方式更改的输入框内容不会自动更新到 this.state.content 中,因此需要用户手动输入一个字符
                    Blockey.Utils.Alerter.info('请至少再手动任意输入一个字符以更新输入框内容');
                })
                // 创建一个“如果鼠标摁下但是摁的不是自己就关闭自己”的事件
                addEventListener('click', (e) => {
                    if (e.target != emojiSelector && !e.target.classList.contains('explore-open-selector')) {
                        emojiSelector.style.display = 'none';
                    }
                })
                emojiSelector.appendChild(emoji);
            }
            insertBefore(emojiSelector, element.childNodes[0]);
            // 创建打开表情选择器按钮
            let openSelector = document.createElement('span');
            openSelector.classList.add('btn');
            openSelector.classList.add('btn-sm');
            openSelector.innerText = '表情';
            openSelector.classList.add('explore-open-selector');
            openSelector.addEventListener('click', () => {
                let element = document.querySelector('.explore-emoji-selector-' + selectorId);
                element.style.display = element.style.display == 'flex' ? 'none' : 'flex';
            });
            insertBefore(openSelector, element.childNodes[0]);
        })
        addStyle(`
        .explore-emoji-selector {
            display: flex;
            position: absolute;
            flex-wrap: wrap;
            z-index: 1999;
            background: white;
            box-shadow: 1px 1px 5px rgb(0 0 0 / 20%);
            border-radius: 4px;
            padding: 0.5em;
            margin-right: 4em;
            left: 30%;
            margin-top: 0.5em;
            max-width: 30em;
            justify-content: center;
        }
        .explore-emoji-selector > img {
            margin: 0.3em !important;
            width: 2em;
        }
        .comment_comment_P_hgY .comment_info_2Sjc0 {
            overflow: inherit;
        }
    `);
    }

    // 在作品全屏显示时禁用鼠标滚轮滚动
    if (localStorage['explore:fullscreenDisableScroll'] == 'true') {
        let scrollY = 0;
        addEventListener('scroll', () => {
            if ($('.stage-wrapper_full-screen_3WIKP').length > 0) {
                document.documentElement.scrollTop = scrollY;
            } else {
                scrollY = document.documentElement.scrollTop;
            }
        })
    }

    // 自动检查更新
    // Get 请求工具函数
    const RequestInGet = (url) => {
        let XHR = new XMLHttpRequest();
        XHR.open('GET', url, false);
        XHR.send();
        return XHR.responseText;
    }; // 不知道为啥用 $.ajax 去请求一个 Javascript 文件会自动执行一遍那个 Javascript 文件...
    // 获取更新函数,如果有更新则返回一个对象,否则返回 false
    const checkUpdate = () => {
        // 获取最新版本号(本来用 jsdeliver 的,但是因为缓存的原因,有时候你都更新了 Github 上的最新版本了,但是 jsdeliver 里存的还是旧版,这就导致了会提示用户逆向升级的问题)
        let lastestFile = atob(JSON.parse(RequestInGet('https://api.github.com/repos/waterblock79/aerfaying-explore/contents/aerfaying-explore.user.js?ref=main')).content);
        let lastestVersion = lastestFile.match(/@version\s+([\d.]+)/)[1]; // copilot 都比你会写正则.jpg
        console.log(`从 Github 仓库检查插件更新成功,最新版本 ${lastestVersion},当前版本 ${version}`);
        // 获取 Commit 消息
        if (version != lastestVersion) {
            let lastestCommit = JSON.parse(
                RequestInGet('https://api.github.com/repos/waterblock79/aerfaying-explore/commits')
            )[0];
            return {
                version: lastestVersion,
                message: lastestCommit.commit.message,
                date: new Date(lastestCommit.commit.author.date),
            }
        }
        return false;
    };

    // 检查更新
    if (localStorage['explore:disabledAutoCheckUpdate'] != 'true' && (localStorage['explore:lastCheckUpdate'] == undefined || new Date().getTime() - new Date(Number(localStorage['explore:lastCheckUpdate'])).getTime() > 1000 * 60 * 60)) {
        (async () => {
            let lastestVersion = checkUpdate();
            localStorage['explore:lastCheckUpdate'] = new Date().getTime();
            if (lastestVersion) {
                // 显示提示框
                Blockey.Utils.confirm(`发现新版本`,
                    `
                    <p style="
                        margin: 0 auto 1em auto;
                        display: flex;
                        width: 10em;
                        justify-content: space-between;
                    ">
                        <span style="color: darkgrey;">${version}</span>
                        <span>→</span>
                        <span style="color: limegreen;">${encodeHTML(lastestVersion.version)}</span>
                    </p>
                    <p style="font-size: 100%">
                        ${encodeHTML(lastestVersion.message)}<br/>
                        <small>更新于:${lastestVersion.date.toLocaleString()}</small>
                        <small style="display: block">根据 Github 仓库提交信息显示,请以实际更新内容为准!</small>
                    </p>
                    <p>
                        <small>获取更新的数据来源以及更新渠道均为 Github,因此可能无法打开链接,或者一些浏览器插件可能就不支持直接通过打开链接更新插件,如果您遇到了这些情况,请尝试移除该插件并重新按照<a href="https://waterblock79.github.io/aerfaying-explore/#%E5%AE%89%E8%A3%85%E6%8F%92%E4%BB%B6">文档中的教程</a>进行安装,亦或禁用自动检查更新功能。</small>
                    </p>
                `
                );
                // 给 ok-button 加事件
                $('.ok-button')[0].addEventListener('click', () => {
                    window.open('https://github.com/waterblock79/aerfaying-explore/raw/main/aerfaying-explore.user.js');
                })
                // 不再提示摁钮
                let dontShowAgain = document.createElement('button');
                dontShowAgain.classList.add('btn');
                dontShowAgain.classList.add("ok-button");
                dontShowAgain.innerText = '不再提示';
                dontShowAgain.style.background = "coral";
                dontShowAgain.style.color = "white";
                dontShowAgain.addEventListener('click', () => {
                    localStorage['explore:disabledAutoCheckUpdate'] = 'true';
                    $('.footer.text-right.box_box_tWy-0>button')[1].click();
                });
                insertBefore(dontShowAgain, $('.footer.text-right.box_box_tWy-0>button')[0]);
            }
        })();
    }

    // 在手机端的物品页面也显示物品图鉴、拍卖行按钮
    addHrefChangeEvent(() => {
        if (window.location.href.match(/\/Users\/[0-9]*\/My\/Items/)) { // 匹配 /Users/[NUMBER]/My/Items
            addFindElement('.user-items_wrapper_2Jxfd', (element) => {
                // 创建元素
                let newElement = document.createElement('div');
                newElement.classList.add('navigation-list_wrapper_1RqLP');
                newElement.classList.add('explore-mobile-items-nav');
                newElement.innerHTML = `
                <li class="guide">
                    <i class="guide"></i>
                    <div class="navigation-list_content_2S2K9">
                        <div class="navigation-list_title_SOF67">物品图鉴</div>
                    </div>
                </li>
                <li class="sell">
                    <i class="auction"></i>
                    <div class="navigation-list_content_2S2K9">
                        <div class="navigation-list_title_SOF67">拍卖行</div>
                    </div>
                </li>
            `;
                // 特别的 CSS
                addStyle(`
                @media (min-width: 769px) {
                    .explore-mobile-items-nav {
                        display: none;
                    }
                }
                @media (max-width: 768px) {
                    .navigation-list_wrapper_1RqLP {
                        display: flex;
                        justify-content: center;
                    }
                    .navigation-list_wrapper_1RqLP>li {
                        margin: 0 1em 1em 1em;
                        width: 100%;
                    }
                }
            `);
                insertBefore(newElement, element);
                // 绑定点击事件
                $('.explore-mobile-items-nav>li.guide')[0].addEventListener('click', () => {
                    window.location.href = '/Items/Guide';
                });
                $('.explore-mobile-items-nav>li.sell')[0].addEventListener('click', () => {
                    window.location.href = '/stars/mars/0001';
                });
            })
        }
    });

    // 任务列表中若经验/金币奖励为 0 则不显示这个图标(原来就是这样的,前些日子改成了即使奖励为 0 也显示一个图标加上一个数字 0)
    addFindElement('.mission-prizes_wrapper_2HfN8 > .prize_wrapper_Nbm6l', (element) => {
        if (element.querySelector('span').innerText === '0') {
            element.style.display = 'none';
        }
    });

    // 自动跳转
    if (window.GMAvailable) {
        // 受理设置
        window.SetRedirect = (target) => {
            GM_setValue('explore:autoRedirect', target);
        }
        // 进行跳转
        let autoRedirect = GM_getValue('explore:autoRedirect', 'none');
        if (autoRedirect == 'aerfaying' && window.location.host != 'aerfaying.com') {
            window.location.host = 'aerfaying.com';
        } else if (autoRedirect == '3eworld' && window.location.host != '3eworld.cn') {
            window.location.host = '3eworld.cn';
        } else if (autoRedirect == 'gitblock' && window.location.host != 'gitblock.cn') {
            window.location.host = 'gitblock.cn';
        }
    }

    // 评论区编辑消息时允许预览消息
    if (localStorage['explore:previewCommentMarkdown'] == 'true') {
        addStyle(`
            .comment-panel_comment-panel_3pBsc form {
                margin-top: 0;
            }

            .comment-panel_comment-panel_3pBsc form .markdown-editor_previewTab_e6pLX {
                margin-left: 4px;
            }
        `);
        addFindElement(`.reply-box_replyBox_3Fg5C`, (element) => {
            // 创建预览摁钮组及其子摁钮
            let previewButtonGroup = {
                parent: document.createElement('ul'),
                edit: document.createElement('li'),
                preview: document.createElement('li'),
            }
            // parent
            previewButtonGroup.parent.classList.add('nav');
            previewButtonGroup.parent.classList.add('nav-tabs');
            previewButtonGroup.parent.classList.add('markdown-editor_previewTab_e6pLX');
            // edit
            previewButtonGroup.edit.classList.add('active');
            previewButtonGroup.edit.innerHTML = `
                <a>编辑</a>
            `;
            previewButtonGroup.edit.addEventListener('click', (e) => {
                previewButtonGroup.edit.classList.add('active');
                previewButtonGroup.preview.classList.remove('active');
                element.querySelector('textarea').style.display = 'block';
                element.querySelector('div.explore-comment-preview').style.display = 'none';
            })
            // preview
            previewButtonGroup.preview.innerHTML = `
                <a>预览</a>
            `;
            previewButtonGroup.preview.addEventListener('click', (e) => {
                previewButtonGroup.edit.classList.remove('active');
                previewButtonGroup.preview.classList.add('active');
                element.querySelector('textarea').style.display = 'none';
                element.querySelector('div.explore-comment-preview').style.display = 'block';
                element.querySelector('div.explore-comment-preview').innerHTML = window.Blockey.Utils.markdownToHtml(encodeHTML(element.querySelector('textarea').value));
            });
            // 把子摁钮加入摁钮组
            previewButtonGroup.parent.appendChild(previewButtonGroup.edit);
            previewButtonGroup.parent.appendChild(previewButtonGroup.preview);
            // 把摁钮组加入页面
            insertBefore(previewButtonGroup.parent, element);

            // 添加预览元素
            let previewElement = document.createElement('div');
            previewElement.classList.add('explore-comment-preview');
            previewElement.style.display = 'none';
            previewElement.style.minHeight = '75px';
            element.appendChild(previewElement);

            // 发送消息后自动复位
            element.parentElement.querySelector('.btn.btn-submit.btn-sm')?.addEventListener('click', () => {
                previewButtonGroup.edit.click();
            })
        })
    }

    // 消息页面预览回复
    if (localStorage['explore:previewReply'] == 'true') {
        let messagePool = {};
        // 请求频率锁
        let requestLock = {
            time: Date.now(),
            times: 0
        };
        // 截短字符串
        let Shorter = (str, length) => {
            if (!length) length = 30;
            return str.length > length ? `${str.substring(0, length)}...` : str;
        };
        let HandleMessagePreview = async (messageListElement) => {
            let messageList = [];
            // 从消息元素提取该消息的信息,并加入到列表
            messageListElement.childNodes.forEach((messageElement) => {
                // 根本没链接那就直接退出
                if (!messageElement.querySelector('.user-messages_content_3IDNx p > a')) return;
                // 提取链接
                let href = messageElement.querySelector('.user-messages_content_3IDNx p > a').getAttribute('href');
                // 如果不是消息回复就跳过
                if (!messageElement.querySelector('.user-messages_content_3IDNx p').innerText.match(/在[\S\s]*给你 留言 了/))
                    return;
                // 提取信息并加入列表
                messageList.push({
                    element: messageElement,
                    forType: {
                        'Users': 'User',
                        'Projects': 'Project',
                        'Reports': 'Report'
                    }[href.split('/')[1]] || null,
                    forId: href.split('/')[2].split('#')[0],
                    scrollToCommentId: href.split('#')[1].split('=')[1]
                });
            });
            // 按 forId 整理,forId 相同的消息按顺序同步处理(节约请求)
            let messageListByForId = {};
            messageList.forEach((message) => {
                if (!messageListByForId[message.forType + message.forId]) {
                    messageListByForId[message.forType + message.forId] = [];
                }
                messageListByForId[message.forType + message.forId].push(message);
            });
            console.log(messageListByForId);
            // 屮,走,忽略
            Object.keys(messageListByForId).forEach((forId) => {
                for (let i in messageListByForId[forId]) {
                    let message = messageListByForId[forId][i];
                    // 巧了,消息池里已经有这个消息的信息了,那就别请求了,直接用消息池中的数据就好了
                    if (messagePool[message.scrollToCommentId]) {
                        let previewElement = document.createElement('p');
                        previewElement.innerText = Shorter(encodeHTML(messagePool[message.scrollToCommentId]));
                        previewElement.classList.add('explore-comment-preview');
                        message.element.querySelector('.user-messages_content_3IDNx p').appendChild(previewElement);
                    } else {
                        // 没有那就调 api 请求
                        // 请求频率锁(如果近一秒内平均请求了超过三次,那就稍等一会)
                        if (requestLock.time + 1000 < Date.now()) requestLock.time = Date.now();
                        while ((Date.now() - requestLock.time) / requestLock.times < 300) { }
                        window.$.ajax({
                            url: `/WebApi/Comment/GetPage`, method: 'post', data: {
                                forType: message.forType,
                                forId: message.forId,
                                pageIndex: 1,
                                scrollToCommentId: message.scrollToCommentId
                            },
                            async: false,
                            success: (data) => {
                                // 把返回的这些数据加入消息池
                                data.pagedThreads.items.forEach((m) => {
                                    messagePool[m.id] = m.status ? m.content : '[评论不存在]';
                                });
                                data.replies.forEach((m) => {
                                    messagePool[m.id] = m.status ? m.content : '[评论不存在]';
                                });
                                messagePool[data.scrollToThread.id] = data.scrollToThread.status ? data.scrollToThread.content : '[评论不存在]';
                                // 把评论内容加入页面
                                let previewElement = document.createElement('p');
                                previewElement.innerText = Shorter(encodeHTML(messagePool[message.scrollToCommentId]));
                                previewElement.classList.add('explore-comment-preview');
                                message.element.querySelector('.user-messages_content_3IDNx p').appendChild(previewElement);
                            }
                        });
                    }
                }
            });
        };
        // 页面中的消息更新时触发插入回复预览
        addFindElement(`.user-messages_card_2ITqW`, (element) => {
            if (!(element.parentNode.childNodes[0] == element)) return;
            /*
                元素的结构是这样的:
                div.user-messages_wrapper_1hI8b
                    div.user-messages_card_2ITqW // 每个消息卡片
                    div.user-messages_card_2ITqW
                    ......
                如果要侦测 .user-messages_wrapper_1hI8b 是否变化,这个怪麻烦的(直接判断现在的元素是否先前的元素相等的话,这个里面怎么变,判断的时候都是相等的;判断 innerHTML 是否改变的话,把消息内容插入页面的时候它还会再触发一次,就无限循环了)
                所以就用 .user-messages_card_2ITqW 是否变化来侦测这个消息列表是否更新了
                这里判断了一下是否 .user-messages_card_2ITqW 是 .user-messages_wrapper_1hI8b 中第一个元素,防止重复触发
            */
            HandleMessagePreview(element.parentNode);
        });
        // 对应样式
        addStyle(`
            .explore-comment-preview {
                margin-top: 0.25em !important;
                font-size: 0.75em !important;
            }
        `);
    }

    // 修复在切换过页面大小的情况下,点击绿旗后作品播放器上的遮盖仍存在的问题(详见 issue #31)
    addFindElement('.stage_green-flag-overlay-wrapper_3bCO-.box_box_tWy-0', (element) => {
        element.addEventListener('click', (e) => {
            element.style.display = 'none';
        })
    });

    // 快捷搜索
    if (localStorage['explore:localSearch'] == 'true') {
        // 读取搜索数据
        let searchDb = JSON.parse(localStorage['explore:searchDb'] || '[]');

        // 记录所访问页面的信息,用于快捷搜索
        let interval;
        addHrefChangeEvent((href) => {
            // a. 转 https://gitblock.cn/Users/1 这样的链接为 /Users/1
            // b. 去除链接中 # 后内容
            // c. 转小写
            href = location.href.split(location.origin)[1].split('#')[0].toLowerCase();

            // 字符串完全匹配该正则表达式?
            let FullyMatched = (reg, str) => {
                return str.match(reg) && str.match(reg)[0] == str;
            };
            // 从 URL 匹配类型
            let GetTypeFromURL = (url) => {
                let regs = {
                    'Studio': /\/studios\/[0-9]*\/?/,
                    'User': /\/users\/[0-9]*\/?/,
                    'Project': /\/projects\/[0-9]*\/?/,
                    'ForumPost': /\/studios\/[0-9]*\/forum\/postview\?postid=[0-9]*/
                };
                for (let type in regs) {
                    if (FullyMatched(regs[type], url.toLowerCase())) return type;
                }
                return null;
            }
            // 从 Blockey 匹配类型
            let GetTypeFromBlockey = () => {
                return window.Blockey.Utils.getContext().targetType;
            };
            // 从 Blockey 获取 target
            let GetTargetFromBlockey = () => {
                return window.Blockey.Utils.getContext().target;
            };
            // 获取记录 Title
            let GetTitle = (target, type) => {
                if (type == 'Studio') return target.name;
                if (type == 'User') return target.username;
                if (type == 'Project') return target.title;
                if (type == 'ForumPost') return target.title;
                return null;
            };
            // 获取 Keywords
            let GetKeywords = (target, type) => {
                let keywords = [];
                if (type == 'Studio') {
                    keywords.push(target.name);
                    keywords.push(target.id);
                    keywords.push(target.creator.username);
                } else if (type == 'User') {
                    keywords.push(target.username);
                    keywords.push(target.id);
                    if (localStorage['explore:remark'] && JSON.parse(localStorage['explore:remark'])[target.id]) {
                        keywords.push(JSON.parse(localStorage['explore:remark'])[target.id]);
                    }
                } else if (type == 'Project') {
                    keywords.push(target.title);
                    keywords.push(target.id);
                    keywords.push(target.creator.username);
                } else if (type == 'ForumPost') {
                    keywords.push(target.title);
                    keywords.push(target.id);
                } else {
                    return [];
                }
                return keywords;
            };
            // 获取 Image
            let GetImage = (target, type) => {
                if (type == 'Studio') return target.thumbId;
                if (type == 'User') return target.thumbId;
                if (type == 'Project') return target.thumbId;
                return null;
            };

            // 记录页面信息
            let RecordPageInfo = (href) => {
                if (GetTypeFromURL(href) != null) {
                    if (searchDb.filter((item) => item.href.toLowerCase() == href.toLowerCase()).length == 0) { // 记录不存在
                        searchDb.push({
                            href: location.href.split(location.origin)[1].split('#')[0],
                            type: GetTypeFromURL(href),
                            title: GetTitle(GetTargetFromBlockey(), GetTypeFromBlockey()),
                            keywords: GetKeywords(GetTargetFromBlockey(), GetTypeFromBlockey()),
                            image: GetImage(GetTargetFromBlockey(), GetTypeFromBlockey())
                        });
                        localStorage['explore:searchDb'] = JSON.stringify(searchDb);
                    } else {
                        // 更新记录
                        let record = searchDb.filter((item) => item.href.toLowerCase() == href.toLowerCase())[0];
                        record.type = GetTypeFromURL(href);
                        record.title = GetTitle(GetTargetFromBlockey(), GetTypeFromBlockey());
                        record.keywords = GetKeywords(GetTargetFromBlockey(), GetTypeFromBlockey());
                        record.image = GetImage(GetTargetFromBlockey(), GetTypeFromBlockey());
                        localStorage['explore:searchDb'] = JSON.stringify(searchDb);
                    }
                }
            }

            // 等待 Blockey 数据加载完毕然后调用记录函数
            let waitInterval = setInterval(() => {
                // 1. GetTargetFromBlockey()
                //    值为 Null 说明 Blockey 数据还没加载完,得等加载完了再记录
                // 2. GetTypeFromBlockey() == GetTypeFromURL(href)
                //    这里的条件是从 URL 完全匹配得到的类型和从 Blockey 获取的类型一致,这么做原因有如下两条:
                //       a. 防止如 /Users/*/My/InvitedUsers 这类地址混入记录中
                //       b. ForumPost 最开始没加载完的时候 Blockey 会返回 Studio 类型,不等加载完再记录的话就会出岔子
                if (GetTargetFromBlockey() && GetTypeFromBlockey() == GetTypeFromURL(href)) {
                    clearInterval(waitInterval);
                    RecordPageInfo(href);
                }
            }, 100);

        });


        // 随机搜索提示语
        let GetRandomSearchTips = () => {
            return [
                '本地搜索的数据来源于您曾访问过的页面',
                '本地搜索的数据不会上传到服务器,只会存储在您的计算机中',
                '使用空格来分开多个搜索关键词',
            ][Math.floor(Math.random() * 3)];
        };
        // 映射搜索结果类型到图标
        let TypeToIcon = (type) => {
            return {
                'project': 'projects',
                'user': 'member',
                'studio': 'studio',
            }[type] || 'mission';
        }

        // 创建样式及元素
        addStyle(`
            .explore-quick-search-background {        
                z-index: 10010;
                display: flex;
                left: 0;
                top: 0;
                background: rgb(0,0,0,0.25);
                /* backdrop-filter: blur(2px); 太卡了*/
                width: 100%;
                height: 100%;
                position: fixed;
            }

            .explore-quick-search { 
                top: 10%;
                left: 20%;
                width: 60%;
                height: 80%;
                position: fixed;
                z-index: 10086;
            }

            .explore-quick-search input {
                box-shadow: 0px 0px 15px rgb(0 0 0 / 20%);
                width: 100%;
                outline: none;
                border: none;
                padding: .75em 1em;
                font-size: 1.25em;
                border-radius: 8px;
                background: rgb(256,256,256,0.925);
                backdrop-filter: blur(2px);
            }

            .explore-quick-search .results { 
                box-shadow: 0px 0px 15px rgb(0 0 0 / 20%);
                width: 100%;
                padding: 0.5em 1.5em;
                border-radius: 8px;
                margin-top: 1.5em;
                background: rgb(256,256,256,0.925);
                backdrop-filter: blur(2px);
                max-height: calc(100% - 2em);
                overflow-y: auto;
            }

            .explore-quick-search .results .result {
                display: flex;
                align-items: center;
                border-bottom: solid 1px rgb(0,0,0,0.075);
                padding: 1em 0;
                color: black;
                text-decoration: none;
            }
            .no-result {
                margin: 1em 0;
            }
            .explore-quick-search .results .result:last-child {
                border: none;
            }

            .explore-quick-search .results .result .icon {
                font-size: 2.5em;
                color: dimgrey;
                padding: 0;
            }

            .explore-quick-search .results .result .image {
                width: 2.5em;
                padding: 0;
            }

            .explore-quick-search .results .result .item {
                cursor: pointer;
                margin-left: 1em;
                width: 100%;
            }
            .explore-quick-search .results .result .item .title {
                font-size: 1.5em;
            }
            .explore-quick-search .results .result .item .link {
                font-size: 0.75em;
                color: grey;
            }
        `);

        let searchElement = document.createElement('div');
        searchElement.style.display = 'none';
        searchElement.classList.add('explore-quick-search-background');
        searchElement.innerHTML = `
            <div class="explore-quick-search">
                <input type="text" placeholder="进行本地搜索">
                <div class="results">
                    <div class="no-result"> ${encodeHTML(GetRandomSearchTips())} </div>
                </div>
            </div>
        `;
        document.body.appendChild(searchElement);

        // 搜索
        // 获取相关元素
        let searchInput = searchElement.querySelector('.explore-quick-search input');
        let searchResults = searchElement.querySelector('.explore-quick-search .results');

        // 搜索函数
        let search = (keyword) => {
            if (keyword == '') return []; // 关键词为空就不搜索了,直接 return 空列表
            let results = [];
            // 全部小写化,避免大小写搜不着问题
            keyword = keyword.toLowerCase();
            // 按空格分开关键词,并删除全部空关键词
            let keywordList = keyword.split(' ');
            keywordList = keywordList.filter((item) => item != '');
            // 遍历搜索数据
            // 遍历:关键词列表 => { 索引列表 => { 某索引的关键词列表 } }
            keywordList.forEach((keyword) => {
                searchDb.forEach((item, index) => {
                    // 遍历每个搜索数据下的关键词
                    item.keywords.forEach((key, index) => {
                        // 如果发现搜索数据关键词、搜索关键词间存在包含关系,那就把这个结果加入到结果列表里
                        if (!key) return; // 关键词为空就 return 跳过
                        key = String(key).toLowerCase();
                        if ((key.includes(keyword) || keyword.includes(key)) && !results.includes(item)) {
                            results.push(item);
                            return;
                        }
                    })
                })
            });
            return {
                results: results.length > 75 ? results.slice(0, 75) : results,
                split: results.length > 75
            };
        };

        // 当搜索框内容改变时,进行搜索并显示搜索结果
        searchInput.addEventListener('input', (e) => {
            let { results, split } = search(e.target.value);
            searchResults.innerHTML = '';
            if (e.target.value == '') {
                // 没有输入内容时,显示随机提示
                searchResults.innerHTML = `
                    <div class="no-result">
                        ${encodeHTML(GetRandomSearchTips())}
                    </div>
                `;
                return;
            } else if (results.length > 0) {
                results.forEach((item, index) => {
                    searchResults.innerHTML += `
                        <a class="result" href="${encodeURI(item.href).replace('javascript:', 'scratch:')}" target="_blank">
                            ${item.image ?
                            `<img class="image" src="https://cdn.gitblock.cn/Media?name=${encodeURI(item.image)}">` : // 敲黑板,这里如果直接字符串拼接的话,如果这个图片的值为这样的:xxx" onerror="alert(1),那就会执行 onerror,造成安全性问题
                            `<i class="icon ${TypeToIcon(encodeHTML(item.type))}"></i>`
                        }
                            <div class="item">
                                <div class="title">${encodeHTML(item.title)}</div>
                                <div class="link">${encodeHTML(item.href)}</div>
                            </div>
                        </div>
                    `;
                });
                if (split) {
                    searchResults.innerHTML += `
                        <div class="no-result">
                            搜索结果最多显示 75 条,若需要查看全部的记录,请前往:<a href="/AboutLocalSearch">关于搜索</a>
                        </div>
                    `;
                }
            } else {
                // 没有搜索结果:
                searchResults.innerHTML = `
                    <div class="no-result">
                        找不到与关键词匹配的内容
                    </div>
                `;
            }
            /*searchResults.innerHTML += `
                <a class="result">
                    <div class="item" style="font-size: 0.8em">
                        <div class="title">全域搜索此内容 ></div>
                    </div>
                </a>
            `;*/
            // 本来是要加全域搜索(一键自动搜索帖子、用户、作品)的,但是发现要 ts,公开代码里这么弄不合适,就砍掉了
        })

        // 呼出搜索框!!!
        addEventListener('keydown', (e) => {
            if ((e.ctrlKey || e.metaKey) && e.key == 'k') { // 开启
                searchElement.style.display = 'block';
                searchInput.focus();
                searchInput.value = '';
                searchInput.dispatchEvent(new Event('input')); // 触发输入事件以更新搜索结果
                e.preventDefault();
            }
            // Esc 关闭
            if (e.key == 'Escape') {
                searchElement.style.display = 'none';
            }
        });

        // 关闭搜索框。。。
        searchElement.addEventListener('click', (e) => {
            if (e.path[0] == searchElement) searchElement.style.display = 'none';
        });
    }
    addFindElement('.layout_content_20yil.layout_margin_3C6Zp > .container > div', (element) => {
        if (location.href.includes('AboutLocalSearch')) {
            document.head.querySelector('title').innerText = '本地搜索'
            // 修改样式
            $('.layout_content_20yil.layout_margin_3C6Zp .container')[0].style.textAlign = 'center';
            $('.layout_content_20yil.layout_margin_3C6Zp .container div')[0].style.margin = '2em 3em';
            // 创建元素
            let db = localStorage.getItem('explore:searchDb') ? JSON.parse(localStorage.getItem('explore:searchDb')) : [];
            element.innerHTML = `
                <h2 style="font-weight: 500">本地搜索</h2>
                <p style="font-size: .9em">
                    本地搜索功能可以在您访问页面时自动索引该页面,您可以通过快捷键<b> Ctrl + K </b>来呼出快捷搜索栏并搜索已索引内容。全部索引数据将只会存储在本地,不会上传至任何服务器。
                </p>
                <p>
                    共存储 ${db.length} 条数据,占用 ${(JSON.stringify(db).length / 1024).toFixed(2)} KB 存储空间。
                </p>
                <p>
                    <a id="export">导出</a> 
                    <a id="import">导入</a> 
                    <a id="delete">清空</a>
                </p>
                <div class="markdown_body_1wo0f">
                    ${window.Blockey.Utils.markdownToHtml(
                `|Type|Title|Href|Image|Keywords|  \n|----|-----|----|-----|----|  \n${encodeHTML(db.map(item => {
                    return `| ${item.type} | ${item.title} | [${item.href}](${item.href}) | ${item.image ? `![](https://cdn.gitblock.cn/Media?name=${item.image}){.explore-search-datatable-image}` : null} | ${item.keywords.join(', ')} | `;
                }).join('  \n'))
                }`
            )
                }
                </div>
            `;
            addStyle(`
                .explore-search-datatable-image {
                    width: 100px;
                }
            `);
            // copliot 写的导出导入,比我原来想好的方案强
            // 导出
            element.querySelector('#export').addEventListener('click', () => {
                let a = document.createElement('a');
                a.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(db, '', 4));
                a.download = 'searchDb.json';
                a.click();
            });
            // 导入
            element.querySelector('#import').addEventListener('click', () => {
                let input = document.createElement('input');
                input.type = 'file';
                input.accept = '.json';
                input.onchange = () => {
                    let file = input.files[0];
                    let reader = new FileReader();
                    reader.readAsText(file);
                    reader.onload = () => {
                        try {
                            let data = JSON.parse(reader.result);
                            localStorage.setItem('explore:searchDb', JSON.stringify(data));
                            alert('导入成功!');
                            location.reload();
                        } catch (e) {
                            alert(e);
                        }
                    }
                }
                input.click();
            });
            // 清空
            element.querySelector('#delete').addEventListener('click', () => {
                if (confirm('确定要删除全部本地搜索索引数据吗?') == true) {
                    localStorage.removeItem('explore:searchDb');
                    location.reload();
                }
            });
        }
    });

    // 在评论输入框显示这个评论是回复给谁的
    addFindElement('.reply-box_replyBox_3Fg5C', (element) => {
        const getParent = (target, level) => {
            if (level == 0) return target;
            return getParent(target.parentNode, level - 1);
        };
        const username = getParent(element, 5).querySelector('.comment_base_info > a')?.innerText;
        if (!username || getParent(element, 5).classList.contains('panel2_panel_1hPqt')) return;
        element.querySelector('textarea').placeholder = `回复 @${username}`;
    });
    // Your code here...
})();