B站评论直达

为 bilibili 上的评论生成支持 web 端和手机 app 直达的链接

// ==UserScript==
// @name         B站评论直达
// @namespace    http://tampermonkey.net/
// @version      0.3.3
// @description  为 bilibili 上的评论生成支持 web 端和手机 app 直达的链接
// @author       5ec1cff
// @license      AGPL
// @match        *://*.bilibili.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=bilibili.com
// @grant        unsafeWindow
// @grant        GM_setClipboard
// @grant        GM_addStyle
// @run-at       document-start
// ==/UserScript==

(function(window) {
    'use strict';

    // 动态:1-转发 2-相簿 4-动态 8-视频 16-小视频 64-文章 256-音频 512-番剧
    // 评论:1-视频,5-小视频,6-小黑屋,11-相簿,12-文章,14-音频,17-动态
    const commentToDynamicTypeMap = {
        11: 2,
        1: 8,
        12: 64,
        14: 256
    }

    GM_addStyle(`
.selected-comment { background-color: pink; }
.my-message {
    border-radius: 8px;
    color: #fff;
    font-size: 14px;
    left: 50%;
    overflow: hidden;
    padding: 12px 24px;
    position: fixed;
    transform: translate(-50%,-50%);
    transition: all .4s;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    background-color: #47d279;
    box-shadow: 0 .2em .1em .1em rgba(71,210,121,.2);
    top: calc(50% - 0px);
    z-index: 2004;
    text-align: center;
    white-space: nowrap;
}
    `);

    let is_new_comment = false;

    // 判断是否为 new_comment
    if (location.href.indexOf('www.bilibili.com/video') > 0 || location.href.indexOf('www.bilibili.com/opus') > 0) { // && (!getJumpId() || location.href.indexOf('old_comment=1') > 0)) {
        /*
        let origMatch = String.prototype.match;
        let count = 0;
        String.prototype.match = function (...args) {
            let r = origMatch.call(this, ...args);
            if (!r && this == location.href && args[0]?.source == '#reply([0-9]+)$') {
                r = '00';
                count++;
                if (count >= 2) String.prototype.match = origMatch;
            }
            return r;
        }*/
        // window.history.replaceState({}, '', "?old_comment=1");
        is_new_comment = true;
        console.warn('use new comment');
    }

    let main_comment_classes = ["list-item", "reply-wrap"],
        reply_classes = ["reply-item", "reply-wrap"],
        bar_selector = 'div.info';
    if (is_new_comment) {
        main_comment_classes = ['reply-item'];
        reply_classes = ['sub-reply-item'];
        bar_selector = 'div.reply-info, div.sub-reply-info';
    }
    let update_selector = `${'.' + main_comment_classes.join('.')}, ${'.' + reply_classes.join('.')}`;

    function getJumpId() {
        let r = location.hash.match(/#reply(\d+)/);
        return r && r[1];
    }

    let jumpId = getJumpId();

    window.addEventListener('hashchange', function () {
        jumpId = getJumpId();
        // console.log('new jumpId:', jumpId);
        document.querySelectorAll(update_selector).forEach(updateSelectedState);
    })

    function getCommentRpid(e) {
        if (is_new_comment) {
            if (classNameMatch(e, 'sub-reply-item')) {
                return e.__vnode?.children[1].props.reply.rpid || WARN('Failed to get sub-reply-item rpid on', e);
            }
            // return e.__vnode?.props['mr-show']?.msg?.rpid;
            return e?.__vueParentComponent?.props?.reply?.rpid;
        } else {
            return e.dataset?.id;
        }
    }

    // 高亮选择的评论
    function updateSelectedState(e) {
        if (getCommentRpid(e) == jumpId && jumpId != null) {
            e.classList.add('selected-comment');
        } else {
            e.classList.remove('selected-comment');
        }
    }

    function showToast(content) {
        let d = document.createElement('div');
        d.classList.add('my-message');
        d.textContent = content;
        document.body.append(d);
        setTimeout((e) => { d.remove() }, 2000);
    }

    let _isDetail = null;

    // 视频页面、动态页面、专栏页面
    function isDetailPage() {
        if (_isDetail == null) {
            _isDetail = Boolean(location.href.match(/www\.bilibili\.com\/(video|read|opus)\/|t\.bilibili\.com\/\d+/));
            console.log('isDetail:', _isDetail);
        }
        return _isDetail;
    }

    function WARN(msg, append) {
        // console.warn(msg, append);
        // debugger
        return null;
    }

    function getBaseUrl(type, oid) {
        let url = null;
        if (!isDetailPage() && type != null && oid != null) {
            if (type in commentToDynamicTypeMap) {
                url = new URL(`https://t.bilibili.com/${oid}?type=${commentToDynamicTypeMap[type]}`);
            } else if (type == 17) {
                url = new URL(`https://t.bilibili.com/${oid}`);
            } else {
                console.warn('unsupported comment type:', type, oid);
                url = new URL(location.href);
            }
        } else {
            url = new URL(location.href);
        }
        let params = new URLSearchParams(url.search);
        for (let k of Array.from(params.keys())) {
            if (k.match(/spm|vd_source/)) { params.delete(k); }
        }
        url.search = params.toString();
        return url;
    }

    function getUrl(e) {
        let p = e.parentElement, root_id = null, second_id = null, comment_on = null, comment_on_oid = null;
        while (p != null) {
            if (classNameMatch(p, "reply-item", "reply-wrap")) second_id = p.dataset.id;
            if (classNameMatch(p, 'sub-reply-item')) second_id = p?.__vnode?.children[1]?.props?.reply?.rpid || WARN('Failed to get new comment second id on', p);
            if (classNameMatch(p, ...main_comment_classes)) {
                let data;
                if (p.attributes['mr-show']) {
                    data = JSON.parse(p.attributes['mr-show'].value);
                    root_id = p.dataset.id;
                    comment_on = data.msg.type;
                    comment_on_oid = data.msg.oid;
                } else if (is_new_comment) {
                    data = p.__vueParentComponent.props.reply;
                    root_id = data.rpid;
                    comment_on = data.type;
                    comment_on_oid = data.oid;
                }
            }
            if (root_id != null) break;
            p = p.parentElement;
        }
        // console.log(root_id, second_id, comment_on, comment_on_oid);
        let u = getBaseUrl(comment_on, comment_on_oid);
        let params = new URLSearchParams(u.search);
        params.append('comment_on', 1);
        params.append('comment_root_id', root_id);
        if (second_id != null) params.append('comment_secondary_id', 'second_id');
        u.search = params.toString();
        u.hash = `#reply${second_id || root_id}`;
        return [u.hash, u.toString()];
    }

    function classNameMatch(elem, ...classNames) {
        if (!(elem instanceof Element)) return false;
        for (let name of classNames) {
            if (!elem.classList.contains(name)) return false;
        }
        return true;
    }

    document.addEventListener('DOMContentLoaded', function() {
        let observer = new MutationObserver(function(mutationsList, observe) {
            mutationsList.forEach(l => {
                l.addedNodes?.forEach(e => {
                    if (classNameMatch(e, ...main_comment_classes) || classNameMatch(e, ...reply_classes)) {
                        e.querySelectorAll(bar_selector).forEach(info => {
                            let newSpan = document.createElement('a');
                            newSpan.textContent = "直达链接";
                            newSpan.className = "btn-hover btn-highlight";
                            newSpan.title = '点击复制';
                            newSpan.addEventListener('click', function (e) {
                                GM_setClipboard(e.target.dataset.url, 'text');
                                showToast('已复制');
                                if (!isDetailPage()) {
                                    e.preventDefault();
                                    return false;
                                }
                            });
                            if (is_new_comment) {
                                newSpan.style = 'margin-left: 19px;';
                            }
                            info.insertBefore(newSpan, info.querySelector('div'));
                            let [hash, url] = getUrl(newSpan);
                            newSpan.dataset.url = url;
                            newSpan.href = isDetailPage() ? hash : url;
                        });
                        updateSelectedState(e);
                    }
                })
            })
        });
        observer.observe(document.body, { 'childList': true, 'subtree': true })
    });
    console.log("b comment loaded");
})(unsafeWindow);