Twitch Custom Section Remover

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.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==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

})();