Twitter(X)-Plus

Unmute videos, disable automatic looping of videos, delete retweets and Grok/view count buttons for tweets, add one-click blocking, and one-click copy tweet URL buttons.

Από την 16/03/2026. Δείτε την τελευταία έκδοση.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==UserScript==
// @name               Twitter(X)-Plus
// @name:zh-CN         Twitter(X)-Plus
// @description        Unmute videos, disable automatic looping of videos, delete retweets and Grok/view count buttons for tweets, add one-click blocking, and one-click copy tweet URL buttons.
// @description:zh-CN  视频取消静音,视频取消自动循环播放,推文删除转发、Grok/浏览量按钮,添加一键屏蔽,一键复制推文网址按钮
// @author             Kdroidwin,@dmtri,legendary_warrior_1, Hzbrrbmin,人民的勤务员 <[email protected]>
// @namespace          https://github.com/ChinaGodMan/UserScripts
// @supportURL         https://github.com/ChinaGodMan/UserScripts/issues
// @homepageURL        https://github.com/ChinaGodMan/UserScripts
// @homepage           https://github.com/ChinaGodMan/UserScripts
// @license            MIT
// @match              https://twitter.com/*
// @match              https://x.com/*
// @match              https://mobile.x.com/*
// @icon               https://raw.githubusercontent.com/ChinaGodMan/UserScriptsHistory/main/scriptsIcon/x.svg
// @grant              GM_setClipboard
// @compatible         chrome
// @compatible         firefox
// @compatible         edge
// @compatible         opera
// @compatible         safari
// @compatible         kiwi
// @compatible         qq
// @compatible         via
// @compatible         brave
// @version            2026.3.16.1
// @created            2026-03-16 13:09:27
// ==/UserScript==

//LINK - 隐藏按钮
const style = document.createElement('style')
style.id = 'x-btn-hider-styles'
style.textContent = `
/* 右上角Grok 暂时只测试了 中文页面 后续待添加 */
button[aria-label="Grok 操作"] {
    display: none !important;
}

/* 转发 */
[data-testid="retweet"] {
    display: none !important;
}

/* 浏览量 */
a[href*="/analytics"] {
    display: none !important;

`
document.head.appendChild(style)

//SECTION - 拉黑
// --- SVG icon---
const blockIconSVG = `
    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
        viewBox="0 0 24 24" fill="none"
        stroke="currentColor" stroke-width="2"
        stroke-linecap="round" stroke-linejoin="round">
        <circle cx="12" cy="12" r="10"></circle>
        <line x1="4.93" y1="4.93" x2="19.07" y2="19.07"></line>
    </svg>`

const copyIconSVG = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
    viewBox="0 0 24 24" fill="none"
    stroke="currentColor" stroke-width="2"
    stroke-linecap="round" stroke-linejoin="round">
    <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
    <path d="M5 15H4a2 2 0 0 1-2-2V4
             a2 2 0 0 1 2-2h9
             a2 2 0 0 1 2 2v1"></path>
</svg>`

const checkIconSVG = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
    viewBox="0 0 24 24" fill="none"
    stroke="currentColor" stroke-width="2"
    stroke-linecap="round" stroke-linejoin="round">
    <polyline points="20 6 9 17 4 12"></polyline>
</svg>`

// --- Utilities ---
const simulateClick = (element) => {
    if (element) {
        element.dispatchEvent(new MouseEvent('click', { bubbles: true }))
    }
}

const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))

// --- Block sequence ---
async function blockUser(event) {
    event.preventDefault()
    event.stopPropagation()

    console.log('OneClickBlock: Starting block sequence...')

    const tweetElement = event.currentTarget.closest('article[data-testid="tweet"]')
    if (!tweetElement) return

    const moreButton = tweetElement.querySelector('button[data-testid="caret"]')
    if (!moreButton) return

    simulateClick(moreButton)
    await sleep(150)

    const blockMenuItem = document.querySelector('div[data-testid="block"]')
    if (!blockMenuItem) {
        simulateClick(moreButton)
        return
    }

    simulateClick(blockMenuItem)
    await sleep(150)

    const confirmButton = document.querySelector('button[data-testid="confirmationSheetConfirm"]')
    if (!confirmButton) return

    simulateClick(confirmButton)

    // Fade out blocked tweet
    tweetElement.style.transition = 'opacity 0.4s'
    tweetElement.style.opacity = '0.3'

    setTimeout(() => {
        tweetElement.style.display = 'none'
    }, 400)
}

// --- Add button to tweet ---
function addBlockButtons() {
    const tweets = document.querySelectorAll('article[data-testid="tweet"]')

    tweets.forEach(tweet => {
        if (tweet.querySelector('.one-click-block-btn')) return

        const insertionPoint = tweet.querySelector('[role="group"]')
        if (!insertionPoint) return

        const button = document.createElement('div')
        button.className = 'one-click-block-btn'
        button.innerHTML = blockIconSVG
        button.style.cursor = 'pointer'
        button.style.marginLeft = '8px'
        button.style.display = 'flex'
        button.style.alignItems = 'center'
        button.style.color = 'rgb(113, 118, 123)'
        button.title = 'Block this user'

        button.addEventListener('mouseenter', () => {
            button.style.color = 'rgb(239, 68, 68)'
        })

        button.addEventListener('mouseleave', () => {
            button.style.color = 'rgb(113, 118, 123)'
        })

        button.addEventListener('click', blockUser)

        insertionPoint.appendChild(button)
    })
}

function copyLink(event) {
    event.preventDefault()
    event.stopPropagation()
    const button = event.currentTarget
    console.log('OneClickBlock: Starting copy this tweet url...')
    const tweetElement = button.closest('article[data-testid="tweet"]')
    if (!tweetElement) return
    const timeLink = tweetElement.querySelector('a[href*="/status/"] > time')?.parentElement
    if (!timeLink) return
    const url = 'https://x.com' + timeLink.getAttribute('href')
    GM_setClipboard(url)
    button.innerHTML = checkIconSVG
    button.style.color = 'rgb(13, 194, 58)'
    setTimeout(() => {
        button.innerHTML = copyIconSVG
        button.style.color = 'rgb(113, 118, 123)'
    }, 2000)
}
function addCopyButtons() {
    const tweets = document.querySelectorAll('article[data-testid="tweet"]')
    tweets.forEach(tweet => {
        if (tweet.querySelector('.one-click-copy-btn')) return
        const insertionPoint = tweet.querySelector('[role="group"]')
        if (!insertionPoint) return
        const button = document.createElement('div')
        button.className = 'one-click-copy-btn'
        button.innerHTML = copyIconSVG
        button.style.cursor = 'pointer'
        button.style.marginLeft = '8px'
        button.style.display = 'flex'
        button.style.alignItems = 'center'
        button.style.color = 'rgb(113, 118, 123)'
        button.title = 'Copy this tweet URL'
        button.addEventListener('mouseenter', () => {
            button.style.color = 'rgb(13, 194, 58)'
        })
        button.addEventListener('mouseleave', () => {
            button.style.color = 'rgb(113, 118, 123)'
        })
        button.addEventListener('click', copyLink)
        insertionPoint.appendChild(button)
    })
}

//SECTION - 视频操作
// Keep track of processed videos
const processedVideos = new WeakSet()

const stopVideoLoop = () => {
    const vids = document.querySelectorAll('video')
    vids.forEach((vid) => {
        if (vid.muted) {
            vid.muted = false
            vid.volume = 0.5
            vid.play().catch(() => { })
        }
        if (processedVideos.has(vid)) return
        processedVideos.add(vid)
        vid.removeAttribute('loop')
        let arr = []
        const setupTimeout = (extra = 0) => {
            let vidLen = vid.duration
            let vidCurr = vid.currentTime
            // ignore any buggy timeout triggered < 500ms
            if ((vidLen - vidCurr) * 1000 < 500) return
            // Ensure duration is available
            if (isNaN(vid.duration) || vid.duration === Infinity) return

            console.log(vidLen > vidCurr, 'setting a timeout', (vidLen - vidCurr - 0.05) * 1000)
            if (vidLen > vidCurr) {
                const timeout = setTimeout(() => {
                    // hack: sometimes video just stops randomly, so need to stop that behavior here
                    vidLen = vid.duration
                    vidCurr = vid.currentTime
                    const remaining = (vidLen - vidCurr) * 1000
                    console.log(remaining)
                    if (remaining < 500) vid.pause()
                }, (vidLen - vidCurr - 0.01) * 1000 + extra) // Adjusted to account for potential delays
                arr.push(timeout)
            }
        }

        // need this for the initial load, 1st time playing. Some extra buffer for 1st time too
        vid.addEventListener('loadedmetadata', () => setupTimeout(-200))

        vid.addEventListener('seeked', () => {
            clearAll(arr)
            setupTimeout()
        })

        vid.addEventListener('pause', () => {
            clearAll(arr)
        })

        vid.addEventListener('play', () => {
            setupTimeout()
        })

        vid.addEventListener('ended', () => {
            setTimeout(() => vid.pause(), 10)
        })
    })
}

const clearAll = (arr) => {
    arr.forEach((timeout) => {
        clearTimeout(timeout)
    })
    arr = []
}
// --- Observe dynamic loading ---
const observer = new MutationObserver(() => {
    stopVideoLoop()
    addBlockButtons()
    addCopyButtons()
})
observer.observe(document.body, { childList: true, subtree: true })

//SECTION - 注入外挂
stopVideoLoop()
addBlockButtons()
addCopyButtons()