Greasy Fork

更佳的 YouTube 剧场模式

此脚本将 YouTube 播放器调整为延伸至屏幕底部,提供类似 Twitch.tv 的沉浸式观看体验,减少干扰。

当前为 2025-01-02 提交的版本,查看 最新版本

// ==UserScript==
// @name                Better YouTube Theater Mode
// @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.3
// @match               *://www.youtube.com/*
// @match               *://www.youtube-nocookie.com/*
// @grant               GM_addStyle
// @grant               GM.addStyle
// @license             MIT
// @description         This script adjusts YouTube's player to extend to the bottom of the screen, creating a Twitch.tv-like viewing experience with fewer distractions.
// @description:zh-TW   此腳本會將 YouTube 播放器調整為延伸至螢幕底部,提供類似 Twitch.tv 的沉浸式觀看體驗,減少干擾。
// @description:zh-CN   此脚本将 YouTube 播放器调整为延伸至屏幕底部,提供类似 Twitch.tv 的沉浸式观看体验,减少干扰。
// @description:ja      このスクリプトは、YouTubeのプレーヤーを画面の下部まで拡張し、Twitch.tvのようなより没入感のある視聴体験を提供します。
// ==/UserScript==

/*jshint esversion: 11 */

(function () {
    'use strict';

    const GMCustomAddStyle = typeof GM_info !== 'undefined' ? GM_addStyle : GM.addStyle;

    let mastHeadContainer = null;
    let pageManager = null;
    let chatFrame = null;
    let isTheaterMode = false;
    let chatVisible = false;
    let resizeTimeout = null;

    const RETRY_COUNT = 5; // Number of retries
    const RETRY_DELAY = 500; // Delay between retries in milliseconds
    const RESIZE_DELAY = 200; // Delay for debounce after iron-resize

    function updateTheaterStatus() {
        if (pageManager?.getState) {
            isTheaterMode = pageManager.getState().watch.isTheaterMode;
        }
    }

    function updateChatStatus() {
        if (pageManager?.getState) {
            chatVisible = !pageManager.getState().watch.isLiveChatCollapsed || false;
        }
    }

    function updateStyle() {

        console.log('Theater Mode:', isTheaterMode);
        console.log('Chat Visible:', chatVisible);

        if (!mastHeadContainer) {
            mastHeadContainer = document.querySelector('#masthead-container');
        }

        if (mastHeadContainer) {
            if (isTheaterMode && chatVisible && chatFrame && !pageManager.getState().watch.isWatchFullscreen && chatFrame.getAttribute('theater-watch-while') === '') {
                const chatWidth = chatFrame.offsetWidth || 0;
                console.log('chatWidth', chatWidth);
                mastHeadContainer.style.maxWidth = `calc(100% - ${chatWidth}px)`;
            } else {
                mastHeadContainer.style.maxWidth = '100%';
            }
        }
    }

    function debounceStyleUpdate() {
        clearTimeout(resizeTimeout);
        resizeTimeout = setTimeout(() => {
            updateStyle();
        }, RESIZE_DELAY);
    }

    function ensurePlayerStatus() {
        ensureChatFrameWithRetry(RETRY_COUNT);
        updateChatStatus();
        updateTheaterStatus();
    }

    function ensureChatFrameWithRetry(retries) {
        if (chatFrame) {
            return; // Exit if chatFrame is already defined
        }

        chatFrame = document.querySelector('ytd-live-chat-frame');

        if (!chatFrame && retries > 0) {
            console.log(`Chat frame not found, retrying... (${RETRY_COUNT - retries + 1})`);
            setTimeout(() => ensureChatFrameWithRetry(retries - 1), RETRY_DELAY);
        } else if (!chatFrame) {
            console.log('Chat frame not found after maximum retries.');
        } else {
            console.log('Chat frame found!');
            updateChatStatus();
            updateStyle();
        }
    }

    function theaterToggled() {
        updateTheaterStatus();
        updateStyle();
    }

    function chatToggled() {
        updateChatStatus();
        updateStyle();
    }

    function waitForPageManager() {
        const observer = new MutationObserver(() => {
            pageManager = document.getElementsByClassName("ytd-page-manager")[0];
            if (pageManager) {
                observer.disconnect();
                ensurePlayerStatus();
                updateStyle();
                attachEventListeners();
            }
        });

        observer.observe(document, { childList: true, subtree: true });
    }

    function attachEventListeners() {
        window.addEventListener('yt-set-theater-mode-enabled', theaterToggled, true);
        window.addEventListener('yt-chat-collapsed-changed', chatToggled, true);
        window.addEventListener('yt-navigate-finish', ensurePlayerStatus, true);
        window.addEventListener('iron-resize', debounceStyleUpdate, true);
    }

    function init() {
        GMCustomAddStyle(`
            ytd-watch-flexy[full-bleed-player] #full-bleed-container.ytd-watch-flexy {
                max-height: calc(100vh - var(--ytd-watch-flexy-masthead-height)) !important;
            }

            ytd-live-chat-frame[theater-watch-while][rounded-container] {
                border-radius: 0 !important;
                border-top: 0px !important;
                border-bottom: 0px !important;
            }

            #panel-pages.yt-live-chat-renderer {
                border-top: 0px !important;
                border-bottom: 0px !important;
            }

            ytd-watch-flexy[fixed-panels] #chat.ytd-watch-flexy {
                top: 0 !important;
            }
        `);

        waitForPageManager();
    }

    init();
})();