Greasy Fork is available in English.
一条一条匹配推文关键词,支持悬浮球模式,不遮挡视野。找到后定位页面顶部,支持停止、暗黑界面优化。
当前为
// ==UserScript==
// @name X 推文关键词逐条搜索滚动器(隐形悬浮版)
// @namespace http://tampermonkey.net/
// @version 3.0
// @description 一条一条匹配推文关键词,支持悬浮球模式,不遮挡视野。找到后定位页面顶部,支持停止、暗黑界面优化。
// @author @喂你吃药
// @match https://x.com/*
// @match https://twitter.com/*
// @grant GM_addStyle
// ==/UserScript==
(function () {
'use strict';
// --- 核心配置 ---
let stopRequested = false;
let processedTweets = new Set();
let scrollStep = 1000;
let isRunning = false;
// --- CSS 样式注入 (现代化 UI) ---
const styles = `
#ts-floater {
position: fixed;
z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
user-select: none;
transition: opacity 0.3s;
}
/* 悬浮小球模式 */
.ts-mini-ball {
width: 40px;
height: 40px;
background: rgba(29, 155, 240, 0.6); /* Twitter Blue 半透明 */
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;
margin-bottom: 4px;
}
.ts-title {
font-weight: 700;
font-size: 14px;
color: #eff3f4;
}
.ts-controls {
display: flex;
gap: 8px;
}
.ts-btn-icon {
background: none;
border: none;
color: #71767b;
cursor: pointer;
font-size: 16px;
padding: 0 4px;
}
.ts-btn-icon:hover { color: #1d9bf0; }
input.ts-input {
background: #202327;
border: 1px solid #333;
color: #eff3f4;
padding: 8px 12px;
border-radius: 20px;
outline: none;
font-size: 14px;
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-danger { background: #f4212e; color: white; }
.ts-btn:hover { opacity: 0.9; }
.ts-btn:disabled { background: #555; cursor: not-allowed; }
.ts-status {
font-size: 12px;
color: #71767b;
text-align: center;
min-height: 1.2em;
}
/* 高亮样式 */
.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;
}
`;
// 注入 CSS
const styleEl = document.createElement('style');
styleEl.innerHTML = styles;
document.head.appendChild(styleEl);
// --- UI 构建 ---
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;
// HTML 结构
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 推文搜索器</span>
<button class="ts-btn-icon" id="ts-minimize" title="最小化">_</button>
</div>
<input type="text" class="ts-input" id="ts-keyword" placeholder="输入关键词...">
<div class="ts-row">
<input type="number" class="ts-input" id="ts-speed" value="${scrollStep}" style="width: 80px;" placeholder="速度" title="滚动像素/次">
<span style="font-size:12px; color:#71767b;">像素/次</span>
</div>
<div class="ts-row">
<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 statusText = document.getElementById('ts-status');
const header = document.getElementById('ts-header');
const keywordInput = document.getElementById('ts-keyword');
const speedInput = document.getElementById('ts-speed');
// --- 交互逻辑 ---
// 切换显示模式
function toggleMode(showPanel) {
if (showPanel) {
ball.style.display = 'none';
panel.style.display = 'flex';
} else {
panel.style.display = 'none';
ball.style.display = 'flex';
}
}
ball.addEventListener('click', () => toggleMode(true));
minimizeBtn.addEventListener('click', () => toggleMode(false));
// 拖拽逻辑 (通用)
let isDragging = false;
let 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();
const dx = e.clientX - startX;
const dy = e.clientY - startY;
const newLeft = initialLeft + dx;
const newTop = initialTop + dy;
container.style.left = newLeft + 'px';
container.style.top = newTop + '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);
// --- 搜索业务逻辑 ---
startBtn.addEventListener('click', () => {
const keyword = keywordInput.value.trim();
if (!keyword) {
statusText.textContent = "请输入关键词";
return;
}
scrollStep = parseInt(speedInput.value) || 1000;
stopRequested = false;
isRunning = true;
processedTweets.clear();
// UI 状态更新
statusText.textContent = `正在搜索: ${keyword}...`;
startBtn.disabled = true;
stopBtn.disabled = false;
startScrolling(keyword);
});
stopBtn.addEventListener('click', () => {
stopRequested = true;
statusText.textContent = "已手动停止";
isRunning = false;
startBtn.disabled = false;
stopBtn.disabled = true;
});
// 辅助函数:延迟
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
// 滚动逻辑
async function startScrolling(keyword) {
let notFoundCount = 0;
while (!stopRequested) {
const tweets = document.querySelectorAll('[data-testid="tweet"]');
let foundInBatch = false;
for (let tweet of tweets) {
// 获取推文唯一标识(优先用链接,其次用文本hash模拟)
const linkNode = tweet.querySelector('a[href*="/status/"]');
const id = linkNode ? linkNode.href : tweet.innerText.slice(0, 30);
if (processedTweets.has(id)) continue;
processedTweets.add(id);
const textBlock = tweet.querySelector('[data-testid="tweetText"]');
const tweetText = textBlock ? textBlock.innerText : '';
if (tweetText.includes(keyword)) {
foundInBatch = true;
// 找到目标
tweet.scrollIntoView({ behavior: 'smooth', block: 'center' });
// 高亮处理
tweet.classList.add('ts-highlight');
// 自动停止
stopRequested = true;
isRunning = false;
statusText.textContent = `找到匹配!`;
startBtn.disabled = false;
stopBtn.disabled = true;
// 稍微闪烁一下 UI 提醒
panel.style.borderColor = '#1d9bf0';
setTimeout(() => panel.style.borderColor = '#333', 1000);
return; // 退出函数
}
}
// 没找到,继续滚
if (!foundInBatch) {
notFoundCount++;
}
window.scrollBy({ top: scrollStep, behavior: 'smooth' });
// 动态调整等待时间,防止网速慢加载不出来
await delay(800);
// 简单的防死循环机制(如果页面到底了或者很久没新内容)
// 这里可以根据需要添加逻辑,比如检测高度是否变化
}
}
}
// --- 初始化 ---
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createUI);
} else {
createUI();
}
})();