您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Blocks Twitch's combos overlay using CSS and DOM manipulation
// ==UserScript== // @name Twitch Combos Overlay Blocker // @namespace https://github.com/nebhay/twitch-combos-blocker // @version 1.0.0 // @description Blocks Twitch's combos overlay using CSS and DOM manipulation // @author nebhay // @match https://www.twitch.tv/* // @grant none // @run-at document-start // @license MIT // @homepageURL https://github.com/nebhay/twitch-combos-blocker // @supportURL https://github.com/nebhay/twitch-combos-blocker/issues // ==/UserScript== (function() { 'use strict'; // Configuration const CONFIG = { DEBUG: true, SELECTORS: [ '[data-a-target*="onetap"]', 'div[class*="onetap" i]', '.onetap-overlay' ] }; /** * Log messages with consistent formatting * @param {string} message - The message to log * @param {string} type - The log type (info, success, warning, error) * @param {any} data - Additional data to log */ function log(message, type = 'info', data = null) { if (!CONFIG.DEBUG) return; const colors = { info: 'color: blue; font-weight: bold;', success: 'color: green; font-weight: bold;', warning: 'color: orange; font-weight: bold;', error: 'color: red; font-weight: bold;' }; const prefix = '[Combos Blocker]'; if (data) { console.log(`%c${prefix} ${message}`, colors[type], data); } else { console.log(`%c${prefix} ${message}`, colors[type]); } } /** * Apply CSS rules to hide combos elements */ function applyCSSRules() { const style = document.createElement('style'); style.id = 'twitch-combos-blocker-styles'; style.textContent = ` /* Hide combos overlay containers */ ${CONFIG.SELECTORS.join(',\n ')} { display: none !important; visibility: hidden !important; opacity: 0 !important; pointer-events: none !important; z-index: -9999 !important; } /* Prevent body scroll lock when overlay would be present */ body.ReactModal__Body--open { overflow: auto !important; } /* Additional selectors for combos variants */ [data-test-selector*="onetap"], [aria-label*="onetap" i], [title*="onetap" i] { display: none !important; visibility: hidden !important; } `; // Insert into head or fallback to document const target = document.head || document.documentElement; target.appendChild(style); log('CSS rules applied successfully', 'success'); } /** * Check if an element matches combos criteria * @param {Element} element - The element to check * @returns {boolean} True if element appears to be a combos element */ function isCombosElement(element) { if (!element || element.nodeType !== 1) return false; // Check data attributes const dataTarget = element.dataset?.aTarget; if (dataTarget && dataTarget.includes('onetap')) return true; const testSelector = element.dataset?.testSelector; if (testSelector && testSelector.toLowerCase().includes('onetap')) return true; // Check class names const className = element.className || ''; const classStr = typeof className === 'string' ? className : className.toString?.() || ''; if (classStr.toLowerCase().includes('onetap')) return true; // Check aria-label and title attributes const ariaLabel = element.getAttribute?.('aria-label') || ''; const title = element.getAttribute?.('title') || ''; if (ariaLabel.toLowerCase().includes('onetap') || title.toLowerCase().includes('onetap')) { return true; } return false; } /** * Remove combos elements from the DOM * @param {Element} element - The element to remove */ function removeCombosElement(element) { const details = { tagName: element.tagName, className: element.className?.toString?.() || element.className || '', dataTarget: element.dataset?.aTarget, id: element.id, outerHTML: element.outerHTML?.substring(0, 200) + '...' }; log('Removing combos element:', 'warning', details); element.remove(); } /** * Set up MutationObserver to watch for new combos elements */ function setupDOMObserver() { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType !== 1) return; // Skip non-element nodes // Check if the node itself is a combos element if (isCombosElement(node)) { removeCombosElement(node); return; } // Check for combos elements within the added node if (node.querySelectorAll) { const combosElements = node.querySelectorAll(CONFIG.SELECTORS.join(', ')); if (combosElements.length > 0) { log(`Found ${combosElements.length} combos element(s) in added content`, 'warning'); combosElements.forEach(removeCombosElement); } } }); }); }); // Start observing const target = document.body || document.documentElement; observer.observe(target, { childList: true, subtree: true }); log('DOM observer started', 'success'); return observer; } /** * Perform initial cleanup of existing combos elements */ function initialCleanup() { const existingElements = document.querySelectorAll(CONFIG.SELECTORS.join(', ')); if (existingElements.length > 0) { log(`Removing ${existingElements.length} existing combos element(s)`, 'warning'); existingElements.forEach(removeCombosElement); } } /** * Initialize the blocker */ function init() { log('Initializing Combos Blocker', 'info'); // Apply CSS rules immediately applyCSSRules(); // Set up DOM observer const observer = setupDOMObserver(); // Perform initial cleanup when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialCleanup); } else { initialCleanup(); } // Cleanup on page unload window.addEventListener('beforeunload', () => { if (observer) { observer.disconnect(); log('Observer disconnected', 'info'); } }); log('Combos Blocker initialized successfully', 'success'); } // Start the blocker init(); })();