// ==UserScript==
// @name Wider Bilibili
// @name:zh 哔哩哔哩宽屏
// @namespace https://greasyfork.org/users/1125570
// @description 哔哩哔哩宽屏体验
// @description:en BiliBili, but wider
// @version 0.3.5.2
// @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 = {
common: `:root {
--layout-padding: ${GM_getValue('左右边距', 30)}px;
}
html, body {
width: initial !important;
height: initial !important;
}
/* 搜索栏 */
.center-search-container {
min-width: 0;
}
.nav-search-input {
width: 0 !important;
padding-right: 0 !important;
}
/* 脚本设置样式 */
#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;
}`,
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
}
.left {
display: none;
}
}
`,
space: `/* 空间页 */
.wrapper, .search-page {
width: initial !important;
margin: 0 var(--layout-padding) !important;
}
/* 视频卡片 */
.small-item {
padding-left: 10px !important;
padding-right: 10px !important;
}
/* 主页, 动态 */
#page-index, #page-dynamic {
display: flex;
justify-content: space-between;
gap: 10px;
&::before, &::after {
content: none;
}
.col-1 {
flex: 1;
>.video>.content {
display: flex;
flex-wrap: wrap;
}
}
.channel>.content {
width: initial !important;
.channel-video {
overflow-x: auto;
}
}
.fav-item {
margin-right: 20px !important;
}
}
/* 投稿, 搜索 */
#page-video .col-full {
display: flex;
>.main-content {
flex: 1;
.cube-list {
width: initial !important;
display: flex;
flex-wrap: wrap;
justify-content: center;
}
}
}
/* 合集 */
.channel-index {
width: 100% !important;
}
.feed-dynamic {
flex: 1;
}`,
search: `/* 搜索页 */
.i_wrapper {
padding: 0 var(--layout-padding);
}`,
read: `/* 阅读页 */
.article-detail {
width: 90%;
.article-up-info {
width: initial;
margin: 0 80px 20px;
}
.right-side-bar {
right: 0;
}
}`,
video: `/* 播放器 */
:root {
--nav-height: 64px;
--video-height: calc(100vh - var(--nav-height));
}
.video-container-v1, /* 视频页 */
.left-container,
.main-container, /* 番剧页 */
.playlist-container--left /* 收藏/稍后再看页 */
{
position: initial !important;
}
#playerWrap,
#bilibili-player-wrap {
position: absolute;
left: 0;
right: 0;
top: var(--nav-height);
height: var(--video-height);
padding-right: 0 !important; /* 番剧页加载时会有右填充 */
}
div#bilibili-player {
width: 100%;
height: 100%;
box-shadow: none !important;
}
/* 修复加载动画不显示 */
.bpx-player-loading-panel-blur {
display: flex !important;
}
/* 原弹幕发送区域不显示 */
.bpx-player-sending-area {
display: none;
}
/* 导航栏 */
#biliMainHeader {
margin-bottom: var(--video-height);
position: sticky;
top: 0;
z-index: 3;
}
.bili-header.fixed-header {
min-height: 0 !important;
}
.bili-header__bar {
position: relative !important;
}
/* 视频页、番剧页、收藏/稍后再看页的下方容器 */
.video-container-v1, .main-container, .playlist-container {
z-index: 0;
margin-top: var(--video-height);
padding: 0 var(--layout-padding);
}
.left-container, .plp-l, .playlist-container--left {
flex: 1;
}
.plp-r {
position: sticky !important; /* 番剧页加载时不会先使用sticky */
}
/* 番剧/影视页下方容器 */
.main-container {
width: 100%;
margin: 0;
padding-top: 15px;
padding-left: var(--layout-padding);
padding-right: var(--layout-padding);
box-sizing: border-box;
display: flex;
}
.player-left-components {
padding-right: 30px !important;
}
.toolbar {
padding-top: 0;
}
/* 视频标题换行显示 */
#viewbox_report {
height: auto;
}
.video-title {
white-space: normal !important;
}
/* bgm浮窗 */
#bgm-entry {
z-index: 114514 !important;
left: 0 !important;
}
/* 笔记浮窗 */
.note-pc {
z-index: 114514 !important;
}
/* 番剧页右下方浮动按钮修正 */
div[class^=navTools_floatNav] {
z-index: 2 !important;
}
/* Bilibili Evolved侧栏 */
.be-settings .sidebar {
z-index: 114514 !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;
}`,
controls: `/* 播放器控件 */
.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;
}`,
mini: `/* 小窗 */
.bpx-player-container[data-screen="mini"] {
/* 以视频长宽比为准,不显示黑边和阴影 */
height: auto !important;
box-shadow: none;
translate: 32px 40px; /* 修正小窗位置 */
}
/* 非小窗不使用自定义宽度 */
.bpx-player-container[data-screen="web"] {
width: 100% !important;
}
/* 最小宽度,以防不可见 */
.bpx-player-container {
min-width: 180px;
}
.bpx-player-mini-resizer {
position: absolute;
left: 0;
width: 10px;
height: 100%;
cursor: ew-resize;
}`,
lowerNavigation: `/* 导航栏下置 */
#biliMainHeader {
margin-top: 100vh;
margin-bottom: 0;
}
#playerWrap,
#bilibili-player-wrap {
top: 0;
height: 100vh;
}`
}
GM_addStyle(styles.common)
/**
* @typedef {Object} Option
* @property {string} name
* @property {boolean|number} default
*/
const /** @type {Option[]} */ options = [
{
name: '导航栏下置',
default: true
},
{
name: '播放器控件样式',
default: true
},
{
name: '左右边距',
default: 30
}
]
/** @param {Option} option */
function optionInput (option) {
switch (typeof option.default) {
case 'boolean':
return `<input type="checkbox" ${GM_getValue(option.name, option.default) ? ' checked' : ''}>`
case 'number':
return `<input type="number" min="0" value="${GM_getValue(option.name, option.default)}">`
}
}
// 设置选项功能
const optionsDiv = document.body.appendChild(document.createElement('div'))
optionsDiv.id = 'WBOptions'
optionsDiv.innerHTML = `<button id="WBOptionsClose">×</button>
<header>⚙️宽屏选项</header>
${options.map(option => `<label>${optionInput(option)}${option.name}</label>`).join('\n')}`
// 调出设置选项
GM_registerMenuCommand('选项', () => { optionsDiv.style.display = 'grid' })
// 关闭设置选项
document.getElementById('WBOptionsClose')?.addEventListener('click', () => { optionsDiv.style.display = 'none' })
// 设置选项事件
for (const input of optionsDiv.getElementsByTagName('input')) {
const key = input.parentElement?.textContent ?? ''
if (!key) { continue }
switch (input.type) {
case 'checkbox':
input.onchange = () => { GM_setValue(key, input.checked) }
break
case 'number':
input.oninput = () => {
const val = Number(input.value)
Number.isInteger(val) && GM_setValue(key, val)
}
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)
})
/**
* 直接获取元素或等待元素被添加
* @param {string} className
* @param {Element} [parent]
* @returns {Promise<Element>}
*/
const observeFor = (className, parent = document.body) => new Promise(resolve => {
const elem = parent.getElementsByClassName(className)[0]
if (elem) { return resolve(elem) }
new MutationObserver((mutations, observer) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof Element && node.classList.contains(className)) {
observer.disconnect()
return resolve(node)
}
}
}
}).observe(parent, { childList: true })
})
switch ((new URL(window.location.href)).host) {
case 't.bilibili.com':
GM_addStyle(styles.t)
waitFor(() => document.getElementsByClassName('right')[0], '动态右栏').then(right => {
right.prepend(...document.getElementsByClassName('left')[0]?.childNodes ?? [])
})
console.info('使用动态样式')
break
case 'space.bilibili.com':
GM_addStyle(styles.space)
console.info('使用空间样式')
break
case 'search.bilibili.com':
GM_addStyle(styles.search)
console.info('使用搜索页样式')
break
case 'www.bilibili.com': {
if (document.getElementById('i_cecream')) { // 首页
GM_addStyle(styles.home)
console.info('使用首页宽屏样式')
break
}
if (document.getElementsByClassName('article-detail')[0]) { // 阅读页
GM_addStyle(styles.read)
console.info('使用阅读页宽屏样式')
break
}
// #region 视频页
const player = document.getElementById('bilibili-player')
if (!player) { return console.info('未找到播放器,仅启用通用样式') }
// 播放器外容器,视频播放页为#playerWrap,番剧/影视播放页为#bilibili-player-wrap
const stylesheets = {
video: GM_addStyle(styles.video),
controls: GM_addStyle(styles.controls),
mini: GM_addStyle(styles.mini),
lowerNavigation: GM_addStyle(styles.lowerNavigation)
}
const header = document.getElementById('biliMainHeader')
if (!header) { return console.error('页面加载错误:未找到导航栏') }
// 改变导航栏位置至下方
const lowerNavigation = (value = true) => { stylesheets.lowerNavigation.disabled = !value }
lowerNavigation(GM_getValue('导航栏下置'))
GM_addValueChangeListener('导航栏下置', (_k, _o, newVal) => { lowerNavigation(newVal) })
const styledControls = (value = true) => { stylesheets.controls.disabled = !value }
styledControls(GM_getValue('播放器控件样式'))
GM_addValueChangeListener('播放器控件样式', (_k, _o, newVal) => { styledControls(newVal) })
// 等待人数加载完成,再进行dom操作
const infos = player.getElementsByClassName('bpx-player-video-info')
await waitFor(() => infos[0], '正在观看')
// 播放器内容器
const container = player.getElementsByClassName('bpx-player-container')[0]
if (!(container instanceof HTMLDivElement)) { return console.error('页面加载错误:播放器内容器不存在') }
// 播放器底中部框 (用于放置弹幕框内容)
const bottomCenter = container.getElementsByClassName('bpx-player-control-bottom-center')[0]
const center = bottomCenter?.previousElementSibling?.hasChildNodes()
? bottomCenter
: player.getElementsByClassName('squirtle-controller-wrap-center')[0]
// 原弹幕框
const danmaku = container.getElementsByClassName('bpx-player-sending-bar')[0]
if (!center || !danmaku) { return console.error('页面加载错误:弹幕框不存在') }
// 立即使用宽屏样式 (除非当前是小窗模式)
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])
})
// 移除原 宽屏/网页全屏 按钮,因为没有用了
for (const className of [
'bpx-player-ctrl-wide', 'bpx-player-ctrl-web',
'squirtle-widescreen-wrap', 'squirtle-pagefullscreen-wrap'
]) { player.getElementsByClassName(className)[0]?.remove() }
// 退出全屏时弹幕框移至播放器下方
document.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) { center.replaceChildren(danmaku) }
})
// 立即将弹幕框移至播放器下方一次
center.replaceChildren(danmaku)
// 将自定义顶栏插入默认顶栏后
observeFor('custom-navbar').then(nav => header.insertAdjacentElement('afterend', nav))
console.info('宽屏模式成功启用')
// 添加拖动调整大小的部件
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 })
}
const videoArea = container.getElementsByClassName('bpx-player-video-area')[0]
videoArea && observeFor('bpx-player-mini-warp', videoArea).then(wrap => wrap.appendChild(miniResizer))
// #endregion
break
}
default:
console.info('未知页面,仅启用通用样式')
break
}
})()