// ==UserScript== // @name Maximizer For YouTube™ // @description Maximizes the YouTube player to fill the entire browser viewport when in theater mode, plus a few other enhancements. // @license MIT // @author Rotem Dan // @match https://www.youtube.com/* // @version 0.2.4 // @run-at document-start // @grant none // @namespace https://github.com/rotemdan // @homepageURL https://github.com/rotemdan/MaximizerForYouTube // @require https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js // @require https://code.jquery.com/jquery-3.3.1.slim.min.js // @downloadURL none // ==/UserScript== //////////////////////////////////////////////////////////////////////////////////////////////////////// // Utility definitions //////////////////////////////////////////////////////////////////////////////////////////////////////// const debugModeEnabled = true; function log(...args) { if (debugModeEnabled) { console.log("[MaximizerForYoutube]", ...args) } } // Try to emulate setImmediate() like execution: function setImmediate(func) { const channel = new MessageChannel(); channel.port1.onmessage = () => func(); channel.port2.postMessage(""); } //////////////////////////////////////////////////////////////////////////////////////////////////////// // Core script modification functions //////////////////////////////////////////////////////////////////////////////////////////////////////// // Install or uninstall full-size player page stylesheet if needed function installOrUninstallPlayerModIfNeeded() { if (inWatchPage() && theaterModeEnabled()) { if ($("#MaximizerForYouTube_PlayerMod").length == 0) { const styleSheet = ` `; $("head").append(styleSheet); log("Player mod installed"); } if (pageScrolledToTop()) hideTopBar(); } else { if ($("#MaximizerForYouTube_PlayerMod").length > 0) { $("#MaximizerForYouTube_PlayerMod").remove(); showTopBar(); log("Player mod uninstalled"); } } } // Automatically shows/hides the top bar based on different properties of the view. function installTopBarAutohide() { function onPageScroll() { if (!inWatchPage()) return; if (pageScrolledToTop() && theaterModeEnabled()) hideTopBar(); else showTopBar(); } function onKeyDown(e) { if (!inWatchPage() || !theaterModeEnabled()) return; if (e.which === 27) { // Handle escape key log("esc"); if (pageScrolledToTop()) { if (topBarIsVisible()) { hideTopBar(); e.stopPropagation(); } else { showTopBar(); setTimeout(() => $("input#search").focus(), 50); } } } } function installEscHandlerToSearchInput() { let inputElement = $("input#search"); if (inputElement.length > 0) { inputElement.on("keydown", onKeyDown); //inputElement[0].addEventListener("keydown", onKeyDown, true); log("Esc handler installed on search input"); } else { setTimeout(() => installEscHandlerToSearchInput(), 50); } } installEscHandlerToSearchInput(); $(document).on("keydown", onKeyDown); $(document).on("scroll", onPageScroll); } // Continuously auto-focus the player keyboard input when some conditions are met. function installPlayerInputAutoFocus() { function autoFocusIfNeeded() { if (inWatchPage() && !topBarIsVisible()) { getVideoContainer().focus(); } setTimeout(autoFocusIfNeeded, 20); } autoFocusIfNeeded(); } function installPlayerKeyboardShortcutExtensions() { // Install keyboard shortcut extensions function onKeyDown(e) { if (!inWatchPage()) return; if (getVideoContainer().is(":focus")) { if (e.ctrlKey) { if (e.which === 37) { // Handle ctl + left key var previousButton = $("a.ytp-prev-button")[0]; if (previousButton) previousButton.click(); } else if (e.which === 39) { // Handle ctl + right key var nextButton = $("a.ytp-next-button")[0] if (nextButton) nextButton.click(); } } } } $(document).on("keydown", onKeyDown); } // Expands video description function ensureExpandedVideoDescription() { setInterval(() => { $("ytd-expander.description, ytd-expander.ytd-video-secondary-info-renderer").removeAttr("collapsed") }, 50); } // Expands video description function ensureModdedTheaterModeButton() { setInterval(() => { const playerModeButton = $("button.ytp-size-button"); if (playerModeButton.length === 0 || playerModeButton.hasClass("MaximizerForYouTube_PlayerMod_Modded")) return; playerModeButton.on("click", () => { setTimeout(() => { installOrUninstallPlayerModIfNeeded(); const resizeEvent = new Event('resize'); window.dispatchEvent(resizeEvent); }, 0) }) playerModeButton.addClass("MaximizerForYouTube_PlayerMod_Modded"); }, 50); } function ensurePlayerIsAlwaysPaused() { setInterval(() => { const player = getVideoPlayer().get(0); if (player) { player.pause(); } }, 1000); } function hideSPFLoadingBar() { $("head").append(""); } // Pauses playing videos in other tabs when a video play event is detected (works in both watch and channel page videos) function ensurePlayerAutoPause() { const videoPlayer = getVideoPlayer(); if (videoPlayer.length > 0 && !videoPlayer.hasClass("MaximizerForYouTube_Modded_Autopause") && !inHomePage() && !inFeedPage() && !inSearchPage()) { // Generate a random script instance ID const instanceID = Math.random().toString(); function onVideoPlay() { log("onVideoPlay") localStorage["MaximizerForYouTube_PlayingInstanceID"] = instanceID; function pauseWhenAnotherPlayerStartsPlaying() { if (localStorage["MaximizerForYouTube_PlayingInstanceID"] !== instanceID) videoPlayer[0].pause(); else setTimeout(pauseWhenAnotherPlayerStartsPlaying, 20); } pauseWhenAnotherPlayerStartsPlaying(); } // If video isn't paused on startup, fire the handler immediately if (!videoPlayer[0].paused) onVideoPlay(); // Add event handler for the "play" event. videoPlayer.on("play", onVideoPlay); // Mark the player as modded to ensure the autopause mod isn't installed again videoPlayer.addClass("MaximizerForYouTube_Modded_Autopause"); } setTimeout(ensurePlayerAutoPause, 50); } //////////////////////////////////////////////////////////////////////////////////////////////////////// // Utility functions //////////////////////////////////////////////////////////////////////////////////////////////////////// // Get the video player element function getVideoContainer() { // Note: the channel page has another hidden video except the main one (if it exists). The hidden video doesn't have an "src" attribute. return $("div.html5-video-player"); } // Get the video player element function getVideoPlayer() { // Note: the channel page has another hidden video except the main one (if it exists). The hidden video doesn't have an "src" attribute. return $('.html5-main-video').filter(function (index) { return $(this).attr("src") !== undefined }); } // Get the top bar element function getTopBar() { return $("#masthead-container"); } function showTopBar() { getTopBar().css("visibility", "visible"); getTopBar().css("opacity", "1"); } function hideTopBar() { getTopBar().css("opacity", "0"); getTopBar().css("visibility", "hidden"); } function pageScrolledToTop() { return $(document).scrollTop() === 0; } function scrollPageToTopIfNeeded() { setTimeout(() => { if (inWatchPage() && $(document).scrollTop() > 0) { log("Scrolling page to top"); $(document).scrollTop(0); } }, 20); } function topBarIsVisible() { return getTopBar().css("visibility") === "visible"; } function inWatchPage() { return location.href.indexOf("https://www.youtube.com/watch?") === 0; } function inSearchPage() { return location.href.indexOf("https://www.youtube.com/results?") === 0; } function inFeedPage() { return location.href.indexOf("https://www.youtube.com/feed") === 0; } function inHomePage() { return location.href == "https://www.youtube.com"; } function theaterModeEnabled() { return Cookies.get("wide") === "1"; } //////////////////////////////////////////////////////////////////////////////////////////////////////// // Event handlers //////////////////////////////////////////////////////////////////////////////////////////////////////// let installInterval; function onDocumentStart() { log("onDocumentStart") log("Script loaded, theater mode enabled:", theaterModeEnabled()); //installOrUninstallPlayerModIfNeeded(); installInterval = setInterval(() => { installOrUninstallPlayerModIfNeeded(); //log("Trying to install mod") }, 1); } function onDocumentEnd() { log("onDocumentEnd"); clearInterval(installInterval) installOrUninstallPlayerModIfNeeded(); //ensurePlayerIsAlwaysPaused(); hideSPFLoadingBar(); installTopBarAutohide(); installPlayerInputAutoFocus(); installPlayerKeyboardShortcutExtensions(); //ensureExpandedVideoDescription(); } function onWindowLoad() { log("onWindowLoad"); ensureModdedTheaterModeButton(); ensurePlayerAutoPause(); } function onNavigation() { log("onNavigation, new location:", location.href); //scrollPageToTopIfNeeded(); installOrUninstallPlayerModIfNeeded(); } //////////////////////////////////////////////////////////////////////////////////////////////////////// // Install event handlers and start script //////////////////////////////////////////////////////////////////////////////////////////////////////// function startScript() { document.addEventListener('DOMContentLoaded', onDocumentEnd, false); $(window).on("load", onWindowLoad); $(window).on("yt-navigate-start", () => { log("yt-navigate-start"), onNavigation() }); $(window).on("popstate", () => { log("popstate"); onNavigation() }); onDocumentStart(); } if (window.self === window.top) { startScript(); }