Greasy Fork

Greasy Fork is available in English.

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

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

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