Greasy Fork 还支持 简体中文。

Bilibili ViewSync

在哔哩哔哩实现类似viewsync的功能。WIP。装好以后功能入口位于t.bilibili.com右侧栏。

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Bilibili ViewSync
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  在哔哩哔哩实现类似viewsync的功能。WIP。装好以后功能入口位于t.bilibili.com右侧栏。
// @author       yuyuyzl
// @match        https://live.bilibili.com/*
// @match        https://t.bilibili.com/*
// @grant        unsafeWindow
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_setClipboard
// @grant        GM_info
// ==/UserScript==
var config={
    "roomids":[],
};
(function() {
    'use strict';
    var reloadConfig=function(){
        Object.keys(config).forEach(function(key){

            //console.log(key,config[key]);
            var valuet=GM_getValue(key);
            if(valuet!=null){
                config[key]=valuet;
            }else {
                GM_setValue(key,config[key]);
            }
        });
    };
    reloadConfig();
    // throttle 和 debouce 函数的底层实现
    var limit = function(func, wait, debounce) {
        var timeout;
        return function() {
            var context = this, args = arguments;
            // 封装函数,用于延迟调用
            var throttler = function() {
                // 只是节流函数的时候,对其timeout进行赋值为null,这样可以设置下一次的setTimtout
                timeout = null;
                func.apply(context, args);
            };
            // 如果debouce是true的话,前一个函数的调用timeout会被清空,不会被执行
            // 就是debounce函数的调用,这个前一个函数的不会执行.下面会重新设定setTimeout用于
            // 执行这一次的调用.
            // 但是如果是throttle函数,则会执行前一个函数的调用,同时下面的setTimeout在
            // 函数没有运行的时候,是无法再次设定的.
            if (debounce) clearTimeout(timeout);
            // 如果debouce是true 或者 timeout 为空的情况下,设置setTimeout
            if (debounce || !timeout) timeout = setTimeout(throttler, wait);
        };
    };

    // throttle 节流函数
    var throttle = function(func, wait) {
        return limit(func, wait, false);
    };

    // debouce 多次调用,只执行最后一次.
    var debounce = function(func, wait) {
        return limit(func, wait, true);
    };

    if(window.top!==window){
        setInterval(()=>{document.querySelector("body").className=" player-full-win over-hidden hide-aside-area";},100);

    }

    if(window.location.href.match(/.*t.bilibili.com.*/)){
        window.onload=function() {
            var mystyle=document.createElement("style");
            mystyle.innerHTML= "<!-- -->    .ratio{\n        display: flex;\n        flex-grow: 1;\n    }\n    #root{\n        width: 100vw;height: 100vh;margin: 0;\n        display: flex;\n    }\n    iframe{\n        position: absolute;\n        border: 0;\n        background: #FFF;\n        top:0;\n        left: 0;\n    }\n    .mypanel{\n        position: relative;\n        width: 100%;\n        background-color: #fff;\n        border-radius: 4px;\n        margin: 8px 0;\n    }\n    .mypanel .title{\n        padding-top: 12px;\n        padding-left: 16px;\n        padding-bottom: 8px;\n    }\n    .mypanel div{\nmargin-top:4px;\n}\n.mypanel button{\n-webkit-appearance: none;\nwidth: 24px;\nfont-size: 12px;\ncolor: #00a1d6;\nborder: 1px solid #00a1d6;\nborder-radius: 4px;\nbackground: transparent;\ncursor: pointer;\n}\n    .mypanel .more-button{\n        position: absolute;\n        width: 30px;\n        height: 20px;\n        top: 13px;\n        right: 20px;\n    }";

            document.getElementsByTagName("head")[0].appendChild(mystyle);
            var mypanel=document.createElement("div");
            mypanel.className="mypanel";
            mypanel.innerHTML="<p class=\"title tc-black fs-14 ls-0\">Bili ViewSync</p>\n<a href=\'https://live.bilibili.com/sync\' style=\'color: darkgray\' target=\'_blank\' class=\'more-button\'>Go></a>\n<div style=\'padding-left: 16px;padding-bottom: 14px;\'>\n    <input type=\'text\' id=\'bvs-addinput\' >\n    <button id=\'bvs-add\'>+</button>\n    <div id=\'rooms-container\'></div>\n</div>\n";
            document.querySelector(".live-panel").after(mypanel);
            function addroomid(id,doAdd){
                if(doAdd && config.roomids.indexOf(id)>=0)return;
                if(id==null)return;
                if(doAdd){
                    config.roomids.push(id);
                    GM_setValue("roomids",config["roomids"]);
                }
                let line=document.createElement("div");
                let buttonDel=document.createElement("button");
                buttonDel.innerText="-";
                buttonDel.onclick=function () {
                    document.getElementById("rooms-container").removeChild(line);
                    config.roomids.splice(config.roomids.indexOf(id),1);
                    GM_setValue("roomids",config["roomids"]);
                }
                line.appendChild(buttonDel);
                line.appendChild(document.createTextNode(id));
                document.getElementById("rooms-container").appendChild(line);
            }
            document.getElementById("bvs-add").onclick=function () {
                addroomid(document.getElementById("bvs-addinput").value.match(/[0-9]+/)[0],true);
                document.getElementById("bvs-addinput").value="";
            };
            for(let id of config.roomids)addroomid(id,false);
        }
    }

    if(window.location.href.match(/.*live.bilibili.com\/sync.*/)){
        var body;
        var clientRatio=[];
        var splitNow=[];
        var roomids=config.roomids;
        if(roomids.length==0){
            alert("直播间列表为空,请先去动态首页添加!");
            window.location.href="https://t.bilibili.com/";
        }
        var possibleSplit=solve(roomids.length);
        var iframes=roomids.map(id=>{
            let iframe=document.createElement("iframe");
            iframe.setAttribute("src","https://live.bilibili.com/"+id);
            return iframe;
        });
        var iframeCount=0;
        function solve( x,limit) {
            if(limit===undefined)limit=x-1;
            if (x === 1) return [[1]];
            let ret = [];
            for (let i = 1; i < Math.min(limit, x-1) + 1; i++) {
                let sub = solve(i, i - 1);
                let now = solve(x-i, i);
                for (let info of sub)
                    for (let more of now)
                        if (JSON.stringify(info) === "[1]") {
                            //console.log("1*" + " " + JSON.stringify(info) + " " + JSON.stringify(more) + " " + JSON.stringify(info.concat(more)));
                            ret.push(more.concat(info));
                        }
                        else {
                            //console.log("2*" + " " + JSON.stringify(info) + " " + JSON.stringify(more) + " " + JSON.stringify([info].concat(more)));
                            ret.push(more.concat([info]));
                        }
            }
            if(limit>=x){
                let now=[];
                for (let more of ret){
                    if(JSON.stringify(more) !== "[1]")now.push([more]);
                }
                ret=ret.concat(now);
            }
            return ret
        }

        function refreshFramesPos(){

            clientRatio=[body.clientWidth,body.clientHeight];
            let best=null;
            let bestArea=0;
            for(let i of possibleSplit){
                var ratio=getParentRatio(i,false);
                var area=Math.min(ratio[0]*ratio[1]*(clientRatio[0]/ratio[0])*(clientRatio[0]/ratio[0]),ratio[0]*ratio[1]*(clientRatio[1]/ratio[1])*(clientRatio[1]/ratio[1]));
                if(area>bestArea){
                    best=[i,false];
                    bestArea=area;
                }

                var ratio=getParentRatio(i,true);
                var area=Math.min(ratio[0]*ratio[1]*(clientRatio[0]/ratio[0])*(clientRatio[0]/ratio[0]),ratio[0]*ratio[1]*(clientRatio[1]/ratio[1])*(clientRatio[1]/ratio[1]));
                if(area>bestArea){
                    best=[i,true];
                    bestArea=area;
                }
            }
            //console.log(JSON.stringify(best));
            iframeCount=0;
            if(JSON.stringify(best)!==JSON.stringify(splitNow)) {
                splitNow=best;
                let newChild = getDOMNodes(best[0], best[1]);
                newChild.id = "splitter";
                console.log(newChild);
                console.log(newChild.myRatio);
                body.replaceChild(newChild, body.children.splitter);
            }
            for(let i=0;i<iframes.length;i++){
                let parent=document.getElementById("frame"+i);
                iframes[i].style.top=parent.offsetTop+"px";
                iframes[i].style.left=parent.offsetLeft+"px";
                iframes[i].style.width=parent.offsetWidth+"px";
                iframes[i].style.height=parent.offsetHeight+"px";

            }
        }
        var refreshFramesPosDebounced=debounce(refreshFramesPos,1000);

        window.onresize=function(){
            //console.log(body.clientWidth+" "+body.clientHeight);
            refreshFramesPosDebounced();
        };

        window.onload=function(){


            document.getElementsByTagName("head")[0].innerHTML="<style>\n" +
                "    .ratio{\n" +
                "        display: flex;\n" +
                "        flex-grow: 1;\n" +
                "    }\n" +
                "    #root{\n" +
                "        width: 100vw;height: 100vh;margin: 0;\n" +
                "        display: flex;\n" +
                "    }\n" +
                "    iframe{\n" +
                "        position: absolute;\n" +
                "        border: 0;\n" +
                "        background: #FFF;\n" +
                "        top:0;\n" +
                "        left: 0;\n" +
                "    }\n    .mypanel{\n        position: relative;\n        width: 268px;\n        background-color: #fff;\n        border-radius: 4px;\n        margin-bottom: 8px;\n    }\n    .mypanel .title{\n        padding-top: 12px;\n        padding-left: 16px;\n        padding-bottom: 8px;\n    }\n" +
                "</style>";

            document.getElementsByTagName("body")[0].outerHTML="<body style=\"width: 100vw;height: 100vh;margin: 0\">\n" +
                "<div id=\"root\"><div id=\"splitter\"/></div>\n" +
                "<div id=\"frames\"></div>\n" +
                "</body>";


            body=document.getElementById("root");
            for(let iframe of iframes)document.getElementById("frames").appendChild(iframe);
            window.onresize();
        };
        var videoW=16;
        var videoH=9;
        function getParentRatio(arr,isVertical){
            let ratio=null;
            if(Array.isArray(arr)){
                for(let item of arr){
                    let res=getParentRatio(item,!isVertical);
                    if(ratio==null)ratio=res;else
                    if (isVertical) ratio[1]+=res[1]/res[0]*ratio[0];else ratio[0]+=res[0]/res[1]*ratio[1];
                }
            }else return [videoW,videoH];
            return ratio;
        }

        function getDOMNodes(arr,isVertical){
            let ratio=null;
            if(Array.isArray(arr)){
                let dom=document.createElement("div");
                dom.className="ratio container";
                if (isVertical) dom.style.flexDirection="column";
                for(let item of arr){
                    let newNode=getDOMNodes(item,!isVertical);
                    if(ratio==null){
                        ratio=newNode.myRatio;
                        if (isVertical) {
                            newNode.style.flexGrow=newNode.myRatio[1]/newNode.myRatio[0]*ratio[0];
                        }else {
                            newNode.style.flexGrow=newNode.myRatio[0]/newNode.myRatio[1]*ratio[1];
                        }
                    }else
                    if (isVertical) {
                        ratio[1]+=newNode.myRatio[1]/newNode.myRatio[0]*ratio[0];
                        newNode.style.flexGrow=newNode.myRatio[1]/newNode.myRatio[0]*ratio[0];
                    }else {
                        ratio[0]+=newNode.myRatio[0]/newNode.myRatio[1]*ratio[1];
                        newNode.style.flexGrow=newNode.myRatio[0]/newNode.myRatio[1]*ratio[1];
                    }

                    dom.appendChild(newNode);
                }
                dom.myRatio=ratio;
                return dom;
            }else {
                let dom= document.createElement("div");
                //dom.innerText="FRAME";
                //dom.appendChild(iframes[iframeCount]);
                dom.id="frame"+iframeCount;
                iframeCount++;
                dom.className="ratio iframe";
                dom.myRatio=[videoW,videoH];
                return dom;
            }

        }
    }


})();