// ==UserScript==
// @name Twitch Custom Section Remover
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Removes sections from Twitch.tv homepage based on user-defined strings in their rendered text. Gemini 2.5 Pro wrote the whole thing. Oneshot. No modifications. I'm unlikely to fix any issues.
// @author SumOfAllN00bs
// @license MIT
// @match https://www.twitch.tv/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
const SCRIPT_PREFIX = 'TwitchSectionRemover';
// --- Configuration ---
// Load blocked strings from storage, with a default example.
let blockedStrings = GM_getValue('twitchBlockedStrings', ['Streamer University']);
// --- Styling for Hidden Sections ---
// This class will be added to sections that should be hidden.
GM_addStyle(`
.gm-hidden-section {
display: none !important;
visibility: hidden !important;
height: 0 !important;
width: 0 !important;
overflow: hidden !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
}
`);
// --- Functions to Manage Blocked Strings (via Tampermonkey Menu) ---
function listBlockedStrings() {
const currentList = GM_getValue('twitchBlockedStrings', []);
console.log(`${SCRIPT_PREFIX}: Currently blocked strings:`, currentList);
alert(`${SCRIPT_PREFIX}: Currently blocked strings:\n${currentList.join('\n') || '(No strings blocked)'}`);
}
function addBlockedString() {
const newString = prompt(`${SCRIPT_PREFIX}: Enter a string to block (case-sensitive):`);
if (newString && newString.trim() !== '') {
let currentBlockedStrings = GM_getValue('twitchBlockedStrings', []);
if (!currentBlockedStrings.includes(newString.trim())) {
currentBlockedStrings.push(newString.trim());
GM_setValue('twitchBlockedStrings', currentBlockedStrings);
blockedStrings = currentBlockedStrings; // Update local cache
console.log(`${SCRIPT_PREFIX}: Added '${newString.trim()}'. New list:`, currentBlockedStrings);
alert(`${SCRIPT_PREFIX}: Added '${newString.trim()}'.`);
checkAndRemoveSections(); // Re-run to apply the new string immediately
} else {
alert(`${SCRIPT_PREFIX}: String '${newString.trim()}' is already in the list.`);
}
}
}
function removeBlockedString() {
let currentBlockedStrings = GM_getValue('twitchBlockedStrings', []);
if (currentBlockedStrings.length === 0) {
alert(`${SCRIPT_PREFIX}: Blocklist is currently empty.`);
return;
}
const stringToRemove = prompt(`${SCRIPT_PREFIX}: Enter the exact string to remove from the blocklist:\n\nCurrently blocked:\n${currentBlockedStrings.join('\n')}`);
if (stringToRemove && stringToRemove.trim() !== '') {
const index = currentBlockedStrings.indexOf(stringToRemove.trim());
if (index > -1) {
const removed = currentBlockedStrings.splice(index, 1);
GM_setValue('twitchBlockedStrings', currentBlockedStrings);
blockedStrings = currentBlockedStrings; // Update local cache
console.log(`${SCRIPT_PREFIX}: Removed '${removed[0]}'. New list:`, currentBlockedStrings);
alert(`${SCRIPT_PREFIX}: Removed '${removed[0]}'.`);
// Re-evaluate all sections: those that were hidden ONLY by the removed string will now be unhidden.
checkAndRemoveSections();
} else {
alert(`${SCRIPT_PREFIX}: String '${stringToRemove.trim()}' not found in the list.`);
}
}
}
// Register menu commands for Tampermonkey
GM_registerMenuCommand("List Blocked Strings", listBlockedStrings);
GM_registerMenuCommand("Add String to Blocklist", addBlockedString);
GM_registerMenuCommand("Remove String from Blocklist", removeBlockedString);
// --- Core Logic to Find and Remove/Hide Elements ---
function checkAndRemoveSections() {
// Refresh blockedStrings from storage in case it was modified
blockedStrings = GM_getValue('twitchBlockedStrings', []);
// If no strings are blocked, ensure all sections previously hidden by this script are unhidden.
if (blockedStrings.length === 0) {
document.querySelectorAll('.gm-hidden-section').forEach(el => {
el.classList.remove('gm-hidden-section');
});
return;
}
// Selector for the "great-grandparent" div.
// Based on your input: "The parent of the parent of this div is this element: <div class="Layout-sc-1xcs6mc-0 cwJXDZ">"
// This means `div.Layout-sc-1xcs6mc-0.cwJXDZ` is the great-grandparent of the text holder,
// and the grandparent of the div-to-be-removed.
const greatGrandParentSelector = 'div.Layout-sc-1xcs6mc-0.cwJXDZ';
const greatGrandParents = document.querySelectorAll(greatGrandParentSelector);
greatGrandParents.forEach(ggpElement => {
// Iterate through children of ggpElement (these are potential GrandParents of the target sections)
Array.from(ggpElement.children).forEach(gpElement => {
if (gpElement.nodeType !== Node.ELEMENT_NODE) return;
// Iterate through children of gpElement (these are the "grand-child" divs, candidates for removal)
Array.from(gpElement.children).forEach(gcDiv => {
if (gcDiv.nodeType !== Node.ELEMENT_NODE) return;
let sectionShouldBeHidden = false;
// Check this gcDiv and its descendants for any of the blocked strings.
// Query for elements that are likely to contain primary text/titles.
const elementsToCheckForText = [gcDiv, ...Array.from(gcDiv.querySelectorAll('h1, h2, h3, h4, h5, h6, p, a, span, div[class*="title"], div[data-a-target*="title"]'))];
for (const textHoldingElement of elementsToCheckForText) {
// Ensure the element is visible (not display:none via other means) unless we hid its parent (gcDiv).
// If gcDiv itself is hidden, its children won't have an offsetParent.
if (textHoldingElement.offsetParent === null && !gcDiv.classList.contains('gm-hidden-section')) {
continue;
}
// Get text from direct child text nodes of the current textHoldingElement.
// This adheres to "rendered text of the same element".
let currentElementOwnText = "";
for (const childNode of textHoldingElement.childNodes) {
if (childNode.nodeType === Node.TEXT_NODE) {
currentElementOwnText += childNode.textContent;
}
}
currentElementOwnText = currentElementOwnText.replace(/\s+/g, ' ').trim(); // Normalize whitespace
if (currentElementOwnText) {
for (const blockedStr of blockedStrings) {
if (currentElementOwnText.includes(blockedStr)) {
sectionShouldBeHidden = true;
// console.log(`${SCRIPT_PREFIX}: Matched "${blockedStr}" in text "${currentElementOwnText}" of element:`, textHoldingElement);
break; // Found a match for this textHoldingElement
}
}
}
if (sectionShouldBeHidden) break; // No need to check more text elements if one match already decided to hide gcDiv
}
// Apply or remove the hidden class based on the check
if (sectionShouldBeHidden) {
if (!gcDiv.classList.contains('gm-hidden-section')) {
// console.log(`${SCRIPT_PREFIX}: Hiding section (gcDiv):`, gcDiv);
gcDiv.classList.add('gm-hidden-section');
}
} else {
// If it was previously hidden by this script but no longer matches any current blocked string
if (gcDiv.classList.contains('gm-hidden-section')) {
// console.log(`${SCRIPT_PREFIX}: Unhiding section (gcDiv) as it no longer matches:`, gcDiv);
gcDiv.classList.remove('gm-hidden-section');
}
}
});
});
});
}
// --- Utility: Debounce Function ---
// This prevents the checkAndRemoveSections function from running too frequently during rapid DOM changes.
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const context = this;
const later = () => {
timeout = null;
func.apply(context, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// --- Mutation Observer ---
// Observes changes in the DOM and triggers the removal logic.
const debouncedSectionCheck = debounce(checkAndRemoveSections, 500); // 500ms debounce window
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
// We are interested in changes to the child list (elements added/removed)
// and subtree modifications.
if (mutation.type === 'childList' && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)) {
debouncedSectionCheck();
return; // Debounced call will handle it
}
// if (mutation.type === 'subtree' ) { // Too frequent, childList is usually enough
// debouncedSectionCheck();
// return;
// }
}
});
// Start observing the entire document for changes.
observer.observe(document.documentElement, {
childList: true, // Observe direct children additions/removals
subtree: true // Observe all descendants as well
});
// --- Initial Execution ---
// Run the check function a couple of times after the page initially loads,
// as content might still be populating.
setTimeout(checkAndRemoveSections, 1000); // After 1 second
setTimeout(checkAndRemoveSections, 3000); // After 3 seconds for more dynamic content
})();