Greasy Fork

Greasy Fork is available in English.

X/Twitter Comments Blurrifier

Automagically blur comments on X (Twitter) and adds a button to toggle between visible/blurred.

当前为 2025-10-31 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         X/Twitter Comments Blurrifier
// @namespace    http://tampermonkey.net/
// @version      7.0
// @description  Automagically blur comments on X (Twitter) and adds a button to toggle between visible/blurred.
// @author       nereids
// @license      MIT
// @match        https://twitter.com/*
// @match        https://x.com/*
// @icon         https://icons.duckduckgo.com/ip3/x.com.ico
// @grant        GM_addStyle
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    const CONVERSATION_SELECTOR = 'div[aria-label="Timeline: Conversation"]';
    const ACTION_BAR_SELECTOR = 'article div[role="group"]';
    const ICON_ID = 'toggleBlurIcon';
    const ACTIVE_CLASS = 'blur-disabled';

    // --- Custom Colors ---
    const X_BLUE = '#1d9bf0'; // For Active/Unblurred state
    const X_PINK = '#f91880';  // For Inactive/Blurred state (your custom color)
    const X_GRAY_ICON = '#71767B'; // Default dark gray icon color on X

    // Initial state: Comments are blurred
    let isBlurred = true;

    // --- SVG Definitions ---
    // Blurred (Inactive state - Icon should be your X_BLUE color)
    const blurredSvg = `
      <svg xmlns="http://www.w3.org/2000/svg" height="22.5px" viewBox="0 -960 960 960" width="22.5px" style="fill:${X_BLUE}">
        <path d="M480-320q75 0 127.5-52.5T660-500q0-75-52.5-127.5T480-680q-75 0-127.5 52.5T300-500q0 75 52.5 127.5T480-320Zm0-72q-45 0-76.5-31.5T372-500q0-45 31.5-76.5T480-608q45 0 76.5 31.5T588-500q0 45-31.5 76.5T480-392Zm0 192q-134 0-244.5-72T61-462q-5-9-7.5-18.5T51-500q0-10 2.5-19.5T61-538q64-118 174.5-190T480-800q134 0 244.5 72T899-538q5 9 7.5 18.5T909-500q0 10-2.5 19.5T899-462q-64 118-174.5 190T480-200Zm0-300Zm0 220q113 0 207.5-59.5T832-500q-50-101-144.5-160.5T480-720q-113 0-207.5 59.5T128-500q50 101 144.5 160.5T480-280Z"/>
      </svg>`;

    // Unblurred (Active state - Icon should be X_PINK color)
    const unblurredSvg = `
      <svg xmlns="http://www.w3.org/2000/svg" height="22.5px" viewBox="0 -960 960 960" width="22.5px" style="fill:${X_PINK}">
        <path d="M607-627q29 29 42.5 66t9.5 76q0 15-11 25.5T622-449q-15 0-25.5-10.5T586-485q5-26-3-50t-25-41q-17-17-41-26t-51-4q-15 0-25.5-11T430-643q0-15 10.5-25.5T466-679q38-4 75 9.5t66 42.5Zm-127-93q-19 0-37 1.5t-36 5.5q-17 3-30.5-5T358-742q-5-16 3.5-31t24.5-18q23-5 46.5-7t47.5-2q137 0 250.5 72T904-534q4 8 6 16.5t2 17.5q0 9-1.5 17.5T905-466q-18 40-44.5 75T802-327q-12 11-28 9t-26-16q-10-14-8.5-30.5T753-392q24-23 44-50t35-58q-50-101-144.5-160.5T480-720Zm0 520q-134 0-245-72.5T60-463q-5-8-7.5-17.5T50-500q0-10 2-19t7-18q20-40 46.5-76.5T166-680l-83-84q-11-12-10.5-28.5T84-820q11-11 28-11t28 11l680 680q11 11 11.5 27.5T820-84q-11 11-28 11t-28-11L624-222q-35 11-71 16.5t-73 5.5ZM222-624q-29 26-53 57t-41 67q50 101 144.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z"/>
      </svg>`;

    // 2. Inject Base CSS for Blurring and Hover State
    GM_addStyle(`
        /* Blur and Alignment Styles */
        ${CONVERSATION_SELECTOR} div[data-testid="cellInnerDiv"]:not(:first-child) {
            filter: blur(4px) !important;
            transition: filter 0.2s ease-in-out;
            pointer-events: none !important;
            user-select: none !important;
        }

        body.${ACTIVE_CLASS} ${CONVERSATION_SELECTOR} div[data-testid="cellInnerDiv"]:not(:first-child) {
            filter: none !important;
            pointer-events: auto !important;
            user-select: auto !important;
        }

        /* ICON WRAPPER STYLING */
        #${ICON_ID} {
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            height: 100%;
            width: 46px;
            margin: 0 0 0 15px !important;
            padding: 0 !important;
            transition: background-color 0.2s ease-in-out;
        }

        /* Pseudo-element for circular background */
        div[id="${ICON_ID}"]::before {
             content: '';
             position: absolute;
             width: 38.5px;
             height: 38.5px;
             border-radius: 50%;
             transition: background-color 0.2s ease-in-out;
             z-index: 1; /* Ensures it sits below the icon itself */
        }

        /* The SVG is a direct child of the icon container */
        #${ICON_ID} svg {
            z-index: 2; /* Ensures the icon sits above the pseudo-element background */
        }

        /* --- HOVER STATE LOGIC --- */

        /* 1. Default (Blurred/Inactive) State Hover: Apply PINK hover */
        /* If the body does NOT have the active class (comments are blurred), apply pink hover. */
        body:not(.${ACTIVE_CLASS}) div[id="${ICON_ID}"]:hover {
             color: ${X_PINK}; /* Changes the icon color to pink */
        }

        body:not(.${ACTIVE_CLASS}) div[id="${ICON_ID}"]:hover::before {
             background-color: rgba(29, 161, 242, 0.1); /* Low-opacity X pink background (f91880) */
        }

        /* 2. Active (Unblurred) State Hover: Apply BLUE hover */
        /* If the body HAS the active class (comments are unblurred), apply blue hover. */
        body.${ACTIVE_CLASS} div[id="${ICON_ID}"]:hover {
             color: ${X_BLUE}; /* Changes the icon color to blue */
        }

        body.${ACTIVE_CLASS} div[id="${ICON_ID}"]:hover::before {
             background-color: rgba(249, 24, 128, 0.1); /* Low-opacity X blue background */
        }


        /* The cloned button wrapper needs fixed dimensions for consistent alignment */
        #${ICON_ID}:not(:first-child) {
             width: 38.5px;
             height: 38.5px;
        }
    `);

    // 3. Logic to inject the icon and attach the event handler (SPA logic)
    function injectToggleIcon() {
        const actionBar = document.querySelector(ACTION_BAR_SELECTOR);
        if (actionBar && !document.getElementById(ICON_ID)) {

            const iconContainer = document.createElement('div');
            iconContainer.id = ICON_ID;

            // Re-check the body class and set initial SVG
            isBlurred = !document.body.classList.contains(ACTIVE_CLASS);
            iconContainer.innerHTML = isBlurred ? blurredSvg : unblurredSvg;

            const existingButton = actionBar.children[0];
            if (!existingButton) return;

            const targetButton = existingButton.querySelector('svg') ? existingButton : actionBar.children[1];

            const buttonWrapper = targetButton.cloneNode(false);
            buttonWrapper.innerHTML = '';
            buttonWrapper.appendChild(iconContainer);

            actionBar.appendChild(buttonWrapper);

            // Click Handler: Toggle state and update icon
            iconContainer.addEventListener('click', function(e) {
                e.stopPropagation();

                isBlurred = !isBlurred;
                document.body.classList.toggle(ACTIVE_CLASS);

                // Update SVG
                iconContainer.innerHTML = isBlurred ? blurredSvg : unblurredSvg;
            });
        }
    }

    // --- MutationObserver Setup (SPA logic) ---
    const observerCallback = (mutations) => {
        for (const mutation of mutations) {
            if (mutation.addedNodes.length) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === 1 && (node.matches(CONVERSATION_SELECTOR) || node.querySelector(CONVERSATION_SELECTOR))) {
                        setTimeout(injectToggleIcon, 50);
                        return;
                    }
                }
            }
        }
    };

    const observer = new MutationObserver(observerCallback);
    const targetNode = document.body;
    const config = { childList: true, subtree: true };

    observer.observe(targetNode, config);

    // Initial call
    injectToggleIcon();

})();