Greasy Fork

Greasy Fork is available in English.

视频网站增强工具

增强YouTube和B站体验:YouTube视频列表6列布局、视频新标签页打开、可选的剧场模式和宽屏模式

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

(function() {
    'use strict';

    // 配置管理
    const CONFIG_KEY = 'video_enhancer_config';
    const defaultConfig = {
        enableTheaterMode: true,
        enableBiliWideScreen: true
    };

    // 获取配置
    function getConfig() {
        try {
            const saved = GM_getValue(CONFIG_KEY);
            return saved ? JSON.parse(saved) : defaultConfig;
        } catch {
            return defaultConfig;
        }
    }

    // 保存配置
    function saveConfig(config) {
        GM_setValue(CONFIG_KEY, JSON.stringify(config));
    }

    let config = getConfig();
    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() {
        // 检查开关状态
        if (!config.enableTheaterMode) return;

        // 仅在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() {
        // 检查开关状态
        if (!config.enableBiliWideScreen) return;

        // 仅在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 registerMenuCommands() {
        // YouTube剧场模式开关
        GM_registerMenuCommand(
            `${config.enableTheaterMode ? '✓' : '✗'} YouTube自动剧场模式`,
            () => {
                config.enableTheaterMode = !config.enableTheaterMode;
                saveConfig(config);
                theaterModeProcessed = false;
                alert(`YouTube自动剧场模式已${config.enableTheaterMode ? '开启' : '关闭'}\n刷新页面后生效`);
                location.reload();
            }
        );

        // Bilibili宽屏模式开关
        GM_registerMenuCommand(
            `${config.enableBiliWideScreen ? '✓' : '✗'} Bilibili自动宽屏模式`,
            () => {
                config.enableBiliWideScreen = !config.enableBiliWideScreen;
                saveConfig(config);
                biliWideScreenProcessed = false;
                alert(`Bilibili自动宽屏模式已${config.enableBiliWideScreen ? '开启' : '关闭'}\n刷新页面后生效`);
                location.reload();
            }
        );
    }

    // 应用设置
    function applySettings() {
        processVideoLinks();

        if (isYouTube) {
            enableTheaterMode();
            adjustGridLayout();
        }

        if (isBilibili) {
            enableBiliWideScreen();
        }
    }

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

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

    // 初始化
    function initialize() {
        // 注册油猴菜单
        registerMenuCommands();

        if (isYouTube) {
            setupYouTube6Columns();
        }

        applySettings();

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

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

            if (isYouTube) {
                if (window.location.pathname.includes('/watch') && !theaterModeProcessed) {
                    enableTheaterMode();
                }
                debouncedAdjustGrid();
            }

            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);
            }
        });

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