Youtube Fullpage Theater

Make theater mode fill the entire page view with hidden navbar

目前为 2024-01-31 提交的版本。查看 最新版本

// ==UserScript==
// @name         Youtube Fullpage Theater
// @version      0.7.6
// @description  Make theater mode fill the entire page view with hidden navbar
// @run-at       document-body
// @match        https://www.youtube.com/*
// @exclude      https://*.youtube.com/live_chat*
// @exclude      https://*.youtube.com/embed*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        GM.getValue
// @grant        GM.setValue
// @author       Fznhq
// @namespace    https://github.com/fznhq
// @homepageURL  https://github.com/fznhq/userscript-collection
// @license      GNU GPLv3
// ==/UserScript==

(async function () {
    "use strict";

    const config = {
        // Open theater, every time video changed
        auto_theater_mode: undefined,
        hide_scrollbar: undefined,
        // if value is false, it will
        // get replace by focus search on esc
        close_theater_with_esc: undefined,
    };

    /**
     * Don't change `fallback` below, change `config` above.
     * It's just a default value
     * when config not yet applied (undefined)
     */
    const fallbackConfig = {
        auto_theater_mode: false,
        hide_scrollbar: true,
        close_theater_with_esc: true,
    };

    for (const name in config) {
        if (GM.getValue && GM.setValue) {
            if (config[name] !== undefined) {
                GM.setValue(name, config[name]);
            } else {
                config[name] = await GM.getValue(name, fallbackConfig[name]);
            }
        } else if (config[name] === undefined) {
            config[name] = fallbackConfig[name];
        }
    }

    /**
     * @param {string} query
     * @returns {() => HTMLElement | null}
     */
    function $(query) {
        return () => document.querySelector(query);
    }

    /**
     * @param {string} css
     */
    function addStyle(css) {
        const style = document.createElement("style");
        style.textContent = css;
        document.head.appendChild(style);
    }

    if (config.hide_scrollbar) {
        addStyle(/*css*/ `
            html[theater],
            html[theater] body {
                scrollbar-width: none !important;
            }

            html[theater]::-webkit-scrollbar,
            html[theater] body::-webkit-scrollbar {
                display: none !important;
            }
        `);
    }

    addStyle(/*css*/ `
        html[theater] #movie_player .ytp-paid-content-overlay,
        html[theater] #movie_player .iv-branding,
        html[theater] #movie_player .ytp-ce-element,
        html[theater] #movie_player .ytp-chrome-top,
        html[theater] #movie_player .ytp-suggested-action {
            display: none !important;
        }

        html[theater][masthead-hidden] #masthead-container {
            transform: translateY(-100%) !important;
        }

        html[theater] #page-manager {
            margin: 0 !important;
        }

        html[theater] #full-bleed-container {
            height: 100vh !important;
            max-height: none !important;
        }
    `);

    /** @type {Window} */
    const win = unsafeWindow;
    const html = document.documentElement;
    const main = $("ytd-watch-flexy");
    const inputSearch = $("input#search");
    const attr = {
        video_id: "video-id",
        role: "role",
        theater: "theater",
        fullscreen: "fullscreen",
        hidden_header: "masthead-hidden",
    };

    const keyToggleTheater = new KeyboardEvent("keydown", {
        key: "t",
        code: "KeyT",
        which: 84,
        keyCode: 84,
        bubbles: true,
        cancelable: true,
        view: win,
    });

    /**
     * @param {MutationCallback} callback
     * @param {Node} target
     * @param {MutationObserverInit | undefined} options
     * @returns
     */
    function observer(callback, target, options) {
        const mutation = new MutationObserver(callback);
        mutation.observe(target, options || { subtree: true, childList: true });
        return mutation;
    }

    function isTheater() {
        return (
            main().getAttribute(attr.role) == "main" &&
            !main().hasAttribute(attr.fullscreen) &&
            main().hasAttribute(attr.theater)
        );
    }

    function toggleHeader() {
        if (document.activeElement != inputSearch()) {
            html.toggleAttribute(attr.hidden_header, !win.scrollY);
        }
    }

    function toggleTheater() {
        document.dispatchEvent(keyToggleTheater);
    }

    /**
     * @param {KeyboardEvent} event
     */
    function onEscapePress(event) {
        if (event.key != "Escape") return;

        if (config.close_theater_with_esc) {
            toggleTheater();
        } else {
            const input = inputSearch();

            if (document.activeElement != input) {
                html.removeAttribute(attr.hidden_header);
                setTimeout(() => input.focus(), 1);
            } else if (!win.scrollY) {
                html.setAttribute(attr.hidden_header, "");
                input.blur();
            }
        }
    }

    function openTheater() {
        setTimeout(() => {
            if (
                !main().hasAttribute(attr.theater) &&
                !main().hasAttribute(attr.fullscreen)
            ) {
                toggleTheater();
            }
        }, 1);
    }

    function watchTheaterMode() {
        const state = isTheater();
        const input = inputSearch();

        if (state && !html.hasAttribute(attr.theater)) {
            html.setAttribute(attr.theater, "");
            html.setAttribute(attr.hidden_header, "");

            input.addEventListener("blur", toggleHeader);
            win.addEventListener("scroll", toggleHeader);
            win.addEventListener("keydown", onEscapePress, true);
        } else if (!state && html.hasAttribute(attr.theater)) {
            html.removeAttribute(attr.theater);
            html.removeAttribute(attr.hidden_header);

            input.removeEventListener("blur", toggleHeader);
            win.removeEventListener("scroll", toggleHeader);
            win.removeEventListener("keydown", onEscapePress, true);
        }
    }

    observer((_, observe) => {
        if (!main()) return;

        if (config.auto_theater_mode) {
            observer(openTheater, main(), {
                attributeFilter: [attr.video_id, attr.role],
            });
        }

        observer(watchTheaterMode, main(), { attributes: true });
        observe.disconnect();
    }, document.body);
})();

QingJ © 2025

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