// ==UserScript==
// @name 哔哩哔哩宽屏
// @namespace https://gf.qytechs.cn/users/1125570
// @description 哔哩哔哩宽屏体验
// @version 0.2.1
// @author posthumz
// @license MIT
// @match http*://www.bilibili.com
// @match http*://*.bilibili.com/video/*
// @match http*://*.bilibili.com/bangumi/*
// @match http*://*.bilibili.com/list/*
// @icon https://www.bilibili.com/favicon.ico
// @run-at document-end
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @noframes
// ==/UserScript==
(async () => {
'use strict'
const videoStyles = `/* 播放器 */
#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 30px;
}
.left-container, .plp-l, .playlist-container--left {
flex: 1;
}
.plp-r {
padding-top: 0 !important;
}
/* 番剧/影视页下方容器 */
.main-container {
width: calc(100% - 75px);
margin: 15px 50px 15px 25px !important;
padding: 0;
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: 12px;
>:not(.bpx-player-ctrl-viewpoint, .bpx-player-ctrl-time) {
width: auto !important;
}
}
.bpx-player-ctrl-viewpoint {
margin: 0;
}
.bpx-player-control-bottom-center {
padding: 0 !important;
display: flex;
justify-content: center;
}
.bpx-player-sending-bar {
margin: 0 !important;
}
.bpx-player-control-bottom-right {
min-width: initial !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-info {
margin-right: 0 !important;
display: flex !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;
}`
const homeStyles = `:root {
--layout-padding: 60px;
}
.feed-card, .floor-single-card, .bili-video-card {
margin-top: 0px !important;
}`
// 播放器
const player = document.getElementById('bilibili-player')
// 播放器不存在时不执行
if (!player) {
if (document.getElementById('i_cecream')) {
GM_addStyle(homeStyles)
return console.info('使用首页宽屏样式')
}
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(', ')},请检查是否为新版页面`
) }
// 每一定时间检测某个条件是否满足,超时则reject
const waitFor = (/** @type {() => boolean}*/ 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(null)
}
if (retry % 10 == 0) {
console.debug('等待页面加载')
}
}, interval)
})
// 等待人数加载
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')
header?.style.setProperty('height', 'initial', 'important')
if (header) {
// 将导航栏移至主容器最前
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(() => Boolean(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(', ')}`
)
}
// 视频置于导航栏上方
navigation.insertAdjacentElement('beforebegin', wrap)
// 使用宽屏样式 (除非当前是小窗模式)
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) }
)
// 添加自定义样式
GM_addStyle(videoStyles)
// 移除原 宽屏/网页全屏 按钮,因为没有用了
for (const className of [
'bpx-player-ctrl-wide', 'bpx-player-ctrl-web',
'squirtle-widescreen-wrap', 'squirtle-pagefullscreen-wrap',
]) { player.getElementsByClassName(className)[0]?.remove() }
// 改变导航栏位置,true为视频下方,false为视频上方
const lowerNavigation = (value = true) => {
if (value) {
wrap.style.removeProperty('height')
wrap.insertAdjacentElement('afterend', navigation)
} else {
wrap.style.setProperty('height', `calc(100vh - ${navigation.clientHeight}px)`)
wrap.insertAdjacentElement('beforebegin', navigation)
}
return (GM_setValue('lowerNavigation', value), value)
}
// 检测导航栏设置,如果是false则上置导航栏,默认为false
if (!GM_getValue('lowerNavigation', true)) { lowerNavigation(false) }
// 将弹幕框移至播放器下方一次
bottomCenter.replaceChildren(danmaku)
// 添加切换导航栏位置的菜单项
GM_registerMenuCommand('切换导航栏位置', () =>
GM_setValue('lowerNavigation', lowerNavigation(!GM_getValue('lowerNavigation', true)))
)
const note = document.getElementsByClassName('note-pc')[0]
if (note) {
// 将笔记移至主容器,不然会被视频和导航栏遮挡
navigation.insertAdjacentElement('afterend', note)
}
console.info('宽屏模式成功启用')
GM_addStyle(`.bpx-player-container {
min-width: 360px;
}
.bpx-player-mini-resizer {
position: absolute;
left: 0;
width: 10px;
height: 100%;
cursor: ew-resize;
}
.bpx-player-resizer {
position: absolute;
bottom: 0;
width: 100%;
height: 15px;
cursor: ns-resize;
z-index: 114514;
}`)
const miniResizer = document.createElement('div')
miniResizer.className = 'bpx-player-mini-resizer'
miniResizer.onmousedown = (ev) => {
ev.stopImmediatePropagation()
ev.preventDefault()
const resize = (/** @type MouseEvent */ 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})
}
container.getElementsByClassName('bpx-player-mini-warp')[0]?.appendChild(miniResizer) ||
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(container.getElementsByClassName('bpx-player-video-area')[0], {childList: true})
})()