YouTubeTV Volume Control with Memory

Remembers and controls volume levels on YouTube TV with keyboard shortcuts and a UI for manual input

بۇ قوليازمىنى قاچىلاش؟
ئاپتورنىڭ تەۋسىيەلىگەن قوليازمىسى

سىز بەلكىم YouTube Volume Control with Memory نى ياقتۇرۇشىڭىز مۇمكىن.

بۇ قوليازمىنى قاچىلاش
// ==UserScript==
// @name         YouTubeTV Volume Control with Memory
// @namespace    https://github.com/Nick2bad4u/UserStyles
// @version      4.0
// @description  Remembers and controls volume levels on YouTube TV with keyboard shortcuts and a UI for manual input
// @author       Nick2bad4u
// @match        *://tv.youtube.com/*
// @grant        GM.setValue
// @grant        GM.getValue
// @icon         https://www.google.com/s2/favicons?sz=64&domain=tv.youtube.com
// @license      UnLicense
// @tag          youtube
// ==/UserScript==

(function () {
	'use strict';

	// Wait for the YouTube player and controls to load
	const playerReady = setInterval(() => {
		const videoPlayer =
			document.querySelector('video');
		const leftControls = document.querySelector(
			'.ytp-left-controls',
		);
		const volumeSliderHandle =
			document.querySelector(
				'.ytp-volume-slider-handle',
			);
		const volumePanel = document.querySelector(
			'.ytp-volume-panel',
		);
		const muteButton = document.querySelector(
			'.ytp-mute-button',
		);

		if (
			videoPlayer &&
			leftControls &&
			volumeSliderHandle &&
			muteButton
		) {
			clearInterval(playerReady);

			// Retrieve the saved volume level from localStorage
			let ytVolumeData = localStorage.getItem(
				'yt-player-volume',
			);
			let savedVolume = videoPlayer.volume;
			let savedMuted = videoPlayer.muted;

			if (ytVolumeData) {
				try {
					ytVolumeData = JSON.parse(ytVolumeData);
					const data = JSON.parse(
						ytVolumeData.data,
					);
					savedVolume = data.volume / 100; // YouTube stores volume from 0 to 100
					savedMuted = data.muted;
				} catch (e) {
					console.error(
						'Failed to parse yt-player-volume:',
						e,
					);
				}
			}

			// Ensure savedVolume is within [0, 1] range
			videoPlayer.volume = Math.max(
				0,
				Math.min(1, savedVolume),
			);
			videoPlayer.muted = savedMuted;

			// Update the slider handle position
			const updateSliderHandle = () => {
				if (videoPlayer.muted) {
					volumeSliderHandle.style.left = `0%`;
				} else {
					volumeSliderHandle.style.left = `${videoPlayer.volume * 100}%`;
				}
			};
			updateSliderHandle();

			// Set the aria-valuenow attribute on the volume panel
			if (volumePanel) {
				volumePanel.setAttribute(
					'aria-valuenow',
					videoPlayer.volume * 100,
				);
			}

			// Create input element for volume control
			const volumeInput =
				document.createElement('input');
			volumeInput.type = 'number';
			volumeInput.min = 0;
			volumeInput.max = 100;
			volumeInput.value = videoPlayer.muted
				? 0
				: Math.round(videoPlayer.volume * 100);

			// Style the input field
			Object.assign(volumeInput.style, {
				width: '40px',
				marginLeft: '10px',
				backgroundColor:
					'rgba(255, 255, 255, 0.0)',
				color: 'white',
				border:
					'0px solid rgba(255, 255, 255, 0.0)',
				borderRadius: '4px',
				zIndex: 9999,
				height: '24px',
				fontSize: '16px',
				padding: '0 4px',
				transition:
					'border-color 0.3s, background-color 0.3s',
				outline: 'none',
				position: 'relative',
				top: '13px',
			});

			// Prevent hotkeys from interfering with the input
			volumeInput.addEventListener(
				'keydown',
				(e) => e.stopPropagation(),
			);

			// Input focus and hover styling
			volumeInput.addEventListener(
				'focus',
				() =>
					(volumeInput.style.borderColor =
						'rgba(255, 255, 255, 0.6)'),
			);
			volumeInput.addEventListener(
				'blur',
				() =>
					(volumeInput.style.borderColor =
						'rgba(255, 255, 255, 0.3)'),
			);
			volumeInput.addEventListener(
				'mouseenter',
				() =>
					(volumeInput.style.backgroundColor =
						'rgba(0, 0, 0, 0.8)'),
			);
			volumeInput.addEventListener(
				'mouseleave',
				() =>
					(volumeInput.style.backgroundColor =
						'rgba(255, 255, 255, 0.0)'),
			);

			// Handle volume change from input
			let lastSetVolume = videoPlayer.volume;
			volumeInput.addEventListener(
				'input',
				() => {
					let volume = parseInt(
						volumeInput.value,
						10,
					);
					volume = isNaN(volume)
						? 100
						: Math.max(0, Math.min(100, volume)); // Clamp between 0 and 100

					videoPlayer.volume = volume / 100; // Convert to [0, 1] range

					if (volume === 0) {
						videoPlayer.muted = true;
					} else {
						videoPlayer.muted = false;
					}

					lastSetVolume = videoPlayer.volume;

					// Update the slider handle position
					updateSliderHandle();

					// Save the new volume to localStorage
					const ytVolumeObject = {
						data: JSON.stringify({
							volume: volume, // Volume from 0 to 100
							muted: videoPlayer.muted,
						}),
						expiration: Date.now() + 2592000000, // Expires in 30 days
						creation: Date.now(),
					};
					const ytVolumeString = JSON.stringify(
						ytVolumeObject,
					);
					localStorage.setItem(
						'yt-player-volume',
						ytVolumeString,
					);
				},
			);

			// Update input value when volume changes from other controls
			let previousMutedState = videoPlayer.muted;

			videoPlayer.addEventListener(
				'volumechange',
				() => {
					if (
						previousMutedState &&
						!videoPlayer.muted
					) {
						// Player was muted and is now unmuted
						videoPlayer.volume = lastSetVolume;
						volumeInput.value = Math.round(
							videoPlayer.volume * 100,
						);
						updateSliderHandle();
					}

					previousMutedState = videoPlayer.muted;

					// Update lastSetVolume if the volume changed and not muted
					if (!videoPlayer.muted)
						lastSetVolume = videoPlayer.volume;

					volumeInput.value = videoPlayer.muted
						? 0
						: Math.round(
								videoPlayer.volume * 100,
							);

					// Update the slider handle position
					updateSliderHandle();

					// Save the volume to localStorage
					const volumePercent = Math.round(
						videoPlayer.volume * 100,
					);
					const ytVolumeObject = {
						data: JSON.stringify({
							volume: volumePercent,
							muted: videoPlayer.muted,
						}),
						expiration: Date.now() + 2592000000,
						creation: Date.now(),
					};
					const ytVolumeString = JSON.stringify(
						ytVolumeObject,
					);
					localStorage.setItem(
						'yt-player-volume',
						ytVolumeString,
					);
				},
			);

			// Insert the input into the left controls
			leftControls.appendChild(volumeInput);
		}
	}, 500);
})();