Udemy Fix Video Controls

Fix stupid idiot video controls of Udemy not disappearing.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Udemy Fix Video Controls
// @namespace    https://github.com/Equiel-1703
// @version      1.2.1
// @description  Fix stupid idiot video controls of Udemy not disappearing.
// @author       Henrique Rodrigues Barraz
// @license 	 GPL-3.0
// @match        https://www.udemy.com/course/*
// @icon         https://www.udemy.com/staticx/udemy/images/v7/apple-touch-icon.png
// ==/UserScript==

(function() {
    "use strict";

	// Function to add CSS code to the page
    const addCSS = function (cssText) {
		let newStyleSection = document.createElement("style");

		newStyleSection.textContent = cssText;

		document.querySelector("head").appendChild(newStyleSection);
	}

	// New CSS classes
	const animateOpacity = "animate-opacity";
	const animateBottom = "animate-bottom";
	const hideUIClass = "hide-UI";
	const unhideUIClass = "unhide-UI";
	const hideMouse = "hide-mouse";
	const noBottomSpace = "no-bottom-space";

	// New CSS code to be added
	const newCSS = `
	.animate-opacity {
		transition-duration: 0.7s;
		transition-property: opacity;
	}

	.animate-bottom {
		transition-duration: 0.5s;
		transition-property: bottom;
	}

	.hide-UI {
		opacity: 0%;
	}

	.unhide-UI {
		opacity: 100%;
	}

	.hide-mouse {
		cursor: none;
	}

	.no-bottom-space {
		bottom: 0rem;
	}
	`;
	
	// Add this new CSS to the <head> of the page
	addCSS(newCSS);

	// Elements Classes to be found and manipulated
	const controlBarClass = "div.shaka-control-bar--control-bar-container--OfnMI";
	const videoUIElementsClass = "div.user-activity--hide-when-user-inactive--Oc6Cn";
	const videoContainerClass = "div.curriculum-item-view--content--aaJOw";
	const captionsClass = "div.captions-display--captions-container--PqdGQ";
	
	// Flag to check if the video changes are being monitored or not
	let monitoringVideoChanges = false;
	// Global var to store the cursor position
	let cursorPosition = { x: 0, y: 0 };
	
	// Function to track the cursor position
	const trackCursorPosition = () => {
		document.addEventListener("mousemove", (event) => {
			cursorPosition = { x: event.clientX, y: event.clientY };
		});
	};

	// Function to fix the video controls
	const fixUdemyVideoControls = () => {
		// Get the captions div
		const captionsDiv = document.querySelector(captionsClass);
	
		// Check if returned null
		if (captionsDiv == null) {
			console.log("UdemyVideoFix> Fuck, no captions div found.");
		} else {
			captionsDiv.classList.add(animateBottom);
		}

		// Get all video ui (progress bar, title etc)
		const videoUIElements = document.querySelectorAll(videoUIElementsClass + ", " + controlBarClass)
	
		console.log("UdemyVideoFix> Found video UI elements: ", videoUIElements);
	
		// Add animate opacity class for all video UI elements
		videoUIElements.forEach((el) => {
			el.classList.add(animateOpacity);
		});
	
		// Now, I need the video container div
		const videoContainerDiv = document.querySelector(videoContainerClass);
	
		// Check if returned null
		if (videoContainerDiv == null) {
			console.log("UdemyVideoFix> Fuck, something went really wrong. Couldn't find VideoContainer div.");
			return;
		}
		
		// Flag indicating if UI is visible
		let uiVisible = true;
	
		// Function to hide UI elements
		const hideUI = () => {
			if (!uiVisible) {
				return;
			}

			if (captionsDiv) {
				captionsDiv.classList.add(noBottomSpace);
			}
	
			videoUIElements.forEach((vUI) => {
				if (!vUI.classList.contains(hideUIClass)) {
					vUI.classList.add(hideUIClass);
				}
				vUI.classList.remove(unhideUIClass);
			});
	
			videoContainerDiv.classList.add(hideMouse);
	
			uiVisible = false;
		};

		// Function to show UI elements
		const showUI = () => {
			resetMouseTimeout();

			if (uiVisible) {
				return;
			}

			if (captionsDiv) {
				captionsDiv.classList.remove(noBottomSpace);
			}
	
			videoUIElements.forEach((vUI) => {
				if (!vUI.classList.contains(unhideUIClass)) {
					vUI.classList.add(unhideUIClass);
				}
				vUI.classList.remove(hideUIClass);
			});
	
			videoContainerDiv.classList.remove(hideMouse);
	
			uiVisible = true;	
		};

		// Helper function. Returns true if the cursor is inside 'elementRec'.
		const cursorIsInsideElement = (elementRec) => {
			if (cursorPosition.x >= elementRec.left && cursorPosition.x <= elementRec.right &&
				cursorPosition.y >= elementRec.top && cursorPosition.y <= elementRec.bottom) {
				return true;
			}
			return false;
		};

		// Function to hide UI elements if the cursor is not inside any of them
		const conditionalHideUIElements = () => {
			let canHideCursor = true;

			for (const vUI of videoUIElements) {
				let vUIRec = vUI.getBoundingClientRect();

				if (cursorIsInsideElement(vUIRec)) {
					canHideCursor = false;
					break;
				}
			}

			if (canHideCursor) {
				hideUI();
			} else {
				resetMouseTimeout();
			}
		};

		// Timeout settings
		const timeoutDelayMS = 4_000; // 4 sec to controls hide after no mouse movement
		let timeoutID = null;

		const resetMouseTimeout = () => {
			// Clear previous timeout (if any)
			clearTimeout(timeoutID);

			// Set a new timeout
			timeoutID = setTimeout(conditionalHideUIElements, timeoutDelayMS);
		};
	
		// I will add a listener to detect when the mouse hover the video container.
		videoContainerDiv.addEventListener("mouseenter", showUI);
	
		// And a listener to detect when the mouse leaves the div
		videoContainerDiv.addEventListener("mouseleave", hideUI);
	
		// Add listener to show UI when mouse is moved (while hovering the video only)
		videoContainerDiv.addEventListener("mousemove", showUI);

		// By default, we will show the UI once this function is called
		showUI();
	};

	// This function will monitor the body to check when the video elements are loaded, then fix the video controls
	const monitorElementsLoadAndFix = () => {
		const observer = new MutationObserver((_mutationsList, observer) => {
			let videoContainerDiv = document.querySelector(videoContainerClass);
			let controlBarDiv = document.querySelector(controlBarClass);

			// Check if the divs were loaded
			if (videoContainerDiv && controlBarDiv) {
				console.log("UdemyVideoFix> Video container found: ", videoContainerDiv);
				console.log("UdemyVideoFix> Control bar found: ", controlBarDiv);

				observer.disconnect(); // Stop observing once the divs are found
				
				// Fix the video controls
				fixUdemyVideoControls();

				// Start monitoring for next and previous video changes if not already monitoring
				if (!monitoringVideoChanges) {
					monitorNextPreviousVideo();
				}
			}
		});
		
		// Start observing the body to check when the divs are loaded
		observer.observe(document.body, { childList: true, subtree: true });
	};

	// This function will monitor the video container div to check when the video changes
	const monitorNextPreviousVideo = () => {
		// Get the video container div
		const videoContainerDiv = document.querySelector(videoContainerClass);

		// Check if the div was found
		if (videoContainerDiv == null) {
			console.log("UdemyVideoFix> Video container not found.");
			return;
		} else {
			console.log("UdemyVideoFix> Monitoring for next and previous video changes.");
		}

		// Observer to check when the video container changes
		const observer = new MutationObserver((mutationsList, _observer) => {
			mutationsList.forEach((mutation) => {
				if (mutation.type === "childList") {
					mutation.addedNodes.forEach((node) => {
						if (node.nodeName === "SECTION") {
							console.log("UdemyVideoFix> Video changed. Fixing new video controls.");
		
							// Restart monitoring for video elements load and then fix the video controls
							monitorElementsLoadAndFix();
						}
					});
				}
			});
		});

		// Start observing the video container div
		observer.observe(videoContainerDiv, { childList: true, subtree: false });

		// Set the flag to true
		monitoringVideoChanges = true;
	};

	// Start tracking the cursor position
	trackCursorPosition();
	// Start monitoring for video elements load
	monitorElementsLoadAndFix();
})();