YouTube 全萤幕管理器

管理 YouTube 全萤幕模式切换,包含四种模式:原生、浏览器API、网页全萤幕(置中容器)、网页全萤幕(置顶容器)。仅在影片播放页面启用核心功能。

// ==UserScript==
// @name         YouTube Fullscreen Manager
// @name:zh-CN   YouTube 全萤幕管理器
// @name:en      YouTube Fullscreen Manager
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  管理 YouTube 全螢幕模式切換,包含四種模式:原生、瀏覽器API、網頁全螢幕(置中容器)、網頁全螢幕(置頂容器)。僅在影片播放頁面啟用核心功能。
// @description:zh-CN 管理 YouTube 全萤幕模式切换,包含四种模式:原生、浏览器API、网页全萤幕(置中容器)、网页全萤幕(置顶容器)。仅在影片播放页面启用核心功能。
// @description:en  Manages YouTube fullscreen switching with four modes: Native, Browser API, Web Fullscreen (Centered Container), Web Fullscreen (Top Container). Core functionality only activates on video playback pages.
// @match        https://www.youtube.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const LANG = /^zh-(cn|tw|hk|mo|sg)/i.test(navigator.language) ? 'zh' : 'en';
    const i18n = {
        zh: {
            menuFullscreenMode: '📺 設定 YouTube 全螢幕模式',
            fullscreenModeOptions: {
                1: '1. 原生最大化 (點擊 .ytp-fullscreen-button)',
                2: '2. 原生API最大化 (toggleNativeFullscreen)',
                3: '3. 網頁全螢幕 (容器置中)',
                4: '4. 網頁全螢幕 (容器置頂)'
            },
            promptFullscreen: '選擇 YouTube 全螢幕模式:',
            saveAlert: '設定已保存,需重新整理頁面後生效'
        },
        en: {
            menuFullscreenMode: '📺 Set YouTube Fullscreen Mode',
            fullscreenModeOptions: {
                1: '1. Native maximization (click .ytp-fullscreen-button)',
                2: '2. Native API maximization (toggleNativeFullscreen)',
                3: '3. Web Fullscreen (Centered Container)',
                4: '4. Web Fullscreen (Top Container)'
            },
            promptFullscreen: 'Select YouTube fullscreen mode:',
            saveAlert: 'Settings saved. Refresh page to apply'
        }
    };

    // 配置管理 / Configuration management
    const CONFIG_STORAGE_KEY = 'YouTubeFullscreenManagerConfig';
    const DEFAULT_CONFIG = {
        youtubeFullscreenMode: 2 // 預設模式改為2 / Default mode changed to 2
    };

    const getConfig = () => {
        const savedConfig = GM_getValue(CONFIG_STORAGE_KEY, {});
        return { ...DEFAULT_CONFIG, ...savedConfig };
    };

    const saveConfig = (config) => {
        const currentConfig = { ...config };
        const isDefault = Object.keys(DEFAULT_CONFIG).every(key =>
            currentConfig[key] === DEFAULT_CONFIG[key]
        );
        if (isDefault) {
            GM_setValue(CONFIG_STORAGE_KEY, {});
            return;
        }
        GM_setValue(CONFIG_STORAGE_KEY, currentConfig);
    };

    let CONFIG = getConfig();

    // 註冊選單 / Register menu
    const registerMenuCommands = () => {
        const t = i18n[LANG];
        GM_registerMenuCommand(t.menuFullscreenMode, handleFullscreenModeSetting);
    };

    const handleFullscreenModeSetting = () => {
        const t = i18n[LANG];
        const options = t.fullscreenModeOptions;
        const choice = prompt(
            `${t.promptFullscreen}\n${Object.values(options).join('\n')}`,
            CONFIG.youtubeFullscreenMode
        );
        if (choice && options[choice]) {
            CONFIG.youtubeFullscreenMode = parseInt(choice);
            saveConfig(CONFIG);
            alert(t.saveAlert);
        }
    };

    // 核心功能控制變量 / Core functionality control variables
    let isCoreActive = false; // 核心功能是否啟動 / Whether core functionality is active
    let videoDoubleClickHandler = null; // 用於存儲雙擊處理函數 / Used to store the double-click handler
    let keydownHandler = null; // 用於存儲按鍵處理函數 / Used to store the keydown handler
    let mutationObserver = null; // 用於監聽DOM變更 / Used to observe DOM changes

    // 狀態變量 / State variables
    let isWebFullscreened = false;
    let originalVideoParent = null;
    let originalVideoStyles = {};
    let originalParentStyles = {};
    let webFullscreenContainer = null;

    // 切換函數 / Toggle functions
    function toggleWebFullscreen(video) {
        if (!video) return;

        if (isWebFullscreened) {
            // 恢復原狀 / Restore original state
            if (webFullscreenContainer && webFullscreenContainer.contains(video)) {
                webFullscreenContainer.removeChild(video);
            }
            if (webFullscreenContainer && document.body.contains(webFullscreenContainer)) {
                document.body.removeChild(webFullscreenContainer);
                webFullscreenContainer = null;
            }
            if (originalVideoParent && !originalVideoParent.contains(video)) {
                originalVideoParent.appendChild(video);
            }
            Object.assign(video.style, originalVideoStyles);
            if (originalVideoParent) {
                Object.assign(originalVideoParent.style, originalParentStyles);
            }
            isWebFullscreened = false;
            originalVideoParent = null;
        } else {
            // 進入全螢幕 / Enter fullscreen
            originalVideoParent = video.parentElement;
            if (!originalVideoParent) return;

            originalVideoStyles = {
                position: video.style.position,
                top: video.style.top,
                left: video.style.left,
                width: video.style.width,
                height: video.style.height,
                zIndex: video.style.zIndex,
                objectFit: video.style.objectFit,
                objectPosition: video.style.objectPosition
            };
            originalParentStyles = {
                position: originalVideoParent.style.position,
                overflow: originalVideoParent.style.overflow
            };

            if (!webFullscreenContainer) {
                webFullscreenContainer = document.createElement('div');
                webFullscreenContainer.id = 'web-fullscreen-container';
                // 根據模式設定容器樣式 / Set container styles based on mode
                let containerStyles;
                if (CONFIG.youtubeFullscreenMode === 3) { // 模式3: 容器置中 (覆蓋整個視窗) / Mode 3: Centered container (covers entire window)
                    containerStyles = {
                        position: 'fixed', // 固定定位 / Fixed positioning
                        top: '0',
                        left: '0',
                        width: '100vw',
                        height: '100vh',
                        zIndex: '2147483645',
                        backgroundColor: 'black',
                        display: 'flex',
                        alignItems: 'center', // 垂直置中 / Center vertically
                        justifyContent: 'center' // 水平置中 / Center horizontally
                    };
                } else { // 模式4: 容器置頂 / Mode 4: Top container
                    containerStyles = {
                        position: 'relative',
                        zIndex: '2147483645',
                        backgroundColor: 'black',
                        display: 'flex',
                        alignItems: 'center',
                        justifyContent: 'center',
                        margin: '0 auto',
                        maxWidth: '100%',
                        maxHeight: '100vh'
                    };
                }
                Object.assign(webFullscreenContainer.style, containerStyles);
                webFullscreenContainer.addEventListener('click', () => {
                    if (video && !video.paused) {
                        video.pause();
                    } else if (video) {
                        video.play().catch(() => {});
                    }
                });
            }

            Object.assign(originalVideoParent.style, {
                position: 'static',
                overflow: 'visible'
            });

            originalVideoParent.removeChild(video);
            webFullscreenContainer.appendChild(video);
            document.body.insertBefore(webFullscreenContainer, document.body.firstChild);

            // 模式3: 設定影片置中並最大化 / Mode 3: Set video to center and maximize
            // 模式4: 設定影片置中並最大化 / Mode 4: Set video to center and maximize
            video.style.position = '';
            video.style.top = '';
            video.style.left = '';
            video.style.width = CONFIG.youtubeFullscreenMode === 3 ? '100%' : '100%';
            video.style.height = CONFIG.youtubeFullscreenMode === 3 ? '100%' : 'auto';
            video.style.maxWidth = CONFIG.youtubeFullscreenMode === 3 ? 'none' : 'none';
            video.style.maxHeight = CONFIG.youtubeFullscreenMode === 3 ? 'none' : '100vh';
            video.style.zIndex = '';
            video.style.objectFit = 'contain'; // 保持比例並填滿容器 (模式3) 或適應容器 (模式4) / Maintain aspect ratio and fit within container (Mode 3) or adapt to container (Mode 4)
            video.style.objectPosition = 'center'; // 置中 / Center
            isWebFullscreened = true;
        }
    }

    function toggleNativeFullscreen(video) {
        if (!video) return;
        try {
            if (document.fullscreenElement) {
                document.exitFullscreen();
            } else {
                let elementToFullscreen = video;
                for (let i = 0; i < 2; i++) {
                    elementToFullscreen = elementToFullscreen.parentElement || elementToFullscreen;
                }
                elementToFullscreen.requestFullscreen?.() ||
                elementToFullscreen.webkitRequestFullscreen?.() ||
                elementToFullscreen.msRequestFullscreen?.() ||
                video.requestFullscreen?.() ||
                video.webkitRequestFullscreen?.() ||
                video.msRequestFullscreen?.();
            }
        } catch (e) {
            console.error('Fullscreen error:', e);
        }
    }

    function toggleFullscreen(video) {
        switch(CONFIG.youtubeFullscreenMode) {
            case 1:
                document.querySelector('.ytp-fullscreen-button')?.click();
                break;
            case 2:
                toggleNativeFullscreen(video);
                break;
            case 3:
            case 4: // 模式3和4都使用相同的函數,僅容器定位不同 / Mode 3 and 4 use same function, only container positioning differs
                toggleWebFullscreen(video);
                break;
        }
    }

    // 雙擊處理 / Double-click handling
    function setupVideoEventOverrides(video) {
        if (videoDoubleClickHandler) {
            video.removeEventListener('dblclick', videoDoubleClickHandler);
        }
        videoDoubleClickHandler = (e) => {
            e.preventDefault();
            e.stopPropagation();
            toggleFullscreen(video);
        };
        video.addEventListener('dblclick', videoDoubleClickHandler);
    }

    // 按鍵處理 / Key handling
    function handleKeyEvent(e) {
        if (e.target.matches('input, textarea, select') || e.target.isContentEditable) return;

        const video = document.querySelector('video, ytd-player video');
        if (!video) return;

        // Enter鍵切換全螢幕 / Enter key to toggle fullscreen
        if (e.code === 'Enter' || e.code === 'NumpadEnter') {
            e.preventDefault();
            toggleFullscreen(video);
        }
    }

    // 綁定核心功能 / Bind core functionality
    function bindCoreFeatures() {
        if (isCoreActive) return; // 如果已啟動則不重複綁定 / Don't re-bind if already active

        document.querySelectorAll('video').forEach(video => {
            if (!video.dataset.fullscreenBound) {
                setupVideoEventOverrides(video);
                video.dataset.fullscreenBound = 'true';
            }
        });

        keydownHandler = handleKeyEvent;
        document.addEventListener('keydown', keydownHandler, true);

        // 監聽動態內容 / Listen for dynamic content
        mutationObserver = new MutationObserver(() => {
            document.querySelectorAll('video').forEach(video => {
                if (!video.dataset.fullscreenBound) {
                    setupVideoEventOverrides(video);
                    video.dataset.fullscreenBound = 'true';
                }
            });
        });
        mutationObserver.observe(document.body, { childList: true, subtree: true });

        isCoreActive = true;
    }

    // 釋放核心功能 / Release core functionality
    function unbindCoreFeatures() {
        if (!isCoreActive) return; // 如果未啟動則不需釋放 / Don't release if not active

        document.querySelectorAll('video[data-fullscreen-bound]').forEach(video => {
            if (videoDoubleClickHandler) {
                video.removeEventListener('dblclick', videoDoubleClickHandler);
            }
            delete video.dataset.fullscreenBound;
        });

        if (keydownHandler) {
            document.removeEventListener('keydown', keydownHandler, true);
            keydownHandler = null;
        }

        if (mutationObserver) {
            mutationObserver.disconnect();
            mutationObserver = null;
        }

        // 退出全螢幕狀態 / Exit fullscreen state
        if (isWebFullscreened) {
            // 觸發一次切換以恢復原狀 / Trigger a toggle to restore original state
            const video = document.querySelector('video, ytd-player video');
            if (video) {
                // 手動調用切換函數恢復狀態 / Manually call toggle function to restore state
                if (webFullscreenContainer && webFullscreenContainer.contains(video)) {
                    webFullscreenContainer.removeChild(video);
                }
                if (webFullscreenContainer && document.body.contains(webFullscreenContainer)) {
                    document.body.removeChild(webFullscreenContainer);
                    webFullscreenContainer = null;
                }
                if (originalVideoParent && !originalVideoParent.contains(video)) {
                    originalVideoParent.appendChild(video);
                }
                if (video && originalVideoStyles) Object.assign(video.style, originalVideoStyles);
                if (originalVideoParent && originalParentStyles) Object.assign(originalVideoParent.style, originalParentStyles);
                isWebFullscreened = false;
                originalVideoParent = null;
            }
        }

        isCoreActive = false;
    }

    // 檢查是否為影片播放頁面 / Check if it's a video playback page
    const isVideoPage = () => location.pathname.startsWith('/watch');

    // 初始化 / Initialization
    function init() {
        registerMenuCommands();

        // 初始檢查 / Initial check
        if (isVideoPage()) {
            bindCoreFeatures();
        }

        // 監聽 URL 變化 / Listen for URL changes
        let currentPath = location.pathname;
        const observer = new MutationObserver(() => {
            if (location.pathname !== currentPath) {
                currentPath = location.pathname;
                if (isVideoPage()) {
                    bindCoreFeatures();
                } else {
                    unbindCoreFeatures();
                }
            }
        });
        observer.observe(document, { childList: true, subtree: true });

        // 監聽 popstate 事件 (瀏覽器前後按鈕) / Listen for popstate event (browser back/forward buttons)
        window.addEventListener('popstate', () => {
            if (isVideoPage()) {
                bindCoreFeatures();
            } else {
                unbindCoreFeatures();
            }
        });
    }

    init();
})();