Greasy Fork is available in English.
推特搜索助手(描述不变)
// ==UserScript==
// @name 推特搜索助手-Twitter Search Assistant Enhanced
// @namespace example.twitter.enhanced
// @version 2.24
// @description 推特搜索助手(描述不变)
// @match https://twitter.com/*
// @match https://x.com/*
// @grant none
// @license MIT
// @icon https://abs.twimg.com/favicons/twitter.2.ico
// ==/UserScript==
(function () {
'use strict';
// 定义搜索预设
const presets = {
"📷 图片": "filter:images -filter:retweets -filter:replies",
"🎬 视频": "filter:videos -filter:retweets -filter:replies",
"🔥 高热度": "min_faves:200 -filter:retweets",
"🈶 日语": "lang:ja -filter:retweets -filter:replies",
"🌎 英语": "lang:en -filter:retweets -filter:replies",
"⏰ 近期": "within_time:180d -filter:retweets", // 最近半年(180天)
};
// 历史记录管理
const MAX_HISTORY = 20;
// 主面板HTML
const container = document.createElement('div');
container.id = 'tw-search-container';
container.innerHTML = `
<div id="tw-search-assistant">
<div class="panel-header header-row">
<span class="tw-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="16" height="16" fill="#1da1f2" style="display:block">
<path d="M19.633 7.997c.013.178.013.355.013.533 0 5.42-4.127 11.675-11.675 11.675-2.32 0-4.474-.682-6.287-1.855.321.038.63.05.964.05a8.258 8.258 0 0 0 5.123-1.767 4.129 4.129 0 0 1-3.853-2.86c.25.038.5.063.763.063.367 0 .733-.05 1.075-.138A4.123 4.123 0 0 1 2.8 9.71v-.05c.551.304 1.19.488 1.867.513A4.116 4.116 0 0 1 2.87 6.3c0-.763.203-1.463.558-2.075a11.71 11.71 0 0 0 8.497 4.312 4.65 4.65 0 0 1-.101-.945 4.12 4.12 0 0 1 7.134-2.82 8.13 8.13 0 0 0 2.617-.995 4.13 4.13 0 0 1-1.812 2.28 8.26 8.26 0 0 0 2.372-.639 8.86 8.86 0 0 1-1.902 1.579z"></path>
</svg>
</span>
<span>搜索助手</span>
</div>
<div class="mode-indicator clickable">单选模式</div>
<div class="keyword-container">
<input id="tw-keyword" type="text" placeholder="输入关键词(自动获取当前搜索词)">
</div>
<div class="preset-grid"></div>
<div class="action-buttons">
<button class="btn-clear">清空</button>
<button class="btn-apply" style="display: none;">应用搜索</button>
</div>
</div>
<div id="tw-history-panel">
<div class="history-header header-row header-split">
<span>搜索历史</span>
<button class="clear-history" title="清空历史">🗑️</button>
</div>
<div class="history-list"></div>
</div>
`;
// 样式(合并公共头部样式、保留原有外观)
const style = document.createElement('style');
style.textContent = `
#tw-search-container {
position: fixed;
top: 5px;
right: 70px;
display: flex;
gap: 4px;
z-index: 10000;
align-items: stretch;
width: auto;
min-width: 0;
}
#tw-search-assistant, #tw-history-panel {
background: #ffffff;
border: 1px solid #e1e8ed;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
font-size: 13px;
color: #0f1419;
transition: all 0.3s ease;
opacity: 0;
transform: translateY(-10px);
box-sizing: border-box;
}
#tw-search-assistant { width: 280px; min-width: 0; flex-shrink: 0; }
#tw-history-panel { width: 160px; min-width: 0; max-width: 160px; flex-shrink: 0; overflow: hidden; }
#tw-search-assistant.show, #tw-history-panel.show { opacity: 1; transform: translateY(0); }
#tw-search-assistant.hidden, #tw-history-panel.hidden { opacity: 0 !important; pointer-events: none !important; z-index: -1 !important; }
#tw-search-assistant:hover, #tw-history-panel:hover { box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12); }
.header-row {
display: flex;
align-items: center;
font-weight: 600;
border-bottom: 1px solid #eff3f4;
width: 100%;
box-sizing: border-box;
overflow: hidden;
padding: 12px 16px 8px;
gap: 6px;
justify-content: flex-start;
}
.header-split { justify-content: space-between; gap: 0; padding: 10px 12px 6px; }
.tw-icon {
display: inline-flex;
align-items: center;
justify-content: center;
margin-right: 2px;
width: 16px;
height: 16px;
flex: 0 0 16px;
}
.mode-indicator.clickable {
padding: 6px 16px;
font-size: 12px;
color: #536471;
background: #f7f9fa;
margin: 0 16px 8px;
border-radius: 6px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
user-select: none;
}
.mode-indicator.clickable:hover { background: #e1e8ed; }
.mode-indicator.multi-mode { background: #e8f5fe; color: #1da1f2; }
.mode-indicator.multi-mode:hover { background: #d0e9f9; }
.clear-history {
background: none; border: none; cursor: pointer; font-size: 14px;
padding: 2px 4px; border-radius: 4px; transition: background 0.2s; flex-shrink: 0;
}
.clear-history:hover { background: #f7f9fa; }
.keyword-container { padding: 0 16px 12px; width: 100%; box-sizing: border-box; }
#tw-keyword {
width: 100%; padding: 8px 12px; border: 1px solid #eff3f4; border-radius: 8px; font-size: 14px;
outline: none; transition: border-color 0.2s; box-sizing: border-box;
}
#tw-keyword:focus { border-color: #1da1f2; box-shadow: 0 0 0 3px rgba(29, 161, 242, 0.1); }
.preset-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; padding: 0 16px 12px; width: 100%; box-sizing: border-box; }
.preset-btn {
padding: 8px 12px; background: #f7f9fa; border: 1px solid #eff3f4; border-radius: 8px; color: #0f1419;
cursor: pointer; font-size: 12px; transition: all 0.2s; display: flex; align-items: center; gap: 4px;
box-sizing: border-box; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.preset-btn:hover { background: #e8f5fe; border-color: #cfe5f7; }
.preset-btn.selected { background: #e8f5fe; color: #1da1f2; border-color: #1da1f2; }
.preset-btn.selected::after { content: '✓'; margin-left: auto; font-size: 11px; font-weight: bold; }
.history-list {
padding: 4px 8px; max-height: 400px; overflow-y: auto; width: 100%; box-sizing: border-box; overflow-x: hidden;
}
.history-item {
padding: 6px 8px; border-radius: 6px; cursor: pointer; transition: background 0.2s; font-size: 12px; color: #0f1419;
margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%; max-width: 100%;
box-sizing: border-box; display: block;
}
.history-item:hover { background: #f7f9fa; }
.history-item:active { background: #e1e8ed; }
.empty-history { padding: 16px 8px; text-align: center; color: #536471; font-size: 11px; width: 100%; box-sizing: border-box; }
.action-buttons { display: flex; gap: 8px; padding: 0 16px 16px; width: 100%; box-sizing: border-box; }
.btn-clear, .btn-apply {
flex: 1; padding: 8px; border: none; border-radius: 8px; font-size: 13px; cursor: pointer; transition: all 0.2s; font-weight: 500;
box-sizing: border-box; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.btn-clear { background: #f7f9fa; color: #536471; border: 1px solid #eff3f4; }
.btn-clear:hover { background: #e1e8ed; }
.btn-apply { background: #1da1f2; color: white; }
.btn-apply:hover { background: #1a91da; }
.btn-apply.active { background: #17bf63; box-shadow: 0 2px 8px rgba(23, 191, 99, 0.3); }
`;
document.head.appendChild(style);
document.body.appendChild(container);
// 缓存常用DOM节点
const assistantEl = document.getElementById('tw-search-assistant');
const historyPanelEl = document.getElementById('tw-history-panel');
const modeIndicatorEl = container.querySelector('.mode-indicator');
const applyBtnEl = container.querySelector('.btn-apply');
const clearBtnEl = container.querySelector('.btn-clear');
const clearHistoryEl = container.querySelector('.clear-history');
const historyListEl = container.querySelector('.history-list');
const keywordInputEl = container.querySelector('#tw-keyword');
const gridEl = container.querySelector('.preset-grid');
// 状态管理
let isMultiSelectMode = false;
let selectedPresets = new Set();
// 历史记录功能
function getHistory() {
try {
const history = localStorage.getItem('tw-search-history');
return history ? JSON.parse(history) : [];
} catch (e) {
return [];
}
}
function saveHistory(keyword) {
if (!keyword || keyword.trim() === '') return;
keyword = keyword.trim();
let history = getHistory();
history = history.filter(item => item !== keyword);
history.unshift(keyword);
if (history.length > MAX_HISTORY) history = history.slice(0, MAX_HISTORY);
try {
localStorage.setItem('tw-search-history', JSON.stringify(history));
} catch (e) {}
renderHistory();
}
function clearAllHistory() {
try {
localStorage.removeItem('tw-search-history');
renderHistory();
} catch (e) {}
}
function renderHistory() {
const history = getHistory();
if (history.length === 0) {
historyListEl.innerHTML = '<div class="empty-history">暂无搜索历史</div>';
return;
}
historyListEl.innerHTML = history.map(item =>
`<div class="history-item" data-keyword="${encodeURIComponent(item)}">${escapeHtml(item)}</div>`
).join('');
historyListEl.querySelectorAll('.history-item').forEach(item => {
item.addEventListener('click', () => {
const keyword = decodeURIComponent(item.getAttribute('data-keyword'));
keywordInputEl.value = keyword;
});
});
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 媒体层检测(仅childList,减少无谓触发)
function initMediaDetection() {
const observer = new MutationObserver(() => {
const modal = document.querySelector('[aria-modal="true"]') ||
document.querySelector('[data-testid="swipe-to-dismiss-container"]') ||
document.querySelector('[data-testid="media-modal"]');
if (modal) {
assistantEl.classList.add('hidden');
historyPanelEl.classList.add('hidden');
} else {
assistantEl.classList.remove('hidden');
historyPanelEl.classList.remove('hidden');
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
// 仅提取关键词
function extractKeywordOnly(url) {
if (!url.includes('/search?q=')) return '';
try {
const match = url.match(/\/search\?q=([^&]+)/);
if (!match) return '';
const query = decodeURIComponent(match[1]);
const keyword = query.split(/\s+(?:filter:|lang:|min_faves:|since:|from:|to:|until:|OR|AND|NOT)/)[0].trim();
return keyword;
} catch (e) {
return '';
}
}
// 处理预设点击
function handlePresetClick(btn, filter) {
if (isMultiSelectMode) {
if (selectedPresets.has(filter)) {
selectedPresets.delete(filter);
btn.classList.remove('selected');
} else {
selectedPresets.add(filter);
btn.classList.add('selected');
}
updateApplyButton();
} else {
const keyword = keywordInputEl.value.trim() || extractKeywordOnly(window.location.href);
if (!keyword) {
alert('请输入关键词');
return;
}
saveHistory(keyword);
const searchUrl = `https://twitter.com/search?q=${encodeURIComponent(keyword + ' ' + filter)}&src=typed_query&f=top`;
window.location.href = searchUrl;
}
}
// 更新应用按钮
function updateApplyButton() {
if (isMultiSelectMode && selectedPresets.size > 0) {
applyBtnEl.classList.add('active');
applyBtnEl.textContent = `应用搜索(${selectedPresets.size})`;
} else if (isMultiSelectMode) {
applyBtnEl.classList.remove('active');
applyBtnEl.textContent = '应用搜索';
}
}
// 多选模式搜索
function applyMultiSelect() {
if (!isMultiSelectMode || selectedPresets.size === 0) {
alert('多选模式下请至少选择一个筛选条件');
return;
}
const keyword = keywordInputEl.value.trim() || extractKeywordOnly(window.location.href);
if (!keyword) {
alert('请输入关键词');
return;
}
saveHistory(keyword);
const selectedFilters = Array.from(selectedPresets).join(' ');
const finalQuery = `${keyword} ${selectedFilters}`;
const searchUrl = `https://twitter.com/search?q=${encodeURIComponent(finalQuery.trim())}&src=typed_query&f=top`;
window.location.href = searchUrl;
}
// 清空选择
function clearSelection() {
selectedPresets.clear();
container.querySelectorAll('.preset-btn.selected').forEach(btn => btn.classList.remove('selected'));
updateApplyButton();
}
// 切换模式
function toggleMode() {
isMultiSelectMode = !isMultiSelectMode;
if (isMultiSelectMode) {
modeIndicatorEl.textContent = '多选模式';
modeIndicatorEl.classList.add('multi-mode');
applyBtnEl.style.display = 'block';
clearSelection();
} else {
modeIndicatorEl.textContent = '单选模式';
modeIndicatorEl.classList.remove('multi-mode');
applyBtnEl.style.display = 'none';
clearSelection();
}
}
// 自动填充关键词
function autoFillKeyword() {
const keyword = extractKeywordOnly(window.location.href);
if (keyword && !keywordInputEl.value) {
keywordInputEl.value = keyword;
}
}
// 初始化按钮
function initButtons() {
Object.keys(presets).forEach(name => {
const btn = document.createElement('button');
btn.className = 'preset-btn';
btn.textContent = name;
btn.onclick = () => handlePresetClick(btn, presets[name]);
gridEl.appendChild(btn);
});
}
// 无轮询的 URL 监听
function observeUrlChanges() {
let current = location.href;
const handler = () => {
if (location.href !== current) {
current = location.href;
autoFillKeyword();
clearSelection();
}
};
const wrap = (fnName) => {
const raw = history[fnName];
history[fnName] = function(...args) {
const ret = raw.apply(this, args);
window.dispatchEvent(new Event('locationchange'));
return ret;
};
};
wrap('pushState'); wrap('replaceState');
window.addEventListener('popstate', () => window.dispatchEvent(new Event('locationchange')));
window.addEventListener('locationchange', handler);
}
// 绑定事件
modeIndicatorEl.onclick = toggleMode;
clearBtnEl.onclick = clearSelection;
applyBtnEl.onclick = applyMultiSelect;
clearHistoryEl.onclick = clearAllHistory;
// 初始化
document.head.appendChild(style);
document.body.appendChild(container);
initButtons();
autoFillKeyword();
renderHistory();
initMediaDetection();
observeUrlChanges();
// 显示面板
setTimeout(() => {
assistantEl.classList.add('show');
historyPanelEl.classList.add('show');
}, 100);
})();