Maximizer For YouTube™

Maximizes the YouTube player to fill the entire browser viewport when in theater mode, plus a few other enhancements.

// ==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 <rotemdan@gmail.com>
// @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
// ==/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 = `
					<style id='MaximizerForYouTube_PlayerMod' type='text/css'>
						ytd-page-manager { margin-top: 0px !important; }

						#masthead-container { visibility: hidden; opacity: 0; transition: opacity 0.2s ease-in-out; }

						#full-bleed-container { height: 100vh !important; min-height: 0vh !important; max-height: 100vh !important; }

						:focus { outline: 0; }

						#movie_player { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; }

						/*
						body {
							scrollbar-width: 10px;
							scrollbar-color: #000000;
						}
						*/

						body::-webkit-scrollbar {
							width: 10px;
							background-color: #000000;
						}

						body::-webkit-scrollbar-track {
							border-radius: 10px;
							background: rgba(0,0,0,0.1);
							border: 1px solid #383838;
						}

						body::-webkit-scrollbar-thumb {
							border-radius: 10px;
							background: linear-gradient(left, #fff, #e4e4e4);
							border: 1px solid #5b5b5b;
						}

						body::-webkit-scrollbar-thumb:hover {
							background: #fff;
						}
					</style>`

			$('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('<style>#progress, yt-page-navigation-progress {display: none !important}</style>')
}

// 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()
}