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.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

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

})();