Twitch Tweaks

Add clips tab, make theatre mode toggle fullscreen, hide the annoying red dot

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Twitch Tweaks
// @namespace    https://greasyfork.org/en/users/8615-joeytwiddle
// @version      0.2.4
// @description  Add clips tab, make theatre mode toggle fullscreen, hide the annoying red dot
// @author       joeytwiddle
// @license      ISC
// @match        https://www.twitch.tv/*
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

	// Options

	const quietenPrimeOffersRedDot = true;

	const makeTheatreModeButtonToggleFullscreen = true;

	const addClipsTab = true;

	//

	if (quietenPrimeOffersRedDot) {
		//GM_addStyle(`.prime-offers__pill { display: none }`);
		GM_addStyle(`.prime-offers__pill { filter: saturate(0%) brightness(250%) }`);
	}

	//

	// BUG: The first time we click this button, it does go fullscreen, but then it disappears!
	// (Presumably it's a kind of fullscreen that Twitch thinks Theatre Mode does not belong in.)
	// Then when I use Twitch's button to exit fullscreen, the Theatre button re-appears, but no longer toggles fullscreen.
	// I guess that's because it's a new button, without our event listener attached.

	if (makeTheatreModeButtonToggleFullscreen) {
		const theatreModeButtonSelector = '[data-a-target="player-theatre-mode-button"]';

		waitForElement(theatreModeButtonSelector, (element) => {
			// Actually there seem to be two of these (perhaps for different page widths?)
			Array.from(document.querySelectorAll(theatreModeButtonSelector)).forEach(element => {
				console.info("Adding event listener to", element);
				element.addEventListener('click', () => {
					console.info("Toggling fullscreen");
					// Hides the theatre button
					// Although hitting Firefox's own fullscreen button doesn't do that
					// Also this only seems to work once
					// But at least it works the first time!
					toggleFullScreen();
					//
					// Hides chat
					//const fullscreenButton = document.querySelector('[data-a-target="player-fullscreen-button"]')
					//fullscreenButton.click();
				}, true);
			});
		});
	}

	// From https://stackoverflow.com/a/19442811
	function toggleFullScreen() {
       if (!document.fullscreenElement &&
        !document.mozFullScreenElement && !document.webkitFullscreenElement) {
         if (document.documentElement.requestFullscreen) {
           document.documentElement.requestFullscreen();
         } else if (document.documentElement.mozRequestFullScreen) {
           document.documentElement.mozRequestFullScreen();
         } else if (document.documentElement.webkitRequestFullscreen) {
           document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
         }
       } else {
          if (document.cancelFullScreen) {
             document.cancelFullScreen();
          } else if (document.mozCancelFullScreen) {
             document.mozCancelFullScreen();
          } else if (document.webkitCancelFullScreen) {
            document.webkitCancelFullScreen();
          }
       }
     }

	function waitForElement(selector, optionsOrCallback) {
		var callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : optionsOrCallback.callback;

		var timeoutSeconds = optionsOrCallback.timeoutSeconds || 120;
		var retrySeconds = optionsOrCallback.retrySeconds || 5;

		var startTime = Date.now();

		var tryForElement = function() {
			var element = document.querySelector(selector);
			if (element) {
				callback(element);
			} else {
				if (Date.now() < startTime + timeoutSeconds * 1000) {
					setTimeout(tryForElement, retrySeconds * 1000);
				} else {
					throw new Error('Could not find element "' + selector + '" within ' + timeoutSeconds + ' seconds');
				}
			}
		};

		tryForElement();
	}

	//

	if (addClipsTab) {
		setTimeout(checkForMissingClipsLink, 1 * 1000);
		setInterval(checkForMissingClipsLink, 15 * 1000);
		// After a click, check a few times if we need to add the link
		document.body.addEventListener('click', function(evt) {
			setTimeout(checkForMissingClipsLink, 0.1 * 1000);
			setTimeout(checkForMissingClipsLink, 0.2 * 1000);
			setTimeout(checkForMissingClipsLink, 0.5 * 1000);
			setTimeout(checkForMissingClipsLink, 1.0 * 1000);
			setTimeout(checkForMissingClipsLink, 1.5 * 1000);
		});
	}

	function checkForMissingClipsLink() {
		if (document.querySelector('a[tabname="videos"]') && !document.querySelector('a[tabname="clips"]')) {
			const videosLink = document.querySelector('a[tabname="videos"]');
			const videosNode = videosLink.parentNode; // The container li

			const streamerName = videosLink.pathname.split('/')[1];
			const range = '7d'; // or '24hr' or '30d' or 'all'
			const clipsPath = `/${streamerName}/clips?featured=false&filter=clips&range=${range}`;

			const clipsNode = videosNode.cloneNode(true);
			const clipsLink = clipsNode.querySelector('a');
			//console.log('clipsNode:', clipsNode);
			clipsLink.querySelector('p').textContent = 'Clips';
			clipsLink.setAttribute('tabname', 'clips'); // Doesn't work without setAttribute?
			clipsLink.setAttribute('data-a-target', 'channel-home-tab-Clips');
			clipsLink.href = clipsPath;

			// Add the new clips tab/link after the videos tab/link
			videosNode.parentNode.insertBefore(clipsNode, videosNode.nextSibling);

			// We may need to fix the styling which shows which tab is currently selected
			// When we are viewing videos, the URL is /videos
			// When we are viewing clips, the URL is also /videos, but with a filter param
			// In either case, we get two selectedTabMarkers, because the video node is styled as selected, and the clips node was cloned from that.
			// So we just need to decide which marker to remove..
			const onClipsTab = document.location.search.match(/\bfilter=clips\b/);
			const onVideosTab = document.location.pathname.match(/[/]videos\b/) && !onClipsTab;
			if (onVideosTab) {
				removeSelectedStyleFrom(clipsNode);
			}
			if (onClipsTab) {
				removeSelectedStyleFrom(videosNode);
			}

			// BUG TODO: Now all that works, but the style doesn't update if we navigate from Clips to Videos tab.  Not a big concern, since I rarely do that.
		}
	}

	function removeSelectedStyleFrom(node) {
		// The underline for the currently selected tab can be found at ul > li > a > div > div > .ScActiveIndicator-sc-17qqzr5-1.kAuTTn
		// Unselected tabs have the container div but not this child.
		const underlineElem = Array.from(node.querySelectorAll('*')).find(
			childNode => String(childNode.className).includes('ScActiveIndicator')
		);
		if (underlineElem) {
			underlineElem.parentNode.removeChild(underlineElem);
		} else {
			console.warn(`[Twitch Tweaks] removeSelectedStyleFrom() underlineElem not found below`, node);
		}

		const textColorElem = node.querySelector('li > a > div');
		if (textColorElem) {
			// Since the class looks variable, let's just overwrite the style directly (back to the unstyled value)
			// For some reason, when I do this on Firefox, even in the console, it often doesn't take O_o
			//textColorElem.style.color = 'inherit !important';
			// But this works
			GM_addStyle(`.color-inherit { color: inherit !important }`);
			textColorElem.classList.add('color-inherit');
		} else {
			console.warn(`[Twitch Tweaks] removeSelectedStyleFrom() textColorElem not found below`, node);
		}
	}
})();