SearXNG搜尋選項增強介面 🔍️

為 SearXNG 新增詳細搜尋選項側邊欄,僅保留英文與日文並啟用自動偵測。側邊欄可拖曳移動並保存位置。

// ==UserScript==
// @name         SearXNG検索オプション強化UI 🔍️
// @name:ja      SearXNG検索オプション強化UI 🔍️
// @name:en      Enhanced Search Options UI for SearXNG 🔍️
// @name:zh-CN   SearXNG搜索选项增强界面 🔍️
// @name:zh-TW   SearXNG搜尋選項增強介面 🔍️
// @name:ko      SearXNG 검색 옵션 강화 UI 🔍️
// @name:fr      Interface améliorée pour les options de recherche SearXNG 🔍️
// @name:es      Interfaz mejorada de opciones de búsqueda para SearXNG 🔍️
// @name:de      Verbesserte Suchoptionen-Oberfläche für SearXNG 🔍️
// @name:pt-BR   Interface aprimorada de opções de pesquisa para SearXNG 🔍️
// @name:ru      Улучшенный интерфейс опций поиска SearXNG 🔍️
// @version      3.9.1
// @description         SearXNG検索エンジンに詳細検索オプションサイドバーを追加(言語選択も自動検出と英語と日本語のみにしてすっきり)。サイドバーはドラッグで移動でき、位置も保存されます。
// @description:en      Adds a detailed search options sidebar to SearXNG. Simplifies language selection to English and Japanese with auto-detection. The sidebar is draggable and its position is persisted.
// @description:zh-CN   为SearXNG添加详细搜索选项侧边栏,仅保留英文与日文并启用自动检测。侧边栏支持拖拽移动并可保存位置。
// @description:zh-TW   為 SearXNG 新增詳細搜尋選項側邊欄,僅保留英文與日文並啟用自動偵測。側邊欄可拖曳移動並保存位置。
// @description:ko      SearXNG에 상세 검색 옵션 사이드바를 추가합니다. 언어 선택은 영어/일본어와 자동 감지로 간소화됩니다. 사이드바는 드래그 이동 및 위치 저장을 지원합니다.
// @description:fr      Ajoute une barre latérale d’options de recherche à SearXNG. Langues réduites à anglais/japonais avec détection automatique. La barre est déplaçable et sa position est conservée.
// @description:es      Añade una barra lateral con opciones avanzadas a SearXNG. Selección de idioma simplificada a inglés y japonés con autodetección. La barra se puede arrastrar y guarda su posición.
// @description:de      Fügt SearXNG eine Seitenleiste mit erweiterten Suchoptionen hinzu. Sprachwahl auf Englisch/Japanisch mit Auto-Erkennung. Die Leiste ist verschiebbar und speichert ihre Position.
// @description:pt-BR   Adiciona uma barra lateral com opções detalhadas ao SearXNG, com idioma reduzido a inglês/japonês e detecção automática. A barra é arrastável e tem posição persistente.
// @description:ru      Добавляет в SearXNG боковую панель расширенных параметров поиска. Языки: английский/японский с автоопределением. Панель можно перетаскивать; позиция сохраняется.
// @namespace    https://github.com/koyasi777/searxng-search-options-enhancer
// @author       koyasi777
// @match        *://*/searx/search*
// @match        *://*/searxng/search*
// @match        *://searx.*/*
// @match        *://*.searx.*/*
// @match        https://search.localhost/*
// @grant        GM_addStyle
// @license      MIT
// @icon         https://docs.searxng.org/_static/searxng-wordmark.svg
// ==/UserScript==

(function () {
  'use strict';

  /*** 🌐 言語フィルタ処理を先に定義しておく ***/
  function filterLanguageDropdown() {
    const allowedLanguages = [
      "all", "auto",          // デフォルト・自動検出
      "ja", "ja-JP",          // 日本語
      "en"
    ];

    const select = document.getElementById("language");
    if (!select) return;

    for (let i = select.options.length - 1; i >= 0; i--) {
      const opt = select.options[i];
      if (!allowedLanguages.includes(opt.value)) {
        select.remove(i);
      }
    }
  }

  /*** 🧩 以下、検索オプションサイドバー ***/
  const SIDEBAR_ID = 'gso-advanced-sidebar';
  const COLLAPSE_KEY = 'gso_sidebar_collapsed';
  const POS_KEY = 'gso_sidebar_pos'; // ⬅ 追加: 位置保存用

  const STYLE = `
    #${SIDEBAR_ID} {
      position: fixed;
      top: 100px;
      right: 20px;
      width: 260px;
      max-height: 90vh;
      overflow-y: auto;
      background: #ffffff;
      border: 1px solid #dadce0;
      border-radius: 12px;
      padding: 16px;
      font-family: Roboto, Arial, sans-serif;
      font-size: 13px;
      z-index: 99999;
      box-shadow: 0 2px 6px rgba(0,0,0,0.2);
      color: #202124;
    }
    /* ⬇ 右寄せデフォルトをユーザ移動後は無効化 */
    #${SIDEBAR_ID}[data-user-pos="1"] { right: auto !important; }

    #${SIDEBAR_ID}.collapsed {
      width: 180px;
      max-height: 21px;
      overflow: hidden;
      padding: 6px 12px;
      padding-top:10px;
    }
    #${SIDEBAR_ID}.collapsed label,
    #${SIDEBAR_ID}.collapsed input,
    #${SIDEBAR_ID}.collapsed select,
    #${SIDEBAR_ID}.collapsed .gso-body {
      display: none;
    }
    #${SIDEBAR_ID} .gso-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 10px;
      cursor: grab;            /* ⬅ ドラッグハンドル */
      user-select: none;
      touch-action: none;      /* ⬅ モバイルでのスクロール抑止(ヘッダ上) */
    }
    #${SIDEBAR_ID}.dragging {
      cursor: grabbing;
      box-shadow: 0 6px 18px rgba(0,0,0,0.30);
    }
    #${SIDEBAR_ID} .gso-header h3 {
      font-size: 14px;
      font-weight: bold;
      margin: 0;
      padding: 0;
    }
    #${SIDEBAR_ID} .gso-toggle {
      font-size: 12px;
      cursor: pointer;
      color: #3367d6;
      margin: 0;
      user-select: none;
      background: none;
      border: none;
      padding: 0;
    }
    #${SIDEBAR_ID} label {
      display: block;
      margin-top: 10px;
      font-weight: 500;
    }
    #${SIDEBAR_ID} input,
    #${SIDEBAR_ID} select {
      width: 100%;
      margin-top: 4px;
      padding: 6px;
      border: 1px solid #ccc;
      border-radius: 6px;
      background-color: #fff;
      color: #202124;
      box-sizing: border-box;
    }
    @media (prefers-color-scheme: dark) {
      #${SIDEBAR_ID} {
        background: #202124;
        color: #e8eaed;
        border: 1px solid #5f6368;
      }
      #${SIDEBAR_ID} input,
      #${SIDEBAR_ID} select {
        background-color: #303134;
        color: #e8eaed;
        border: 1px solid #5f6368;
      }
    }


    #${SIDEBAR_ID} .gso-buttons {
      display: flex;
      gap: 10px;
      margin-top: 16px;
    }

    #${SIDEBAR_ID} .gso-buttons button {
      flex: 1;
      padding: 8px 12px;
      border: none;
      border-radius: 6px;
      font-size: 13px;
      font-weight: 500;
      cursor: pointer;
      transition: background 0.2s ease, box-shadow 0.2s ease;
    }

    #${SIDEBAR_ID} .gso-buttons button:focus {
      outline: 2px solid #4285f4;
      outline-offset: 2px;
    }

    #${SIDEBAR_ID} .gso-buttons button:hover {
      filter: brightness(1.03);
    }

    #${SIDEBAR_ID} .gso-buttons button:active {
      transform: scale(0.97);
    }

    #${SIDEBAR_ID} .gso-clear {
      background: #f1f3f4;
      color: #202124;
    }

    #${SIDEBAR_ID} .gso-search {
      background: #1a73e8;
      color: white;
    }

    @media (prefers-color-scheme: dark) {
      #${SIDEBAR_ID} .gso-clear {
        background: #3c4043;
        color: #e8eaed;
      }
      #${SIDEBAR_ID} .gso-search {
        background: #8ab4f8;
        color: #202124;
      }
    }
  `;
  GM_addStyle(STYLE);

  const selects = {
    filetype: [['', 'すべて'], ['filetype:pdf', 'PDF'], ['filetype:doc', 'DOC'], ['filetype:xls', 'XLS'], ['filetype:ppt', 'PPT'], ['filetype:txt', 'TXT']],
    region: [['', 'すべて'], ['region:jp', '日本'], ['region:us', 'アメリカ'], ['region:cn', '中国']],
    occt: [['', '全体'], ['intitle:', 'タイトル'], ['inurl:', 'URL'], ['inanchor:', 'リンク先']],
    rights: [['', '制限なし'], ['cc_publicdomain', 'パブリックドメイン'], ['cc_attribute', '帰属'], ['cc_sharealike', '継承'], ['cc_noncommercial', '非営利']],
    date: [['', '指定なし'], ['date:h', '1時間以内'], ['date:d', '1日以内'], ['date:w', '1週間以内'], ['date:m', '1か月以内'], ['date:y', '1年以内']]
  };

  const timeRangeMap = {
    'hour': 'date:h',
    'day': 'date:d',
    'week': 'date:w',
    'month': 'date:m',
    'year': 'date:y'
  };
  const reverseTimeMap = Object.fromEntries(Object.entries(timeRangeMap).map(([k, v]) => [v, k]));

  // ==== 双方向同期(Sidebar <-> #q)====
  let syncingFromSidebar = false;
  let syncingFromQ = false;
  let syncTimer = 0;
  function debounce(fn, wait = 120) {
    return (...args) => {
      clearTimeout(syncTimer);
      syncTimer = setTimeout(() => fn(...args), wait);
    };
  }
  function syncQFromSidebarImmediate() {
    const qInput = document.querySelector('#q');
    if (!qInput) return;
    if (syncingFromQ) return;
    syncingFromSidebar = true;
    qInput.value = buildQueryFromUI();
    // 時間範囲のリアルタイム同期
    const dateValue = document.getElementById('gso-date')?.value || '';
    const timeRange = reverseTimeMap[dateValue] || '';
    const timeRangeSelect = document.getElementById('time_range');
    if (timeRangeSelect) timeRangeSelect.value = timeRange;
    syncingFromSidebar = false;
  }
  const syncQFromSidebar = debounce(syncQFromSidebarImmediate, 120);

  const fields = [
    ['all', 'すべてのキーワード'],
    ['exact', '完全一致キーワード'],
    ['any', 'いずれかのキーワード'],
    ['none', '含めないキーワード'],
    ['site', 'サイト・ドメイン'],
    ['filetype', 'ファイル形式'],
    ['region', '地域'],
    ['occt', '検索対象の範囲'],
    ['rights', 'ライセンス'],
    ['date', '最終更新']
  ];

  function parseQuery(query) {
    const result = Object.fromEntries(fields.map(([id]) => [id, '']));
    const tokens = query.match(/"[^"]+"|\S+/g) || [];
    const skipIndexes = new Set();
    const orWords = [];

    for (let i = 0; i < tokens.length; i++) {
      if (tokens[i + 1] === 'OR') {
        orWords.push(tokens[i]);
        skipIndexes.add(i);
        skipIndexes.add(i + 1);
        i += 1;
      } else if (tokens[i - 1] === 'OR') {
        orWords.push(tokens[i]);
        skipIndexes.add(i);
      }
    }
    result.any = [...new Set(orWords)].join(' ');

    for (let i = 0; i < tokens.length; i++) {
      if (skipIndexes.has(i)) continue;
      const token = tokens[i];

      if (token.startsWith('site:')) result.site = token.slice(5);
      else if (token.startsWith('filetype:')) result.filetype = token;
      else if (token.startsWith('region:')) result.region = token;
      else if (token.startsWith('date:')) result.date = token;
      else if (token.startsWith('cc_')) result.rights = token;
      else if (/^(intitle|inurl|inanchor):/.test(token)) result.occt = token.split(':')[0] + ':';
      else if (token.startsWith('"') && token.endsWith('"')) result.exact += token.slice(1, -1) + ' ';
      else if (token.startsWith('-')) result.none += token.slice(1) + ' ';
      else result.all += token + ' ';
    }

    return Object.fromEntries(Object.entries(result).map(([k, v]) => [k, v.trim()]));
  }

  function buildQueryFromUI(base = '') {
    const get = id => document.getElementById(`gso-${id}`)?.value.trim() || '';
    const parts = [];

    const exact = get('exact');
    const any = get('any');
    const none = get('none');
    const site = get('site');
    const filetype = get('filetype');
    const region = get('region');
    const rights = get('rights');
    const occt = get('occt');
    const all = get('all');

    const allWords = all.split(/\s+/).filter(Boolean);
    const anyWords = any.split(/\s+/).filter(Boolean);
    const noneWords = none.split(/\s+/).filter(Boolean);
    const exclusionWords = new Set([...anyWords, ...noneWords]);

    const filteredAll = allWords.filter(w => !exclusionWords.has(w));
    if (filteredAll.length > 0) {
      parts.push(occt ? `${occt}${filteredAll.join(' ')}` : filteredAll.join(' '));
    } else if (occt && allWords.length > 0) {
      parts.push(`${occt}${allWords.join(' ')}`);
    }

    if (exact) parts.push(`"${exact}"`);
    if (anyWords.length > 1) parts.push(anyWords.join(' OR '));
    else if (anyWords.length === 1) parts.push(anyWords[0]);
    noneWords.forEach(w => parts.push(`-${w}`));
    if (site) parts.push(`site:${site}`);
    if (filetype) parts.push(filetype);
    if (region) parts.push(region);
    if (rights) parts.push(rights);

    return parts.join(' ').trim();
  }

  function stripOrClauses(query) {
    const tokens = query.match(/"[^"]+"|\S+/g) || [];
    const result = [];
    let i = 0;

    while (i < tokens.length) {
      if (tokens[i + 1] === 'OR') {
        while (tokens[i + 1] === 'OR') {
          i += 2;
        }
        i += 1;
      } else {
        result.push(tokens[i]);
        i += 1;
      }
    }

    return result.join(' ');
  }

  function submitQuery() {
    const form = document.querySelector('form[action="/search"]');
    const input = form?.querySelector('input[name="q"]');
    if (!input) return;

    input.value = buildQueryFromUI();

    const dateValue = document.getElementById('gso-date')?.value || '';
    const timeRange = reverseTimeMap[dateValue] || '';
    const timeRangeSelect = document.getElementById('time_range');
    if (timeRangeSelect) {
        const trVal = timeRangeSelect.value;
        if (trVal && timeRangeMap[trVal]) {
          const dateSel = document.getElementById('gso-date');
          if (dateSel) dateSel.value = timeRangeMap[trVal];
        }
        // ネイティブ time_range 変更→サイドバー&#q を即時同期
        timeRangeSelect.addEventListener('change', () => {
          const dateSel = document.getElementById('gso-date');
          if (dateSel && timeRangeMap[timeRangeSelect.value]) {
            dateSel.value = timeRangeMap[timeRangeSelect.value];
          }
          syncQFromSidebarImmediate();
        });
      }

    form.submit();
  }

  // ===== 🧲 ここからドラッグ&位置保存ロジック =====
  function clampToViewport(left, top, el) {
    const pad = 8; // 画面端の余白
    const w = el.offsetWidth || 260; // 未描画時の保険
    const h = el.offsetHeight || 200;
    const maxLeft = Math.max(0, window.innerWidth - w - pad);
    const maxTop  = Math.max(0, window.innerHeight - h - pad);
    return {
      left: Math.min(Math.max(left, pad), maxLeft),
      top:  Math.min(Math.max(top,  pad), maxTop)
    };
  }

  function applyPos(sidebar, left, top) {
    const L = Math.round(left);
    const T = Math.round(top);
    sidebar.style.left = `${L}px`;
    sidebar.style.top  = `${T}px`;
    sidebar.style.right = 'auto';
    sidebar.dataset.userPos = '1';
  }

  function savePos(sidebar) {
    const r = sidebar.getBoundingClientRect();
    localStorage.setItem(POS_KEY, JSON.stringify({ left: r.left, top: r.top }));
  }

  function loadPos(sidebar) {
    const raw = localStorage.getItem(POS_KEY);
    if (!raw) return;
    try {
      const { left, top } = JSON.parse(raw);
      const { left: L, top: T } = clampToViewport(left, top, sidebar);
      applyPos(sidebar, L, T);
    } catch { /* noop */ }
  }

  function addDragBehavior(sidebar, handle) {
    let pointerId = null;
    let start = null;

    handle.addEventListener('pointerdown', (e) => {
      pointerId = e.pointerId;
      handle.setPointerCapture(pointerId);
      const r = sidebar.getBoundingClientRect();
      start = { x: e.clientX, y: e.clientY, left: r.left, top: r.top };
      sidebar.classList.add('dragging');
      document.body.style.userSelect = 'none';
    });

    handle.addEventListener('pointermove', (e) => {
      if (pointerId == null || !handle.hasPointerCapture(pointerId)) return;
      const dx = e.clientX - start.x;
      const dy = e.clientY - start.y;
      const { left, top } = clampToViewport(start.left + dx, start.top + dy, sidebar);
      applyPos(sidebar, left, top);
    });

    const endDrag = () => {
      if (pointerId == null) return;
      handle.releasePointerCapture(pointerId);
      pointerId = null;
      sidebar.classList.remove('dragging');
      document.body.style.userSelect = '';
      savePos(sidebar);
    };

    handle.addEventListener('pointerup', endDrag);
    handle.addEventListener('pointercancel', endDrag);

    // ウィンドウリサイズで画面外に行かないように補正
    window.addEventListener('resize', () => {
      const raw = localStorage.getItem(POS_KEY);
      if (!raw) return;
      try {
        const { left, top } = JSON.parse(raw);
        const { left: L, top: T } = clampToViewport(left, top, sidebar);
        applyPos(sidebar, L, T);
        savePos(sidebar);
      } catch { /* noop */ }
    });

    // ダブルクリックで位置リセット(デフォルト: 右 20px / 上 100px)
    handle.addEventListener('dblclick', () => {
      localStorage.removeItem(POS_KEY);
      sidebar.dataset.userPos = '';
      sidebar.style.left = '';
      sidebar.style.top = '';
      sidebar.style.right = '';
    });
  }
  // ===== ここまでドラッグ&位置保存ロジック =====

  // createInput に autoSubmit フラグ追加(Enterのみ有効)
  function createInput(labelText, id) {
    const label = document.createElement('label');
    label.textContent = labelText;
    const input = document.createElement('input');
    input.id = `gso-${id}`;
    input.name = id;
    label.appendChild(input);

    input.addEventListener('keydown', e => {
      if (e.key === 'Enter') {
        e.preventDefault();
        submitQuery();
      }
    });
    // 入力のたびに #q をリアルタイム更新(デバウンス)
    input.addEventListener('input', () => {
      syncQFromSidebar();
    });

    return label;
  }

  // 修正: createSelect も同様に、Enterキー以外でsubmitしない
  function createSelect(labelText, id, options) {
    const label = document.createElement('label');
    label.textContent = labelText;
    const select = document.createElement('select');
    select.id = `gso-${id}`;
    select.name = id;
    options.forEach(([val, text]) => {
      const opt = document.createElement('option');
      opt.value = val;
      opt.textContent = text;
      select.appendChild(opt);
    });
    label.appendChild(select);
    // セレクト変更時は即時反映(待ち無し)
    select.addEventListener('change', () => {
      syncQFromSidebarImmediate();
    });
    return label;
  }

  // 🆕 ✅ Clearボタン用
  function clearSidebarInputs() {
    fields.forEach(([id]) => {
      const el = document.getElementById(`gso-${id}`);
      if (!el) return;
      if (el.tagName === 'INPUT') {
        el.value = '';
      } else if (el.tagName === 'SELECT') {
        el.selectedIndex = 0;
      }
    });

    ['uilang', 'safesearch'].forEach(id => {
      const el = document.getElementById(`gso-${id}`);
      if (el && el.tagName === 'SELECT') {
        el.selectedIndex = 0;
      }
    });
  }

  function createSelectFromNative(labelText, id, nativeSelector) {
    const native = document.querySelector(nativeSelector);
    if (!native) return null;

    const label = document.createElement('label');
    label.textContent = labelText;
    const select = document.createElement('select');
    select.id = `gso-${id}`;
    select.name = id;

    Array.from(native.options).forEach(opt => {
      const clone = opt.cloneNode(true);
      select.appendChild(clone);
    });

    select.value = native.value;
    select.addEventListener('change', () => {
      native.value = select.value;
      native.dispatchEvent(new Event('change'));
    });

    label.appendChild(select);
    return label;
  }

  // Sidebar生成関数(ドラッグ&位置保存に対応)
  function insertSidebar() {
    if (document.getElementById(SIDEBAR_ID)) return;

    filterLanguageDropdown();

    const sidebar = document.createElement('div');
    sidebar.id = SIDEBAR_ID;

    const header = document.createElement('div');
    header.className = 'gso-header';
    const title = document.createElement('h3');
    title.textContent = '詳細検索オプション';
    const toggle = document.createElement('button');
    toggle.type = 'button';
    toggle.className = 'gso-toggle';
    toggle.textContent = '▲ 閉じる';
    toggle.setAttribute('aria-expanded', 'true');
    toggle.addEventListener('pointerdown', (e) => { e.stopPropagation(); });
    toggle.addEventListener('mousedown', (e) => { e.stopPropagation(); });
    const onToggle = () => {
      // 折りたたみ時も「右上起点」に見えるよう、トグル前の右端を保持
      const pre = sidebar.getBoundingClientRect();
      const preLeft = pre.left;
      const preTop = pre.top;
      const preWidth = pre.width;

      const collapsed = sidebar.classList.toggle('collapsed');
      toggle.textContent = collapsed ? '▼ 開く' : '▲ 閉じる';
      toggle.setAttribute('aria-expanded', String(!collapsed));
      localStorage.setItem(COLLAPSE_KEY, collapsed ? '1' : '0');

      if (sidebar.dataset.userPos === '1') {
        requestAnimationFrame(() => {
          const post = sidebar.getBoundingClientRect();
          // 右端(preLeft + preWidth)を不変にして、新しい幅に合わせて left を再計算
          const desiredLeft = preLeft + preWidth - post.width;
          const { left, top } = clampToViewport(desiredLeft, preTop, sidebar);
          applyPos(sidebar, left, top);
          savePos(sidebar);
        });
      }
    };
    toggle.addEventListener('click', (e) => { e.stopPropagation(); onToggle(); });
    toggle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggle(); } });
    header.appendChild(title);
    header.appendChild(toggle);
    sidebar.appendChild(header);

    const body = document.createElement('div');
    body.className = 'gso-body';
    body.id = 'gso-body';
    fields.forEach(([id, label]) => {
      body.appendChild(selects[id] ? createSelect(label, id, selects[id]) : createInput(label, id));
    });

    const languageSyncUI = createSelectFromNative('言語設定', 'uilang', '#language');
    if (languageSyncUI) body.appendChild(languageSyncUI);

    const safeSearchUI = createSelectFromNative('セーフサーチ', 'safesearch', '#safesearch');
    if (safeSearchUI) body.appendChild(safeSearchUI);

    // 🆕 ✅ Clear/Searchボタン
    const buttonContainer = document.createElement('div');
    buttonContainer.className = 'gso-buttons';

    const clearButton = document.createElement('button');
    clearButton.textContent = '🧹 Clear';
    clearButton.className = 'gso-clear';
    clearButton.onclick = () => clearSidebarInputs();

    const searchButton = document.createElement('button');
    searchButton.textContent = '🔍 Search';
    searchButton.className = 'gso-search';
    searchButton.onclick = () => { setTimeout(() => submitQuery(), 0); };

    buttonContainer.appendChild(clearButton);
    buttonContainer.appendChild(searchButton);
    body.appendChild(buttonContainer);

    sidebar.appendChild(body);
    document.body.appendChild(sidebar);

    // ⬇ ここでドラッグ&位置保存ハンドラを有効化
    addDragBehavior(sidebar, header);

    const qInput = document.querySelector('#q');
    if (qInput) {
      const parsed = parseQuery(qInput.value);
      fields.forEach(([id]) => {
        const el = document.getElementById(`gso-${id}`);
        if (el && parsed[id]) el.value = parsed[id];
      });

      const syncSidebarFromQ = () => {
        if (syncingFromSidebar) return; // 片方向同期の循環防止
        syncingFromQ = true;
        const updated = parseQuery(qInput.value);
        fields.forEach(([id]) => {
          const el = document.getElementById(`gso-${id}`);
          if (el) el.value = updated[id] || '';
        });
        // time_range -> gso-date は SearXNG 側が変わる場合もあるため再同期
        const timeRangeSelect = document.getElementById('time_range');
        const dateSel = document.getElementById('gso-date');
        if (timeRangeSelect && dateSel && timeRangeMap[timeRangeSelect.value]) {
          dateSel.value = timeRangeMap[timeRangeSelect.value];
        }
        syncingFromQ = false;
      };
      qInput.addEventListener('input', syncSidebarFromQ);
      qInput.addEventListener('change', syncSidebarFromQ);

      const timeRangeSelect = document.getElementById('time_range');
      if (timeRangeSelect) {
        const trVal = timeRangeSelect.value;
        if (trVal && timeRangeMap[trVal]) {
          const dateSel = document.getElementById('gso-date');
          if (dateSel) dateSel.value = timeRangeMap[trVal];
        }
      }

      const form = document.querySelector('form[action="/search"]');
      if (form) {
        qInput.addEventListener('keydown', (e) => {
          if (e.key === 'Enter') {
            e.preventDefault();
            submitQuery();
          }
        });
      }
    }

    const saved = localStorage.getItem(COLLAPSE_KEY);
    if (saved === '1') {
      sidebar.classList.add('collapsed');
      // toggle.textContent は上の onclick ロジックに合わせる
      const toggleEl = sidebar.querySelector('.gso-toggle');
      if (toggleEl) toggleEl.textContent = '▼ 開く';
    }

    // ⬇ 最後に保存済み位置を反映(collapsed 状態も考慮して補正)
    loadPos(sidebar);
  }

  window.addEventListener('load', insertSidebar);
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址