YouTube 首页视频排列数量调整+自动继续播放

自定义 YouTube 首页视频排列数量,同时支持自动忽略“视频已暂停”提示继续播放,所有功能都有开关控制。

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         YouTube 首页视频排列数量调整+自动继续播放
// @namespace    https://www.acy.moe
// @supportURL   https://www.acy.moe
// @version      1.1.0
// @description  自定义 YouTube 首页视频排列数量,同时支持自动忽略“视频已暂停”提示继续播放,所有功能都有开关控制。
// @author       NEET姬
// @match        *://www.youtube.com/*
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @license      GPL-3.0-only
// ==/UserScript==

(function () {
    'use strict';

    const STORAGE_KEY_COLUMNS = 'yt_grid_columns';
    const STORAGE_KEY_ENABLED = 'yt_grid_enabled';
    const STORAGE_KEY_AUTOPLAY = 'yt_continue_enabled';
    const DEFAULT_COLUMNS = 6;

    function getColumns() {
        return parseInt(localStorage.getItem(STORAGE_KEY_COLUMNS)) || DEFAULT_COLUMNS;
    }

    function setColumns(n) {
        localStorage.setItem(STORAGE_KEY_COLUMNS, n);
        applyGridStyle(n);
    }

    function isEnabled() {
        return localStorage.getItem(STORAGE_KEY_ENABLED) !== 'false';
    }

    function setEnabled(state) {
        localStorage.setItem(STORAGE_KEY_ENABLED, state);
        if (state) {
            applyGridStyle(getColumns());
        } else {
            removeGridStyle();
        }
    }

    function isAutoplayEnabled() {
        return localStorage.getItem(STORAGE_KEY_AUTOPLAY) !== 'false';
    }

    function setAutoplayEnabled(state) {
        localStorage.setItem(STORAGE_KEY_AUTOPLAY, state);
        alert(`自动播放功能已${state ? "启用" : "禁用"},页面将刷新`);
        location.reload();
    }

    function applyGridStyle(columns) {
        if (!isEnabled()) return;

        const styleId = 'yt-grid-style';
        let styleTag = document.getElementById(styleId);
        if (!styleTag) {
            styleTag = document.createElement('style');
            styleTag.id = styleId;
            document.head.appendChild(styleTag);
        }

        styleTag.textContent = `
            ytd-rich-grid-renderer {
                --ytd-rich-grid-items-per-row: ${columns} !important;
            }
            ytd-rich-grid-video-renderer {
                max-width: ${Math.floor(1200 / columns)}px !important;
                zoom: 0.9 !important;
            }
            ytd-app {
                overflow-x: hidden !important;
            }
        `;
    }

    function removeGridStyle() {
        const styleTag = document.getElementById('yt-grid-style');
        if (styleTag) styleTag.remove();
    }

    function createMenu() {
        GM_registerMenuCommand("设置每行视频数量", () => {
            const input = prompt("请输入每行视频数量(4~8)", getColumns());
            const value = parseInt(input);
            if (value >= 4 && value <= 8) {
                setColumns(value);
                alert(`已设置为每行显示 ${value} 个视频`);
                location.reload();
            } else {
                alert("请输入有效的数字(4 到 8)!");
            }
        });

        GM_registerMenuCommand(isEnabled() ? "🔴 禁用视频排列调整" : "🟢 启用视频排列调整", () => {
            const newState = !isEnabled();
            setEnabled(newState);
            alert(`视频排列调整已${newState ? "启用" : "禁用"},页面将刷新以更新菜单`);
            location.reload();
        });

        GM_registerMenuCommand(isAutoplayEnabled() ? "🔴 禁用自动播放" : "🟢 启用自动播放", () => {
            const newState = !isAutoplayEnabled();
            setAutoplayEnabled(newState);
        });
    }

    function init() {
        if (isEnabled()) applyGridStyle(getColumns());
        createMenu();
    }

    // 页面加载完成后初始化
    const observer = new MutationObserver(() => {
        if (document.querySelector('ytd-rich-grid-renderer')) {
            init();
            observer.disconnect();
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });

    // 自动继续播放功能(带开关)
    if (isAutoplayEnabled()) {
        (function autoplayContinueWatcher() {
            function searchDialog(videoPlayer) {
                if (videoPlayer.currentTime < videoPlayer.duration) {
                    let dialog = document.querySelector('yt-confirm-dialog-renderer') ||
                                 document.querySelector('ytmusic-confirm-dialog-renderer') ||
                                 document.querySelector('dialog');

                    if (dialog && (dialog.parentElement.style.display !== 'none' || document.hidden)) {
                        console.debug('自动继续播放');
                        videoPlayer.play();
                    } else if (videoPlayer.paused && videoPlayer.src) {
                        setTimeout(() => searchDialog(videoPlayer), 1000);
                    }
                }
            }

            function pausedFun({ target: videoPlayer }) {
                setTimeout(() => searchDialog(videoPlayer), 500);
            }

            function setPauseListener(player) {
                if (!player.dataset.pauseWatcher) {
                    player.dataset.pauseWatcher = true;
                    player.addEventListener('pause', pausedFun);
                }
            }

            function observerPlayerRoot(root) {
                const player = root.querySelector('video');
                if (player) setPauseListener(player);

                const ycpObserver = new MutationObserver(mutations => {
                    mutations.flatMap(m => [...m.addedNodes]).forEach(node => {
                        if (node.tagName && node.tagName === 'VIDEO') {
                            setPauseListener(node);
                        } else if (node.querySelector) {
                            const video = node.querySelector('video');
                            if (video) setPauseListener(video);
                        }
                    });
                });

                ycpObserver.observe(root, { childList: true, subtree: true });
            }

            const playerRoot = document.querySelector('#player');
            if (playerRoot) {
                observerPlayerRoot(playerRoot);
            } else {
                const rootObserver = new MutationObserver(mutations => {
                    mutations.flatMap(m => [...m.addedNodes]).forEach(node => {
                        if (node.querySelector) {
                            const pr = node.querySelector('#player');
                            if (pr) {
                                observerPlayerRoot(pr);
                                rootObserver.disconnect();
                            }
                        }
                    });
                });
                rootObserver.observe(document, { childList: true, subtree: true });
            }
        })();
    }
})();