哔哩哔哩宽屏

哔哩哔哩宽屏体验

目前為 2023-12-10 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name            哔哩哔哩宽屏
// @name:en         Wider Bilibili
// @namespace       https://greasyfork.org/users/1125570
// @description     哔哩哔哩宽屏体验
// @description:en  BiliBili, but wider
// @version         0.3.3.1
// @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 () => {
  '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; /* 以视频长宽比为准,且不显示黑边 */
  transform: translateY(24px) !important;
}

.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;

  >:not(.bpx-player-ctrl-time) {
    width: auto !important;
    margin: 0 !important;
  }
}

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

.bpx-player-control-bottom-center {
  padding: 0 30px !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-ctrl-time, .bpx-player-ctrl-quality {
  margin-right: 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;
}

.bili-header__bar {
  position: relative !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 {
  padding-right: 0;
}

.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);
  box-shadow: 0 0 4px #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;
}

.slider::before {
  content: "";
  display: block;
  position: relative;

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

.slider {
  appearance: none;
  width: 40px;
  border-radius: 20px;
  box-sizing: content-box;
  cursor: pointer;
  background-color: #ccc;
  transition: .4s;
}

.slider:checked::before {
  transform: translateX(20px);
}

.slider:checked {
  background-color: #00a0d8;
}

.slider:hover {
  box-shadow: 0 0 4px #00a0d8;
}

.slider:active {
  opacity: 0.5;
}

#WBOptions input[type=number] {
  width: 60px;
  border: none;
  border-radius: 5px;
  background: transparent;
  box-shadow: 0 0 0 2px #00a0d8;
  color: var(--text1) !important;
}`}

  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" class="slider"${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 Element of options.children) {
    if (Element instanceof HTMLLabelElement) {
      const input = Element.getElementsByTagName('input')[0]
      const key = Element.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('左右边距', (_n, _o, newVal) =>
    document.documentElement.style.setProperty('--layout-padding', `${newVal}px`))

  /**
   * @param {() => any} loaded
   * @description 每一定时间检测某个条件是否满足,超时则reject
   */
  const waitFor = (loaded, retry = 100, interval = 100) => new Promise((resolve, reject) => {
    const intervalID = setInterval(() => {
      if (--retry == 0) {
        console.error('页面加载超时')
        clearInterval(intervalID)
        return reject()
      }
      if (loaded()) {
        clearInterval(intervalID)
        return resolve(undefined)
      }
      if (retry % 10 == 0) { console.debug('等待页面加载') }
    }, interval)
  })

  const url = new URL(window.location.href)
  switch (url.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 b = player.getElementsByTagName('b')
    await waitFor(() => b[0]?.textContent != null)
    await waitFor(() => b[0]?.textContent != '-')

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

    // 正常情况应该都存在
    if (!navigation || !(container instanceof HTMLDivElement) || !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)
      }
      return value
    }
    lowerNavigation(GM_getValue('导航栏下置', true))

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

    // 使用宽屏样式 (除非当前是小窗模式)
    if (container.getAttribute('data-screen') != 'mini') {
      container.setAttribute('data-screen', 'web')
    }
    // 重载container的setAttribute:data-screen被设置为mini(小窗)以外的值时将其设置为web(宽屏)
    const setAttributeContainer = container.setAttribute.bind(container)
    container.setAttribute = (name, value) =>
      setAttributeContainer(name, name == 'data-screen' && value != 'mini' ? 'web' : value)
    // 番剧页面需要初始与退出全屏时移除#bilibili-player-wrap的class
    if (wrap.id == 'bilibili-player-wrap') {
      wrap.className = ''
      document.addEventListener('fullscreenchange',
        () => { document.fullscreenElement ?? (wrap.className = '') }
      )
    }
    // 退出全屏时弹幕框移至播放器下方
    document.addEventListener('fullscreenchange',
      () => { 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')[0]
    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')[0]
      videoArea && new MutationObserver((mutations, observer) => {
        mutations.filter(mutation => mutation.type == 'childList').forEach(mutation =>
          mutation.addedNodes.forEach(node => {
            if (node instanceof Element && node.classList.contains('bpx-player-mini-warp')) {
              node.appendChild(miniResizer)
              observer.disconnect()
            }
          })
        )
      }
      ).observe(videoArea, {childList: true})
    }
    // #endregion

    break
    // #endregion
  }

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