// ==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.8.1
// @description SearXNG検索エンジンに詳細検索オプションサイドバーを追加(言語選択も自動検出と英語と日本語のみにしてすっきり)
// @description:en Adds a detailed search options sidebar to SearXNG. Simplifies language selection to English and Japanese with auto-detection.
// @description:zh-CN 为SearXNG搜索引擎添加详细选项侧边栏,仅保留英文和日文语言选项,并启用自动检测。
// @description:zh-TW 為SearXNG搜尋引擎新增詳細選項側邊欄,自動偵測語言並僅顯示英文和日文選項。
// @description:ko SearXNG에 검색 옵션 사이드바를 추가하고 언어 선택을 영어와 일본어로 간소화하며 자동 감지 지원.
// @description:fr Ajoute une barre latérale d’options de recherche à SearXNG. Seules les langues anglais et japonais sont disponibles, avec détection automatique.
// @description:es Añade una barra lateral con opciones de búsqueda avanzadas en SearXNG. Simplifica la selección de idiomas a inglés y japonés con detección automática.
// @description:de Fügt eine erweiterte Suchoptionen-Seitenleiste zu SearXNG hinzu. Nur Englisch und Japanisch als Sprachen mit automatischer Erkennung verfügbar.
// @description:pt-BR Adiciona uma barra lateral com opções detalhadas de pesquisa ao SearXNG, com seleção de idioma reduzida para inglês e japonês e detecção automática.
// @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.charleseroop.com/*
// @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 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}.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;
}
#${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;
}
#${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]));
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) {
// ORグループの開始を検知(例: tok i+1 === 'OR')
if (tokens[i + 1] === 'OR') {
// OR連鎖を全てスキップ
while (tokens[i + 1] === 'OR') {
i += 2; // skip current + OR + next
}
i += 1; // skip最後の単語
} 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) {
timeRangeSelect.value = timeRange;
}
// 検索実行
form.submit();
}
// 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);
// 🔄 Enterキーだけでsubmit。blurやchangeでは送信しない
input.addEventListener('keydown', e => {
if (e.key === 'Enter') {
e.preventDefault();
submitQuery();
}
});
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);
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生成関数にボタンUI追加
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('div');
toggle.className = 'gso-toggle';
toggle.textContent = '▲ 閉じる';
toggle.onclick = () => {
const collapsed = sidebar.classList.toggle('collapsed');
toggle.textContent = collapsed ? '▼ 開く' : '▲ 閉じる';
localStorage.setItem(COLLAPSE_KEY, collapsed ? '1' : '0');
};
header.appendChild(title);
header.appendChild(toggle);
sidebar.appendChild(header);
const body = document.createElement('div');
body.className = '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 = () => {
// 入力反映(ただし submitQuery が UIの最新値を使うようになったのでこの blur はなくてもよい)
setTimeout(() => submitQuery(), 0);
};
buttonContainer.appendChild(clearButton);
buttonContainer.appendChild(searchButton);
body.appendChild(buttonContainer);
sidebar.appendChild(body);
document.body.appendChild(sidebar);
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 = () => {
const updated = parseQuery(qInput.value);
fields.forEach(([id]) => {
const el = document.getElementById(`gso-${id}`);
if (el) el.value = updated[id] || '';
});
};
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 = '▼ 開く';
}
}
window.addEventListener('load', insertSidebar);
})();