// ==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.9 // @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.7.1.min.js // @downloadURL none // ==/UserScript== //////////////////////////////////////////////////////////////////////////////////////////////////////// // Utility definitions //////////////////////////////////////////////////////////////////////////////////////////////////////// const debugModeEnabled = false 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 const previousButton = $('a.ytp-prev-button')[0] if (previousButton) { previousButton.click() } } else if (e.which === 39) { // Handle ctl + right key const nextButton = $('a.ytp-next-button')[0] if (nextButton) { nextButton.click() } } } } } $(document).on('keydown', onKeyDown) } // Expands video description function ensureExpandedVideoDescription() { setInterval(() => { if (!inWatchPage()) { return } if ($('#ytd-watch-info-text').attr('detailed') == null) { $('#description-inline-expander tp-yt-paper-button#expand').click() } }, 50) } // Expands video description function ensureModdedTheaterModeButton() { setInterval(() => { if (!inWatchPage()) { return } 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 locationPathname() == '/watch' } function inSearchPage() { return locationPathname().startsWith('/results') } function inFeedPage() { return locationPathname().startsWith('/feed') } function inHomePage() { return locationPathname() == '/' } function locationPathname() { return (new URL(location.href)).pathname } 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') }, 10) } 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() }