Wider Bilibili

哔哩哔哩宽屏体验

当前为 2024-02-28 提交的版本,查看 最新版本

// ==UserScript==
// @name            Wider Bilibili
// @name:zh         哔哩哔哩宽屏
// @namespace       https://gf.qytechs.cn/users/1125570
// @description     哔哩哔哩宽屏体验
// @description:en  BiliBili, but wider
// @version         0.3.4.3
// @author          posthumz
// @license         MIT
// @match           http*://*.bilibili.com/*
// @icon            https://www.bilibili.com/favicon.ico
// @grant           GM_addStyle
// @grant           GM_getValue
// @grant           GM_setValue
// @grant           GM_registerMenuCommand
// @grant           GM_addValueChangeListener
// @noframes
// ==/UserScript==

(async function () {
  'use strict'

  const styles = {
    home:
`.feed-card, .floor-single-card, .bili-video-card {
  margin-top: 0px !important;
}
.feed-roll-btn {
  left: initial !important;
  right: calc(10px - var(--layout-padding));
}
.palette-button-outer {
  padding: 0;
}
.palette-button-wrap {
  left: initial !important;
  right: 10px;
}`,

    t:
`.bili-dyn-home--member {
  margin: 0 var(--layout-padding) !important;

  main {
    flex: 1
  }
}
`,

    read:
`.article-detail {
  width: 90%;

  .article-up-info {
    width: initial;
    margin: 0 80px 20px;
  }
  .right-side-bar {
    right: 0;
  }
}`,

    video:
`/* 播放器 */
#bilibili-player {
  position: relative;
  z-index: 1;
  width: 100%;
  height: 100%;
}

#playerWrap,
#bilibili-player-wrap {
  position: relative;
  height: 100vh;
  min-height: 20vh;
  padding: 0;
}

/* 小窗以视频长宽比为准,不显示黑边和阴影 */
.bpx-player-container[data-screen="mini"] {
  height: auto !important;
  box-shadow: none;
}

.bpx-player-container:not([data-screen="mini"]) {
  width: 100% !important;
}

/* 视频标题换行显示 */
#viewbox_report {
  height: auto;
}

.video-title {
  white-space: normal !important;
}

/* 视频页、番剧页、收藏/稍后再看页的下方容器 */
.video-container-v1, .main-container, .playlist-container {
  z-index: 0;
  padding: 0 var(--layout-padding);
}

.left-container, .plp-l, .playlist-container--left {
  flex: 1;
}

/* 不然无评论时小窗无法显示 */
.left-container {
  min-height: calc(100vh + 80px) !important;
}

.plp-r {
  padding-top: 0 !important;
}

/* 番剧/影视页下方容器 */
.main-container {
  width: 100%;
  margin: 0;
  padding: 15px 50px 15px 25px !important;
  box-sizing: border-box;
  display: flex;
}

.player-left-components {
  padding-right: 30px !important;
}

.toolbar {
  padding-top: 0;
}

/* 播放器控件 */
.bpx-player-top-left-title, .bpx-player-top-left-music {
  display: block !important;
}

.bpx-player-control-bottom {
  padding: 0 24px;
}

.bpx-player-control-bottom-left,
.bpx-player-control-bottom-right,
.bpx-player-sending-bar,
.be-video-control-bar-extend {
  gap: 10px;
}

.bpx-player-ctrl-btn {
  width: auto !important;
  margin: 0 !important;
}

.bpx-player-ctrl-time-seek {
  width: 100% !important;
  padding: 0 !important;
  left: 0 !important;
}

.bpx-player-control-bottom-left {
  min-width: initial !important;
}

.bpx-player-control-bottom-center {
  padding: 0 20px !important;
}

.bpx-player-control-bottom-right {
  min-width: initial !important;

  >div {
    padding: 0 !important;
  }
}

.bpx-player-ctrl-time-label {
  text-align: center !important;
  text-indent: 0 !important;
}

.bpx-player-video-inputbar {
  min-width: initial !important;
}

/* 右下方浮动按钮 */
div[class^=navTools_floatNav] {
  z-index: 1 !important;
}

/* 笔记位移 (不然笔记会超出页面初始范围) */
.note-pc {
  transform: translate(-12px, 64px);
}

/* 导航栏 (兼容Bilibili Evolved自定义导航栏) */
#biliMainHeader, .custom-navbar {
  position: sticky !important;
  top: 0;
  z-index: 3 !important;
}

#biliMainHeader > .bili-header {
  min-height: 0 !important;
}

/* Bilibili Evolved 夜间模式修正 */
.bpx-player-container .bpx-player-sending-bar {
  background-color: transparent !important;
}

.bpx-player-container .bpx-player-video-info {
  color: hsla(0,0%,100%,.9) !important;
}

.bpx-player-container .bpx-player-sending-bar .bpx-player-video-btn-dm,
.bpx-player-container .bpx-player-sending-bar .bpx-player-dm-setting,
.bpx-player-container .bpx-player-sending-bar .bpx-player-dm-switch {
  fill: hsla(0,0%,100%,.9) !important;
}

/* Bilibili Evolved侧栏 */
.be-settings {
  z-index: 3;
  position: fixed;
}`,

    mini:
`.bpx-player-container {
  min-width: 180px;
}
.bpx-player-mini-resizer {
  position: absolute;
  left: 0;
  width: 10px;
  height: 100%;
  cursor: ew-resize;
}`,

    common:
`:root {
  --layout-padding: ${GM_getValue('左右边距', 30)}px;
}

/* 搜索栏 */
.center-search-container {
  min-width: 0;
}

.nav-search-input {
  width: 0 !important;
  padding-right: 0 !important;
}

.nav-search-clean {
  display: none;
}

/* 脚本设置样式 */
#WBOptions {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 114514;

  border-radius: 15px;
  padding: 20px;
  display: none;
  grid-template-columns: repeat(2, 1fr);
  gap: 20px 30px;

  background-color: var(--bg1);
  color: var(--text1);
  outline: 4px solid #00a0d8;
  font-size: 18px;
}

#WBOptionsClose {
  position: absolute;
  border: none;
  right: 0;
  font-size: 30px;
  line-height: 30px;
  width: 30px;
  border-top-right-radius: 15px;
  border-bottom-left-radius: 5px;
  transition: .1s;
  background-color: transparent;
  color: var(--text1);
}

#WBOptionsClose:hover {
  background-color: #e81123;
}

#WBOptionsClose:active {
  opacity: 0.5;
}

#WBOptions>header {
  grid-column: 1/-1;
}

#WBOptions>label {
  align-items: center;
  display: flex;
  gap: 10px;
}

#WBOptions input {
  height: 20px;
  margin: 0;
  padding: 4px !important;
  box-sizing: content-box !important;
  font-size: 16px;
}

#WBOptions input[type=checkbox] {
  width: 40px;
  appearance: none;
  border-radius: 20px;
  box-sizing: content-box;
  cursor: pointer;
  background-color: #ccc;
  transition: .2s;
}

#WBOptions input[type=checkbox]::before {
  content: "";
  display: flex;
  position: relative;

  height: 100%;
  aspect-ratio: 1/1;
  border-radius: 50%;
  background-color: #fff;
  transition: .2s;
}

#WBOptions input[type=checkbox]:checked {
  background-color: #00a0d8;
}

#WBOptions input[type=checkbox]:checked::before {
  transform: translateX(20px);
}

#WBOptions input[type=checkbox]:hover {
  box-shadow: 0 0 4px #00a0d8;
}

#WBOptions input[type=checkbox]:active {
  opacity: 0.5;
}

#WBOptions input[type=number] {
  width: 60px;
  border: none;
  border-radius: 5px;
  background: none;
  outline: 2px solid #00a0d8;
  color: var(--text1) !important;
  appearance: textfield;
}

#WBOptions input[type=number]::-webkit-inner-spin-button {
  appearance: none;
}`
  }

  GM_addStyle(styles.common)

  // 设置选项功能
  const options = document.body.appendChild(document.createElement('div'))
  options.id = 'WBOptions'
  options.innerHTML =
`<button id="WBOptionsClose">×</button>
<header>⚙️宽屏选项</header>
<label><input type="checkbox" ${GM_getValue('导航栏下置', true) ? ' checked' : ''}>导航栏下置</label>
<label><input type="number" placeholder="px" min="0" value="${GM_getValue('左右边距', 30)}">左右边距</label>`

  // 调出设置选项
  GM_registerMenuCommand('选项', () => { options.style.display = 'grid' })
  // 关闭设置选项
  options.getElementsByTagName('button')[0]?.addEventListener('click', () => { options.style.display = 'none' })

  for (const child of options.children) {
    if (child instanceof HTMLLabelElement) {
      const [input] = child.getElementsByTagName('input')
      const key = child.textContent ?? ''
      switch (input?.type) {
        case 'checkbox':
          input.onchange = () => { GM_setValue(key, input.checked) }
          break
        case 'number':
          input.oninput = () => {
            const val = Number(input.value)
            if (Number.isInteger(val)) {
              GM_setValue(key, val)
            }
          }
          break
        default:
          console.error('啊?')
          break
      }
    }
  }

  GM_addValueChangeListener('左右边距', (_k, _o, newVal) => {
    document.documentElement.style.setProperty('--layout-padding', `${newVal}px`)
  })

  /**
   * @template T
   * @param {() => T} loaded
   * @returns {Promise<NonNullable<T>>}
   * @description 每一定时间检测某个条件是否满足,超时则reject
   */
  const waitFor = (loaded, desc = '页面加载', retry = 100, interval = 100) => new Promise((resolve, reject) => {
    const intervalID = setInterval((res = loaded()) => {
      if (res) {
        clearInterval(intervalID)
        console.log(`${desc}已加载`)
        return resolve(res)
      }
      if (--retry === 0) {
        console.error('页面加载超时')
        clearInterval(intervalID)
        return reject(new Error('timeout'))
      }
      if (retry % 10 === 0) { console.debug(`等待${desc}`) }
    }, interval)
  })

  switch ((new URL(window.location.href)).host) {
    case 't.bilibili.com':
      GM_addStyle(styles.t)
      console.info('使用动态样式')
      break

    case 'www.bilibili.com': {
    // #region 首页
      if (document.getElementById('i_cecream')) {
        GM_addStyle(styles.home)
        console.info('使用首页宽屏样式')
        break
      }
      // #endregion

      // #region 阅读页
      if (document.getElementsByClassName('article-detail')[0]) {
        GM_addStyle(styles.read)
        console.info('使用阅读页宽屏样式')
        break
      }
      // #endregion

      // #region 播放页
      // 播放器不存在时不执行
      const player = document.getElementById('bilibili-player')
      if (!player) { return console.info('未找到播放器,不启用宽屏模式') }

      // 主容器,视频播放页为#app,番剧/影视播放页为.home-container
      const home = document.getElementById('app') ?? document.getElementsByClassName('home-container')[0]
      // 播放器外容器,视频播放页为#playerWrap,番剧/影视播放页为#bilibili-player-wrap
      const wrap = document.getElementById('playerWrap') ?? document.getElementById('bilibili-player-wrap')
      // 在新版本页面,播放器存在时都应该存在
      if (!wrap || !home) {
        return console.error(
      `页面加载错误:${[
        wrap ? '' : '播放器外容器',
        home ? '' : '主容器'
      ].filter(Boolean).join(', ')},请检查是否为新版页面`
        )
      }

      // 等待人数加载
      const infos = player.getElementsByClassName('bpx-player-video-info')
      await waitFor(() => infos[0], '正在观看')

      // 导航栏 (兼容Bilibili Evolved自定义顶栏,有可能延后加载)
      const navigation = await (async () => {
        const header = document.getElementById('biliMainHeader')
        if (header) {
          header.style.setProperty('height', 'initial', 'important')
          home.insertAdjacentElement('afterbegin', header)
          const [headerBar] = header.getElementsByClassName('bili-header__bar')
          if (headerBar instanceof HTMLElement) {
            headerBar.style.position = 'relative'
            // bili-header__bar不可见时使用自定义顶栏
            if (window.getComputedStyle(headerBar).display === 'none') {
              const navbar = document.getElementsByClassName('custom-navbar')
              return home.insertAdjacentElement('afterbegin', await waitFor(() => navbar[0], '自定义顶栏'))
            }
          }
        }
        return header
      })()
      // 播放器内容器
      const [container] = player.getElementsByClassName('bpx-player-container')
      // 播放器底中部框 (用于放置弹幕框内容)
      const bottomCenter = ((center = player.getElementsByClassName('bpx-player-control-bottom-center')[0]) =>
      // 番剧版使用squirtle-controller-wrap-center,但也存在bpx-player-control-bottom-center
      // 所以通过检测前一个元素(bpx-player-control-bottom-left)是否有子元素来判断使用哪个
        center?.previousElementSibling?.hasChildNodes()
          ? center
          : player.getElementsByClassName('squirtle-controller-wrap-center')[0]
      )()
      // 弹幕框
      const [danmaku] = player.getElementsByClassName('bpx-player-sending-bar')

      // 正常情况应该都存在
      if (!navigation || !(container instanceof HTMLElement) || !bottomCenter || !danmaku) {
        return console.error(
        `页面加载错误:${[
          navigation ? '' : '导航栏',
          container ? '' : '播放器内容器',
          bottomCenter ? '' : '播放器底中部框',
          danmaku ? '' : '弹幕框'
        ].filter(Boolean).join(', ')}`
        )
      }

      // 改变导航栏位置,true为视频下方,false为视频上方,默认为下方
      const lowerNavigation = (value = true) => {
        if (value) {
          wrap.style.removeProperty('height')
          navigation.insertAdjacentElement('beforebegin', wrap)
        } else {
          wrap.style.height = `calc(100vh - ${navigation.clientHeight}px)`
          navigation.insertAdjacentElement('afterend', wrap)
        }
      }

      const searchInput = document.getElementsByClassName('nav-search-input')
      await waitFor(() => searchInput[0], '搜索栏')
      lowerNavigation(GM_getValue('导航栏下置', true))

      GM_addValueChangeListener('导航栏下置', (_k, _o, newVal) => { lowerNavigation(newVal) })

      // 使用宽屏样式 (除非当前是小窗模式)
      if (container.getAttribute('data-screen') !== 'mini') {
        container.setAttribute('data-screen', 'web')
      }
      // 重载container的setAttribute:data-screen被设置为mini(小窗)以外的值时将其设置为web(宽屏)
      container.setAttribute = new Proxy(container.setAttribute, {
        apply: (target, thisArg, /** @type {[string, string]} */ [name, val]) =>
          target.apply(thisArg, [name, name === 'data-screen' && val !== 'mini' ? 'web' : val])
      })

      // 番剧页面需要初始与退出全屏时移除#bilibili-player-wrap的class
      if (wrap.id === 'bilibili-player-wrap') {
        wrap.className = ''
        document.addEventListener('fullscreenchange', () => {
          if (document.fullscreenElement) { wrap.className = '' }
        })
      }
      // 退出全屏时弹幕框移至播放器下方
      document.addEventListener('fullscreenchange', () => {
        if (!document.fullscreenElement) { bottomCenter.replaceChildren(danmaku) }
      })

      // 移除原 宽屏/网页全屏 按钮,因为没有用了
      for (const className of [
        'bpx-player-ctrl-wide', 'bpx-player-ctrl-web',
        'squirtle-widescreen-wrap', 'squirtle-pagefullscreen-wrap'
      ]) { player.getElementsByClassName(className)[0]?.remove() }

      // 添加视频样式
      GM_addStyle(styles.video)

      // 将弹幕框移至播放器下方一次
      bottomCenter.replaceChildren(danmaku)

      // 将笔记移至主容器,不然会被视频和导航栏遮挡
      const [note] = document.getElementsByClassName('note-pc')
      if (note) { document.body.appendChild(note) }

      console.info('宽屏模式成功启用')

      // #region 小窗
      GM_addStyle(styles.mini)

      const miniResizer = document.createElement('div')
      miniResizer.className = 'bpx-player-mini-resizer'
      miniResizer.onmousedown = ev => {
        ev.stopImmediatePropagation()
        ev.preventDefault()

        /** @param {MouseEvent} ev */
        const resize = ev => {
          container.style.width = `${container.offsetWidth + container.offsetLeft - ev.x + 1}px`
        }
        document.addEventListener('mousemove', resize)
        document.addEventListener('mouseup', () => document.removeEventListener('mousemove', resize), { once: true })
      }

      // 小窗模式下添加拖动调整大小的部件,若直接添加失败则通过MutationObserver监听添加
      if (!container.getElementsByClassName('bpx-player-mini-warp')[0]?.appendChild(miniResizer)) {
        const [videoArea] = container.getElementsByClassName('bpx-player-video-area')
        if (!videoArea) { return }

        new MutationObserver((mutations, observer) => {
          for (const mutation of mutations) {
            if (mutation.type === 'childList') {
              for (const node of mutation.addedNodes) {
                if (node instanceof Element && node.classList.contains('bpx-player-mini-warp')) {
                  node.appendChild(miniResizer)
                  observer.disconnect()
                  break
                }
              }
            }
          }
        }).observe(videoArea, { childList: true })
      }
      // #endregion
      break
    // #endregion
    }

    default:
      console.info('未知页面')
      break
  }
})()

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址