// ==UserScript==
// @name 推特搜索助手-Twitter Search Assistant Enhanced
// @namespace example.twitter.enhanced
// @version 2.25
// @description 推特搜索助手(描述不变)
// @match https://twitter.com/*
// @match https://x.com/*
// @author Devol
// @grant none
// @license MIT
// @icon https://abs.twimg.com/favicons/twitter.2.ico
// @downloadURL none
// ==/UserScript==
(function () {
'use strict';
const S = {
containerId: 'tw-search-container',
assistantId: 'tw-search-assistant',
historyId: 'tw-history-panel',
keywordId: 'tw-keyword',
gridCls: 'preset-grid',
historyListCls: 'history-list',
modeIndicatorCls: 'mode-indicator',
btnApplyCls: 'btn-apply',
btnClearCls: 'btn-clear',
btnClearHistCls: 'clear-history',
presetBtnCls: 'preset-btn',
show: 'show',
hideSoft: 'hide-soft', // 新增:面板丝滑隐藏类(用于媒体期间)
selected: 'selected',
multi: 'multi-mode',
emptyHistoryHtml: '
暂无搜索历史
',
storageKey: 'tw-search-history',
};
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",
};
const MAX_HISTORY = 20;
const container = document.createElement('div');
container.id = S.containerId;
container.innerHTML = `
`;
const style = document.createElement('style');
style.textContent = `
#${S.containerId} {
position: fixed; top: 5px; right: 70px; display: flex; gap: 4px; z-index: 10000;
align-items: stretch; width: auto; min-width: 0;
will-change: transform; transform: translateZ(0); transition: transform .12s ease-out;
}
/* 媒体打开时:容器轻微上移 + 禁用指针(保留合成层,丝滑) */
#${S.containerId}.is-offscreen { transform: translate3d(0,-20px,0); pointer-events: none; }
#${S.assistantId}, #${S.historyId} {
background:#fff; border:1px solid #e1e8ed; border-radius:12px; box-shadow:0 4px 20px rgba(0,0,0,.08);
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif; font-size:13px; color:#0f1419;
opacity:0; visibility:hidden; transform: translateZ(0); will-change: opacity, visibility;
transition: opacity .12s ease-out, visibility .12s step-end;
}
#${S.assistantId} { width: 280px; flex-shrink: 0; }
#${S.historyId} { width: 160px; max-width: 160px; flex-shrink: 0; overflow: hidden; }
/* 常规显示 */
#${S.assistantId}.${S.show}, #${S.historyId}.${S.show} { opacity: 1; visibility: visible; }
/* 丝滑隐藏类:媒体期间叠加,避免“前端没缩回去”;并禁用交互 */
#${S.assistantId}.${S.hideSoft}, #${S.historyId}.${S.hideSoft} {
opacity: 0 !important; visibility: hidden !important; pointer-events: none !important;
transition: opacity .12s ease-out, visibility .12s step-end;
}
#${S.assistantId}:hover, #${S.historyId}: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; }
.${S.modeIndicatorCls}.clickable {
padding:6px 16px; font-size:12px; color:#536471; background:#f7f9fa; margin:0 16px 8px; border-radius:6px;
text-align:center; cursor:pointer; transition:background .12s ease-out; user-select:none;
}
.${S.modeIndicatorCls}.clickable:hover { background:#e1e8ed; }
.${S.modeIndicatorCls}.${S.multi} { background:#e8f5fe; color:#1da1f2; }
.${S.modeIndicatorCls}.${S.multi}:hover { background:#d0e9f9; }
.${S.btnClearHistCls} { background:none; border:none; cursor:pointer; font-size:14px; padding:2px 4px; border-radius:4px; transition:background .12s ease-out; flex-shrink:0; }
.${S.btnClearHistCls}:hover { background:#f7f9fa; }
.keyword-container { padding:0 16px 12px; }
#${S.keywordId} { width:100%; padding:8px 12px; border:1px solid #eff3f4; border-radius:8px; font-size:14px; outline:none; transition:border-color .12s ease-out; box-sizing:border-box; }
#${S.keywordId}:focus { border-color:#1da1f2; box-shadow:0 0 0 3px rgba(29,161,242,.1); }
.${S.gridCls} { display:grid; grid-template-columns:1fr 1fr; gap:6px; padding:0 16px 12px; }
.${S.presetBtnCls} {
padding:8px 12px; background:#f7f9fa; border:1px solid #eff3f4; border-radius:8px; color:#0f1419; cursor:pointer;
font-size:12px; transition:background .12s ease-out, border-color .12s ease-out; display:flex; align-items:center; gap:4px; box-sizing:border-box; overflow:hidden; white-space:nowrap; text-overflow:ellipsis;
}
.${S.presetBtnCls}:hover { background:#e8f5fe; border-color:#cfe5f7; }
.${S.presetBtnCls}.${S.selected} { background:#e8f5fe; color:#1da1f2; border-color:#1da1f2; }
.${S.presetBtnCls}.${S.selected}::after { content:'✓'; margin-left:auto; font-size:11px; font-weight:bold; }
.${S.historyListCls} { padding:4px 8px; max-height:400px; overflow:auto; }
.history-item { padding:6px 8px; border-radius:6px; cursor:pointer; transition:background .12s ease-out; font-size:12px; color:#0f1419; margin-bottom:2px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; 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; }
.action-buttons { display:flex; gap:8px; padding:0 16px 16px; }
.${S.btnClearCls}, .${S.btnApplyCls} { flex:1; padding:8px; border:none; border-radius:8px; font-size:13px; cursor:pointer; transition:background .12s ease-out; font-weight:500; }
.${S.btnClearCls} { background:#f7f9fa; color:#536471; border:1px solid #eff3f4; }
.${S.btnClearCls}:hover { background:#e1e8ed; }
.${S.btnApplyCls} { background:#1da1f2; color:#fff; }
.${S.btnApplyCls}:hover { background:#1a91da; }
.${S.btnApplyCls}.active { background:#17bf63; box-shadow:0 2px 8px rgba(23,191,99,.3); }
@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
`;
document.head.append(style);
document.body.append(container);
const $ = {
container,
assistant: container.querySelector('#' + S.assistantId),
history: container.querySelector('#' + S.historyId),
mode: container.querySelector('.' + S.modeIndicatorCls),
apply: container.querySelector('.' + S.btnApplyCls),
clear: container.querySelector('.' + S.btnClearCls),
clearHist: container.querySelector('.' + S.btnClearHistCls),
historyList: container.querySelector('.' + S.historyListCls),
keyword: container.querySelector('#' + S.keywordId),
grid: container.querySelector('.' + S.gridCls),
};
let isMulti = false;
const selected = new Set();
const on = (el, ev, fn, opt) => el.addEventListener(ev, fn, opt);
const toggleClass = (el, cls, cond) => { if (cond) el.classList.add(cls); else el.classList.remove(cls); };
const escapeHtml = (t) => { const div = document.createElement('div'); div.textContent = t; return div.innerHTML; };
const getHistory = () => { try { return JSON.parse(localStorage.getItem(S.storageKey) || '[]'); } catch { return []; } };
const setHistory = (arr) => { try { localStorage.setItem(S.storageKey, JSON.stringify(arr)); } catch {} };
const saveHistory = (kw) => {
if (!kw || !(kw = kw.trim())) return;
const hist = getHistory();
const idx = hist.indexOf(kw);
if (idx >= 0) hist.splice(idx, 1);
hist.unshift(kw);
if (hist.length > MAX_HISTORY) hist.length = MAX_HISTORY;
setHistory(hist);
renderHistory(hist);
};
const clearAllHistory = () => { try { localStorage.removeItem(S.storageKey); } catch {} renderHistory([]); };
const renderHistory = (hist = getHistory()) => {
if (hist.length === 0) { $.historyList.innerHTML = S.emptyHistoryHtml; return; }
$.historyList.innerHTML = hist.map(item =>
`${escapeHtml(item)}
`
).join('');
};
const extractKeywordOnly = (url) => {
const i = url.indexOf('/search?q=');
if (i < 0) return '';
const m = url.slice(i).match(/\/search\?q=([^&]+)/);
if (!m) return '';
try {
const query = decodeURIComponent(m[1]);
return query.split(/\s+(?:filter:|lang:|min_faves:|since:|from:|to:|until:|OR|AND|NOT)/)[0].trim();
} catch { return ''; }
};
const initButtons = () => {
const frag = document.createDocumentFragment();
Object.keys(presets).forEach(name => {
const b = document.createElement('button');
b.className = S.presetBtnCls;
b.textContent = name;
b.dataset.filter = presets[name];
frag.appendChild(b);
});
$.grid.appendChild(frag);
};
on($.grid, 'click', (e) => {
const btn = e.target.closest('.' + S.presetBtnCls);
if (!btn) return;
const filter = btn.dataset.filter;
if (isMulti) {
const has = selected.has(filter);
if (has) { selected.delete(filter); btn.classList.remove(S.selected); }
else { selected.add(filter); btn.classList.add(S.selected); }
updateApply();
return;
}
const kw = ($.keyword.value.trim() || extractKeywordOnly(location.href));
if (!kw) { alert('请输入关键词'); return; }
saveHistory(kw);
location.href = `https://twitter.com/search?q=${encodeURIComponent(kw + ' ' + filter)}&src=typed_query&f=top`;
});
on($.historyList, 'click', (e) => {
const item = e.target.closest('.history-item');
if (!item) return;
const kw = decodeURIComponent(item.dataset.k || '');
if (kw) $.keyword.value = kw;
});
on($.clear, 'click', () => { selected.clear(); $.grid.querySelectorAll('.' + S.selected).forEach(b => b.classList.remove(S.selected)); updateApply(); });
on($.apply, 'click', () => {
if (!isMulti || selected.size === 0) { alert('多选模式下请至少选择一个筛选条件'); return; }
const kw = ($.keyword.value.trim() || extractKeywordOnly(location.href));
if (!kw) { alert('请输入关键词'); return; }
saveHistory(kw);
const finalQuery = `${kw} ${Array.from(selected).join(' ')}`.trim();
location.href = `https://twitter.com/search?q=${encodeURIComponent(finalQuery)}&src=typed_query&f=top`;
});
on($.mode, 'click', () => {
isMulti = !isMulti;
$.mode.textContent = isMulti ? '多选模式' : '单选模式';
toggleClass($.mode, S.multi, isMulti);
$.apply.style.display = isMulti ? 'block' : 'none';
selected.clear(); $.grid.querySelectorAll('.' + S.selected).forEach(b => b.classList.remove(S.selected));
updateApply();
});
on($.clearHist, 'click', clearAllHistory);
const updateApply = () => {
if (!isMulti) return;
const n = selected.size;
$.apply.classList.toggle('active', n > 0);
$.apply.textContent = n > 0 ? `应用搜索(${n})` : '应用搜索';
};
const observeUrlChanges = () => {
let current = location.href;
let t;
const handler = () => {
if (location.href === current) return;
current = location.href;
const kw = extractKeywordOnly(current);
if (kw && !$.keyword.value) $.keyword.value = kw;
if (selected.size) { selected.clear(); $.grid.querySelectorAll('.' + S.selected).forEach(b => b.classList.remove(S.selected)); updateApply(); }
};
const emit = () => { clearTimeout(t); t = setTimeout(handler, 50); };
const wrap = (fn) => function(...args){ const r = fn.apply(this, args); emit(); return r; };
history.pushState = wrap(history.pushState);
history.replaceState = wrap(history.replaceState);
window.addEventListener('popstate', emit);
};
//
const initMediaDetection = () => {
const isMediaOpen = () =>
document.querySelector('[aria-modal="true"],[data-testid="swipe-to-dismiss-container"],[data-testid="media-modal"]');
let lastOff = false;
let rafId;
const setPanelsHidden = (hidden) => {
$.container.classList.toggle('is-offscreen', hidden);
$.assistant.classList.toggle(S.hideSoft, hidden);
$.history.classList.toggle(S.hideSoft, hidden);
};
const apply = (off) => {
if (off === lastOff) return;
lastOff = off;
setPanelsHidden(off);
};
const scheduleCheck = () => {
if (rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
const open = !!isMediaOpen();
setTimeout(() => apply(open), 0);
});
};
const obs = new MutationObserver((muts) => {
for (const m of muts) { if (m.type === 'childList') { scheduleCheck(); break; } }
});
obs.observe(document.body, { childList: true, subtree: true });
apply(!!isMediaOpen());
};
const boot = () => {
initButtons();
renderHistory();
const kw = extractKeywordOnly(location.href);
if (kw && !$.keyword.value) $.keyword.value = kw;
observeUrlChanges();
initMediaDetection();
requestAnimationFrame(() => {
$.assistant.classList.add(S.show);
$.history.classList.add(S.show);
});
};
if ('requestIdleCallback' in window) {
requestIdleCallback(boot, { timeout: 800 });
} else {
setTimeout(boot, 0);
}
})();