X.com Quick Block Button

Adds a 'Quick Block' button to tweets on x.com for one-click blocking. Robust version.

目前為 2025-04-05 提交的版本,檢視 最新版本

// ==UserScript==
// @name         X.com Quick Block Button
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Adds a 'Quick Block' button to tweets on x.com for one-click blocking. Robust version.
// @author       Your Name Here
// @match        https://x.com/*
// @match        https://twitter.com/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const CHECK_INTERVAL = 1000;
    const CLICK_DELAY = 300;
    const BUTTON_TEXT = 'Block';
    const MAX_RETRIES = 3;

    // --- Styling for the Button ---
    const buttonStyle = `
        color: lightgray;
        background-color: transparent;
        border: none;
        margin-left: 10px;
        font-family: monospace;
        font-weight: bold;
        cursor: pointer;
        overflow: hidden; /* Clip the extra part of the shadow when scaled */
        position: relative; /* Needed for the pseudo-element */
    `;

      // --- Helper Function: Wait for an element to appear (RETRYING) ---
    async function waitForElement(selector, timeout = 2000, interval = 200, maxRetries = MAX_RETRIES) {
        return new Promise((resolve, reject) => {
            let elapsedTime = 0;
            let retries = 0;
            const timer = setInterval(() => {
                const element = document.querySelector(selector);
                if (element) {
                    clearInterval(timer);
                    resolve(element);
                } else {
                    elapsedTime += interval;
                    if (elapsedTime >= timeout) {
                        if (retries < maxRetries) {
                            retries++;
                            elapsedTime = 0;
                            console.warn(`Quick Block: Element not found, retrying (${retries}/${maxRetries}): ${selector}`);
                        } else {
                            clearInterval(timer);
                            reject(new Error(`Element not found after ${timeout}ms and ${maxRetries} retries: ${selector}`));
                        }
                    }
                }
            }, interval);
        });
    }

    // --- Helper Function: Simulate a click with delay ---
    async function clickElement(element) {
        if (element && typeof element.click === 'function') {
            try {
                element.click();
                await new Promise(resolve => setTimeout(resolve, CLICK_DELAY));
                return true;
            } catch (e) {
                console.error("Quick Block: Error clicking element:", e, element);
                return false;
            }
        } else {
            console.error("Quick Block: Invalid element or no click method:", element);
            return false;
        }
    }

    // --- Helper Function: Find Parent Tweet Article (Robust) ---
    function findParentTweetArticle(node) {
        let current = node;
        while (current && current !== document.body) {
            if (current.tagName === 'ARTICLE' && current.getAttribute('data-testid') === 'tweet') {
                return current;
            }
            current = current.parentNode;
        }
        return null;
    }

    // --- Helper Function: Find Action Bar (More Robust) ---
    function findActionBar(tweetArticle) {
       const selectors = [
            'div[role="group"]',
            'div:has(> button[data-testid="reply"])',
            'div:has(> button[data-testid="like"])',
            'div:has(> button[data-testid="retweet"])'
        ];

        for (const selector of selectors) {
            const found = tweetArticle.querySelector(selector);
            if (found) {
                return found;
            }
        }
        return null;
    }

    // --- Main Blocking Logic (Robust) ---
    async function quickBlockUser(event) {
        const button = event.target;
        button.disabled = true;
        button.textContent = 'Blocking...';
        button.style.backgroundImage = 'linear-gradient(to bottom right, #ff9800, #f57c00)'; // Orange gradient
        button.style.boxShadow = '0px 4px 10px rgba(0, 0, 0, 0.3)';

        const tweetArticle = findParentTweetArticle(button);
         if (!tweetArticle) {
            console.error('Quick Block: Could not find parent tweet article.', button, event.currentTarget);
            button.textContent = 'Error';
            button.style.backgroundColor = 'orange';
            return;
        }

        try {
            let moreButton = null;
            for (let i = 0; i < MAX_RETRIES; i++) {
                moreButton = tweetArticle.querySelector('button[data-testid="caret"]');
                if (moreButton) break;
                await new Promise(resolve => setTimeout(resolve, CLICK_DELAY * 2));
            }
            if (!moreButton) {
                throw new Error('Could not find More button (caret) after retries.');
            }

            if (!await clickElement(moreButton)) {
                throw new Error('Failed to click More button.');
            }

            let blockMenuItem = null;
             for (let i = 0; i < MAX_RETRIES; i++) {
                try{
                    blockMenuItem = await waitForElement('div[data-testid="block"]', 2000, 200, 1);
                    if(blockMenuItem){
                        break;
                    }
                }catch(e){
                    await new Promise(resolve => setTimeout(resolve, CLICK_DELAY * 2));
                }
             }
            if (!blockMenuItem) {
                throw new Error('Could not find Block menu item after retries.');
            }
            if (!await clickElement(blockMenuItem)) {
                throw new Error('Failed to click Block menu item.');
            }

            let confirmButton = null;
            for (let i = 0; i < MAX_RETRIES; i++) {
                try{
                    confirmButton = await waitForElement('button[data-testid="confirmationSheetConfirm"]', 2000, 200, 1);
                    if(confirmButton){
                        break;
                    }
                }catch(e){
                    await new Promise(resolve => setTimeout(resolve, CLICK_DELAY * 2));
                }
            }
            if (!confirmButton) {
                throw new Error('Could not find confirmation Block button after retries.');
            }
            if (!await clickElement(confirmButton)) {
                throw new Error('Failed to click confirmation Block button.');
            }

            console.log('Quick Block: User blocked successfully!');
            button.textContent = 'Blocked!';
            button.style.backgroundImage = 'linear-gradient(to bottom right, #4CAF50, #388E3C)'; // Green gradient
            button.style.boxShadow = '0px 4px 10px rgba(0, 0, 0, 0.3)';

        } catch (error) {
            console.error('Quick Block Error:', error.message, tweetArticle);
            button.textContent = 'Error';
            button.style.backgroundColor = 'orange';
            button.style.backgroundImage = 'none';
            setTimeout(() => {
                button.disabled = false;
                button.textContent = BUTTON_TEXT;
                button.style.backgroundImage =  'linear-gradient(to bottom right, #f44336, #d32f2f)';
                button.style.boxShadow = '0px 4px 10px rgba(0, 0, 0, 0.3)';
            }, 2000);
        }
    }

    // --- Function to Add Buttons to Tweets (Robust) ---
    function addBlockButtons() {
        const tweets = document.querySelectorAll('article[data-testid="tweet"]:not([data-quickblock-added])');

        tweets.forEach(tweet => {
            tweet.setAttribute('data-quickblock-added', 'true');

            const actionBar = findActionBar(tweet);
            if (actionBar) {
                const blockButton = document.createElement('button');
                blockButton.textContent = BUTTON_TEXT;
                blockButton.style.cssText = buttonStyle;
                blockButton.title = "Quickly block the user who posted this tweet";

                blockButton.addEventListener('mouseover', () => {
                    if (!blockButton.disabled) {
                        blockButton.style.cssText = buttonHoverStyle;
                    }
                });
                blockButton.addEventListener('mouseout', () => {
                    if (!blockButton.disabled && blockButton.textContent === BUTTON_TEXT) {
                        blockButton.style.cssText = buttonStyle;
                    }
                });
                blockButton.addEventListener('mousedown', () => {
                    if (!blockButton.disabled) {
                         blockButton.style.cssText = buttonActiveStyle;
                    }
                });
                blockButton.addEventListener('mouseup', () => {
                    if (!blockButton.disabled) {
                         blockButton.style.cssText = buttonStyle;
                    }
                });

                blockButton.addEventListener('click', quickBlockUser);

                if (!actionBar.querySelector('.quick-block-button')) {
                    blockButton.classList.add('quick-block-button');
                    actionBar.appendChild(blockButton);
                }

            } else {
                const tweetLinkElement = tweet.querySelector('a[href*="/status/"]');
                let tweetIdentifier = 'Tweet content unavailable or structure changed';
                if (tweetLinkElement && tweetLinkElement.href) {
                     tweetIdentifier = tweetLinkElement.href;
                 } else {
                     const userHandleElement = tweet.querySelector('a[href^="/"][role="link"] > div[dir="ltr"] > span');
                     if (userHandleElement && userHandleElement.textContent.startsWith('@')) {
                         tweetIdentifier = `Tweet by ${userHandleElement.textContent}`;
                     }
                 }
                console.warn(`Quick Block: Could not find action bar for tweet: ${tweetIdentifier}`, tweet);
            }
        });
    }

    // --- Run Periodically and Use MutationObserver ---
    addBlockButtons();

    const observer = new MutationObserver((mutationsList) => {
        let foundTweets = false;
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                 mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE && (node.matches('article[data-testid="tweet"]') || node.querySelector('article[data-testid="tweet"]'))) {
                       foundTweets = true;
                    }
                 });
            }
            if (foundTweets) break;
        }

        if (foundTweets) {
            requestAnimationFrame(addBlockButtons);
        }
    });

    const mainContentArea = document.querySelector('main');
    if (mainContentArea) {
        observer.observe(mainContentArea, { childList: true, subtree: true });
    } else {
        console.warn("Quick Block: Could not find <main> element, observing document.body. This might be less efficient.");
        observer.observe(document.body, { childList: true, subtree: true });
    }

    setInterval(addBlockButtons, CHECK_INTERVAL * 2);

    console.log('X.com Quick Block script loaded (v1.3 - Stylish).');

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址