Better YouTube Theater Mode

This script adjusts YouTube's player to extend to the bottom of the screen, creating a Twitch.tv-like viewing experience with fewer distractions.

La data de 07-01-2025. Vezi ultima versiune.

// ==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.5
// @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 chatFrame = null;
    let isTheaterMode = false;
    let chatCollapsed = true;

    const RETRY_COUNT = 5;
    const RETRY_DELAY = 100;

    const retryOperation = (operation, retries, delay) => {
        return new Promise((resolve, reject) => {
            const attempt = (remainingRetries) => {
                try {
                    const result = operation();
                    if (result) {
                        console.log(`Operation succeeded: ${operation}`);
                        resolve(result);
                    } else if (remainingRetries > 0) {
                        console.log(`Retrying operation: ${operation}, retries left: ${remainingRetries}`);
                        setTimeout(() => attempt(remainingRetries - 1), delay);
                    } else {
                        reject(new Error('Operation failed after retries'));
                    }
                } catch (error) {
                    console.error(`Error in retryOperation: ${error}`);
                    reject(error);
                }
            };
            attempt(retries);
        });
    };

    function updateStyle() {
        console.log('Updating style...');
        if (!mastHeadContainer) {
            mastHeadContainer = document.querySelector('#masthead-container');
        }

        if (mastHeadContainer) {
            const chatWidth = chatFrame?.offsetWidth || 0;
            mastHeadContainer.style.maxWidth = (chatFrame && isTheaterMode && !chatCollapsed && chatFrame.getBoundingClientRect().top === 0)
                ? `calc(100% - ${chatWidth}px)`
                : '100%';
            console.log('Updated mastHeadContainer maxWidth:', mastHeadContainer.style.maxWidth);
        } else {
            console.warn('mastHeadContainer not found.');
        }
    }

    function findElement(selector, retries = RETRY_COUNT, delay = RETRY_DELAY) {
        return retryOperation(() => document.querySelector(selector), retries, delay)
            .catch(() => console.warn(`Element not found: ${selector}`));
    }

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

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

    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-navigate-finish', (event) => {
            if (event.detail.pageType === 'watch'){
                window.addEventListener('yt-page-data-updated', updateStyle, { once: true } );
            } else {
                updateStyle();
            }
        }, true);
    }

    function applyStaticStyles() {
        console.log('Applying static styles...');
        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],
            #panel-pages.yt-live-chat-renderer {
                border-radius: 0 !important;
                border-top: 0px !important;
                border-bottom: 0px !important;
            }

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

            .html5-video-container {
                top: -1px !important
            }
        `);
        console.log('Static styles applied.');
    }

    function init() {
        applyStaticStyles();
        attachEventListeners();
    }

    init();
})();