更佳 YouTube 剧场模式

改进 YouTube 剧场模式,参考 Twitch.tv 的设计,增强视频与聊天室布局,同时保持性能与兼容性。并修复近期 YouTube 更新中损坏的全屏界面。

目前为 2025-01-12 提交的版本。查看 最新版本

// ==UserScript==
// @name                Better Theater Mode for YouTube
// @name:zh-TW          更佳 YouTube 劇場模式
// @name:zh-CN          更佳 YouTube 剧场模式
// @name:ja             より良いYouTubeシアターモード
// @icon                https://www.youtube.com/img/favicon_48.png
// @author              ElectroKnight22
// @namespace           electroknight22_youtube_better_theater_mode_namespace
// @version             1.5.7
// @match               *://www.youtube.com/*
// @match               *://www.youtube-nocookie.com/*
// @grant               GM.addStyle
// @grant               GM.getValue
// @grant               GM.setValue
// @grant               GM.deleteValue
// @grant               GM.listValues
// @grant               GM.registerMenuCommand
// @grant               GM.unregisterMenuCommand
// @grant               GM_addStyle
// @grant               GM_getValue
// @grant               GM_setValue
// @grant               GM_deleteValue
// @grant               GM_listValues
// @grant               GM_registerMenuCommand
// @grant               GM_unregisterMenuCommand
// @license             MIT
// @description         Improves YouTube's theater mode with a Twitch.tv-like design, enhancing video and chat layouts, while maintaining performance and compatibility. Also fixes the broken fullscreen UI from the recent YouTube update.
// @description:zh-TW   改善 YouTube 劇場模式,參考 Twitch.tv 的設計,增強影片與聊天室佈局,同時維持效能與相容性。並修復近期 YouTube 更新中損壞的全螢幕介面。
// @description:zh-CN   改进 YouTube 剧场模式,参考 Twitch.tv 的设计,增强视频与聊天室布局,同时保持性能与兼容性。并修复近期 YouTube 更新中损坏的全屏界面。
// @description:ja      YouTubeのシアターモードを改善し、Twitch.tvのデザインを参考にして、動画とチャットのレイアウトを強化しつつ、パフォーマンスと互換性を維持します。また、最近のYouTubeアップデートによる壊れたフルスクリーンUIを修正します。
// ==/UserScript==

/*jshint esversion: 11 */

(function () {
    'use strict';

    // Default settings for the script
    const DEFAULT_SETTINGS = {
        isScriptActive: true,
        enableOnlyForLiveStreams: false,
        modifyVideoPlayer: true,
        modifyChat: true,
        blacklist: new Set()
    };

    let userSettings = { ...DEFAULT_SETTINGS };
    let useCompatibilityMode = false;

    let menuItems = new Set();
    let activeStyles = new Map();
    let hidChatTemporarily = false;

    let moviePlayer;
    let videoId;
    let chatFrame;
    let isFullscreen = false;
    let isTheaterMode = false;
    let chatCollapsed = true;
    let isLiveStream = false;
    let chatWidth = 0;

    // Greasemonkey API Compatibility Layer
    const GMCustomAddStyle = useCompatibilityMode ? GM_addStyle : GM.addStyle;
    const GMCustomRegisterMenuCommand = useCompatibilityMode ? GM_registerMenuCommand : GM.registerMenuCommand;
    const GMCustomUnregisterMenuCommand = useCompatibilityMode ? GM_unregisterMenuCommand : GM.unregisterMenuCommand;
    const GMCustomGetValue = useCompatibilityMode ? GM_getValue : GM.getValue;
    const GMCustomSetValue = useCompatibilityMode ? GM_setValue : GM.setValue;
    const GMCustomListValues = useCompatibilityMode ? GM_listValues : GM.listValues;
    const GMCustomDeleteValue = useCompatibilityMode ? GM_deleteValue : GM.deleteValue;

    // Style Rules
    const styleRules = {
        chatStyle: {
            id: "chatStyle",
            getRule: () => `
                ytd-live-chat-frame[theater-watch-while][rounded-container] {
                    border-radius: 0 !important;
                }
                ytd-watch-flexy[fixed-panels] #chat.ytd-watch-flexy {
                    top: 0 !important;
                    border-top: 0 !important;
                    border-bottom: 0 !important;
                }
            `,
        },
        videoPlayerStyle: {
            id: "videoPlayerStyle",
            getRule: () => `
                ytd-watch-flexy[full-bleed-player] #full-bleed-container.ytd-watch-flexy {
                    max-height: calc(100vh - var(--ytd-watch-flexy-masthead-height)) !important;
                }
            `,
        },
        headmastStyle: {
            id: "headmastStyle",
            getRule: () => `
                #masthead-container.ytd-app {
                    max-width: calc(100% - ${chatWidth}px) !important;
                }
            `,
        },
    }

    // Apply static styles for specific YouTube UI fixes
    function applyStaticVideoPlayerFixStyles() {
        GMCustomAddStyle(`
            .html5-video-container {
                top: -1px !important;
            }
        `);
    }

    function applyStaticChatFrameFixStyles() {
        GMCustomAddStyle(`
            ytd-live-chat-frame[theater-watch-while][rounded-container] {
                border-top: 0 !important;
                border-bottom: 0 !important;
            }
            #panel-pages.yt-live-chat-renderer {
                border-bottom: 0 !important;
            }
        `);

        const panelPages = document.querySelector('iron-pages#panel-pages');
        if (panelPages.offsetHeight <= 3) {
            GMCustomAddStyle(`
                #panel-pages.yt-live-chat-renderer{
                    border-top: 0 !important;
                }
            `);
        }
    }

    // Fix fullscreen chat issues by toggling chat renderer
    function toggleChatRendererToTemporarilyFixFullscreenIssues() {
        if (isFullscreen) {
            if (chat && !chat.collapsed) {
                chat.getElementsByTagName('button')[0].click();
                hidChatTemporarily = true;
            }
        } else if (hidChatTemporarily) {
            chat.getElementsByTagName('button')[0].click();
            hidChatTemporarily = false;
        }
    }

    // Apply and remove styles dynamically based on settings
    function removeStyle(style) {
        if (!activeStyles.has(style.id)) return;
        const styleElement = activeStyles.get(style.id);
        if (styleElement && styleElement.parentNode) {
            styleElement.parentNode.removeChild(styleElement);
        }
        activeStyles.delete(style.id);
    }

    function removeAllStyles() {
        activeStyles.forEach((styleElement, styleId) => {
            if (styleElement && styleElement.parentNode) {
                styleElement.parentNode.removeChild(styleElement);
            }
        });
        activeStyles.clear();
    }

    function applyStyle(style) {
        if (activeStyles.has(style.id)) return;
        if (typeof style.getRule !== 'function') return;
        const styleElement = GMCustomAddStyle(style.getRule());
        activeStyles.set(style.id, styleElement);
    }

    // Update styles dynamically based on settings and current state
    function updateStyles() {
        const shouldNotActivate =
            !userSettings.isScriptActive ||
            userSettings.blacklist.has(videoId) ||
            (userSettings.enableOnlyForLiveStreams && !isLiveStream);

        if (shouldNotActivate) {
            removeAllStyles();
            if (moviePlayer) moviePlayer.setCenterCrop(); //trigger size update for the html5 video element
            return;
        }

        if (userSettings.modifyChat) {
            applyStyle(styleRules.chatStyle);

            const mastHeadContainer = document.querySelector('#masthead-container');
            let chatFramePositionValid =
                mastHeadContainer.getBoundingClientRect().bottom < 0 ||
                chatFrame?.getBoundingClientRect().top <= mastHeadContainer.getBoundingClientRect().bottom;
            let shouldShrinkHeadmast = isTheaterMode && !chatCollapsed && chatFramePositionValid;
            if (shouldShrinkHeadmast) {
                chatWidth = chatFrame?.offsetWidth || 0;
                applyStyle(styleRules.headmastStyle);
            } else {
                removeStyle(styleRules.headmastStyle);
            }
        } else {
            [styleRules.chatStyle, styleRules.headmastStyle].forEach(removeStyle);
        }

        if (userSettings.modifyVideoPlayer) {
            applyStyle(styleRules.videoPlayerStyle);
        } else {
            removeStyle(styleRules.videoPlayerStyle);
        }
        if (moviePlayer) moviePlayer.setCenterCrop(); //trigger size update for the html5 video element
    }

    function updateTheaterStatus(event) {
        isTheaterMode = !!event?.detail?.enabled;
        updateStyles();
    }

    function updateChatStatus(event) {
        chatFrame = event.target;
        chatCollapsed = event.detail !== false;
        window.addEventListener('player-api-ready', () => { updateStyles(); }, { once: true });
    }

    function updateFullscreenStatus() {
        isFullscreen = document.fullscreenElement;
        toggleChatRendererToTemporarilyFixFullscreenIssues(); // To fix fullscreen issues this needs to alway run
    }

    function updateVideoStatus(event) {
        try {
            videoId = event.detail.pageData.playerResponse.videoDetails.videoId;
            moviePlayer = document.querySelector('#movie_player');
            isLiveStream = event.detail.pageData.playerResponse.videoDetails.isLiveContent;
            showMenuOptions();
        } catch (error) {
            throw ("Failed to update video status due to this error. Error: " + error);
        }
    }

    // Menu management for user interaction
    function processMenuOptions(options, callback) {
        Object.values(options).forEach(option => {
            if (!option.alwaysShow && !userSettings.expandMenu) return;
            if (option.items) {
                option.items.forEach(item => callback(item));
            } else {
                callback(option);
            }
        });
    }
    function removeMenuOptions() {
        menuItems.forEach((menuItem) => {
            GMCustomUnregisterMenuCommand(menuItem);
        });
        menuItems.clear();
    }
    function showMenuOptions() {
        removeMenuOptions();
        const menuOptions = {
            toggleScript: {
                alwaysShow: true,
                label: () => `🔄 ${userSettings.isScriptActive ? "Turn Off" : "Turn On"}`,
                menuId: "toggleScript",
                handleClick: function () {
                    userSettings.isScriptActive = !userSettings.isScriptActive;
                    GMCustomSetValue('isScriptActive', userSettings.isScriptActive);
                    updateStyles();
                    showMenuOptions();
                },
            },
            toggleOnlyLiveStreamMode: {
                alwaysShow: true,
                label: () => `${userSettings.enableOnlyForLiveStreams ? "✅" : "❌"} Livestream Only Mode`,
                menuId: "toggleOnlyLiveStreamMode",
                handleClick: function () {
                    userSettings.enableOnlyForLiveStreams = !userSettings.enableOnlyForLiveStreams;
                    GMCustomSetValue('enableOnlyForLiveStreams', userSettings.enableOnlyForLiveStreams);
                    updateStyles();
                    showMenuOptions();
                },
            },
            toggleChatStyle: {
                alwaysShow: true,
                label: () => `${userSettings.modifyChat ? "✅" : "❌"} Apply Chat Styles`,
                menuId: "toggleChatStyle",
                handleClick: function () {
                    userSettings.modifyChat = !userSettings.modifyChat;
                    GMCustomSetValue('modifyChat', userSettings.modifyChat);
                    updateStyles();
                    showMenuOptions();
                },
            },
            toggleVideoPlayerStyle: {
                alwaysShow: true,
                label: () => `${userSettings.modifyVideoPlayer ? "✅" : "❌"} Apply Video Player Styles`,
                menuId: "toggleVideoPlayerStyle",
                handleClick: function () {
                    userSettings.modifyVideoPlayer = !userSettings.modifyVideoPlayer;
                    GMCustomSetValue('modifyVideoPlayer', userSettings.modifyVideoPlayer);
                    updateStyles();
                    showMenuOptions();
                },
            },
            addVideoToBlacklist: {
                alwaysShow: true,
                label: () => `${userSettings.blacklist.has(videoId) ? "Unblacklist Video " : "Blacklist Video"} [id: ${videoId}]`,
                menuId: "addVideoToBlacklist",
                handleClick: function () {
                    if (userSettings.blacklist.has(videoId)) {
                        userSettings.blacklist.delete(videoId);
                    } else {
                        userSettings.blacklist.add(videoId);
                    }
                    GMCustomSetValue('blacklist', [...userSettings.blacklist]);
                    updateStyles();
                    showMenuOptions();
                },
            },
        };

        processMenuOptions(menuOptions, (item) => {
            GMCustomRegisterMenuCommand(item.label(), item.handleClick, {
                id: item.menuId,
                autoClose: false,
            });
            menuItems.add(item.menuId);
        });
    }

    // User Setting Handling
    async function loadUserSettings() {
        try {
            const storedValues = await GMCustomListValues();

            for (const [key, value] of Object.entries(DEFAULT_SETTINGS)) {
                if (!storedValues.includes(key)) {
                    await GMCustomSetValue(key, value instanceof Set ? Array.from(value) : value);
                }
            }

            for (const key of storedValues) {
                if (!(key in DEFAULT_SETTINGS)) {
                    await GMCustomDeleteValue(key);
                }
            }

            const keyValuePairs = await Promise.all(
                storedValues.map(async key => [key, await GMCustomGetValue(key)])
            );

            keyValuePairs.forEach(([newKey, newValue]) => {
                userSettings[newKey] = newValue;
            });

            // Convert blacklist to Set if it exists
            if (userSettings.blacklist) {
                userSettings.blacklist = new Set(userSettings.blacklist);
            }

            console.log(`Loaded user settings: ${JSON.stringify(userSettings)}`);
        } catch (error) {
            throw `Error loading user settings: ${error}. Aborting script.`;
        }
    }

    // Check compatibility with Greasemonkey API
    function hasGreasyMonkeyAPI() {
        if (typeof GM != 'undefined') return true;
        if (typeof GM_info != 'undefined') {
            useCompatibilityMode = true;
            console.warn("Running in compatibility mode.");
            return true;
        }
        return false;
    }

    // Attach necessary event listeners
    function attachEventListeners() {
        window.addEventListener('yt-set-theater-mode-enabled', (event) => { updateTheaterStatus(event); }, true);
        window.addEventListener('yt-chat-collapsed-changed', (event) => { updateChatStatus(event); }, true);
        window.addEventListener('yt-page-data-fetched', (event) => {
            updateVideoStatus(event);
        }, true);
        window.addEventListener('yt-page-data-updated', updateStyles, true);
        window.addEventListener("fullscreenchange", updateFullscreenStatus, true);
    }

    // Check if the script is running inside a live chat iframe
    function isLiveChatIFrame() {
        const liveChatIFramePattern = /^https?:\/\/.*youtube\.com\/live_chat.*$/;
        const currentUrl = window.location.href;
        return liveChatIFramePattern.test(currentUrl);
    }

    // Initialize the script
    async function initialize() {
        try {
            if (!hasGreasyMonkeyAPI()) throw "Did not detect valid Grease Monkey API";
            if (isLiveChatIFrame()) return applyStaticChatFrameFixStyles(); // Fixes the terrible css of the live chat iframe.
            applyStaticVideoPlayerFixStyles(); // Fixes video player end screen style rounding issues during certain zoom levels.
            await loadUserSettings();
            updateStyles();
            attachEventListeners();
            showMenuOptions();
        } catch (error) {
            return console.error(`Error when initializing script: ${error}. Aborting script.`);
        }
    }

    // Entry Point
    initialize();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址