bilibili 移动端

b 站移动端网页推荐视频直接看

当前为 2024-03-21 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name               Bilibili Mobile
// @name:zh-CN         bilibili 移动端
// @namespace          https://github.com/jk278/bilibili-mobile
// @version            2.8.1
// @description        view bilibili mobile page recommended video directly
// @description:zh-CN  b 站移动端网页推荐视频直接看
// @author             jk278
// @match              *://m.bilibili.com
// @match              *://m.bilibili.com/video/*
// @grant              none
// @run-at             document-start
// @icon               https://www.bilibili.com/favicon.ico
// ==/UserScript==

(function () {
  'use strict'
  console.log('Bilibili mobile execute!')

  removeAdButton()

  const pathname = window.location.pathname

  if (pathname.startsWith('/video')) {
    customElementStyle()
  }

  waitDOMContentLoaded(() => {
    if (pathname.startsWith('/video')) {
      autoplay()
      preventAutoCallApp()
      removeFullscreenAd()
    } else if (pathname === '/' || pathname === '') {
      runHome()
    }
  })

  // DOM 加载完后
  function waitDOMContentLoaded (callback) {
    document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', callback) : callback()
  }

  // 阻止点击视频区跳转APP
  function preventAutoCallApp () {
    // 阻止点击视频区跳转APP
    const autoCallApp = document.querySelector('.mplayer-display-call-app')
    autoCallApp.addEventListener('click', (event) => {
      event.preventDefault()
      event.stopImmediatePropagation()
    })

    const playButton = document.querySelector('.mplayer-pause-call-app')
    playButton.addEventListener('click', (event) => {
      event.preventDefault()
      event.stopImmediatePropagation()
      // 播放监听此时还未被阻止
    })
  }

  // 全屏后,全屏广告元素样式变为 flex !important
  function removeFullscreenAd () {
    // 全屏后跳App的横幅、清晰度按钮、弹幕提示
    document.addEventListener('fullscreenchange', function () {
      const adBanner = document.querySelector('.mplayer-widescreen-callapp')
      const qualityBtn = document.querySelector('.mplayer-control-btn-quality')
      const text = document.querySelector('.mplayer-comment-text')

      adBanner.style.cssText = 'display: none !important;'
      qualityBtn.style.cssText = 'display: none !important;'
      text.style.cssText = 'display: none !important;'

      const commentContent = document.querySelector('.mplayer-btn-comment-content')
      commentContent.style.cssText = 'position: absolute; left: 11px; width: 24px !important;'
    })
  }

  function removeAdButton () {
    const ad1 = '.home-float-openapp, .open-app, .m-nav-openapp, .m-float-openapp, [class^="m-video2-awaken-btn"]'
    // 两个底部出现的弹窗和一个点击播放后的弹窗
    const ad2 = '.openapp-dialog, .caution-dialog, .v-dialog'
    const style = document.createElement('style')
    style.textContent = `${ad1}, ${ad2}{ display: none !important; }`

    // 如果 document.head 可用,将样式添加到文档
    document.head ? document.head.appendChild(style) : waitDOMContentLoaded(document.head.appendChild(style))
  }

  function customElementStyle () {
    // 全屏跳App、倍速按钮、播完推荐
    const css1 = `
    .mplayer-fullscreen-call-app, .mplayer-control-btn-speed, .mplayer-ending-panel-recommend
        { display: none !important; }
    `

    // 优化视觉
    const css2 = `
    /* 调整分集高度 */
    .m-video-part-panel-content { height: 81vmin !important; }
    /* 阻止跳转APP */
    .launch-app-btn { pointer-events: none; }
    .card-box a { pointer-events: auto; }
    /* 重复的初始图形层 */
    .natural-module, .m-footer { display: none !important; }
    `

    // 居中重播按钮
    const css3 = '.mplayer-ending-panel-buttons { align-self: center !important; img { margin-left: 3px !important; } }'

    // 声音按钮
    const unmuteStyle = `
.unmute {
    position: absolute;
    top: 0;
    left: 0;
    z-index: 1001;
    text-transform: uppercase;
    color: #000;
    font-size: 127%;
    font-weight: 500;
    background: none;
    padding: 12px;
    border: 0;
    text-align: inherit;
}
.unmute-inner {
    position: relative;
    width: 100%;
}
.unmute-icon {
    width: 48px;
    height: 48px;
    display: inline-block;
    vertical-align: middle;

    padding-left: 2px;
    position: relative;
    z-index: 10;
    background-color: rgb(255, 255, 255);
    border-radius: 2px;
    border-bottom: 1px solid #f1f1f1;
}
.unmute svg {
    filter: drop-shadow(0 0 2px rgba(0,0,0,.5));
}
.unmute-text {
    position: relative;
    z-index: 10;
    padding-top: 1px;
    padding-right: 10px;
    max-width: 200px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    vertical-align: middle;
    display: inline-block;

    /* animation: unmute-alpha-anim .25s cubic-bezier(.4,0,1,1) 5.4s reverse forwards; */
    transition: opacity .25s cubic-bezier(.4,0,1,1);
}
.animated .unmute-text {
    opacity: 0;
}
.unmute-box {
    width: 100%;
    display: block;
    background-color: rgb(255, 255, 255);
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    border-radius: 2px;
    border-bottom: 1px solid #f1f1f1;

    /* animation: unmute-width-anim .5s cubic-bezier(.4,0,1,1) 5.5s reverse forwards; */
    transition: width .5s cubic-bezier(.4,0,1,1);
}
.animated .unmute-box {
    width: 0;
}
    `

    const style = document.createElement('style')
    style.textContent = css1 + css2 + css3 + unmuteStyle

    // 如果 document.head 可用,将样式添加到文档
    document.head ? document.head.appendChild(style) : waitDOMContentLoaded(document.head.appendChild(style))
  }

  function goToVideoById (keyword, callback) {
    const callbackName = `jsonp_callback_${Date.now()}_${Math.floor(Math.random() * 100000)}`

    window[callbackName] = function (responseData) {
      if (responseData.data.result[11].data[0]) {
        const bvId = responseData.data.result[11].data[0].bvid
        callback(bvId, null)
      } else {
        callback(null, 'BVId not found')
      }
      delete window[callbackName]
    }

    const script = document.createElement('script')
    script.src = `https://api.bilibili.com/x/web-interface/search/all/v2?page=1&keyword=${keyword}&jsonp=jsonp&callback=${callbackName}`
    document.body.appendChild(script)
  }

  function addTargetElementListener (targetElement) {
    if (targetElement) {
      const anchor = targetElement.firstChild
      const h2Element = anchor.lastChild
      const keyword = encodeURIComponent(h2Element.innerHTML)

      anchor.addEventListener('click', event => {
        event.preventDefault()
        event.stopImmediatePropagation()

        goToVideoById(keyword, (bvId, error) => {
          if (bvId) {
            const videoUrl = `https://m.bilibili.com/video/${bvId}`
            window.history.pushState({}, '', videoUrl)
            window.location.href = videoUrl
          } else {
            console.error('BVId wrong: ', error)
          }
        })
      }, true)
    }
    console.log('Execute Video! 添加监听器')
  }

  // 自动播放
  function autoplay () {
    const play = document.querySelector('.main-cover')

    const style = document.createElement('style')
    style.textContent = '.m-navbar + div { display: block !important }'
    document.head.appendChild(style)

    if (play) {
      const video = document.querySelector('video')
      if (video) {
        // 先添加播放监听,第一次播放时执行
        video.addEventListener('play', function () {
          if (video.muted === true) createUnmuteButton()
        }, { once: true })

        const startPlayPromise = video.play()

        if (startPlayPromise !== undefined) {
          startPlayPromise
            .catch((error) => {
              if (error.name === 'NotAllowedError') {
                video.muted = true
                video.play()
              } else {
                // 处理加载或播放错误
              }
            })
            .then(() => {
              // 仅在播放开始后才开始执行你需要执行的操作。
            })
        }

        // 创建解除静音按钮
        function createUnmuteButton () {
          // 检查是否已存在解除静音按钮
          if (document.getElementById('unmuteButton')) {
            return // 如果已存在,不进行重复创建
          }
          // 创建按钮元素
          const button = document.createElement('button')
          button.classList.add('unmute')
          button.innerHTML = `
<div class="unmute-inner">
    <div class="unmute-icon"><svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%">
            <use class="svg-shadow" xlink:href="#ytp-id-1"></use>
            <path class="ytp-svg-fill"
                d="m 21.48,17.98 c 0,-1.77 -1.02,-3.29 -2.5,-4.03 v 2.21 l 2.45,2.45 c .03,-0.2 .05,-0.41 .05,-0.63 z m 2.5,0 c 0,.94 -0.2,1.82 -0.54,2.64 l 1.51,1.51 c .66,-1.24 1.03,-2.65 1.03,-4.15 0,-4.28 -2.99,-7.86 -7,-8.76 v 2.05 c 2.89,.86 5,3.54 5,6.71 z M 9.25,8.98 l -1.27,1.26 4.72,4.73 H 7.98 v 6 H 11.98 l 5,5 v -6.73 l 4.25,4.25 c -0.67,.52 -1.42,.93 -2.25,1.18 v 2.06 c 1.38,-0.31 2.63,-0.95 3.69,-1.81 l 2.04,2.05 1.27,-1.27 -9,-9 -7.72,-7.72 z m 7.72,.99 -2.09,2.08 2.09,2.09 V 9.98 z"
                id="id-1"></path>
        </svg></div>
    <div class="unmute-text">点按取消静音</div>
    <div class="unmute-box"></div>
</div>
          `
          // 按钮点击事件
          button.addEventListener('click', function () {
            video.muted = false
            button.remove()
          })

          // 将按钮添加到页面的合适位置
          const videoWrapper = document.querySelector('.mplayer-video-wrap')
          videoWrapper.insertAdjacentElement('afterend', button)
          // 第一个参数是函数,而非调用函数的结果
          setTimeout(() => {
            button.classList.add('animated')
          }, 4500)
        }
      }
    }

    observeCardBox()
  }

  // 观察推荐视频的加载
  function observeCardBox () {
    const cardBox = document.querySelector('.card-box')
    const targetElements = cardBox.children

    // 为初始子元素添加监听器
    Array.from(targetElements).forEach(addTargetElementListener)

    // 创建 MutationObserver 以监听子元素的变化
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        if (mutation.type === 'childList') {
          mutation.addedNodes.forEach((addedNode) => {
            addTargetElementListener(addedNode)
          })
        }
      })
    })

    // 配置观察选项
    const observerConfig = {
      childList: true
    }

    // 开始观察
    observer.observe(cardBox, observerConfig)
  }

  function addHomeTargetElementListener (tag) {
    tag.addEventListener('click', async (event) => { // 异步,然后放到 js 末尾执行
      event.preventDefault() // 阻止默认行为
      // event.stopPropagation(); // 阻止事件冒泡
      event.stopImmediatePropagation() // 阻止其他事件监听器的执行
      console.log('test href: ', tag.getAttribute('href'))

      // 等待下一个事件循环迭代
      await new Promise((resolve) => setTimeout(resolve, 0)) // THIS!
      window.location.href = tag.getAttribute('href')
    }, true)
  }

  function runHome () {
    // observeHomeCardBox
    const cardBox = document.querySelector('.card-box')
    const aTags = cardBox.children

    Array.from(aTags).forEach(addHomeTargetElementListener)

    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        if (mutation.type === 'childList') {
          mutation.addedNodes.forEach((addedNode) => {
            addHomeTargetElementListener(addedNode)
          })
        }
      })
    })

    const observerConfig = {
      childList: true
    }

    observer.observe(cardBox, observerConfig)

    console.log('Execute Home!')
  }
})()