Greasy Fork

Greasy Fork is available in English.

X 推文搜索器

支持悬浮球、自动滚动、关键词搜索。优化书签提取逻辑,默认滚动速度更快。

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         X 推文搜索器
// @namespace    http://tampermonkey.net/
// @version      4
// @description  支持悬浮球、自动滚动、关键词搜索。优化书签提取逻辑,默认滚动速度更快。
// @author       喂你吃药
// @match        https://x.com/*
// @match        https://twitter.com/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // --- 核心配置 ---
    // 修改点1:默认滚动像素调整为 3000
    let scrollStep = 3000;

    let stopRequested = false;
    let processedTweets = new Set();

    // ---------------------------------------------------------
    // 🕵️‍♂️ 第一部分:特工逻辑 (只在书签页触发)
    // ---------------------------------------------------------
    if (window.location.href.includes('ts_action=sync_bookmarks')) {

        // 创建遮罩
        const mask = document.createElement('div');
        mask.style = 'position:fixed;top:0;left:0;width:100%;height:100%;background:#000;color:#00ba7c;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:99999;font-size:20px;font-weight:bold;font-family:sans-serif;';
        mask.innerHTML = '<div>🔄 正在提取最新书签关键词...</div><div style="font-size:14px;color:#666;margin-top:10px;">(提取到纯文本后将自动关闭)</div>';
        document.body.appendChild(mask);

        const checkTimer = setInterval(() => {
            const tweetTextNode = document.querySelector('[data-testid="tweetText"]');
            if (tweetTextNode) {
                clearInterval(checkTimer);

                let rawText = tweetTextNode.innerText;

                // 修改点2:纯净提取逻辑
                // 正则说明:以换行(\n)、标点(\p{P})、符号(\p{S})为界进行分割,只取第一部分
                // \p{P} 包含逗号、句号、引号等所有标点
                // \p{S} 包含Emoji、货币符号、数学符号等
                let cleanText = rawText.split(/[\n\r\p{P}\p{S}]/u)[0].trim();

                // 如果第一句实在太短(比如只有一个字),为了防止误判,稍微放宽一点点(可选,目前严格按你要求执行)
                if (cleanText.length === 0) {
                     // 如果第一位就是符号,split后可能为空,尝试直接取前10个字符保底
                     cleanText = rawText.replace(/[\n\r]/g, '').slice(0, 15);
                }

                // 截取适度长度,防止过长
                cleanText = cleanText.slice(0, 40);

                // 发送数据
                localStorage.setItem('ts_sync_result', JSON.stringify({
                    text: cleanText,
                    timestamp: new Date().getTime()
                }));

                mask.innerHTML = `<div style="color:#fff">✅ 提取成功:</div><div style="color:#1d9bf0;margin:10px 0;">"${cleanText}"</div><div>正在关闭...</div>`;

                setTimeout(() => {
                    window.close();
                }, 500); // 稍微展示一下提取结果再关闭
            }
        }, 500);

        setTimeout(() => {
            mask.innerText = '❌ 超时未找到推文,请检查网络。';
        }, 10000);

        return;
    }

    // ---------------------------------------------------------
    // 🎮 第二部分:主界面逻辑
    // ---------------------------------------------------------

    // --- 样式表 ---
    const styles = `
        #ts-floater { position: fixed; z-index: 9999; font-family: -apple-system, BlinkMacSystemFont, sans-serif; user-select: none; }
        .ts-mini-ball {
            width: 40px; height: 40px; background: rgba(29, 155, 240, 0.6);
            border-radius: 50%; box-shadow: 0 4px 10px rgba(0,0,0,0.3); cursor: pointer;
            display: flex; align-items: center; justify-content: center; color: white; font-size: 20px;
            backdrop-filter: blur(4px); transition: transform 0.2s, background 0.3s;
        }
        .ts-mini-ball:hover { background: rgba(29, 155, 240, 1); transform: scale(1.1); }
        .ts-panel {
            width: 320px; background: rgba(0, 0, 0, 0.85); backdrop-filter: blur(10px);
            border: 1px solid #333; border-radius: 16px; padding: 16px;
            box-shadow: 0 8px 32px rgba(0,0,0,0.5); color: #fff; display: none; flex-direction: column; gap: 12px;
        }
        .ts-header { display: flex; justify-content: space-between; align-items: center; cursor: move; border-bottom: 1px solid #333; padding-bottom: 8px; }
        .ts-title { font-weight: 700; font-size: 14px; color: #eff3f4; }
        .ts-btn-icon { background: none; border: none; color: #71767b; cursor: pointer; font-size: 16px; }
        input.ts-input { background: #202327; border: 1px solid #333; color: #eff3f4; padding: 8px 12px; border-radius: 20px; outline: none; width: 100%; box-sizing: border-box; }
        input.ts-input:focus { border-color: #1d9bf0; }
        .ts-row { display: flex; gap: 10px; align-items: center; }
        .ts-btn { flex: 1; padding: 8px; border-radius: 20px; border: none; font-weight: bold; cursor: pointer; font-size: 13px; transition: opacity 0.2s; }
        .ts-btn-primary { background: #1d9bf0; color: white; }
        .ts-btn-success { background: #00ba7c; color: white; }
        .ts-btn-danger { background: #f4212e; color: white; }
        .ts-btn:disabled { background: #555; cursor: not-allowed; }
        .ts-status { font-size: 12px; color: #71767b; text-align: center; min-height: 1.2em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        .ts-highlight { border: 2px solid #1d9bf0 !important; background: rgba(29, 155, 240, 0.1) !important; box-shadow: 0 0 15px rgba(29, 155, 240, 0.3); transition: all 0.5s; }
    `;

    const styleEl = document.createElement('style');
    styleEl.innerHTML = styles;
    document.head.appendChild(styleEl);

    function createUI() {
        const container = document.createElement('div');
        container.id = 'ts-floater';
        const savedPos = JSON.parse(localStorage.getItem('ts_pos') || '{"top":"100px","left":"20px"}');
        container.style.top = savedPos.top;
        container.style.left = savedPos.left;

        container.innerHTML = `
            <div class="ts-mini-ball" id="ts-ball" title="点击展开">🔍</div>
            <div class="ts-panel" id="ts-panel">
                <div class="ts-header" id="ts-header">
                    <span class="ts-title">X 进度同步器 (V5.1)</span>
                    <button class="ts-btn-icon" id="ts-minimize">_</button>
                </div>

                <input type="text" class="ts-input" id="ts-keyword" placeholder="输入关键词或等待同步...">

                <button class="ts-btn ts-btn-success" id="ts-sync-btn" style="width: 100%;">
                    📥 获取最新书签
                </button>

                <div class="ts-row">
                    <input type="number" class="ts-input" id="ts-speed" value="${scrollStep}" style="width: 80px;" placeholder="速度">
                    <button class="ts-btn ts-btn-primary" id="ts-start">开始搜索</button>
                    <button class="ts-btn ts-btn-danger" id="ts-stop" disabled>停止</button>
                </div>

                <div class="ts-status" id="ts-status">准备就绪</div>
            </div>
        `;

        document.body.appendChild(container);

        const ball = document.getElementById('ts-ball');
        const panel = document.getElementById('ts-panel');
        const minimizeBtn = document.getElementById('ts-minimize');
        const startBtn = document.getElementById('ts-start');
        const stopBtn = document.getElementById('ts-stop');
        const syncBtn = document.getElementById('ts-sync-btn');
        const statusText = document.getElementById('ts-status');
        const keywordInput = document.getElementById('ts-keyword');
        const speedInput = document.getElementById('ts-speed');
        const header = document.getElementById('ts-header');

        function toggleMode(showPanel) {
            panel.style.display = showPanel ? 'flex' : 'none';
            ball.style.display = showPanel ? 'none' : 'flex';
        }
        ball.addEventListener('click', () => toggleMode(true));
        minimizeBtn.addEventListener('click', () => toggleMode(false));

        let isDragging = false, startX, startY, initialLeft, initialTop;
        function startDrag(e) {
            if (['INPUT', 'BUTTON'].includes(e.target.tagName)) return;
            isDragging = true; startX = e.clientX; startY = e.clientY;
            const rect = container.getBoundingClientRect();
            initialLeft = rect.left; initialTop = rect.top;
            container.style.opacity = '0.8';
        }
        function onDrag(e) {
            if (!isDragging) return;
            e.preventDefault();
            container.style.left = (initialLeft + e.clientX - startX) + 'px';
            container.style.top = (initialTop + e.clientY - startY) + 'px';
        }
        function stopDrag() {
            if (isDragging) {
                isDragging = false; container.style.opacity = '1';
                localStorage.setItem('ts_pos', JSON.stringify({top: container.style.top, left: container.style.left}));
            }
        }
        ball.addEventListener('mousedown', startDrag);
        header.addEventListener('mousedown', startDrag);
        document.addEventListener('mousemove', onDrag);
        document.addEventListener('mouseup', stopDrag);

        // 监听同步结果
        window.addEventListener('storage', (e) => {
            if (e.key === 'ts_sync_result') {
                try {
                    const data = JSON.parse(e.newValue);
                    if (new Date().getTime() - data.timestamp < 5000) {
                        keywordInput.value = data.text;
                        statusText.textContent = "✅ 同步成功: " + data.text;
                        keywordInput.style.borderColor = '#00ba7c';
                        setTimeout(() => keywordInput.style.borderColor = '#333', 1500);
                    }
                } catch (err) {}
            }
        });

        syncBtn.addEventListener('click', () => {
            statusText.textContent = "正在提取...";
            window.open('https://x.com/i/bookmarks?ts_action=sync_bookmarks', '_blank');
        });

        const delay = ms => new Promise(r => setTimeout(r, ms));

        startBtn.addEventListener('click', () => {
            const keyword = keywordInput.value.trim();
            scrollStep = parseInt(speedInput.value) || 3000; // 允许面板动态修改

            if (!keyword) { statusText.textContent = "关键词为空"; return; }

            stopRequested = false; processedTweets.clear();
            statusText.textContent = `搜索中...`;
            startBtn.disabled = true; stopBtn.disabled = false;

            startScrolling(keyword);
        });

        stopBtn.addEventListener('click', () => {
            stopRequested = true; statusText.textContent = "已停止";
            startBtn.disabled = false; stopBtn.disabled = true;
        });

        async function startScrolling(keyword) {
            let sameHeightCount = 0;
            let lastHeight = 0;

            while (!stopRequested) {
                const tweets = document.querySelectorAll('[data-testid="tweet"]');
                let found = false;

                for (let tweet of tweets) {
                    const textBlock = tweet.querySelector('[data-testid="tweetText"]');
                    const tweetText = textBlock ? textBlock.innerText : '';
                    const id = tweetText.slice(0, 50);

                    if (processedTweets.has(id)) continue;
                    processedTweets.add(id);

                    if (tweetText.includes(keyword)) {
                        found = true;
                        tweet.scrollIntoView({ behavior: 'smooth', block: 'center' });
                        tweet.classList.add('ts-highlight');
                        stopRequested = true;
                        statusText.textContent = "🎉 找到位置!";
                        startBtn.disabled = false; stopBtn.disabled = true;
                        return;
                    }
                }

                if (!found) {
                    window.scrollBy({ top: scrollStep, behavior: 'smooth' });
                    await delay(800);

                    let newHeight = document.body.scrollHeight;
                    if (newHeight === lastHeight) {
                        sameHeightCount++;
                        if (sameHeightCount > 8) { // 增加容错次数
                            statusText.textContent = "到底了或未找到";
                            stopRequested = true;
                            startBtn.disabled = false; stopBtn.disabled = true;
                        }
                    } else {
                        sameHeightCount = 0;
                        lastHeight = newHeight;
                    }
                }
            }
        }
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', createUI);
    else createUI();

})();