Greasy Fork

Greasy Fork is available in English.

b站vtuber直播同传评论转字幕

将vtuber直播时同传man的评论,以类似底部弹幕的形式展现在播放器窗口,免去在众多快速刷过的评论中找同传man的痛苦。

当前为 2020-03-30 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         b站vtuber直播同传评论转字幕
// @namespace    http://tampermonkey.net/
// @version      0.2
// @author       You
// @match        https://live.bilibili.com/*
// @grant        none
// @require      https://cdn.staticfile.org/jquery/3.4.1/jquery.min.js
// @require      https://cdn.staticfile.org/vue/2.6.11/vue.min.js
// @description  将vtuber直播时同传man的评论,以类似底部弹幕的形式展现在播放器窗口,免去在众多快速刷过的评论中找同传man的痛苦。


// ==/UserScript==


(function () {
// 字幕样式配置


  const removeBracket = true

  window.attentionModul = {}
  window.attentionModul.commentApp = {}
  window.attentionModul.consoleApp = {}
  window.attentionModul.users = []
  window.attentionModul.observe = {}
  window.attentionModul.dom = []
  // 测试用组件
  window.attentionModul.debug = {}
  window.attentionModul.debug.setComment = function (index, comment) {
    setComment(index, comment)
  }
  window.attentionModul.debug.addAttentionUser = addAttentionUser
  window.attentionModul.debug.showDOM = function () {
    return [document.getElementById('comment-container'), document.getElementById('console-container')]
  }
  window.attentionModul.config = JSON.parse(localStorage.getItem('config')|| '{"color":"","vertical":"2","fontSize":40}')


  // 添加关注用户
  function addAttentionUser(uid) {
    console.log('add subscribe:', uid)
    $('textarea:eq(0)').click()
    if (window.attentionModul.users.indexOf(uid) < 0) {
      window.attentionModul.users.push(uid)
      window.attentionModul.commentApp.comments.push(null)
    }
  }

  // 移除关注用户
  function removeAttentionUser() {

  }

  // 移除括号
  function removeBreaket(comment) {
    let r = comment.match(/^[ 【]*(.*?)[ 】]*$/i)[1]
    return r
  }

  //将匹配评论添加到vue变量中对应的字幕DOM
  function setComment(index, comment) {
    console.log('comment', comment)
    window.attentionModul.commentApp.comments.splice(index, 1, comment)
  }

  //匹配评论发出者与关注用户
  function chatFilter(nodeList) {
    for (let item of nodeList) {
      const uid = item.getAttribute('data-uid')
      const comment = item.getAttribute('data-danmaku')
      const index = window.attentionModul.users.indexOf(uid)
      if (index > -1) {
        if (removeBreaket) {
          setComment(index, removeBreaket(comment))
        } else {
          setComment(index, comment)
        }
      }
    }
  }

  // DOM突变事件回调函数
  function mutationListener(mutationList) {
    window.attentionModul.observe.message = mutationList

    if (window.attentionModul.observe.message[0].addedNodes.length > 0) {
      chatFilter(window.attentionModul.observe.message[0].addedNodes)
    }
  }

  function saveConfig() {
    const config = {
      "color":window.attentionModul.consoleApp.color,
      "vertical":window.attentionModul.consoleApp.vertical,
      "fontSize":window.attentionModul.consoleApp.fontSize
    }
    localStorage.setItem('config',JSON.stringify(config))
  }

  // 注入控制台组件
  ;(function () {
    const consoleStyle = `
.att-col{
      display: flex;
      flex-flow: column nowrap;
      place-content: center start;
    }
    .att-row{
      display: flex;
      flex-flow: row nowrap;
      place-content: center start;
      margin:3px 0px 3px 0px;
    }
    .att-top{
      z-index: 999;
    }
    .att-input{
      outline: none;
    }
    #console-container{
      position: absolute;
      left: 50px;
      top: 300px;
      min-height: 48px;
      min-width: 48px;
    }
    .att-icon-container {
      position: absolute;
      left: 0px;
      top: 0px;
      height: 48px;
      width: 48px;
      background-color: #fb7299;
      border-radius: 4px;
    }
    .att-icon {
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      height: 32px;
      width: 32px;
      background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADFElEQVRYhe2XX2iPURjHP7+N0oYRWWObMZSUJjQKG/JnlCh/LkSaUG6URLlf8pNSciEX1AqzaEXKhdTMnVHmRogxtOZfG/m/r956Th3v791r7++nXPCt0znv8zznOd/3Oc95zvvyzyPlAiDpd7GYAywAioEioBd4BbQCd5MGMpVK/SoICAzQdku6p3i0S6qP8ZHRMhBhNN8c++iSdFVSk/UvQvo2SbP+BIGNIceNkhZKKgjZFUqqkXTOs/0maXUuBJZ4zh5KWjzIsC6T9NSbm5Y0NymBIknvzcEDSWOT7KukkhCJAGclTRwsgVM26Yuk8oSLu1Yp6WuIRI9tVSyBUm/CviwXd+2g+en3fP6QVB1HYK8ZvpGUnyOBYZJ6jUCDpFvmu9slskOex2GB9ZeAH5kUE+Ez0GKF7iOwxmTjgHSkI0mPjOX20NuMkTQlJJsRse/hhN1t/jrseZe3HSVRBD6ass5zEpyKZ5I+SJpnsrQl2RF7DuR9kp6bvZu71vy982TdLseitsCN/QwZBpQBhcAok00DhgJT7Xk0MBwoNXsHt40pz/dF62ujItBp7LaEQhkcn03ec7GkzXbmnSzQ14bm1XvFLGWybSZ7GkXgmimP5ngCXDtu/i57spUm+xS1BTetX5ck3WPg/LR5JvnW90cROG/9JGBDjotvASbY+IInL7e+K2OGhajFQtRlxy+b0Bd52d4U0rlbs3kgApV2bAIcypLADa/0loWIfTLdjoEIBO20GTUmXLhCUqtXbLaG9CddAkoaEUfghBmmvboefB1NHmDhCZL2S3odc5kt8nQH/LtgSEQCOdksoMESMig634ErQCfQZx+mlUCNFSpMvtNLaIcq698ChyPW/CUCZ5QcHXab+n7Ge+M15vGxk8VFoNcbtwPNViOCklwNzASWhuY8AV4C600/G1gO3AbOWLTcenl+HYiKQJC5eyRVxSRcUJ6vZxGp4HMtz49Akh+TKCwCVthPS4VdRj3AHeA+MB3YZBdWgA/AyGA592OSK4Ew8iM+ZgqAVUAd0AEcI+rP6D/+CoCf0HfCu9e1CkQAAAAASUVORK5CYII=');
    }
    #att-console{
      color: #23ade5;
      background-color: white;
      width: 250px;
      min-height: 48px;
      padding: 14px 8px 14px 8px;
      border-radius: 8px;
    }
    .att-console-title{
      margin:4px;
    }
    .att-input-range{
      width: 200px;
      margin-right: 10px;
      outline: none;
    }
    .att-input-value{
      width: 20px;
      text-align: center;
      border: 0px;
      border-bottom: 1px solid gray;
      outline: none;
    }
    `

    const consoleContainer = $('<div @mouseleave="consoleOut" @mouseover.stop="consoleIn" id="console-container" class="att-top"></div>')
    const widgetIcon = $('<div v-show="!isShowConsole"  class="att-icon-container att-top" ><div class="att-icon"></div></div>')
    const consoleWidget = $('<div v-show="isShowConsole" id="att-console" class="att-col"></div>')
    const verticalWidget = $('<div class="att-console-title">字幕水平高度</div><div class="att-row"><input @input="changeVertical" class="att-input-range" type="range" v-model:value="vertical"><input class="att-input-value" v-model:value="vertical" @change="changeVertical"></div>')
    const fontSizeWidget = $('<div class="att-console-title">字幕大小</div><div class="att-row"><input @input="changeFontsize" class="att-input-range" type="range" v-model:value="fontSize" min="1" max="500"  step="5"><input class="att-input-value" v-model:value="fontSize" @change="changeFontsize"></div>')
    const colorWidget = $('<div class="att-console-title">字幕颜色</div><div class="att-row"><input class="att-input-range" placeholder="示例:#030303" v-model:value="color" @change="changeColor" style="padding: 2px;border-radius: 2px;border: 0;border-bottom: 1px solid gray"><div @click="defualtColor" style="cursor:pointer;">默认</div></div>')
    consoleWidget.append(verticalWidget)
    consoleWidget.append(fontSizeWidget)
    consoleWidget.append(colorWidget)
    consoleContainer.append(widgetIcon)
    consoleContainer.append(consoleWidget)
    $('body').append($('<style></style>').text(consoleStyle))
    $('body').append(consoleContainer)

    const config = window.attentionModul.config
    window.attentionModul.consoleApp = new Vue({
      el: '#console-container',
      data() {
        return {
          isShowConsole: false,
          vertical:config['vertical'],
          fontSize:config['fontSize'],
          color:config['color']
        }
      },
      methods: {
        consoleIn() {
          this.isShowConsole = true
        },
        consoleOut() {
          this.isShowConsole = false
        },
        changeVertical(){
          this.vertical = Math.min(100,Math.max(0,this.vertical))
          $('#comment-container').css('bottom',this.vertical.toString()+'%')
          saveConfig()
        },
        changeFontsize(){
          this.fontSize = Math.min(500,Math.max(0,this.fontSize))
          $('#comment-container').css('fontSize',this.fontSize.toString()+'px')
          saveConfig()
        },
        changeColor(){
          $('#comment-container').css('color',this.color.toString())
          saveConfig()
        },
        defualtColor(){
          this.color = '#ffffff'
          $('#comment-container').css('color',this.color.toString())
          saveConfig()
        }
      }
    })
  })();
  // 注入字幕组件
  ;(function insertCommentWidget() {
    const container = $('.bilibili-live-player-video-danmaku')
    if (container.length > 0) {
      const commentWidget = $('<div id="comment-container" :style="widgetStyle"><div v-for="(comment,index) in comments" :style="boxStyle"><div v-show="comment" :style="commentStyle">{{comment}}</div></div></div>')
      let widgetStyle = 'z-index: 999;position:absolute;left:50%;transform:translateX(-50%);width:90%;'
      let boxStyle = ''
      let commentStyle = 'display:inline-block;backgroundColor:rgba(0,0,0,0.5);position:relative;left:50%;transform:translateX(-50%);word-break: break-all;padding:10px;'
      container.append(commentWidget)
      window.attentionModul.commentApp = new Vue({
        el: '#comment-container',
        data() {
          return {
            widgetStyle: widgetStyle,
            boxStyle: boxStyle,
            comments: new Array(window.attentionModul.users.length),
            commentStyle: commentStyle,
            dom:document.getElementById('comment-container')
          }
        }
      })
      window.attentionModul.consoleApp.changeVertical()
      window.attentionModul.consoleApp.changeColor()
      window.attentionModul.consoleApp.changeFontsize()

    } else {
      requestAnimationFrame(function () {
        insertCommentWidget()
      })
    }
  })();
// 在用户名点击菜单注入关注选项
  ;(function insertMenuWidget() {
      const menu = $('.danmaku-menu')
      const menuItem = menu.find('.report-this-guy')
      if (menuItem.length > 0) {
        let a = $('<a href="javascript:;" class="bili-link pointer" style="display: block">添加字幕特别关注</a>')
        menuItem.append(a)
        // 添加点击函数,获取用户uid,调用add函数添加到关注用户列表
        a.click(function () {
          const uid = menu[0].__vue__.uid
          addAttentionUser(uid.toString())
        })
      } else {
        requestAnimationFrame(function () {
          insertMenuWidget()
        })
      }
    }
  )();
  //

  // 监听评论DOM突变事件
  window.attentionModul.observe.observer = new MutationObserver(mutationListener)
  window.attentionModul.observe.config = {childList: true}
  ;(function openObserver() {
    window.attentionModul.observe.anchor = document.getElementsByClassName('chat-history-list')[0]
    if (window.attentionModul.observe.anchor) {
      window.attentionModul.observe.observer.observe(window.attentionModul.observe.anchor, window.attentionModul.observe.config)
    } else {
      requestAnimationFrame(function () {
          openObserver()
        }
      )
    }
  })();

})();