阻止跳转APP for 哔哩哔哩 (゜-゜)つロ 干杯~-bilibili

自动点开视频页、隐藏打开APP按钮、阻止跳转APP、恢复点击事件

// ==UserScript==
// @name         阻止跳转APP for 哔哩哔哩 (゜-゜)つロ 干杯~-bilibili
// @version      0.3
// @description  自动点开视频页、隐藏打开APP按钮、阻止跳转APP、恢复点击事件
// @author       Mannix
// @namespace    Mannix
// @match        *://m.bilibili.com/*
// @icon         https://www.bilibili.com/favicon.ico
// @run-at       document-start
// @grant        none
// @license      MIT
// ==/UserScript==

(async function () {
  const userScriptID = '阻止跳转APP for 哔哩哔哩 (゜-゜)つロ 干杯~-bilibili by Mannix'
  if (window[userScriptID]) return
  window[userScriptID] = true

  const handlers = [
    [ // 自动点开视频页
      '.btn.light',
      () => document.querySelector('#app').__vue__.$store.commit('video/setTrigglePlay', true)
    ],
    [ // 隐藏打开APP按钮
      '[class*=awaken-btn], [class*=openapp], [class*=callapp], ' +
      '.open-app, .guide-mask, .interact-wrapper, .tab-item.more, .btn-download, .open-app-btn-wrap, .section-preview-header>bili-open-app, ' +
      '.mplayer-buff-app, .mplayer-fullscreen-call-app, .mplayer-ending-panel-button',
      el => el.style.setProperty('display', 'none', 'important')
    ],
    [ // 阻止跳转APP、恢复点击事件
      '.launch-app-btn, .mplayer-display-call-app, .mplayer-ending-panel-video, bili-open-app, .m-search-user-item, a.face, .dynamic-list .card, .video-list>.card-box>div>.v-card-single',
      el => {
        el.addEventListener('click', overrideClick, true)
        for (const [selector, listener] of clickListeners) {
          if (el.matches(selector) || el.querySelector(selector)) {
            el.onclick = listener
            break
          }
        }
      }
    ],
    [ // 全屏播放
      '.btn-widescreen',
      el => el.addEventListener('click', playerFullscreen)
    ]
  ]

  const clickListeners = [
    ['.up .face', function () {
      open(`/space/${window.__INITIAL_STATE__.video.viewInfo.owner.mid}`)
    }],
    ['[data-aid]', function () {
      open(`/video/${av2BV(this.dataset.aid)}`, true)
    }],
    ['.mplayer-ending-panel-video', async function () {
      exitFullscreen()
      const { controller: { endingpanel: { list, index } } } = await window.player.playerPromise
      open(`/video/${list[index - 1].bvid}`, true)
    }],
    ['.icon-spread', function () {
      if (this.style.transform) {
        this.style.transition = 'transform 0.2s'
        this.style.transform = ''
        this.parentNode.nextElementSibling.remove()
      } else {
        const { desc } = window.__INITIAL_STATE__.video.viewInfo
        if (!desc) {
          this.style.display = 'none'
          return
        }
        this.style.transition = 'transform 0.2s'
        this.style.transform = 'rotate(180deg)'
        const div = document.createElement('div')
        div.style.cssText = 'white-space: pre-line; font-size: 3.46667vmin; line-height: 4.8vmin; color: #555;'
        div.textContent = desc
        this.parentNode.after(div)
      }
    }],
    ['.ep-profile', function () {
      this.querySelector('.ep-profile').classList.toggle('expand')
    }],
    ['.ep-info-wrapper, .season-list-wrapper *, .recom-item-wrapper', function () {
      open(this.getAttribute('universallink').match(/(?<!\/)\/[^/][^?]+/)[0], true)
    }],
    ['.dynamic-list .card', function () {
      open(`/opus/${this.__vue__.dynId}`)
    }],
    ['.opus-module-author__avatar, .opus-module-author__name', function () {
      open(`/space/${document.querySelector('.opus-module-author').__vue__.data.mid}`)
    }],
    ['.dyn-header', function () {
      open(`/space/${document.querySelector('[data-mid]').dataset.mid}`)
    }]
  ]

  const av2BV = (function () {
    // https://socialsisteryi.github.io/bilibili-API-collect/docs/misc/bvid_desc.html

    const XOR_CODE = 23442827791579n
    const MAX_AID = 1n << 51n
    const BASE = 58n

    const data = 'FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf'

    return function (aid) {
      const bytes = ['B', 'V', '1', '0', '0', '0', '0', '0', '0', '0', '0', '0']
      let bvIndex = bytes.length - 1
      let tmp = (MAX_AID | BigInt(aid)) ^ XOR_CODE
      while (tmp > 0) {
        bytes[bvIndex] = data[tmp % BASE]
        tmp = tmp / BASE
        bvIndex -= 1
      }
      ;[bytes[3], bytes[9]] = [bytes[9], bytes[3]]
      ;[bytes[4], bytes[7]] = [bytes[7], bytes[4]]
      return bytes.join('')
    }
  })()

  function open (url, useRouter = false) {
    if (useRouter) {
      document.querySelector('#app').__vue__.$router.push(url)
    } else {
      window.open(url, '_self')
    }
  }

  function overrideClick (event) {
    event.stopImmediatePropagation()
    this.onclick?.(event)
  }

  function playerFullscreen () {
    const player = document.querySelector('.player-container')
    if (player.classList.contains('wide')) {
      for (const key of ['requestFullscreen', 'webkitRequestFullscreen', 'mozRequestFullScreen']) {
        if (key in player) {
          player[key]()
          break
        }
      }
    }
  }

  function exitFullscreen () {
    for (const [el, fn] of [['fullscreenElement', 'exitFullscreen'], ['webkitFullscreenElement', 'webkitExitFullscreen'], ['mozFullScreenElement', 'mozCancelFullScreen']]) {
      if (el in document) {
        if (document[el]) document[fn]()
        break
      }
    }
  }

  function handleDescendant (node) {
    for (const [selector, handler] of handlers) {
      node.querySelectorAll(selector).forEach(handler)
    }
  }

  function handleSelf (node) {
    for (const [selector, handler] of handlers) {
      if (node.matches(selector)) handler(node)
    }
  }

  const concat = window.String.prototype.concat
  window.String.prototype.concat = function () {
    // bilibili:// 替换为 javascript://(这里的 // 是注释)以阻止跳转APP
    const result = concat.apply(this.replace(/^bilibili:\/\//, 'javascript://'), arguments)
    return result
  }

  if (document.readyState === 'loading') {
    await new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve))
  }

  handleDescendant(document)

  new window.MutationObserver(mutations => mutations.forEach(mutation => {
    mutation.addedNodes.forEach(node => {
      if (!(node instanceof window.HTMLElement)) return
      handleSelf(node)
      handleDescendant(node)
    })
  })).observe(document, { childList: true, subtree: true })

  const css = document.createElement('style')
  css.innerHTML = '.ep-list-pre-header i::after { display: none !important; }'
  document.body.append(css)
})()