B直播快捷弹幕

b站直播间内使用悬浮列表快捷输入弹幕,保存一条弹幕历史,弹幕内容添加拼音发送

// ==UserScript==
// @name         B直播快捷弹幕
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  b站直播间内使用悬浮列表快捷输入弹幕,保存一条弹幕历史,弹幕内容添加拼音发送
// @author       RecursiveMaple
// @match        https://live.bilibili.com/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @require      https://cdn.staticfile.org/jquery/3.6.3/jquery.min.js
// @require      https://cdn.staticfile.org/jquery-cookie/1.4.1/jquery.cookie.min.js
// @require      https://unpkg.com/[email protected]/dist/index.js
// @license      GNU General Public License v3.0 or later
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        unsafeWindow
// ==/UserScript==

var htmlText = `
<div class="blds-main-window">
    <li class="blds-shotcuts-list"></li>
    <hr>
    <li class="blds-util-list">
        <ul>
            <span>🕑</span>
            <input type="button" id="blds-input-history" value="">
        </ul>
        <ul>
            <span>➕</span>
            <input type="text" id="blds-input-add" maxlength="20" placeholder="添加快捷弹幕(限长20)">
            <button id="blds-btn-add">+</button>
        </ul>
        <ul>
            <span>py</span>
            <input type="text" id="blds-input-py" maxlength="20" placeholder="词语添加拼音(限长20)例:'雫露露1'">
            <button id="blds-btn-py">↵</button>
        </ul>
    </li>
</div>
`;
var cssText = `
.blds-main-window {
    all: initial;
    z-index: 100;
    display: none;
    position: absolute;
    bottom: 100%;
    right: 0%;
    width: 100%;
    border: 2px solid black;
    background-color: lightgray;
    font-size: 14px;
    counter-reset: shortcut-list;
}

.blds-main-window li {
    display: block;
    width: 100%;
    border: none;
    width: 100%;
}

.blds-main-window hr {
    border: none;
    border-top: 3px double black;
    margin: 1px;
}

.blds-main-window ul {
    display: flex;
    border-bottom: 1px solid black;
    padding: 2px;
}

.blds-main-window span {
    width: 23px;
    height: 23px;
    text-align: center;
    font-weight: bold;
    border-right: 1px dashed black;
}

.blds-shotcuts-list {
    max-height: 25em;
    overflow-y: scroll;
    overscroll-behavior: contain;
    scrollbar-width: thin;
}

.blds-shotcuts-list span::before {
    counter-increment: shortcut-list;
    content: counter(shortcut-list) ".";
}

.blds-main-window input {
    flex: auto;
    margin-left: 3px;
    text-align: left;
    font-size: 10px;
}

.blds-main-window button {
    margin: 0px 3px;
    width: 23px;
    height: 23px;
    padding: 0px;
}
`;


/*---------- 快捷弹幕数组相关操作 ----------*/
// templateVarIndex由css自动填写
// templateString不能动态更新,这里用函数填写模板
function getListItem(value) {
    return `
    <ul>
        <span></span>
        <input type="button" value=${value}>
        <button class="blds-btn-del">−</button>
    </ul>
    `;
}
var shortcutList = [];
function initList() {
    $(".blds-shotcuts-list").empty();
    for (var i in shortcutList) {
        // js的for in是以字符串形式的数字作索引!?你妈的为什么。。。
        $(".blds-shotcuts-list").append(getListItem(shortcutList[i]));
    }
}
function addListItem(value) {
    shortcutList.push(value);
    saveList();
    $(".blds-shotcuts-list").append(getListItem(value));
}
function delListItem($ul) {
    // 接受jquery对象,找到下标删除
    var index = $ul.index();
    shortcutList.splice(index, 1);
    saveList();
    $ul.remove();
}
function loadList() {
    shortcutList = GM_getValue("shortcutList", []);
}
function saveList() {
    GM_setValue("shortcutList", shortcutList);
}
/*---------- vvvvvvvvvv ----------*/

/*---------- 拼音转换相关操作 ----------*/
// 引入拼音转换模块
var { pinyin } = pinyinPro;
// 正则格式:中文词语后加数字串(可选)
var regex = /(^[\u4E00-\u9FA5]+)([0-9]*$)/;
function addPinyin(str) {
    // 读入中文词语,判字符串形式
    var resStr = "";
    if (!regex.test(str)) return resStr;
    var regObj = str.match(regex);
    var words = regObj[1];
    var numbers = regObj[2];
    var wordsList = words.split('');
    var numbersList = numbers.split('').map(Number);
    // var resList = pinyin(words, { type: 'array' });//这种形式会被吞弹幕
    var resList = pinyin(words, { toneType: 'num', type: 'array' });
    var mode = numbers == "" ? "FULL" : "PARTIAL";

    for (var i = 0; i < wordsList.length; i++) {
        resStr += wordsList[i];
        if (mode == "FULL" || numbersList.includes(i + 1)) {
            resStr += resList[i];
        }
    }
    return resStr;
}
function setPinyinHint(hintStr) {
    if (hintStr == "") {
        $("#blds-input-add").attr("placeholder", "添加快捷弹幕(限长20)");
    }
    else {
        $("#blds-input-add").attr("placeholder", hintStr);
    }
}
/*---------- vvvvvvvvvv ----------*/

/*---------- 脚本发送弹幕 ----------*/
var postUrl = "https://api.live.bilibili.com/msg/send";
var data = {
    fontsize: "25",
    csrf: "",
    csrf_token: "",
    roomid: "",
    mode: "1",//默认滚动,
    color: "16777215",//默认白色
    bubble: "0",// TODO 弹幕背景气泡,不知道怎么获取,0是无气泡,5是舰长气泡?
    //以上为初始化时一次性填入,以下为每次发送弹幕时填入
    rnd: "",
    msg: "",
}
var danmakuPositionMap = { "滚动": "1", "底部": "4", "顶部": "5" };
function rgbToDecimal(rgbText) {
    // 输入例:"rgb(88, 193, 222)"
    // 输出例:"5816798"
    var colorRegex = /([0-9]+)/g;
    var regObj = rgbText.match(colorRegex);
    var hexStr = regObj.map(function (decimalStr) {
        return parseInt(decimalStr).toString(16);
    }).join("");
    var decimalStr = parseInt(hexStr, 16).toString();
    return decimalStr;
}
function initDataBlock() {
    // 要保证在网页资源加载后执行!
    data.csrf = $.cookie('bili_jct');
    data.csrf_token = data.csrf;
    var roomUrl = $(location).attr('href');
    var roomIdRegex = /.com\/([0-9]+)/;
    data.roomid = roomUrl.match(roomIdRegex)[1];
    $("#control-panel-ctnr-box").one("DOMNodeInserted", function (event) {
        var t = event.target;
        if (!$(t).hasClass("danmakuPreference")) return;
        // console.log("进入DOMNodeInserted处理");//TODO DEBUG
        $(t).hide();
        // 等待加载动画播放完,目标元素动态插入
        var elemSearchCount = 0;
        var timer = setInterval(function () {
            elemSearchCount++;
            if ($(t).find(".dot-wrapper.active").length > 0) {
                clearInterval(timer);
                var modeStr = $(t).find(".danmaku-position-item.active").attr("title");
                data.mode = danmakuPositionMap[modeStr];
                var colorStr = $(t).find(".dot-wrapper.active span").css("background-color");
                data.color = rgbToDecimal(colorStr);
                console.log("Danmaku preference loaded. Mode=", data.mode, ",Color=", data.color);//INFO
                $(this).unbind("DOMNodeInserted");
                $("#control-panel-ctnr-box span[title='弹幕设置']").click();
            }
            else if (elemSearchCount >= 30) {
                clearInterval(timer);
                console.log("Loading danmaku preference element failed. [in function initDataBlock()]");//INFO
                $(this).unbind("DOMNodeInserted");
                $("#control-panel-ctnr-box span[title='弹幕设置']").click();
            }
        }, 100)
    });
    $("#control-panel-ctnr-box span[title='弹幕设置']").click();
}
function sendDanmaku(msg) {
    data.rnd = parseInt(Date.now() / 1000);
    data.msg = msg;
    // console.log(data);//DEBUG
    // 不用ajax发送表单,可能会被吞弹幕,改用formdata
    var formData = new FormData();
    formData.append("bubble", data.bubble);
    formData.append("msg", data.msg);
    formData.append("color", data.color);
    formData.append("mode", data.mode);
    formData.append("fontsize", data.fontsize);
    formData.append("rnd", data.rnd);
    formData.append("roomid", data.roomid);
    formData.append("csrf", data.csrf);
    formData.append("csrf_token", data.csrf_token);
    // $.post()不支持加cookie
    $.ajax({
        type: 'POST',
        url: postUrl,
        // data: data,
        data: formData,
        xhrFields: {
            withCredentials: true // 请求加入cookie
        },
        contentType: false,
        processData: false,
        success: function (data) {
            // console.log(data);//DEBUG
            if (data.msg == "") {
                // 发送成功,保存为历史
                $("#blds-input-history").val(msg);
            }
        },
    });
}
/*---------- vvvvvvvvvv ----------*/

/*---------- 注入窗口、初始化设置、添加按钮逻辑、添加鼠标悬浮事件等 ----------*/
var selWhereInsertHTMLBefore = "textarea.chat-input";
var selWhereGetWinWidth = ".chat-input-ctnr";
var selWhereDetectMouseHover = ".chat-input-ctnr div:last-child";
var selWhereSendDanmaku = "#control-panel-ctnr-box .bl-button";
function main() {
    // 弃用$(".chat-input-ctnr div:last-child").css({"position":"relative","display":"inline-block"});
    // 插入html
    $(selWhereInsertHTMLBefore).before(htmlText);
    if ($(".blds-main-window").length == 0) {
        console.log("Inserting HTML failed. [in function main()]");
        return;
    }
    // 插入css,调整窗口大小
    GM_addStyle(cssText);
    var blds_main_window_width = $(selWhereGetWinWidth).width();
    $(".blds-main-window").css("width", blds_main_window_width);
    // 初始化装入list项
    loadList();
    initList();
    // 抓取弹幕设置
    initDataBlock();

    //按钮:删除快捷弹幕
    $(".blds-shotcuts-list").on("click", "button", function (event) {
        //给未来元素自动绑定事件用on,先绑定到子元素数量变化的父元素
        var t = event.target;
        delListItem($(t).parent());
    });
    // 按钮:添加快捷弹幕
    $("#blds-btn-add").click(function () {
        var value = $("#blds-input-add").val();
        if (value.trim() != "") addListItem(value);
        $("#blds-input-add").val("");// 清空input
    });
    // 按钮:转拼音
    $("#blds-btn-py").click(function () {
        var value = $("#blds-input-py").val();
        var rev = addPinyin(value);
        if (rev != "") {
            sendDanmaku(rev);
            $("#blds-input-py").val("");// 清空input
            setPinyinHint("");// 清空预览
        }
    });
    // 检测输入内容改变,显示拼音预览
    $("#blds-input-py").on("input", function (event) {
        var value = $("#blds-input-py").val();
        var rev = addPinyin(value);
        setPinyinHint(rev);
    });
    // 发送弹幕按钮,要求未来元素自动绑定
    $(".blds-main-window").on("click", "input[type='button']", function (event) {
        var t = event.target;
        var msg = $(t).val();
        sendDanmaku(msg);
    });
    // 检测输入框按下回车键
    $(".blds-main-window input[type='text']").keypress(function (event) {
        if (event.which == '13') {
            // console.log("按下回车");//DEBUG
            // next元素是按钮
            $(this).next().click();
        }
    });

    // 监听B站发送弹幕按钮,将内容转存到历史记录
    // 由于只能在原click事件之后绑定,value会被原事件处理函数清空
    // 也要考虑用回车发弹幕的情况,决定用input事件监视输入框
    // B站发送弹幕后清空输入框不会触发input
    var tempValue = "";
    $(selWhereInsertHTMLBefore).on("input", function (event) {
        tempValue = event.target.value;
    });
    $(selWhereSendDanmaku).click(function () {
        if (tempValue.trim() != "") {
            $("#blds-input-history").val(tempValue);
            // console.log("内容转存到历史记录:",tempValue);//TODO DEBUG
        }
        tempValue = "";
    });
    // B站弹幕输入框响应不了keypress,focus等事件,你妈的为什么
    $(selWhereInsertHTMLBefore).keydown(function (event) {
        if (event.which == '13') {
            // console.log("按下回车");//DEBUG
            if (tempValue.trim() != "") {
                $("#blds-input-history").val(tempValue);
                // console.log("内容转存到历史记录:",tempValue);//TODO DEBUG
            }
            tempValue = "";
        }
    });

    // 控制显示和隐藏窗口
    $(selWhereDetectMouseHover).hover(
        function () {
            // 每次进入后重置输入框
            if ($(".blds-main-window input[type='text']:focus").length == 0) {
                $(".blds-main-window input[type='text']").val("");
            }
            $(".blds-main-window").css("display", "block");
        },
        function () {
            // timer处理hover被输入法界面遮挡的情况
            var timer = setInterval(function () {
                if ($(selWhereDetectMouseHover + ":hover").length > 0) {
                    clearInterval(timer);
                }
                else if ($(".blds-main-window input[type='text']:focus").length == 0) {
                    $(".blds-main-window").css("display", "none");
                    clearInterval(timer);
                }
            }, 100)
        }
    );
}
/*---------- vvvvvvvvvv ----------*/

(function () {
    'use strict';
    // Your code here...
    //最外层$("#chat-control-panel-vm .chat-input-ctnr")
    //上层$(".chat-input-ctnr div:last-child")
    //本层$("textarea.chat-input")

    var maxRetry = 20;
    var elemSearchCount = 0;
    var timer = setInterval(function () {
        elemSearchCount++;
        if ($(selWhereInsertHTMLBefore).length > 0) {
            console.log("Ready after", elemSearchCount, "tries");
            clearInterval(timer);
            main();
        }
        else if (elemSearchCount >= maxRetry) {
            console.log("Searching failed after", elemSearchCount, "tries");
            clearInterval(timer);
        }
    }, 500)
})();