视频网站增强工具

增强YouTube和B站体验:YouTube视频列表6列布局、视频新标签页打开、自动启用YouTube剧场模式

// ==UserScript==
// @name         视频网站增强工具
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description  增强YouTube和B站体验:YouTube视频列表6列布局、视频新标签页打开、自动启用YouTube剧场模式
// @author       dantaKing+Jahn
// @match        https://www.youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // 用于跟踪模式是否已处理的标志
    let theaterModeProcessed = false;
    let biliWideScreenProcessed = false;

    // 检测当前网站
    const isYouTube = location.hostname.includes('youtube.com');
    const isBilibili = location.hostname.includes('bilibili.com');

    // ============= YouTube 6列布局功能 =============
    function setupYouTube6Columns() {
        if (!isYouTube) return;

        // 创建样式元素
        const style = document.createElement('style');

        // 自定义CSS
        style.textContent = `
            /* 增加视频列表至6列 */
            ytd-rich-grid-renderer {
                --ytd-rich-grid-items-per-row: 6 !important;
            }

            /* 调整视频项目宽度以适应6列 */
            ytd-rich-grid-row {
                display: grid !important;
                grid-template-columns: repeat(6, 1fr) !important;
                column-gap: 16px !important;
            }

            /* 保持视频预览位置正确 */
            .ytp-preview-container,
            .ytp-tooltip-bg,
            .ytp-tooltip-text.ytp-tooltip-text-no-title {
                transform-origin: left top;
                transition: none !important;
            }

            /* 确保视频缩略图大小合适 */
            ytd-rich-grid-media {
                width: 100% !important;
            }

            /* 调整视频信息区域宽度 */
            ytd-rich-grid-media #meta {
                width: 100% !important;
            }

            /* 防止视频标题过长时出现省略 */
            ytd-rich-grid-media #video-title {
                max-height: 4.8rem !important;
                -webkit-line-clamp: 3 !important;
            }
        `;

        // 将样式添加到文档头部
        document.head.appendChild(style);
    }

    // 创建调整网格布局的函数
    function adjustGridLayout() {
        if (!isYouTube) return;

        // 强制刷新网格布局
        const richGridRenderer = document.querySelector('ytd-rich-grid-renderer');
        if (richGridRenderer) {
            // 确保布局属性设置正确
            richGridRenderer.style.setProperty('--ytd-rich-grid-items-per-row', '6', 'important');

            // 查找所有行并强制为6列布局
            const gridRows = document.querySelectorAll('ytd-rich-grid-row');
            if (gridRows.length > 0) {
                gridRows.forEach(row => {
                    if (row.style.display !== 'grid') {
                        row.style.cssText = 'display: grid !important; grid-template-columns: repeat(6, 1fr) !important; column-gap: 16px !important;';
                    }
                });
            }
        }
    }

    // ============= 通用工具函数 =============
    // 防抖函数,避免短时间内多次触发
    function debounce(func, wait) {
        let timeout;
        return function() {
            const context = this;
            const args = arguments;
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(context, args), wait);
        };
    }

    // 功能1:处理视频链接,使其在新标签页打开
    function processVideoLinks() {
        if (isYouTube) {
            processYouTubeLinks();
        }

        if (isBilibili) {
            processBilibiliLinks();
        }
    }

    // 处理YouTube视频链接
    function processYouTubeLinks() {
        // 获取所有可能的YouTube视频链接
        const videoLinks = document.querySelectorAll('a[href^="/watch"]:not([processed-by-script])');

        videoLinks.forEach(link => {
            // 如果链接还没有设置target属性
            if (link.getAttribute('target') !== '_blank') {
                link.setAttribute('target', '_blank');
                // 标记这个链接已被处理,避免重复处理
                link.setAttribute('processed-by-script', 'true');

                // 防止YouTube自己的事件处理覆盖我们的设置
                link.addEventListener('click', function(e) {
                    // 允许中键点击和Ctrl+点击的默认行为
                    if (!e.ctrlKey && e.button !== 1) {
                        e.stopPropagation();
                    }
                }, true);
            }
        });
    }

    // 处理哔哩哔哩视频链接
    function processBilibiliLinks() {
        // 获取所有可能的哔哩哔哩视频链接
        // 包括普通视频和番剧链接
        const videoLinks = document.querySelectorAll('a[href*="/video/"], a[href*="/bangumi/play/"]:not([processed-by-script])');

        videoLinks.forEach(link => {
            // 如果链接还没有设置target属性
            if (link.getAttribute('target') !== '_blank') {
                link.setAttribute('target', '_blank');
                // 标记这个链接已被处理,避免重复处理
                link.setAttribute('processed-by-script', 'true');

                // B站某些链接可能有自己的点击事件
                link.addEventListener('click', function(e) {
                    // 允许中键点击和Ctrl+点击的默认行为
                    if (!e.ctrlKey && e.button !== 1) {
                        e.stopPropagation();
                    }
                }, true);
            }
        });
    }

    // 功能2:自动启用YouTube剧场模式(仅限YouTube)
    function enableTheaterMode() {
        // 仅在YouTube视频页面执行,且尚未处理过
        if (isYouTube && window.location.pathname.includes('/watch') && !theaterModeProcessed) {
            // 重置计时器,确保只在稳定状态下执行
            resetTheaterModeTimer();
        }
    }

    // 实际执行YouTube剧场模式切换的函数
    function doEnableTheaterMode() {
        // 再次检查是否在YouTube视频页面
        if (!isYouTube || !window.location.pathname.includes('/watch')) {
            return;
        }

        // 查找剧场模式按钮
        let theaterButton = document.querySelector('.ytp-size-button');
        if (!theaterButton) {
            return;
        }

        // 更可靠地检查当前是否已经是剧场模式
        const playerElement = document.querySelector('ytd-watch-flexy');
        if (playerElement) {
            const isTheaterMode = playerElement.hasAttribute('theater');

            if (!isTheaterMode) {
                // 点击剧场模式按钮
                theaterButton.click();

                // 标记已处理,避免重复切换
                theaterModeProcessed = true;
            } else {
                // 已经是剧场模式,标记为已处理
                theaterModeProcessed = true;
            }
        }
    }

    // 使用延迟来重置YouTube剧场模式状态
    let theaterModeTimer = null;
    function resetTheaterModeTimer() {
        if (theaterModeTimer) {
            clearTimeout(theaterModeTimer);
        }
        theaterModeTimer = setTimeout(doEnableTheaterMode, 1500);
    }

    // 功能3:自动启用哔哩哔哩宽屏模式(仅限B站)
    function enableBiliWideScreen() {
        // 仅在B站视频页面执行,且尚未处理过
        if (isBilibili && !biliWideScreenProcessed && (
            window.location.pathname.includes('/video/') ||
            window.location.pathname.includes('/bangumi/play/')
        )) {
            // 重置计时器,确保只在稳定状态下执行
            resetBiliWideScreenTimer();
        }
    }

    // 实际执行B站宽屏模式切换的函数
    function doEnableBiliWideScreen() {
        // 再次检查是否在B站视频页面
        if (!isBilibili || !(
            window.location.pathname.includes('/video/') ||
            window.location.pathname.includes('/bangumi/play/')
        )) {
            return;
        }

        // 尝试多种可能的宽屏按钮选择器
        let wideScreenButtons = [
            // 普通视频页面的宽屏按钮
            document.querySelector('.bpx-player-ctrl-wide'),
            // 旧版视频页的宽屏按钮
            document.querySelector('.bilibili-player-video-btn-widescreen'),
            // 番剧页面的宽屏按钮
            document.querySelector('.squirtle-video-wide')
        ].filter(Boolean); // 过滤掉null或undefined

        if (wideScreenButtons.length === 0) {
            // 如果没找到按钮,可能是页面还没加载完,再次尝试
            setTimeout(enableBiliWideScreen, 1000);
            return;
        }

        // 获取第一个可用的宽屏按钮
        let wideScreenButton = wideScreenButtons[0];

        // 检查当前是否已经是宽屏模式
        const playerContainer = document.querySelector('.bpx-player-container') ||
                               document.querySelector('.bilibili-player-video-container');

        if (playerContainer) {
            // B站通常通过添加类名来表示宽屏模式
            const isWideScreen = playerContainer.classList.contains('bpx-state-widescreen') ||
                                playerContainer.classList.contains('bilibili-player-video-widescreen');

            if (!isWideScreen) {
                // 点击宽屏按钮
                wideScreenButton.click();

                // 标记已处理,避免重复切换
                biliWideScreenProcessed = true;
            } else {
                // 已经是宽屏模式,标记为已处理
                biliWideScreenProcessed = true;
            }
        }
    }

    // 使用延迟来重置B站宽屏模式状态
    let biliWideScreenTimer = null;
    function resetBiliWideScreenTimer() {
        if (biliWideScreenTimer) {
            clearTimeout(biliWideScreenTimer);
        }
        biliWideScreenTimer = setTimeout(doEnableBiliWideScreen, 1500);
    }

    // 应用设置
    function applySettings() {
        // 处理视频链接
        processVideoLinks();

        // YouTube专用功能
        if (isYouTube) {
            // 启用剧场模式
            enableTheaterMode();

            // 调整网格布局
            adjustGridLayout();
        }

        // B站专用功能
        if (isBilibili) {
            enableBiliWideScreen();
        }
    }

    // 处理URL变化
    function handleUrlChange() {
        // 重置模式处理标志
        theaterModeProcessed = false;
        biliWideScreenProcessed = false;

        // 延迟应用设置
        setTimeout(applySettings, 500);
    }

    // 初始化
    function initialize() {
        // 设置YouTube 6列布局
        if (isYouTube) {
            setupYouTube6Columns();
        }

        // 应用通用设置
        applySettings();

        // 创建MutationObserver监听DOM变化,使用防抖减少触发频率
        const debouncedProcessLinks = debounce(processVideoLinks, 300);
        const debouncedAdjustGrid = debounce(adjustGridLayout, 300);

        const observer = new MutationObserver(() => {
            debouncedProcessLinks();

            // YouTube专用处理
            if (isYouTube) {
                // 尝试启用剧场模式
                if (window.location.pathname.includes('/watch') && !theaterModeProcessed) {
                    enableTheaterMode();
                }

                // 调整网格布局
                debouncedAdjustGrid();
            }

            // B站专用处理
            if (isBilibili && (
                window.location.pathname.includes('/video/') ||
                window.location.pathname.includes('/bangumi/play/')
            ) && !biliWideScreenProcessed) {
                enableBiliWideScreen();
            }
        });

        // 配置并启动观察器
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        // 监听窗口大小变化
        window.addEventListener('resize', function() {
            if (isYouTube) {
                setTimeout(adjustGridLayout, 200);
            }
        });

        // 监听YouTube页面导航
        if (isYouTube) {
            window.addEventListener('yt-navigate-finish', function() {
                setTimeout(adjustGridLayout, 500);

                // 重置剧场模式状态
                theaterModeProcessed = false;
                setTimeout(enableTheaterMode, 500);
            });
        }

        // 定期检查链接
        setInterval(processVideoLinks, 2000);

        // 监听URL变化
        let lastUrl = location.href;
        new MutationObserver(() => {
            const url = location.href;
            if (url !== lastUrl) {
                lastUrl = url;
                handleUrlChange();
            }
        }).observe(document, {subtree: true, childList: true});
    }

    // 启动脚本
    initialize();
})();