// ==UserScript==
// @name 哔哩哔哩工具箱 - Bilibili Toolbox
// @namespace http://tampermonkey.net/
// @version 2.7
// @description 在哔哩哔哩视频页面上,提供用户选项来自动进入网页全屏、自动开启关灯模式(调暗页面背景),以及悬浮评论区选项。
// @author twocold
// @match https://www.bilibili.com/video/*
// @match https://www.bilibili.com/bangumi/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- UI & Notifications ---
GM_addStyle(`
.gm-toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 12px 20px;
border-radius: 8px;
font-size: 14px;
opacity: 0;
transition: opacity 0.3s ease;
}
.gm-toast-container.show {
opacity: 1;
}
.gm-floating-comment {
position: fixed;
top: 100px;
right: 20px;
width: 400px;
height: 600px;
background: white;
border: 2px solid #00a1d6;
border-radius: 8px;
z-index: 9998;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
overflow: hidden;
transition: all 0.3s ease;
}
.gm-floating-comment.collapsed {
height: 40px;
}
.gm-floating-comment.dragging {
opacity: 0.8;
transition: none;
cursor: move;
}
.gm-floating-comment-header {
background: #00a1d6;
color: white;
padding: 8px 12px;
font-size: 14px;
font-weight: bold;
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
}
.gm-floating-comment-toggle {
cursor: pointer;
background: none;
border: none;
color: white;
font-size: 16px;
font-weight: bold;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
transition: background-color 0.2s;
}
.gm-floating-comment-toggle:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.gm-floating-comment-content {
height: calc(100% - 40px);
overflow-y: auto;
}
.gm-floating-comment-content #commentapp {
width: 100% !important;
height: 100% !important;
}
.gm-floating-comment.collapsed .gm-floating-comment-content {
display: none;
}
.gm-floating-comment.collapsed .bili-comments-bottom-fixed-wrapper {
display: none !important;
}
`);
function showToast(message) {
const toast = document.createElement('div');
toast.className = 'gm-toast-container';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.classList.add('show'), 10);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => document.body.removeChild(toast), 300);
}, 3000);
}
// --- Configuration & Menu ---
const CONFIG_WEBFULLSCREEN_KEY = 'config_auto_web_fullscreen';
const CONFIG_LIGHTSOFF_KEY = 'config_lights_off';
const CONFIG_COMMENT_WINDOW_KEY = 'config_comment_window';
// Comment window functionality
let floatingCommentWindow = null;
let commentAppOriginalParent = null;
function createFloatingCommentWindow() {
if (floatingCommentWindow) return;
let commentApp = document.getElementById('commentapp');
if (!commentApp) {
commentApp = document.getElementById('comment-module');
}
if (!commentApp) {
setTimeout(createFloatingCommentWindow, 1000);
return;
}
commentAppOriginalParent = commentApp.parentNode;
floatingCommentWindow = document.createElement('div');
floatingCommentWindow.className = 'gm-floating-comment';
floatingCommentWindow.innerHTML = `
<div class="gm-floating-comment-header">
<span>评论区悬浮窗</span>
<button id="gm-comment-toggle" class="gm-floating-comment-toggle">−</button>
</div>
<div class="gm-floating-comment-content"></div>
`;
const content = floatingCommentWindow.querySelector('.gm-floating-comment-content');
content.appendChild(commentApp);
document.body.appendChild(floatingCommentWindow);
// Drag functionality
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
let xOffset = 0;
let yOffset = 0;
const header = floatingCommentWindow.querySelector('.gm-floating-comment-header');
const toggleBtn = document.getElementById('gm-comment-toggle');
// Initialize position based on current style
function initializeDragPosition() {
const rect = floatingCommentWindow.getBoundingClientRect();
xOffset = rect.left;
yOffset = rect.top;
}
function dragStart(e) {
if (e.target === toggleBtn || toggleBtn.contains(e.target)) return; // Don't drag when clicking toggle button
// Initialize position on first drag
if (xOffset === 0 && yOffset === 0) {
initializeDragPosition();
}
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
if (e.target === header || header.contains(e.target)) {
isDragging = true;
floatingCommentWindow.classList.add('dragging');
}
}
function dragEnd(e) {
initialX = currentX;
initialY = currentY;
isDragging = false;
floatingCommentWindow.classList.remove('dragging');
}
function drag(e) {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
// Keep window within viewport bounds
const rect = floatingCommentWindow.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let newX = currentX;
let newY = currentY;
// Horizontal bounds
if (newX < 0) newX = 0;
if (newX + rect.width > viewportWidth) newX = viewportWidth - rect.width;
// Vertical bounds
if (newY < 0) newY = 0;
if (newY + rect.height > viewportHeight) newY = viewportHeight - rect.height;
floatingCommentWindow.style.left = newX + 'px';
floatingCommentWindow.style.top = newY + 'px';
floatingCommentWindow.style.right = 'auto';
}
}
// Mouse events
header.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
// Touch events for mobile support
header.addEventListener('touchstart', (e) => {
const touch = e.touches[0];
dragStart({ clientX: touch.clientX, clientY: touch.clientY, target: e.target });
});
document.addEventListener('touchmove', (e) => {
if (isDragging) {
e.preventDefault();
const touch = e.touches[0];
drag({ clientX: touch.clientX, clientY: touch.clientY, preventDefault: () => {} });
}
});
document.addEventListener('touchend', dragEnd);
// Toggle functionality
function toggleCollapse() {
const isCollapsed = floatingCommentWindow.classList.toggle('collapsed');
toggleBtn.textContent = isCollapsed ? '+' : '−';
// Handle the bili-comments-bottom-fixed-wrapper visibility
const bottomFixedWrapper = floatingCommentWindow.querySelector('.bili-comments-bottom-fixed-wrapper');
if (bottomFixedWrapper) {
if (isCollapsed) {
bottomFixedWrapper.style.display = 'none';
bottomFixedWrapper.style.visibility = 'hidden';
bottomFixedWrapper.style.position = 'absolute';
bottomFixedWrapper.style.top = '-9999px';
} else {
bottomFixedWrapper.style.display = '';
bottomFixedWrapper.style.visibility = '';
bottomFixedWrapper.style.position = '';
bottomFixedWrapper.style.top = '';
}
}
}
toggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
toggleCollapse();
});
}
function removeFloatingCommentWindow() {
if (!floatingCommentWindow || !commentAppOriginalParent) return;
// Restore the bili-comments-bottom-fixed-wrapper before moving commentapp back
const bottomFixedWrapper = floatingCommentWindow.querySelector('.bili-comments-bottom-fixed-wrapper');
if (bottomFixedWrapper) {
bottomFixedWrapper.style.display = '';
bottomFixedWrapper.style.visibility = '';
bottomFixedWrapper.style.position = '';
bottomFixedWrapper.style.top = '';
}
const commentApp = document.getElementById('commentapp');
if (commentApp) {
commentAppOriginalParent.appendChild(commentApp);
}
document.body.removeChild(floatingCommentWindow);
floatingCommentWindow = null;
}
function toggleCommentWindow() {
if (floatingCommentWindow) {
removeFloatingCommentWindow();
} else {
createFloatingCommentWindow();
}
}
// Flag to prevent multiple menu registrations
let menuBuilt = false;
function buildMenu() {
if (menuBuilt) return; // Prevent multiple menu registrations
let isFullscreenEnabled = GM_getValue(CONFIG_WEBFULLSCREEN_KEY, true);
let isLightsOffEnabled = GM_getValue(CONFIG_LIGHTSOFF_KEY, false);
let isCommentWindowEnabled = GM_getValue(CONFIG_COMMENT_WINDOW_KEY, false);
// Create toggle functions that don't rebuild the menu
function toggleFullscreen() {
const newValue = !GM_getValue(CONFIG_WEBFULLSCREEN_KEY, true);
GM_setValue(CONFIG_WEBFULLSCREEN_KEY, newValue);
showToast(`自动网页全屏已${newValue ? '开启' : '关闭'},刷新页面后生效`);
}
function toggleLightsOff() {
const newValue = !GM_getValue(CONFIG_LIGHTSOFF_KEY, false);
GM_setValue(CONFIG_LIGHTSOFF_KEY, newValue);
showToast(`自动关灯模式已${newValue ? '开启' : '关闭'},刷新页面后生效`);
}
function toggleCommentWindow() {
const newValue = !GM_getValue(CONFIG_COMMENT_WINDOW_KEY, false);
GM_setValue(CONFIG_COMMENT_WINDOW_KEY, newValue);
showToast(`评论区悬浮窗已${newValue ? '开启' : '关闭'},刷新页面后生效`);
}
// Register menu commands once
GM_registerMenuCommand(`自动网页全屏: ${isFullscreenEnabled ? '✅' : '❌'}`, toggleFullscreen);
GM_registerMenuCommand(`自动关灯模式: ${isLightsOffEnabled ? '✅' : '❌'}`, toggleLightsOff);
GM_registerMenuCommand(`评论区悬浮窗: ${isCommentWindowEnabled ? '✅' : '❌'}`, toggleCommentWindow);
menuBuilt = true;
}
// Initial build of the menu
buildMenu();
// --- Core Logic ---
// Wait for page to load and then initialize features
function initializeFeatures() {
// Auto web fullscreen functionality
const isFullscreenEnabled = GM_getValue(CONFIG_WEBFULLSCREEN_KEY, true);
if (isFullscreenEnabled) {
// Look for web fullscreen button and click it
setTimeout(() => {
const fullscreenBtn = document.querySelector('.bpx-player-ctrl-btn.bpx-player-ctrl-web') ||
document.querySelector('.bpx-player-ctrl-web') ||
document.querySelector('[aria-label*="网页全屏"]') ||
document.querySelector('[title*="网页全屏"]');
if (fullscreenBtn) {
fullscreenBtn.click();
}
}, 2000);
}
// Auto lights off functionality
const isLightsOffEnabled = GM_getValue(CONFIG_LIGHTSOFF_KEY, false);
if (isLightsOffEnabled) {
setTimeout(() => {
// B站关灯模式:齿轮按钮 -> 更多播放设置 -> 关灯模式
// Step 1: 点击设置按钮(齿轮图标)
const settingsBtn = document.querySelector('.bpx-player-ctrl-btn.bpx-player-ctrl-setting') ||
document.querySelector('.bpx-player-ctrl-setting') ||
document.querySelector('[aria-label*="设置"]') ||
document.querySelector('[title*="设置"]');
if (settingsBtn) {
settingsBtn.click();
// Step 2: 等待设置菜单展开,然后点击关灯模式选项
setTimeout(() => {
const lightsOffCheckbox = document.querySelector('input.bui-checkbox-input[aria-label="关灯模式"]') ||
document.querySelector('input[type="checkbox"][aria-label*="关灯"]') ||
document.querySelector('input[type="checkbox"]')?.closest('div')?.querySelector('span')?.textContent?.includes('关灯');
if (lightsOffCheckbox && !lightsOffCheckbox.checked) {
lightsOffCheckbox.click();
}
// Step 3: 关闭设置菜单
setTimeout(() => {
if (settingsBtn) {
settingsBtn.click();
}
}, 300);
}, 500);
} else {
// Alternative: Try to find the lights off checkbox directly in the player
const lightsOffDirect = document.querySelector('input.bui-checkbox-input[aria-label="关灯模式"]');
if (lightsOffDirect && !lightsOffDirect.checked) {
lightsOffDirect.click();
}
}
}, 2500);
}
// Auto comment window functionality
const isCommentWindowEnabled = GM_getValue(CONFIG_COMMENT_WINDOW_KEY, false);
if (isCommentWindowEnabled) {
setTimeout(() => {
createFloatingCommentWindow();
}, 3000);
}
}
// Initialize features when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeFeatures);
} else {
initializeFeatures();
}
})();