// ==UserScript== // @name X 推文关键词逐条搜索滚动器(增强版) // @namespace http://tampermonkey.net/ // @version 2.3 // @description 一条一条匹配推文关键词,找到后将其定位在页面顶部,支持停止按钮,暗黑界面优化,美观浮窗,滚动速度可调 // @author _Sure.Lee // @match https://x.com/* // @match https://twitter.com/* // @grant none // @downloadURL none // ==/UserScript== (function () { 'use strict'; let stopRequested = false; let processedTweets = new Set(); let currentSearchKeyword = ''; let scrollStep = 1000; // 默认滚动像素 let searchStatus = '准备就绪'; let matchCount = 0; let totalProcessed = 0; function createSearchUI() { const container = document.createElement('div'); container.id = 'custom-search-ui'; container.style.position = 'fixed'; container.style.top = '10px'; container.style.left = '10px'; container.style.zIndex = '9999'; container.style.backgroundColor = '#1e1e1e'; container.style.color = '#f5f5f5'; container.style.border = '1px solid #444'; container.style.padding = '10px'; container.style.borderRadius = '10px'; container.style.boxShadow = '0 4px 12px rgba(0,0,0,0.6)'; container.style.fontSize = '14px'; container.style.display = 'flex'; container.style.flexDirection = 'column'; container.style.gap = '8px'; container.style.minWidth = '300px'; container.style.transition = 'height 0.3s ease'; let isCollapsed = false; const header = document.createElement('div'); header.style.display = 'flex'; header.style.justifyContent = 'space-between'; header.style.alignItems = 'center'; header.style.cursor = 'move'; const title = document.createElement('div'); title.textContent = 'X 推文搜索'; title.style.fontWeight = 'bold'; title.style.color = '#ff9632'; const collapseBtn = document.createElement('button'); collapseBtn.textContent = '—'; collapseBtn.style.background = 'none'; collapseBtn.style.border = 'none'; collapseBtn.style.fontSize = '18px'; collapseBtn.style.cursor = 'pointer'; collapseBtn.style.color = '#aaa'; collapseBtn.addEventListener('click', () => { isCollapsed = !isCollapsed; contentArea.style.display = isCollapsed ? 'none' : 'flex'; collapseBtn.textContent = isCollapsed ? '+' : '—'; container.style.height = isCollapsed ? '30px' : 'auto'; }); header.appendChild(title); header.appendChild(collapseBtn); const contentArea = document.createElement('div'); contentArea.style.display = 'flex'; contentArea.style.flexDirection = 'column'; contentArea.style.gap = '6px'; const input = document.createElement('input'); input.type = 'text'; input.placeholder = '输入关键词'; input.style.padding = '6px'; input.style.borderRadius = '4px'; input.style.border = '1px solid #666'; input.style.backgroundColor = '#2a2a2a'; input.style.color = '#fff'; const scrollSpeedInput = document.createElement('input'); scrollSpeedInput.type = 'number'; scrollSpeedInput.placeholder = '滚动速度'; scrollSpeedInput.min = '100'; scrollSpeedInput.step = '100'; scrollSpeedInput.value = scrollStep; scrollSpeedInput.style.width = '30%'; scrollSpeedInput.style.padding = '6px'; scrollSpeedInput.style.borderRadius = '4px'; scrollSpeedInput.style.border = '1px solid #666'; scrollSpeedInput.style.backgroundColor = '#2a2a2a'; scrollSpeedInput.style.color = '#ccc'; scrollSpeedInput.title = '滚动步长(像素)'; scrollSpeedInput.addEventListener('change', () => { scrollStep = parseInt(scrollSpeedInput.value) || 1000; }); const searchBtn = document.createElement('button'); searchBtn.textContent = '开始搜索'; searchBtn.style.padding = '6px'; searchBtn.style.backgroundColor = '#007acc'; searchBtn.style.border = 'none'; searchBtn.style.color = '#fff'; searchBtn.style.borderRadius = '4px'; searchBtn.style.cursor = 'pointer'; searchBtn.style.width = '48%'; const stopBtn = document.createElement('button'); stopBtn.textContent = '停止'; stopBtn.style.padding = '6px'; stopBtn.style.backgroundColor = '#c62828'; stopBtn.style.border = 'none'; stopBtn.style.color = '#fff'; stopBtn.style.borderRadius = '4px'; stopBtn.style.cursor = 'pointer'; stopBtn.style.width = '48%'; const statusText = document.createElement('div'); statusText.textContent = searchStatus; statusText.style.fontSize = '12px'; statusText.style.color = '#aaa'; contentArea.appendChild(input); contentArea.appendChild(scrollSpeedInput); contentArea.appendChild(searchBtn); contentArea.appendChild(stopBtn); contentArea.appendChild(statusText); container.appendChild(header); container.appendChild(contentArea); document.body.appendChild(container); let isDragging = false; let offsetX, offsetY; header.addEventListener('mousedown', (e) => { isDragging = true; offsetX = e.clientX - container.getBoundingClientRect().left; offsetY = e.clientY - container.getBoundingClientRect().top; }); document.addEventListener('mousemove', (e) => { if (isDragging) { container.style.left = (e.clientX - offsetX) + 'px'; container.style.top = (e.clientY - offsetY) + 'px'; } }); document.addEventListener('mouseup', () => isDragging = false); searchBtn.addEventListener('click', () => { const keyword = input.value.trim(); if (keyword) { stopRequested = false; currentSearchKeyword = keyword; searchStatus = '开始搜索: ' + keyword; statusText.textContent = searchStatus; processedTweets.clear(); startScrolling(keyword); } }); stopBtn.addEventListener('click', () => { stopRequested = true; searchStatus = '已停止'; statusText.textContent = searchStatus; }); } async function startScrolling(keyword) { const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); while (!stopRequested) { const tweets = document.querySelectorAll('[data-testid="tweet"]'); for (let tweet of tweets) { const textBlock = tweet.querySelector('[data-testid="tweetText"]'); const tweetText = textBlock?.innerText || ''; const id = tweet.getAttribute('data-tweet-id') || tweet.innerText.slice(0, 50); if (processedTweets.has(id)) continue; processedTweets.add(id); totalProcessed++; if (tweetText.includes(keyword)) { matchCount++; tweet.scrollIntoView({ behavior: 'smooth', block: 'start' }); tweet.style.border = '2px solid #ff9632'; tweet.style.backgroundColor = '#333'; return; } } window.scrollBy({ top: scrollStep, behavior: 'smooth' }); await delay(600); } } function init() { if (!document.getElementById('custom-search-ui')) { createSearchUI(); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();