YouTubeTV Volume Control with Memory

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         YouTubeTV Volume Control with Memory
// @namespace    typpi.online
// @version      4.1
// @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);
})();