哔哩哔哩宽屏

哔哩哔哩宽屏体验

当前为 2023-10-14 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name 哔哩哔哩宽屏
  3. // @name:en Wider Bilibili
  4. // @namespace https://greasyfork.org/users/1125570
  5. // @description 哔哩哔哩宽屏体验
  6. // @description:en BiliBili, but wider
  7. // @version 0.3.1
  8. // @author posthumz
  9. // @license MIT
  10. // @match http*://*.bilibili.com/*
  11. // @icon https://www.bilibili.com/favicon.ico
  12. // @run-at document-end
  13. // @grant GM_addStyle
  14. // @grant GM_getValue
  15. // @grant GM_setValue
  16. // @grant GM_registerMenuCommand
  17. // @grant GM_addValueChangeListener
  18. // @noframes
  19. // ==/UserScript==
  20.  
  21. (async () => {
  22. 'use strict'
  23.  
  24. const styles = {
  25. home:
  26. `.feed-card, .floor-single-card, .bili-video-card {
  27. margin-top: 0px !important;
  28. }`,
  29.  
  30. t:
  31. `.bili-dyn-home--member {
  32. margin: 0 var(--layout-padding) !important;
  33.  
  34. main {
  35. flex: 1
  36. }
  37. }
  38. `,
  39.  
  40. read:
  41. `.article-detail {
  42. width: 90%;
  43.  
  44. .article-up-info {
  45. width: initial;
  46. margin: 0 80px 20px;
  47. }
  48. .right-side-bar {
  49. right: 0;
  50. }
  51. }`,
  52.  
  53. video:
  54. `/* 播放器 */
  55. #bilibili-player {
  56. position: relative;
  57. z-index: 1;
  58. width: 100%;
  59. height: 100%;
  60. }
  61.  
  62. #playerWrap,
  63. #bilibili-player-wrap {
  64. position: relative;
  65. height: 100vh;
  66. min-height: 20vh;
  67. padding: 0;
  68. }
  69.  
  70. /* 小窗 */
  71. .bpx-player-container[data-screen="mini"] {
  72. height: auto !important; /* 以视频长宽比为准,且不显示黑边 */
  73. transform: translateY(24px) !important;
  74. }
  75.  
  76. .bpx-player-container:not([data-screen="mini"]) {
  77. width: 100% !important;
  78. }
  79.  
  80. /* 视频标题换行显示 */
  81. #viewbox_report {
  82. height: auto;
  83. }
  84.  
  85. .video-title {
  86. white-space: normal !important;
  87. }
  88.  
  89. /* 视频页, 番剧页, 收藏(包括稍后再看)页 下方容器 */
  90. .video-container-v1, .main-container, .playlist-container {
  91. z-index: 0;
  92. padding: 0 var(--layout-padding);
  93. }
  94.  
  95. .left-container, .plp-l, .playlist-container--left {
  96. flex: 1;
  97. }
  98.  
  99. .plp-r {
  100. padding-top: 0 !important;
  101. }
  102.  
  103. /* 番剧/影视页下方容器 */
  104. .main-container {
  105. width: 100%;
  106. margin: 0;
  107. padding: 15px 50px 15px 25px !important;
  108. box-sizing: border-box;
  109. display: flex;
  110. }
  111.  
  112. .player-left-components {
  113. padding-right: 30px !important;
  114. }
  115.  
  116. .toolbar {
  117. padding-top: 0;
  118. }
  119.  
  120. /* 播放器控件 */
  121. .bpx-player-top-left-title, .bpx-player-top-left-music {
  122. display: block !important;
  123. }
  124.  
  125. .bpx-player-control-bottom {
  126. padding: 0 24px;
  127. }
  128.  
  129. .bpx-player-control-bottom-left,
  130. .bpx-player-control-bottom-right,
  131. .bpx-player-sending-bar,
  132. .be-video-control-bar-extend {
  133. gap: 12px;
  134.  
  135. >:not(.bpx-player-ctrl-viewpoint, .bpx-player-ctrl-time) {
  136. width: auto !important;
  137. }
  138. }
  139.  
  140. .bpx-player-ctrl-viewpoint {
  141. margin: 0;
  142. }
  143.  
  144. .bpx-player-control-bottom-center {
  145. padding: 0 !important;
  146. display: flex;
  147. justify-content: center;
  148. }
  149.  
  150. .bpx-player-sending-bar {
  151. margin: 0 !important;
  152. }
  153.  
  154. .bpx-player-control-bottom-right {
  155. min-width: initial !important;
  156. }
  157.  
  158. .bpx-player-ctrl-time-label {
  159. text-align: center !important;
  160. text-indent: 0 !important;
  161. }
  162.  
  163. .bpx-player-ctrl-time, .bpx-player-ctrl-quality {
  164. margin-right: 0 !important;
  165. }
  166.  
  167. .bpx-player-video-info {
  168. margin-right: 0 !important;
  169. display: flex !important;
  170. }
  171.  
  172. /* 右下方浮动按钮 */
  173. div[class^=navTools_floatNav] {
  174. z-index: 1 !important;
  175. }
  176.  
  177. /* 笔记位移 (不然笔记会超出页面初始范围) */
  178. .note-pc {
  179. transform: translate(-12px, 64px);
  180. }
  181.  
  182. /* 导航栏 (兼容Bilibili Evolved自定义导航栏) */
  183. #biliMainHeader, .custom-navbar {
  184. position: sticky !important;
  185. top: 0;
  186. z-index: 3 !important;
  187. }
  188.  
  189. #biliMainHeader > .bili-header {
  190. min-height: 0 !important;
  191. }
  192.  
  193. .bili-header__bar {
  194. position: relative !important;
  195. }
  196.  
  197. /* Bilibili Evolved 夜间模式修正 */
  198. .bpx-player-container .bpx-player-sending-bar {
  199. background-color: transparent !important;
  200. }
  201.  
  202. .bpx-player-container .bpx-player-video-info {
  203. color: hsla(0,0%,100%,.9) !important;
  204. }
  205.  
  206. .bpx-player-container .bpx-player-sending-bar .bpx-player-video-btn-dm,
  207. .bpx-player-container .bpx-player-sending-bar .bpx-player-dm-setting,
  208. .bpx-player-container .bpx-player-sending-bar .bpx-player-dm-switch {
  209. fill: hsla(0,0%,100%,.9) !important;
  210. }
  211.  
  212. /* Bilibili Evolved侧栏 */
  213. .be-settings {
  214. z-index: 3;
  215. position: fixed;
  216. }`,
  217.  
  218. mini:
  219. `.bpx-player-container {
  220. min-width: 180px;
  221. }
  222. .bpx-player-mini-resizer {
  223. position: absolute;
  224. left: 0;
  225. width: 10px;
  226. height: 100%;
  227. cursor: ew-resize;
  228. }`,
  229.  
  230. general:
  231. `:root {
  232. --layout-padding: ${GM_getValue('左右边距', 30)}px;
  233. }
  234.  
  235. /* 搜索栏 */
  236. .center-search-container {
  237. min-width: 0;
  238. }
  239.  
  240. .nav-search-input {
  241. padding-right: 0;
  242. }
  243.  
  244. .nav-search-clean {
  245. display: none;
  246. }
  247.  
  248. /* 脚本设置样式 */
  249. #WBOptions {
  250. position: fixed;
  251. left: 50%;
  252. top: 50%;
  253. transform: translate(-50%, -50%);
  254. z-index: 114514;
  255.  
  256. border-radius: 15px;
  257. padding: 20px;
  258. display: none;
  259. grid-template-columns: repeat(2, 1fr);
  260. gap: 20px 30px;
  261.  
  262. background-color: var(--bg1);
  263. color: var(--text1);
  264. box-shadow: 0 0 4px #00a0d8;
  265. font-size: 18px;
  266. }
  267.  
  268. #WBOptionsClose {
  269. position: absolute;
  270. border: none;
  271. right: 0;
  272. font-size: 30px;
  273. line-height: 30px;
  274. width: 30px;
  275. border-top-right-radius: 15px;
  276. border-bottom-left-radius: 5px;
  277. transition: .1s;
  278. background-color: transparent;
  279. color: var(--text1);
  280. }
  281.  
  282. #WBOptionsClose:hover {
  283. background-color: #e81123;
  284. }
  285.  
  286. #WBOptionsClose:active {
  287. opacity: 0.5;
  288. }
  289.  
  290. #WBOptions>header {
  291. grid-column: 1/-1;
  292. }
  293.  
  294. #WBOptions>label {
  295. align-items: center;
  296. display: flex;
  297. gap: 10px;
  298. }
  299.  
  300. #WBOptions input {
  301. height: 20px;
  302. margin: 0;
  303. padding: 4px !important;
  304. box-sizing: content-box !important;
  305. }
  306.  
  307. .slider::before {
  308. content: "";
  309. display: block;
  310. position: relative;
  311.  
  312. height: 100%;
  313. aspect-ratio: 1/1;
  314. border-radius: 50%;
  315. background-color: #fff;
  316. transition: .4s;
  317. }
  318.  
  319. .slider {
  320. appearance: none;
  321. width: 40px;
  322. border-radius: 20px;
  323. box-sizing: content-box;
  324. cursor: pointer;
  325. background-color: #ccc;
  326. transition: .4s;
  327. }
  328.  
  329. .slider:checked::before {
  330. transform: translateX(20px);
  331. }
  332.  
  333. .slider:checked {
  334. background-color: #00a0d8;
  335. }
  336.  
  337. .slider:hover {
  338. box-shadow: 0 0 4px #00a0d8;
  339. }
  340.  
  341. .slider:active {
  342. opacity: 0.5;
  343. }
  344.  
  345. #WBOptions input[type=number] {
  346. width: 60px;
  347. border: none;
  348. border-radius: 5px;
  349. background: transparent;
  350. box-shadow: 0 0 0 2px #00a0d8;
  351. color: var(--text1) !important;
  352. }`}
  353.  
  354. GM_addStyle(styles.general)
  355.  
  356. // /** @type Map<string, Function> */
  357. // const callbacks = new Map()
  358.  
  359. // 设置选项功能
  360. const options = document.body.appendChild(document.createElement('div'))
  361. options.id = 'WBOptions'
  362. options.innerHTML =`<button id="WBOptionsClose">×</button>
  363. <header>⚙️宽屏选项</header>
  364. <label><input type="checkbox" class="slider"${GM_getValue('导航栏下置', true) ? ' checked': ''}>导航栏下置</label>
  365. <label><input type="number" placeholder="px" value="${GM_getValue('左右边距', 30)}">左右边距</label>`
  366.  
  367. GM_registerMenuCommand('选项', () => { options.style.display = 'grid' })
  368. options.getElementsByTagName('button')[0]?.addEventListener('click', () => { options.style.display = 'none' })
  369.  
  370. for (const Element of options.children) {
  371. if (Element instanceof HTMLLabelElement) {
  372. const input = Element.getElementsByTagName('input')[0]
  373. const key = Element.textContent ?? ''
  374. switch (input?.type) {
  375. case null:
  376. default:
  377. console.error('啊?')
  378. break
  379. case 'checkbox':
  380. input.onchange = () => {
  381. GM_setValue(key, input.checked)
  382. }
  383. break
  384. case 'number':
  385. input.oninput = () => {
  386. const val = Number(input.value)
  387. if (Number.isInteger(val)) {
  388. GM_setValue(key, val)
  389. }
  390. }
  391. break
  392. }
  393. }
  394. }
  395.  
  396. GM_addValueChangeListener('左右边距', (_n, _o, newVal) =>
  397. document.documentElement.style.setProperty('--layout-padding', `${newVal}px`))
  398.  
  399. // 每一定时间检测某个条件是否满足,超时则reject
  400. const waitFor = (/** @type {() => boolean}*/ loaded, retry = 100, interval = 100) =>
  401. new Promise((resolve, reject) => {
  402. const intervalID = setInterval(() => {
  403. if (--retry == 0) {
  404. console.error('页面加载超时')
  405. clearInterval(intervalID)
  406. return reject()
  407. }
  408. if (loaded()) {
  409. clearInterval(intervalID)
  410. return resolve(null)
  411. }
  412. if (retry % 10 == 0) { console.debug('等待页面加载') }
  413. }, interval)
  414. })
  415.  
  416. const url = new URL(window.location.href)
  417. switch (url.host) {
  418. case 't.bilibili.com':
  419. GM_addStyle(styles.t)
  420. console.info('使用动态样式')
  421. break
  422.  
  423. case 'www.bilibili.com': {
  424. // #region 首页
  425. if (document.getElementById('i_cecream')) {
  426. GM_addStyle(styles.home)
  427. console.info('使用首页宽屏样式')
  428. break
  429. }
  430. // #endregion
  431.  
  432. // #region 阅读页
  433. if (document.getElementsByClassName('article-detail')[0]) {
  434. GM_addStyle(styles.read)
  435. console.info('使用阅读页宽屏样式')
  436. break
  437. }
  438. // #endregion
  439.  
  440. // #region 播放页
  441. // 播放器不存在时不执行
  442. const player = document.getElementById('bilibili-player')
  443. if (!player) { return console.info('未找到播放器,不启用宽屏模式') }
  444.  
  445. // 主容器,视频播放页为#app,番剧/影视播放页为.home-container
  446. const home = document.getElementById('app') ?? document.getElementsByClassName('home-container')[0]
  447. // 播放器外容器,视频播放页为#playerWrap,番剧/影视播放页为#bilibili-player-wrap
  448. const wrap = document.getElementById('playerWrap') ?? document.getElementById('bilibili-player-wrap')
  449. // 在新版本页面,播放器存在时都应该存在
  450. if (!wrap || !home) { return console.error(
  451. `页面加载错误:${[
  452. wrap ? '' : '播放器外容器',
  453. home ? '' : '主容器',
  454. ].filter(Boolean).join(', ')},请检查是否为新版页面`
  455. ) }
  456.  
  457. // 等待人数加载
  458. const b = player.getElementsByTagName('b')
  459. await waitFor(() => b[0]?.textContent != null)
  460. await waitFor(() => b[0]?.textContent != '-')
  461.  
  462. // 导航栏 (兼容Bilibili Evolved自定义顶栏,有可能延后加载)
  463. const navigation = await (async () => {
  464. const header = document.getElementById('biliMainHeader')
  465. if (header) {
  466. header.style.setProperty('height', 'initial', 'important')
  467. // 将导航栏移至主容器最前
  468.  
  469. home.insertAdjacentElement('afterbegin', header)
  470. // bili-header__bar不可见时使用自定义顶栏
  471. const headerBar = header.getElementsByClassName('bili-header__bar')[0]
  472. if (headerBar && window.getComputedStyle(headerBar).display == 'none') {
  473. const navbar = document.getElementsByClassName('custom-navbar')
  474. await waitFor(() => Boolean(navbar[0]))
  475. return home.insertAdjacentElement('afterbegin', navbar[0])
  476. }
  477. }
  478. return header
  479. })()
  480. // 播放器内容器
  481. const container = player.getElementsByClassName('bpx-player-container')[0]
  482. // 播放器底中部框 (用于放置弹幕框内容)
  483. const bottomCenter = (() => {
  484. const center = player.getElementsByClassName('bpx-player-control-bottom-center')[0]
  485. // 番剧版使用squirtle-controller-wrap-center,但也存在bpx-player-control-bottom-center
  486. // 所以通过检测前一个元素(bpx-player-control-bottom-left)是否有子元素来判断使用哪个
  487. return center?.previousElementSibling?.hasChildNodes() ? center
  488. : player.getElementsByClassName('squirtle-controller-wrap-center')[0]
  489. })()
  490. // 弹幕框
  491. const danmaku = player.getElementsByClassName('bpx-player-sending-bar')[0]
  492.  
  493. // 正常情况应该都存在
  494. if (!navigation || !(container instanceof HTMLDivElement) || !bottomCenter || !danmaku) {
  495. return console.error(
  496. `页面加载错误:${[
  497. navigation ? '' : '导航栏',
  498. container ? '' : '播放器内容器',
  499. bottomCenter ? '' : '播放器底中部框',
  500. danmaku ? '' : '弹幕框',
  501. ].filter(Boolean).join(', ')}`
  502. )
  503. }
  504.  
  505. // 改变导航栏位置,true为视频下方,false为视频上方,默认为下方
  506. const lowerNavigation = (value = true) => {
  507. if (value) {
  508. wrap.style.removeProperty('height')
  509. navigation.insertAdjacentElement('beforebegin', wrap)
  510. } else {
  511. wrap.style.height = `calc(100vh - ${navigation.clientHeight}px)`
  512. navigation.insertAdjacentElement('afterend', wrap)
  513. }
  514. return value
  515. }
  516. lowerNavigation(GM_getValue('导航栏下置', true))
  517.  
  518. GM_addValueChangeListener('导航栏下置', (_n, _o, newVal) => { lowerNavigation(newVal) })
  519.  
  520. // 使用宽屏样式 (除非当前是小窗模式)
  521. if (container.getAttribute('data-screen') != 'mini') {
  522. container.setAttribute('data-screen', 'web')
  523. }
  524. // 重载container的setAttribute:data-screen被设置为mini(小窗)以外的值时将其设置为web(宽屏)
  525. const setAttributeContainer = container.setAttribute.bind(container)
  526. container.setAttribute = (name, value) =>
  527. setAttributeContainer(name, name == 'data-screen' && value != 'mini' ? 'web' : value)
  528. // 番剧页面需要初始与退出全屏时移除#bilibili-player-wrap的class
  529. if (wrap.id == 'bilibili-player-wrap') {
  530. wrap.className = ''
  531. document.addEventListener('fullscreenchange',
  532. () => { document.fullscreenElement ?? (wrap.className = '') }
  533. )
  534. }
  535. // 退出全屏时弹幕框移至播放器下方
  536. document.addEventListener('fullscreenchange',
  537. () => { document.fullscreenElement ?? bottomCenter.replaceChildren(danmaku) }
  538. )
  539.  
  540. // 移除原 宽屏/网页全屏 按钮,因为没有用了
  541. for (const className of [
  542. 'bpx-player-ctrl-wide', 'bpx-player-ctrl-web',
  543. 'squirtle-widescreen-wrap', 'squirtle-pagefullscreen-wrap',
  544. ]) { player.getElementsByClassName(className)[0]?.remove() }
  545.  
  546. // 添加视频样式
  547. GM_addStyle(styles.video)
  548.  
  549. // 将弹幕框移至播放器下方一次
  550. bottomCenter.replaceChildren(danmaku)
  551.  
  552. // 将笔记移至主容器,不然会被视频和导航栏遮挡
  553. const note = document.getElementsByClassName('note-pc')[0]
  554. if (note) {
  555. navigation.insertAdjacentElement('afterend', note)
  556. }
  557.  
  558. console.info('宽屏模式成功启用')
  559.  
  560. // #region 小窗
  561. GM_addStyle(styles.mini)
  562.  
  563. const miniResizer = document.createElement('div')
  564. miniResizer.className = 'bpx-player-mini-resizer'
  565. miniResizer.onmousedown = (ev) => {
  566. ev.stopImmediatePropagation()
  567. ev.preventDefault()
  568.  
  569. const resize = (/** @type MouseEvent */ ev) => {
  570. container.style.width = `${container.offsetWidth + container.offsetLeft - ev.x + 1}px`
  571. }
  572. document.addEventListener('mousemove', resize)
  573. document.addEventListener('mouseup', () => document.removeEventListener('mousemove', resize), {once: true})
  574. }
  575.  
  576. container.getElementsByClassName('bpx-player-mini-warp')[0]?.appendChild(miniResizer) ?? (
  577. container.getElementsByClassName('bpx-player-video-area')[0] &&
  578. new MutationObserver((mutations, observer) => {
  579. mutations.filter(mutation => mutation.type == 'childList').forEach(mutation => {
  580. mutation.addedNodes.forEach(node => {
  581. if (node instanceof Element && node.classList.contains('bpx-player-mini-warp')) {
  582. node.appendChild(miniResizer)
  583. observer.disconnect()
  584. }
  585. })
  586. })
  587. }
  588. ).observe(container.getElementsByClassName('bpx-player-video-area')[0], {childList: true})
  589. )
  590. // #endregion
  591.  
  592. break
  593. // #endregion
  594. }
  595.  
  596. default:
  597. console.info('未知页面')
  598. break
  599. }
  600. })()