Greasy Fork

Greasy Fork is available in English.

推特搜索助手-Twitter Search Assistant Enhanced

推特搜索助手(描述不变)

// ==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);
})();