Add clips tab, make theatre mode toggle fullscreen, hide the annoying red dot
// ==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);
}
}
})();