// ==UserScript==
// @name Wider Bilibili
// @namespace https://greasyfork.org/users/1125570
// @version 0.4.0
// @author posthumz
// @description 哔哩哔哩宽屏体验
// @license MIT
// @icon https://www.bilibili.com/favicon.ico
// @match http*://*.bilibili.com/*
// @exclude http*://www.bilibili.com/correspond/*
// @exclude http*://message.bilibili.com/pages/nav/header_sync
// @grant GM_addStyle
// @grant GM_addValueChangeListener
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @run-at document-start
// ==/UserScript==
(async function () {
'use strict';
const styles = {
video: `/* 播放器 */
:root {
--navbar-height: 64px;
--video-height: 100vh;
}
div#playerWrap,
div#bilibili-player-wrap {
position: absolute;
left: 0;
right: 0;
top: 0;
height: var(--video-height);
/* 番剧页加载时会有右填充 */
padding-right: 0;
}
div#bilibili-player {
width: 100%;
height: 100%;
box-shadow: none !important;
}
/* 导航栏 */
#biliMainHeader {
margin-top: var(--video-height);
margin-bottom: 0;
position: sticky;
top: 0;
z-index: 3;
}
.custom-navbar {
position: sticky !important;
z-index: 3 !important;
}
.bili-header.fixed-header {
min-height: initial !important;
}
.bili-header__bar {
position: relative !important;
}
/* 使用 static 才能让播放器的 absolute 正确定位 */
/* 视频、番剧、收藏/稍后再看页 */
.video-container-v1,
.left-container,
.main-container,
.playlist-container--left {
position: static !important;
}
/* 加载动画强制显示 */
.bpx-player-loading-panel-blur {
display: flex !important;
}
/* 强制显示播放器控件 */
.bpx-player-top-left-title,
.bpx-player-top-left-music,
.bpx-player-top-mask {
display: block !important;
}
/* 原弹幕发送区域不显示 */
.bpx-player-sending-area {
display: none;
}
/* 原宽屏/网页全屏按钮不显示 */
.bpx-player-ctrl-wide,
.bpx-player-ctrl-web {
display: none;
}
/* 视频页、番剧页、收藏/稍后再看页的下方容器 */
.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;
}`,
t: `/* 动态页 */
#app .bg+.content {
box-sizing: border-box;
border-width: 10px var(--layout-padding);
border-style: solid;
border-color: transparent;
width: initial !important;
.sidebar-wrap {
right: 58px;
}
}
.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;
}
.section.coin>.content {
display: flex;
flex-wrap: wrap;
margin: 0;
width: initial !important;
}
}
.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: `/* 阅读页 */
#app>.article-detail {
box-sizing: border-box;
border-width: 0 var(--layout-padding);
border-style: solid;
border-color: transparent;
width: 100%;
.article-up-info {
width: initial;
margin: 0 80px 20px;
}
.right-side-bar {
right: 0;
}
}`,
home: `/* 首页 */
.feed-card,
.floor-single-card,
.bili-video-card {
margin-top: 0px !important;
}
.feed-roll-btn {
/* left: initial !important;
right: calc(30px - var(--layout-padding)); */
/* 右下角已有刷新按键,何必呢 */
display: none;
}
.palette-button-wrap {
left: initial !important;
right: 30px;
}`,
common: `/* This overrides :root style */
body {
--layout-padding: 30px;
}
html,
body {
width: initial !important;
height: initial !important;
}
/* 搜索栏 */
.center-search-container {
min-width: 0;
}
.nav-search-input {
width: 0 !important;
padding-right: 0 !important;
}`,
upperNavigation: `/* 导航栏上置 (默认下置) */
:root {
--video-height: calc(100vh - var(--navbar-height));
}
#biliMainHeader {
margin-top: 0;
margin-bottom: var(--video-height);
}
#playerWrap,
#bilibili-player-wrap {
top: var(--navbar-height);
height: var(--video-height) !important;
}`,
pauseShow: `/* 暂停显示控件 */
.bpx-state-paused {
.bpx-player-top-wrap,
.bpx-player-control-top,
.bpx-player-control-bottom,
.bpx-player-control-mask {
opacity: 1 !important;
visibility: visible !important;
}
.bpx-player-shadow-progress-area {
visibility: hidden !important;
}
.bpx-player-pbp {
bottom: 100% !important;
margin-bottom: 5px;
opacity: 1 !important;
left: 0;
right: 0;
width: initial !important;
.bpx-player-pbp-pin {
opacity: 1 !important;
}
}
}`,
options: `/* 脚本选项 */
#wider-bilibili {
position: fixed;
top: 0;
bottom: 0;
height: fit-content;
max-height: 80vh;
left: 0;
right: 0;
width: fit-content;
max-width: 80vw;
z-index: 114514;
padding: 10px;
border-radius: 10px;
margin: auto;
box-sizing: border-box;
overflow: auto;
flex-direction: column;
gap: 10px;
outline: 2px solid var(--v_brand_blue);
outline-offset: 0;
background-color: var(--bg1);
color: var(--text1);
font-size: 20px;
font-family: "HarmonyOS_Regular", "PingFang SC", "Helvetica Neue", "Microsoft YaHei", sans-serif !important;
opacity: 0.9;
&:hover {
opacity: 1
}
>header {
position: sticky;
z-index: 2;
top: -10px;
display: flex;
justify-content: space-between;
margin: -10px;
text-overflow: clip;
padding-left: 20px;
font-weight: bold;
background-color: var(--bg1);
&::before {
content: "Wider Bilibili 选项";
align-self: center;
flex: 1;
}
}
.wb-button-group {
display: flex;
margin-bottom: 10px;
>* {
height: 100%;
}
}
#wb-close {
box-sizing: border-box;
padding: 4px;
width: fit-content;
background-color: var(--v_stress_red);
fill: var(--text1);
&:hover {
background-color: var(--v_stress_red_hover);
}
&:active {
background-color: var(--v_stress_red_active);
}
}
button {
border: none;
padding: 4px 8px;
background: none;
color: var(--text_white);
transition: opacity .1s;
background-color: var(--v_brand_blue);
font-size: 16px;
text-wrap: nowrap;
&:hover {
background-color: var(--v_brand_blue_hover);
}
&:active {
background-color: var(--v_brand_blue_active);
}
>a {
color: inherit;
text-decoration: none;
}
}
>fieldset {
border: none;
border-radius: 10px;
padding: 10px;
margin: 0;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px 15px;
background-color: rgba(127, 127, 127, 0.1);
&::before {
content: attr(data-title);
border-radius: 4px 4px 0 0;
border-bottom: 2px solid rgba(127, 127, 127, 0.1);
grid-column: 1 / -1;
}
}
label {
display: inline-flex;
gap: 10px;
place-items: center;
position: relative;
text-wrap: nowrap;
&[data-hint]:hover::before {
position: absolute;
bottom: 110%;
left: 0;
right: 0;
margin: 0 auto;
width: fit-content;
padding: 3px 5px;
border-radius: 5px;
content: attr(data-hint);
font-size: 12px;
background-color: var(--v_brand_blue);
white-space: pre-line;
}
&::after {
content: attr(data-key);
}
}
input {
box-sizing: content-box;
margin: 0;
padding: 4px;
height: 20px;
font-size: 16px;
transition: .2s;
&:hover {
box-shadow: 0 0 8px var(--v_brand_blue);
}
&[type=checkbox] {
box-sizing: content-box;
border-radius: 20px;
min-width: 40px;
background-color: #ccc;
appearance: none;
cursor: pointer;
&::before {
content: "";
position: relative;
display: block;
transition: 0.3s;
height: 100%;
aspect-ratio: 1/1;
border-radius: 50%;
background-color: #FFF;
}
&:checked {
background-color: var(--v_brand_blue);
}
&:checked::before {
transform: translateX(20px);
}
&:active {
opacity: 0.5;
}
}
&[type=number] {
width: 40px;
border: none;
border-radius: 5px;
outline: 2px solid var(--v_brand_blue);
background: none;
color: var(--text1);
appearance: textfield;
&::-webkit-inner-spin-button {
appearance: none;
}
}
}
}`,
mini: `/* 小窗 */
.bpx-player-container[data-screen="mini"] {
/* 以视频长宽比为准,不显示黑边和阴影 */
height: auto !important;
box-shadow: none;
/* 修正小窗位置 */
translate: 32px 40px;
}
.bpx-player-container {
&[data-screen="mini"] {
width: var(--mini-width) !important;
}
/* 最小宽度,以防不可见 */
min-width: 180px;
}
.bpx-player-mini-resizer {
position: absolute;
left: 0;
width: 10px;
height: 100%;
cursor: ew-resize;
}`,
controls: `/* 播放器控件 */
.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;
}`
};
function waitFor(loaded, desc = "页面加载", retry = 100, interval = 100) {
return new Promise((resolve, reject) => {
const intervalID = setInterval((res = loaded()) => {
if (res) {
clearInterval(intervalID);
console.info(`${desc}已加载`);
return resolve(res);
}
if (--retry === 0) {
console.error("页面加载超时");
clearInterval(intervalID);
return reject(new Error("timeout"));
}
if (retry % 10 === 0) {
console.debug(`等待${desc}`);
}
}, interval);
});
}
function observeFor(className, parent) {
return 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 });
});
}
function waitReady() {
return new Promise((resolve) => {
document.readyState === "loading" ? window.addEventListener("DOMContentLoaded", () => resolve(), { once: true }) : resolve();
});
}
const html = `<div id="wider-bilibili" style="display: none;">
<header>
<div class="wb-button-group">
<button><a target="_blank" href="https://greasyfork.org/scripts/474507">发布</a></button>
<button><a target="_blank" href="https://github.com/shz42/wider-bilibili">主页</a></button>
<button><a target="_blank" href="https://github.com/shz42/wider-bilibili/issues">反馈</a></button>
<svg id="wb-close" xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>
</div>
</header>
<fieldset data-title="通用">
<label data-key="左右边距" data-hint="可使用滚轮调节"><input type="number" min="0" value="30"></label>
</fieldset>
<fieldset data-title="播放器">
<label data-key="导航栏下置"><input type="checkbox" checked></label>
<label data-key="小窗样式" data-hint="试试拉一下小窗左侧?"><input type="checkbox" checked></label>
<label data-key="控件样式" data-hint="调节控件间距"><input type="checkbox" checked></label>
<label data-key="暂停显示控件" data-hint="默认检测到鼠标活动显示控件 需要一直显示请打开此选项"><input type="checkbox"></label>
</fieldset>
</div>`;
GM_addStyle(styles.options);
function toggleStyle(s, init = true, flip = false) {
if (flip) {
init = !init;
}
const style = GM_addStyle(s);
if (!init) {
style.disabled = true;
}
return flip ? (enable) => {
style.disabled = enable;
} : (enable) => {
style.disabled = !enable;
};
}
function onStyleValueChange(toggle) {
return (_k, _o, val) => toggle(val);
}
const options = {
左右边距: {
page: "common",
default: 30,
callback: (init) => {
document.body.style.setProperty("--layout-padding", `${init}px`);
return (_k, _o, newVal) => document.body.style.setProperty("--layout-padding", `${newVal}px`);
}
},
导航栏下置: {
page: "video",
default: true,
callback: (init) => onStyleValueChange(toggleStyle(styles.upperNavigation, init, true))
},
小窗样式: {
page: "video",
default: true,
callback: (init) => {
document.documentElement.style.setProperty("--mini-width", "320px");
const toggle1 = toggleStyle(styles.mini, init);
const toggle2 = toggleStyle(".bpx-player-container { --mini-width: initial }", init, true);
return onStyleValueChange((enable) => {
toggle1(enable);
toggle2(enable);
});
}
},
控件样式: {
page: "video",
default: true,
callback: (init) => onStyleValueChange(toggleStyle(styles.controls, init))
},
暂停显示控件: {
page: "video",
default: false,
callback: (init) => onStyleValueChange(toggleStyle(styles.pauseShow, init))
}
};
function activate(targetPage) {
for (const [name, { page, default: d, callback }] of Object.entries(options)) {
page === targetPage && GM_addValueChangeListener(name, callback(GM_getValue(name, d)));
}
}
waitReady().then(() => {
document.body.insertAdjacentHTML("beforeend", html);
const app = document.getElementById("wider-bilibili");
GM_registerMenuCommand("选项", () => {
app.style.display = "flex";
});
document.getElementById("wb-close")?.addEventListener("click", () => {
app.style.display = "none";
});
for (const input of app.getElementsByTagName("input")) {
const key = input.parentElement?.dataset.key;
if (!key) {
continue;
}
const option = options[key];
switch (input.type) {
case "checkbox":
input.checked = GM_getValue(key, option.default);
input.onchange = () => GM_setValue(key, input.checked);
break;
case "number":
input.value = GM_getValue(key, option.default);
input.oninput = () => {
const val = Number(input.value);
Number.isInteger(val) && GM_setValue(key, val);
};
break;
}
}
const modifiers = ["ctrlKey", "altKey", "shiftKey", "metaKey"];
const comb = [["altKey", "shiftKey"], "W"];
document.addEventListener("keyup", (ev) => {
const { key } = ev;
if (key === comb[1] && modifiers.every((mod) => comb[0].includes(mod) === ev[mod])) {
app.style.display = app.style.display === "none" ? "flex" : "none";
}
});
activate("common");
});
GM_addStyle(styles.common);
const url = new URL(window.location.href);
switch (url.host) {
case "www.bilibili.com": {
if (url.pathname === "/") {
GM_addStyle(styles.home);
console.info("使用首页宽屏样式");
break;
}
if (url.pathname.startsWith("/read")) {
GM_addStyle(styles.read);
console.info("使用阅读页宽屏样式");
break;
}
const style = GM_addStyle(styles.video);
await( waitReady());
const player = document.getElementById("bilibili-player");
if (!player) {
style.remove();
break;
}
activate("video");
observeFor("custom-navbar", document.body).then((nav) => document.getElementById("biliMainHeader").insertAdjacentElement("beforeend", nav));
const container = await( waitFor(() => player.getElementsByClassName("bpx-player-container")[0], "播放器内容器"));
if (container.getAttribute("data-screen") !== "mini") {
container.setAttribute("data-screen", "web");
}
container.setAttribute = new Proxy(container.setAttribute, {
apply: (target, thisArg, [name, val]) => target.apply(thisArg, [name, name === "data-screen" && val !== "mini" ? "web" : val])
});
const miniResizer = document.createElement("div");
miniResizer.className = "bpx-player-mini-resizer";
miniResizer.onmousedown = (ev) => {
ev.stopImmediatePropagation();
ev.preventDefault();
const resize = (ev2) => document.documentElement.style.setProperty(
"--mini-width",
`${container.offsetWidth + container.offsetLeft - ev2.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];
if (!videoArea) {
console.error("页面加载错误:视频区域不存在");
break;
}
observeFor("bpx-player-mini-warp", videoArea).then((wrap) => wrap.appendChild(miniResizer));
const sendingBar = player.getElementsByClassName("bpx-player-sending-bar")[0];
if (!sendingBar) {
console.error("页面加载错误:发送框不存在");
break;
}
const danmaku = (await( observeFor("bpx-player-video-info", sendingBar))).parentElement;
const bottomCenter = container.getElementsByClassName("bpx-player-control-bottom-center")[0];
if (!bottomCenter || !danmaku) {
console.error("页面加载错误:弹幕框不存在");
break;
}
document.addEventListener("fullscreenchange", () => {
if (!document.fullscreenElement) {
bottomCenter.replaceChildren(danmaku);
}
});
bottomCenter.replaceChildren(danmaku);
console.info("宽屏模式成功启用");
break;
}
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;
default:
console.info(`未适配页面,仅启用通用样式: ${url.href}`);
break;
}
})();