// ==UserScript== // @name Search Engine Switcher // @namespace https://github.com/EchoRan6319/Search-Engine-Switcher // @version 2.5.1 // @description 快捷搜索引擎切换器:支持新增、删除、排序、位置自定义 // @author EchoRan6319 // @license MIT // @homepageURL https://github.com/EchoRan6319/Search-Engine-Switcher // @supportURL https://github.com/EchoRan6319/Search-Engine-Switcher/issues // @match *://*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @run-at document-idle // @downloadURL https://update.greasyfork.icu/scripts/570395/Search%20Engine%20Switcher.user.js // @updateURL https://update.greasyfork.icu/scripts/570395/Search%20Engine%20Switcher.meta.js // ==/UserScript== (function () { 'use strict'; if (window !== window.top) return; const STORAGE_KEY = 'search_engine_switcher_config_v1'; const VERSION_KEY = 'search_engine_switcher_version'; const CURRENT_VERSION = '2.5.1'; const STYLE_ID = 'search-engine-switcher-style'; const ROOT_ID = 'search-engine-switcher-root'; const PANEL_ID = 'search-engine-switcher-panel'; const DEFAULT_CONFIG = { engines: [ // ========== 国外传统搜索引擎 ========== { id: 'google', name: 'Google', searchUrl: 'https://www.google.com/search?q={q}', hosts: ['google.', 'www.google.'], hidden: false }, { id: 'bing', name: 'Bing', searchUrl: 'https://www.bing.com/search?q={q}', hosts: ['bing.com', 'www.bing.com', 'cn.bing.com'], hidden: false }, { id: 'duckduckgo', name: 'DuckDuckGo', searchUrl: 'https://duckduckgo.com/?q={q}', hosts: ['duckduckgo.com'], hidden: false }, { id: 'brave', name: 'Brave', searchUrl: 'https://search.brave.com/search?q={q}', hosts: ['search.brave.com'], hidden: false }, { id: 'yandex', name: 'Yandex', searchUrl: 'https://yandex.com/search/?text={q}&from=browser', hosts: ['yandex.'], hidden: false }, // ========== 国内传统搜索引擎 ========== { id: 'baidu', name: '百度', searchUrl: 'https://www.baidu.com/s?wd={q}', hosts: ['baidu.com', 'www.baidu.com'], hidden: false }, { id: 'sogou', name: '搜狗', searchUrl: 'https://www.sogou.com/web?query={q}', hosts: ['sogou.com'], hidden: true }, { id: '360', name: '360搜索', searchUrl: 'https://www.so.com/s?q={q}', hosts: ['so.com'], hidden: true }, // ========== 国外AI大模型 ========== { id: 'chatgpt', name: 'ChatGPT', searchUrl: 'https://chatgpt.com/?hints=search&q={q}', hosts: ['chatgpt.com'], hidden: true }, { id: 'gemini', name: 'Gemini', searchUrl: 'https://gemini.google.com/app?q={q}', hosts: ['gemini.google.com'], hidden: true }, { id: 'perplexity', name: 'Perplexity', searchUrl: 'https://www.perplexity.ai/?q={q}', hosts: ['perplexity.ai'], hidden: true }, // ========== 国内AI大模型 ========== { id: 'qianwen', name: '千问', searchUrl: 'https://www.qianwen.com/?q={q}', hosts: ['qianwen.com', 'www.qianwen.com'], hidden: true }, { id: 'doubao', name: '豆包', searchUrl: 'https://www.doubao.com/chat/?q={q}', hosts: ['doubao.com'], hidden: true }, { id: 'deepseek', name: 'DeepSeek', searchUrl: 'https://chat.deepseek.com/?q={q}', hosts: ['chat.deepseek.com'], hidden: true }, { id: 'kimi', name: 'Kimi', searchUrl: 'https://kimi.moonshot.cn/?q={q}', hosts: ['kimi.moonshot.cn'], hidden: true }, { id: 'metaso', name: '秘塔AI', searchUrl: 'https://metaso.cn/?q={q}', hosts: ['metaso.cn'], hidden: true }, // ========== 国外社交/社区 ========== { id: 'youtube', name: 'YouTube', searchUrl: 'https://www.youtube.com/results?search_query={q}', hosts: ['youtube.com', 'm.youtube.com'], hidden: true, disableWidget: true }, { id: 'github', name: 'GitHub', searchUrl: 'https://github.com/search?q={q}', hosts: ['github.com'], hidden: true, disableWidget: true }, // ========== 国内社交/社区 ========== { id: 'bilibili', name: '哔哩哔哩', searchUrl: 'https://search.bilibili.com/all?keyword={q}', hosts: ['search.bilibili.com', 'bilibili.com', 'www.bilibili.com'], hidden: true, disableWidget: true }, { id: 'zhihu', name: '知乎', searchUrl: 'https://www.zhihu.com/search?q={q}', hosts: ['zhihu.com', 'www.zhihu.com'], hidden: true, disableWidget: true }, { id: 'xiaohongshu', name: '小红书', searchUrl: 'https://www.xiaohongshu.com/search_result?keyword={q}', hosts: ['xiaohongshu.com'], hidden: true, disableWidget: true }, { id: 'douyin', name: '抖音', searchUrl: 'https://www.douyin.com/search/{q}', hosts: ['douyin.com', 'www.douyin.com'], hidden: true, disableWidget: true }, { id: 'weixin', name: '微信', searchUrl: 'https://weixin.sogou.com/weixin?type=2&s_from=input&query={q}', hosts: ['weixin.sogou.com'], hidden: true, disableWidget: true } ], ui: { vertical: 'bottom', align: 'center', offsetX: 16, offsetY: 16, useCustomXY: false, customX: 16, customY: 16, showWhenNoQuery: false, openInNewTab: false, theme: 'auto' } }; const safeGMGet = (key, fallback) => { try { if (typeof GM_getValue === 'function') return GM_getValue(key, fallback); const v = localStorage.getItem(key); return v == null ? fallback : v; } catch (_) { return fallback; } }; const safeGMSet = (key, value) => { try { if (typeof GM_setValue === 'function') { GM_setValue(key, value); } else { localStorage.setItem(key, value); } } catch (_) { // ignore } }; const uid = () => `se_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; const deepClone = (obj) => JSON.parse(JSON.stringify(obj)); function mergeConfig(raw) { const cfg = deepClone(DEFAULT_CONFIG); if (!raw || typeof raw !== 'object') return cfg; if (Array.isArray(raw.engines) && raw.engines.length > 0) { cfg.engines = raw.engines .map((e) => ({ id: String(e.id || uid()), name: String(e.name || '').trim(), searchUrl: String(e.searchUrl || '').trim(), hosts: Array.isArray(e.hosts) ? e.hosts.map((h) => String(h).trim()).filter(Boolean) : [], hidden: !!e.hidden, disableWidget: !!e.disableWidget })) .filter((e) => e.name && e.searchUrl.includes('{q}')); if (cfg.engines.length === 0) cfg.engines = deepClone(DEFAULT_CONFIG.engines); } if (raw.ui && typeof raw.ui === 'object') { cfg.ui.vertical = raw.ui.vertical === 'top' ? 'top' : 'bottom'; cfg.ui.align = ['left', 'center', 'right'].includes(raw.ui.align) ? raw.ui.align : 'center'; cfg.ui.offsetX = Number.isFinite(raw.ui.offsetX) ? raw.ui.offsetX : cfg.ui.offsetX; cfg.ui.offsetY = Number.isFinite(raw.ui.offsetY) ? raw.ui.offsetY : cfg.ui.offsetY; cfg.ui.useCustomXY = !!raw.ui.useCustomXY; cfg.ui.customX = Number.isFinite(raw.ui.customX) ? raw.ui.customX : cfg.ui.customX; cfg.ui.customY = Number.isFinite(raw.ui.customY) ? raw.ui.customY : cfg.ui.customY; cfg.ui.showWhenNoQuery = raw.ui.showWhenNoQuery !== false; cfg.ui.openInNewTab = !!raw.ui.openInNewTab; cfg.ui.theme = ['light', 'dark'].includes(raw.ui.theme) ? raw.ui.theme : 'auto'; } return cfg; } function loadConfig() { const raw = safeGMGet(STORAGE_KEY, ''); if (!raw) return deepClone(DEFAULT_CONFIG); try { return mergeConfig(JSON.parse(raw)); } catch (_) { return deepClone(DEFAULT_CONFIG); } } function saveConfig(cfg) { safeGMSet(STORAGE_KEY, JSON.stringify(cfg)); } const config = loadConfig(); function isLightTheme() { const theme = config.ui.theme || 'auto'; if (theme === 'light') return true; if (theme === 'dark') return false; return window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches; } function applyTheme() { const theme = config.ui.theme || 'auto'; const root = document.documentElement; if (theme === 'dark') { root.removeAttribute('data-theme'); } else if (isLightTheme()) { root.setAttribute('data-theme', 'light'); } else { root.removeAttribute('data-theme'); } } function injectStyle() { if (document.getElementById(STYLE_ID)) return; const style = document.createElement('style'); style.id = STYLE_ID; style.textContent = ` :root { --ses-bg-primary: rgba(16, 16, 16, 0.92); --ses-bg-secondary: #101114; --ses-bg-input: #151820; --ses-bg-button: #4a4a4a; --ses-text-primary: #fff; --ses-text-secondary: #f3f3f3; --ses-text-muted: #9fa6b2; --ses-text-label: #aeb6c2; --ses-border-color: rgba(255, 255, 255, 0.12); --ses-border-light: rgba(255, 255, 255, 0.14); --ses-border-medium: rgba(255, 255, 255, 0.16); --ses-border-pill: rgba(255, 255, 255, 0.2); --ses-shadow: rgba(0, 0, 0, 0.32); --ses-shadow-panel: rgba(0, 0, 0, 0.35); --ses-overlay: rgba(0, 0, 0, 0.45); --ses-active-border: #7ea1ff; --ses-active-shadow: rgba(126, 161, 255, 0.35); --ses-primary-bg: #2e5fff; --ses-danger-bg: #6b2026; --ses-danger-border: #9f3540; } [data-theme="light"] { color-scheme: light; --ses-bg-primary: rgba(255, 255, 255, 0.95); --ses-bg-secondary: #f5f5f7; --ses-bg-input: #ffffff; --ses-bg-button: #e8e8ed; --ses-text-primary: #1c1c1e; --ses-text-secondary: #2c2c2e; --ses-text-muted: #6c6c70; --ses-text-label: #3a3a3c; --ses-border-color: rgba(0, 0, 0, 0.1); --ses-border-light: rgba(0, 0, 0, 0.12); --ses-border-medium: rgba(0, 0, 0, 0.15); --ses-border-pill: rgba(0, 0, 0, 0.15); --ses-shadow: rgba(0, 0, 0, 0.15); --ses-shadow-panel: rgba(0, 0, 0, 0.2); --ses-overlay: rgba(0, 0, 0, 0.35); --ses-active-border: #007aff; --ses-active-shadow: rgba(0, 122, 255, 0.3); --ses-primary-bg: #007aff; --ses-danger-bg: #ff3b30; --ses-danger-border: #ff3b30; } @media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) { color-scheme: light; --ses-bg-primary: rgba(255, 255, 255, 0.95); --ses-bg-secondary: #f5f5f7; --ses-bg-input: #ffffff; --ses-bg-button: #e8e8ed; --ses-text-primary: #1c1c1e; --ses-text-secondary: #2c2c2e; --ses-text-muted: #6c6c70; --ses-text-label: #3a3a3c; --ses-border-color: rgba(0, 0, 0, 0.1); --ses-border-light: rgba(0, 0, 0, 0.12); --ses-border-medium: rgba(0, 0, 0, 0.15); --ses-border-pill: rgba(0, 0, 0, 0.15); --ses-shadow: rgba(0, 0, 0, 0.15); --ses-shadow-panel: rgba(0, 0, 0, 0.2); --ses-overlay: rgba(0, 0, 0, 0.35); --ses-active-border: #007aff; --ses-active-shadow: rgba(0, 122, 255, 0.3); --ses-primary-bg: #007aff; --ses-danger-bg: #ff3b30; --ses-danger-border: #ff3b30; } } #${ROOT_ID} { position: fixed; z-index: 2147483646; max-width: min(96vw, 860px); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif; user-select: none; box-sizing: border-box; forced-color-adjust: none; } #${ROOT_ID}.hidden { display: none; } #${ROOT_ID} .ses-wrap { display: flex; align-items: center; gap: 8px; background: var(--ses-bg-primary); border: 1px solid var(--ses-border-color); border-radius: 16px; padding: 8px; box-shadow: 0 8px 26px var(--ses-shadow); backdrop-filter: blur(6px); width: 100%; box-sizing: border-box; } #${ROOT_ID} .ses-list { display: flex; gap: 6px; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; flex: 1; mask-image: linear-gradient(to right, transparent 0%, #000 10px, #000 calc(100% - 10px), transparent 100%); -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 10px, #000 calc(100% - 10px), transparent 100%); padding-right: 10px; padding-left: 10px; } #${ROOT_ID} .ses-list::-webkit-scrollbar { display: none; } #${ROOT_ID} .ses-pill, #${ROOT_ID} .ses-btn { border: 1px solid var(--ses-border-pill); background: var(--ses-bg-button); color: var(--ses-text-primary); border-radius: 999px; padding: 6px 12px; font-size: 13px; line-height: 1; cursor: pointer; white-space: nowrap; flex-shrink: 0; } #${ROOT_ID} .ses-pill.active { /* 激活描边由 JS inline style 控制,以防止 Dark Reader 篡改 */ } #${ROOT_ID} .ses-btn { width: 34px; min-width: 34px; height: 30px; padding: 0; display: inline-flex; align-items: center; justify-content: center; } #${PANEL_ID} { position: fixed; inset: 0; z-index: 2147483647; background: var(--ses-overlay); display: none; align-items: center; justify-content: center; } #${PANEL_ID}.show { display: flex; } #${PANEL_ID} .panel { width: min(96vw, 720px); max-height: 90vh; overflow: auto; overscroll-behavior: contain; background: var(--ses-bg-secondary); color: var(--ses-text-secondary); border-radius: 14px; border: 1px solid var(--ses-border-light); box-shadow: 0 18px 48px var(--ses-shadow-panel); padding: 14px; font-size: 14px; } #${PANEL_ID} .panel h3 { margin: 0 0 10px 0; font-size: 16px; } #${PANEL_ID} .sub { margin: 12px 0 8px; font-weight: 600; } #${PANEL_ID} .engine-row { display: grid; grid-template-columns: 1fr auto; gap: 8px; padding: 8px; border: 1px solid var(--ses-border-color); border-radius: 10px; margin-bottom: 8px; } #${PANEL_ID} .engine-row.hidden-engine { opacity: 0.6; background: var(--ses-bg-button); } #${PANEL_ID} .muted { color: var(--ses-text-muted); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #${PANEL_ID} .ops { display: flex; gap: 6px; } #${PANEL_ID} button, #${PANEL_ID} input, #${PANEL_ID} select, #${PANEL_ID} textarea { font: inherit; } #${PANEL_ID} .op, #${PANEL_ID} .primary, #${PANEL_ID} .danger, #${PANEL_ID} .ghost { border-radius: 8px; border: 1px solid var(--ses-border-medium); background: var(--ses-bg-button); color: var(--ses-text-primary); padding: 6px 10px; cursor: pointer; } #${PANEL_ID} .primary { background: var(--ses-primary-bg); border-color: var(--ses-primary-bg); color: #fff; } #${PANEL_ID} .danger { background: var(--ses-danger-bg); border-color: var(--ses-danger-border); color: #fff; } #${PANEL_ID} .ghost { background: transparent; } #${PANEL_ID} .grid2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 8px; } #${PANEL_ID} .form { border: 1px solid var(--ses-border-color); border-radius: 10px; padding: 10px; margin-top: 8px; } #${PANEL_ID} label { display: block; font-size: 12px; margin-bottom: 4px; color: var(--ses-text-label); } #${PANEL_ID} input, #${PANEL_ID} select { width: 100%; box-sizing: border-box; background: var(--ses-bg-input); border: 1px solid var(--ses-border-light); color: var(--ses-text-primary); border-radius: 8px; padding: 6px 8px; } #${PANEL_ID} .panel-footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; } @media (max-width: 560px) { #${ROOT_ID} { max-width: 98vw; } #${ROOT_ID} .ses-pill { font-size: 12px; padding: 6px 10px; } #${PANEL_ID} .engine-row { display: flex; flex-direction: column; } #${PANEL_ID} .ops { justify-content: flex-start; } #${PANEL_ID} .op, #${PANEL_ID} .danger { padding: 7px 10px; min-width: 52px; text-align: center; } #${PANEL_ID} .grid2 { grid-template-columns: 1fr; } } `; document.documentElement.appendChild(style); } function getCurrentQuery() { const url = new URL(location.href); const keys = ['q', 'wd', 'word', 'query', 'text', 'keyword', 'search', 'p', 'k']; for (const key of keys) { const v = url.searchParams.get(key); if (v && v.trim()) return v.trim(); } if (url.hash.includes('=')) { const hashText = url.hash.replace(/^#/, ''); const hashParams = new URLSearchParams(hashText); for (const key of keys) { const v = hashParams.get(key); if (v && v.trim()) return v.trim(); } } const sel = String(window.getSelection && window.getSelection()).trim(); if (sel) return sel; const focused = document.activeElement; if (focused && focused.tagName === 'INPUT') { const input = focused; const v = typeof input.value === 'string' ? input.value.trim() : ''; if (v) return v; } return ''; } function isMatchHost(host, h) { if (h.endsWith('.')) { return host === h.slice(0, -1) || host.startsWith(h) || host.includes('.' + h); } return host === h || host.endsWith('.' + h); } function activeEngineIdByHost() { const host = location.hostname; const exact = config.engines.find((e) => (e.hosts || []).some((h) => isMatchHost(host, h))); return exact ? exact.id : ''; } function buildSearchUrl(engine, query) { return engine.searchUrl.replace('{q}', encodeURIComponent(query)); } async function resolveQuery() { let q = getCurrentQuery(); if (q) return q.trim(); q = prompt('输入搜索关键词'); return q ? q.trim() : ''; } function applyRootPosition(root) { const ui = config.ui; root.style.left = ''; root.style.right = ''; root.style.top = ''; root.style.bottom = ''; root.style.transform = ''; if (ui.useCustomXY) { root.style.left = `${Math.max(0, ui.customX)}px`; root.style.top = `${Math.max(0, ui.customY)}px`; return; } // 垂直位置 if (ui.vertical === 'top') { root.style.top = `${Math.max(0, ui.offsetY)}px`; } else { root.style.bottom = `${Math.max(0, ui.offsetY)}px`; } // 水平位置:默认居中 if (ui.align === 'left') { root.style.left = `${Math.max(0, ui.offsetX)}px`; } else if (ui.align === 'right') { root.style.right = `${Math.max(0, ui.offsetX)}px`; } else { // 居中(默认) root.style.left = '50%'; root.style.transform = 'translateX(-50%)'; } } function createRoot() { let root = document.getElementById(ROOT_ID); if (root) return root; root = document.createElement('div'); root.id = ROOT_ID; root.innerHTML = `
`; document.documentElement.appendChild(root); applyRootPosition(root); const list = root.querySelector(`#${ROOT_ID}-list`); // 支持鼠标滚轮横向滚动(针对桌面端) list.addEventListener('wheel', (e) => { if (e.deltaY !== 0) { e.preventDefault(); list.scrollLeft += e.deltaY; } }, { passive: false }); const settingsBtn = root.querySelector(`#${ROOT_ID}-settings`); settingsBtn.addEventListener('click', () => openPanel()); let holdTimer = null; root.addEventListener('pointerdown', (e) => { if (e.target && e.target.closest('.ses-pill')) return; holdTimer = setTimeout(() => openPanel(), 500); }); root.addEventListener('pointerup', () => { if (holdTimer) clearTimeout(holdTimer); holdTimer = null; }); root.addEventListener('pointerleave', () => { if (holdTimer) clearTimeout(holdTimer); holdTimer = null; }); return root; } function renderEngineButtons() { const root = createRoot(); const list = root.querySelector(`#${ROOT_ID}-list`); list.innerHTML = ''; // 只在未禁用的搜索引擎页面上显示 const host = location.hostname; const currentEngine = config.engines.find((e) => (e.hosts || []).some((h) => isMatchHost(host, h))); if (!currentEngine || currentEngine.disableWidget) { root.classList.add('hidden'); return; } const q = getCurrentQuery(); // 无关键词时根据设置决定是否显示 if (!q && !config.ui.showWhenNoQuery) { root.classList.add('hidden'); return; } const activeId = activeEngineIdByHost(); root.classList.remove('hidden'); for (const engine of config.engines) { if (engine.hidden) continue; const btn = document.createElement('button'); btn.className = 'ses-pill'; if (engine.id === activeId) { btn.classList.add('active'); // 用 inline style 强制设置描边,绕过 Dark Reader 对 CSS 规则的修改 const light = isLightTheme(); const borderColor = light ? '#007aff' : '#7ea1ff'; const shadowColor = light ? 'rgba(0, 122, 255, 0.3)' : 'rgba(126, 161, 255, 0.35)'; btn.style.setProperty('border-color', borderColor, 'important'); btn.style.setProperty('box-shadow', `0 0 0 1px ${shadowColor} inset`, 'important'); } btn.textContent = engine.name; btn.title = `${engine.name}\n${engine.searchUrl}`; const handleAction = async (openInNewTabOverride) => { const query = await resolveQuery(); if (!query) return; const url = buildSearchUrl(engine, query); const shouldOpenNewTab = openInNewTabOverride !== undefined ? openInNewTabOverride : config.ui.openInNewTab; if (shouldOpenNewTab) { window.open(url, '_blank'); } else { location.href = url; } }; btn.addEventListener('click', (e) => { if (e.button === 0) { // 左键 handleAction(); } }); // 阻止中键默认的自动滚动行为 btn.addEventListener('mousedown', (e) => { if (e.button === 1) { e.preventDefault(); } }); btn.addEventListener('mouseup', (e) => { if (e.button === 1) { // 中键 e.preventDefault(); handleAction(true); // 强制新标签页打开 } }); list.appendChild(btn); } } function createPanel() { let overlay = document.getElementById(PANEL_ID); if (overlay) return overlay; overlay = document.createElement('div'); overlay.id = PANEL_ID; overlay.innerHTML = `
`; document.documentElement.appendChild(overlay); overlay.addEventListener('click', (e) => { if (e.target === overlay) closePanel(); }); return overlay; } function editEngine(existing) { const name = prompt('搜索引擎名称', existing ? existing.name : ''); if (name == null) return; const searchUrl = prompt('搜索 URL(必须包含 {q})', existing ? existing.searchUrl : 'https://example.com/search?q={q}'); if (searchUrl == null) return; if (!searchUrl.includes('{q}')) { alert('URL 必须包含 {q} 占位符'); return; } const hostsRaw = prompt('识别当前引擎的域名(可选,逗号分隔)', existing ? (existing.hosts || []).join(',') : ''); if (hostsRaw == null) return; const hosts = hostsRaw .split(',') .map((s) => s.trim()) .filter(Boolean); if (existing) { existing.name = name.trim() || existing.name; existing.searchUrl = searchUrl.trim(); existing.hosts = hosts; } else { config.engines.push({ id: uid(), name: name.trim() || '未命名', searchUrl: searchUrl.trim(), hosts }); } saveConfig(config); renderPanel(); renderEngineButtons(); } function moveEngine(index, delta) { const target = index + delta; if (target < 0 || target >= config.engines.length) return; const tmp = config.engines[index]; config.engines[index] = config.engines[target]; config.engines[target] = tmp; saveConfig(config); renderPanel(); renderEngineButtons(); } function deleteEngine(index) { if (config.engines.length <= 1) { alert('至少保留一个搜索引擎'); return; } const item = config.engines[index]; const ok = confirm(`确定删除 ${item.name} 吗?`); if (!ok) return; config.engines.splice(index, 1); saveConfig(config); renderPanel(); renderEngineButtons(); } function toggleEngineVisibility(index) { const engine = config.engines[index]; if (!engine) return; engine.hidden = !engine.hidden; saveConfig(config); renderPanel(); renderEngineButtons(); } function toggleWidgetVisibility(index) { const engine = config.engines[index]; if (!engine) return; engine.disableWidget = !engine.disableWidget; saveConfig(config); renderPanel(); renderEngineButtons(); } function exportConfig() { const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(config, null, 2)); const downloadAnchorNode = document.createElement('a'); downloadAnchorNode.setAttribute("href", dataStr); downloadAnchorNode.setAttribute("download", "search-engine-switcher-config.json"); document.body.appendChild(downloadAnchorNode); // required for firefox downloadAnchorNode.click(); downloadAnchorNode.remove(); } function importConfig() { const input = document.createElement('input'); input.type = 'file'; input.accept = 'application/json'; input.onchange = e => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = event => { try { const importedData = JSON.parse(event.target.result); const mergedConfig = mergeConfig(importedData); if (!confirm('确定要覆盖当前配置吗?此操作不可逆!')) return; Object.assign(config, mergedConfig); saveConfig(config); applyRootPosition(createRoot()); applyTheme(); renderEngineButtons(); renderPanel(); alert('配置导入成功!'); } catch (error) { alert('配置导入失败:无效的 JSON 格式或数据结构。'); console.error('Import Config Error:', error); } }; reader.readAsText(file); }; input.click(); } function startDragPositioning() { const root = createRoot(); config.ui.useCustomXY = true; saveConfig(config); applyRootPosition(root); alert('拖动切换器到你想要的位置,松开后自动保存'); root.style.cursor = 'move'; root.style.touchAction = 'none'; let dragging = false; let startX = 0; let startY = 0; let originX = config.ui.customX; let originY = config.ui.customY; function onDown(e) { if (e.target && e.target.closest('.ses-pill')) return; dragging = true; startX = e.clientX; startY = e.clientY; originX = config.ui.customX; originY = config.ui.customY; if (e.target.setPointerCapture) { e.target.setPointerCapture(e.pointerId); } document.addEventListener('pointermove', onMove); document.addEventListener('pointerup', onUp); e.preventDefault(); } function onMove(e) { if (!dragging) return; config.ui.customX = Math.max(0, originX + (e.clientX - startX)); config.ui.customY = Math.max(0, originY + (e.clientY - startY)); applyRootPosition(root); } function onUp(e) { dragging = false; saveConfig(config); renderPanel(); root.style.cursor = ''; root.style.touchAction = ''; if (e && e.target && e.target.releasePointerCapture) { try { e.target.releasePointerCapture(e.pointerId); } catch (_) { } } root.removeEventListener('pointerdown', onDown); document.removeEventListener('pointermove', onMove); document.removeEventListener('pointerup', onUp); } root.addEventListener('pointerdown', onDown); } function renderPanel() { const overlay = createPanel(); const panel = overlay.querySelector('.panel'); const engineRows = config.engines .map( (e, i) => `
${escapeHtml(e.name)}${e.hidden ? ' (按钮已隐藏)' : ''}${e.disableWidget ? ' (本站禁用悬浮)' : ''}
${escapeHtml(e.searchUrl)}
识别域名: ${escapeHtml((e.hosts || []).join(', ') || '(空)')}
` ) .join(''); panel.innerHTML = `

搜索引擎切换器设置

引擎列表(支持新增 / 删除 / 排序 / 隐藏)
${engineRows}
显示位置
行为选项
`; panel.querySelectorAll('.engine-row').forEach((row) => { row.addEventListener('click', (event) => { const target = event.target; if (!(target instanceof HTMLElement)) return; const actionEl = target.closest('[data-act]'); if (!actionEl) return; const index = Number(row.getAttribute('data-index')); const act = actionEl.getAttribute('data-act'); if (act === 'up') moveEngine(index, -1); if (act === 'down') moveEngine(index, 1); if (act === 'edit') editEngine(config.engines[index]); if (act === 'del') deleteEngine(index); if (act === 'hide' || act === 'show') toggleEngineVisibility(index); if (act === 'toggle-widget') toggleWidgetVisibility(index); }); }); panel.querySelector('#ses-add').addEventListener('click', () => editEngine(null)); panel.querySelector('#ses-export').addEventListener('click', exportConfig); panel.querySelector('#ses-import').addEventListener('click', importConfig); panel.querySelector('#ses-show-guide').addEventListener('click', () => showOfflineGuide()); // 恢复默认 - 引擎列表 panel.querySelector('#ses-reset-engines').addEventListener('click', () => { if (!confirm('确定恢复默认搜索引擎列表吗?')) return; const reset = deepClone(DEFAULT_CONFIG); config.engines = reset.engines; saveConfig(config); renderPanel(); renderEngineButtons(); }); // 恢复默认 - 显示位置 panel.querySelector('#ses-reset-position').addEventListener('click', () => { if (!confirm('确定恢复默认显示位置吗?')) return; const reset = deepClone(DEFAULT_CONFIG); config.ui.vertical = reset.ui.vertical; config.ui.align = reset.ui.align; config.ui.offsetX = reset.ui.offsetX; config.ui.offsetY = reset.ui.offsetY; config.ui.useCustomXY = reset.ui.useCustomXY; config.ui.customX = reset.ui.customX; config.ui.customY = reset.ui.customY; saveConfig(config); applyRootPosition(createRoot()); renderPanel(); }); // 恢复默认 - 行为选项 panel.querySelector('#ses-reset-behavior').addEventListener('click', () => { if (!confirm('确定恢复默认行为选项吗?')) return; const reset = deepClone(DEFAULT_CONFIG); config.ui.showWhenNoQuery = reset.ui.showWhenNoQuery; config.ui.openInNewTab = reset.ui.openInNewTab; config.ui.theme = reset.ui.theme; saveConfig(config); applyTheme(); renderPanel(); }); panel.querySelector('#ses-drag').addEventListener('click', () => { closePanel(); startDragPositioning(); }); panel.querySelector('#ses-close').addEventListener('click', closePanel); panel.querySelector('#ses-save').addEventListener('click', () => { const posMode = panel.querySelector('#ses-pos-mode').value; const vertical = panel.querySelector('#ses-vertical').value; const align = panel.querySelector('#ses-align').value; const offsetX = Number(panel.querySelector('#ses-offset-x').value) || 0; const offsetY = Number(panel.querySelector('#ses-offset-y').value) || 0; const customX = Number(panel.querySelector('#ses-custom-x').value) || 0; const customY = Number(panel.querySelector('#ses-custom-y').value) || 0; const showNoQuery = panel.querySelector('#ses-show-no-query').value === '1'; const openInNewTab = panel.querySelector('#ses-open-way').value === '1'; const theme = panel.querySelector('#ses-theme').value; config.ui.useCustomXY = posMode === 'custom'; config.ui.vertical = vertical === 'top' ? 'top' : 'bottom'; config.ui.theme = ['light', 'dark'].includes(theme) ? theme : 'auto'; config.ui.align = ['left', 'center', 'right'].includes(align) ? align : 'center'; config.ui.offsetX = Math.max(0, offsetX); config.ui.offsetY = Math.max(0, offsetY); config.ui.customX = Math.max(0, customX); config.ui.customY = Math.max(0, customY); config.ui.showWhenNoQuery = showNoQuery; config.ui.openInNewTab = openInNewTab; saveConfig(config); applyRootPosition(createRoot()); applyTheme(); renderEngineButtons(); closePanel(); }); } function openPanel() { renderPanel(); const overlay = createPanel(); overlay.classList.add('show'); // 阻止面板滚动影响下方网页 const panel = overlay.querySelector('.panel'); if (panel) { panel.addEventListener('wheel', (e) => { const isScrollingUp = e.deltaY < 0; const isScrollingDown = e.deltaY > 0; const isAtTop = panel.scrollTop === 0; const isAtBottom = panel.scrollTop + panel.clientHeight >= panel.scrollHeight - 1; // 如果在顶部继续向上滚动,或在底部继续向下滚动,阻止事件 if ((isScrollingUp && isAtTop) || (isScrollingDown && isAtBottom)) { e.preventDefault(); } }, { passive: false }); // 阻止 touchmove 事件冒泡 panel.addEventListener('touchmove', (e) => { e.stopPropagation(); }, { passive: true }); } } function closePanel() { const overlay = createPanel(); overlay.classList.remove('show'); } function showOfflineGuide() { const guideHtml = ` Search Engine Switcher 使用指南

🎉 欢迎使用 Search Engine Switcher (v${CURRENT_VERSION})

感谢您安装本脚本!为了让您更好地使用,请花 1 分钟阅读以下指南:

👀 悬浮窗去哪了?为什么不显示?

为了给您提供纯净的排版体验并避免遮挡内容,本脚本的默认机制是:在未检测到“搜索需求”时,自动隐藏自身。

具体在以下几种情况下,悬浮窗默认会“消失”:

什么情况下它会自动“呼出”?

💡 觉得这样太麻烦?想要让它在 GitHub 主页也一直保持可见? 如果你希望在主页发呆、或者未搜任何词的情况下也始终能看到这排按钮,请按以下步骤操作:
  1. 长按一次现有的悬浮窗(或点击小齿轮 ⚙ 图标)唤出设置面板
  2. 向下划动,找到底部的【行为选项】分类
  3. 【无关键词时】选项的设定从“隐藏切换器”改为“仍然显示切换器”
  4. 点击右下角保存设置,它就不会再随意消失了!

🖱️ 基础操作

🚫 为什么 B站/YouTube 也会有悬浮窗?怎么关掉?

脚本预设了一些社交/视频类搜索(如 B站、YouTube、知乎)。如果在刷视频时悬浮窗影响了您的体验,您可以:

  1. 打开脚本的设置面板
  2. 在引擎列表中找到对应的网站(例如“哔哩哔哩”)。
  3. 点击列表右侧的 “禁用悬浮” 按钮(如果已经是禁用状态则会显示“本站禁用悬浮”红字)。

这样,该网站就不会再出现悬浮窗了!

`; const blob = new Blob([guideHtml], { type: 'text/html' }); const url = URL.createObjectURL(blob); window.open(url, '_blank'); } function checkFirstInstallOrUpdate() { const lastVersion = safeGMGet(VERSION_KEY, null); if (lastVersion !== CURRENT_VERSION) { safeGMSet(VERSION_KEY, CURRENT_VERSION); // 只在安装/更新后的第一次自动弹出 showOfflineGuide(); } } function escapeHtml(text) { return String(text) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function init() { checkFirstInstallOrUpdate(); injectStyle(); applyTheme(); createRoot(); renderEngineButtons(); if (typeof GM_registerMenuCommand === 'function') { GM_registerMenuCommand('搜索引擎切换器设置', openPanel); GM_registerMenuCommand('搜索引擎切换器开关', () => { const root = createRoot(); root.classList.toggle('hidden'); }); } window.addEventListener('popstate', renderEngineButtons); window.addEventListener('hashchange', renderEngineButtons); // Listen for system theme changes when in auto mode if (window.matchMedia) { const mediaQuery = window.matchMedia('(prefers-color-scheme: light)'); mediaQuery.addEventListener('change', () => { if (config.ui.theme === 'auto') { applyTheme(); } }); } const observer = new MutationObserver(() => { if (!document.getElementById(ROOT_ID)) { createRoot(); } }); observer.observe(document.documentElement, { childList: true, subtree: true }); setInterval(renderEngineButtons, 1600); } init(); })();