// ==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 = `
感谢您安装本脚本!为了让您更好地使用,请花 1 分钟阅读以下指南:
为了给您提供纯净的排版体验并避免遮挡内容,本脚本的默认机制是:在未检测到“搜索需求”时,自动隐藏自身。
具体在以下几种情况下,悬浮窗默认会“消失”:
什么情况下它会自动“呼出”?
q= 或 wd= 等请求参数),它就会马上出现在页面底部供您随时切换。脚本预设了一些社交/视频类搜索(如 B站、YouTube、知乎)。如果在刷视频时悬浮窗影响了您的体验,您可以:
这样,该网站就不会再出现悬浮窗了!