哔哩哔哩工具箱 - Bilibili Toolbox

在哔哩哔哩视频页面上,提供用户选项来自动进入网页全屏、自动开启关灯模式(调暗页面背景),以及悬浮评论区选项。

// ==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();
    }

})();