Greasy Fork is available in English.

搜索引擎切换器 / Search Engine Switcher

🚀 一键切换多个搜索引擎!支持Google、Bing、百度、ChatGPT、Perplexity等13大搜索平台。可拖拽、自动隐藏,提升您的搜索效率。适配暗黑模式,让搜索更智能、更便捷!

  1. // ==UserScript==
  2. // @name 搜索引擎切换器 / Search Engine Switcher
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.2.1
  5. // @description 🚀 一键切换多个搜索引擎!支持Google、Bing、百度、ChatGPT、Perplexity等13大搜索平台。可拖拽、自动隐藏,提升您的搜索效率。适配暗黑模式,让搜索更智能、更便捷!
  6. // @author WUJI (微信: wujiai666)
  7. // @match *://www.google.com*/search*
  8. // @match *://www.bing.com/search*
  9. // @match *://cn.bing.com/search*
  10. // @match *://www.baidu.com/s*
  11. // @match *://www.baidu.com/baidu*
  12. // @match *://chatgpt.com/*
  13. // @match *://metaso.cn/*
  14. // @match *://weixin.sogou.com/weixin*
  15. // @match *://search.bilibili.com/all*
  16. // @match *://www.youtube.com/results*
  17. // @match *://m.youtube.com/results*
  18. // @match *://www.zhihu.com/search*
  19. // @match *://github.com/search*
  20. // @match *://www.xiaohongshu.com/explore*
  21. // @match *://www.douyin.com/search/*
  22. // @match *://www.perplexity.ai/*
  23. // @grant unsafeWindow
  24. // @grant window.onload
  25. // @run-at document-body
  26. // @license MIT
  27. // ==/UserScript==
  28.  
  29. (function() {
  30. 'use strict';
  31.  
  32. const urlMapping = [
  33. { name: "Google", searchUrl: "https://www.google.com/search?q=", keyName: "q", testUrl: /https:\/\/www\.google\.(com|com\.hk)\/search.*/ },
  34. { name: "Bing", searchUrl: "https://www.bing.com/search?q=", keyName: "q", testUrl: /https:\/\/(www|cn)\.bing\.com\/search.*/ },
  35. { name: "百度", searchUrl: "https://www.baidu.com/s?wd=", keyName: "wd", testUrl: /https:\/\/www\.baidu\.com\/(s|baidu).*/ },
  36. { name: "ChatGPT", searchUrl: "https://chatgpt.com/?hints=search&q=", keyName: "q", testUrl: /https:\/\/chatgpt\.com\/.*/ },
  37. { name: "秘塔", searchUrl: "https://metaso.cn/?q=", keyName: "q", testUrl: /https:\/\/metaso\.cn\/.*/ },
  38. { name: "微信", searchUrl: "https://weixin.sogou.com/weixin?type=2&s_from=input&query=", keyName: "query", testUrl: /https:\/\/weixin\.sogou\.com\/weixin.*/ },
  39. { name: "哔站", searchUrl: "https://search.bilibili.com/all?keyword=", keyName: "keyword", testUrl: /https:\/\/search\.bilibili\.com\/all.*/ },
  40. { name: "油管", searchUrl: "https://www.youtube.com/results?search_query=", keyName: "search_query", testUrl: /https:\/\/(www|m)\.youtube\.com\/results.*/ },
  41. { name: "知乎", searchUrl: "https://www.zhihu.com/search?q=", keyName: "q", testUrl: /https:\/\/www\.zhihu\.com\/search.*/ },
  42. { name: "GitHub", searchUrl: "https://github.com/search?q=", keyName: "q", testUrl: /https:\/\/github\.com\/search.*/ },
  43. { name: "小红书", searchUrl: "https://www.xiaohongshu.com/explore?q=", keyName: "q", testUrl: /https:\/\/www\.xiaohongshu\.com\/explore.*/ },
  44. { name: "抖音", searchUrl: "https://www.douyin.com/search/", keyName: "q", testUrl: /https:\/\/www\.douyin\.com\/search\/.*/ },
  45. { name: "Perplexity", searchUrl: "https://www.perplexity.ai/?q=", keyName: "q", testUrl: /https:\/\/www\.perplexity\.ai\/.*/ },
  46. ];
  47.  
  48. const ICON_SIZE = '32px';
  49. const LIST_WIDTH = '100px';
  50. const FONT_SIZE = '14px';
  51. const AUTO_HIDE_DELAY = 5000; // 5 seconds
  52.  
  53. function getQueryVariable(variable) {
  54. const query = window.location.search.substring(1);
  55. const vars = query.split('&');
  56. for (let i = 0; i < vars.length; i++) {
  57. const pair = vars[i].split('=');
  58. if (decodeURIComponent(pair[0]) === variable) {
  59. return decodeURIComponent(pair[1]);
  60. }
  61. }
  62. if (variable === "q" && window.location.pathname.startsWith("/search/")) {
  63. return decodeURIComponent(window.location.pathname.replace("/search/", ""));
  64. }
  65. return "";
  66. }
  67.  
  68. function getKeywords() {
  69. for (const item of urlMapping) {
  70. if (item.testUrl.test(window.location.href)) {
  71. return getQueryVariable(item.keyName);
  72. }
  73. }
  74. return "";
  75. }
  76.  
  77. function isDarkMode() {
  78. return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
  79. }
  80.  
  81. function createStyle() {
  82. const style = document.createElement('style');
  83. style.textContent = `
  84. #search-app-box {
  85. position: fixed;
  86. top: 100px;
  87. left: 0;
  88. width: ${ICON_SIZE};
  89. height: ${ICON_SIZE};
  90. background-color: transparent;
  91. z-index: 2147483647;
  92. cursor: move;
  93. font-size: ${FONT_SIZE};
  94. transition: left 0.3s ease-in-out;
  95. }
  96. #search-app-icon {
  97. width: 100%;
  98. height: 100%;
  99. display: flex;
  100. justify-content: center;
  101. align-items: center;
  102. font-size: ${ICON_SIZE};
  103. user-select: none;
  104. background-color: rgba(255, 255, 255, 0.7);
  105. border-radius: 0 50% 50% 0;
  106. }
  107. #search-engine-list {
  108. position: absolute;
  109. top: 0;
  110. left: ${ICON_SIZE};
  111. width: ${LIST_WIDTH};
  112. max-height: 70vh;
  113. overflow-y: auto;
  114. background-color: rgba(255, 255, 255, 0.9);
  115. border-radius: 8px;
  116. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  117. opacity: 0;
  118. visibility: hidden;
  119. transform: translateX(-10px);
  120. transition: opacity 0.3s, visibility 0.3s, transform 0.3s;
  121. }
  122. #search-engine-list a {
  123. display: block;
  124. padding: 8px 12px;
  125. color: #333;
  126. text-decoration: none;
  127. transition: background-color 0.3s;
  128. }
  129. #search-engine-list a:hover {
  130. background-color: rgba(0, 0, 0, 0.1);
  131. }
  132. .dark-mode #search-app-icon {
  133. background-color: rgba(50, 50, 50, 0.7);
  134. }
  135. .dark-mode #search-engine-list {
  136. background-color: rgba(50, 50, 50, 0.9);
  137. }
  138. .dark-mode #search-engine-list a {
  139. color: #fff;
  140. }
  141. .dark-mode #search-engine-list a:hover {
  142. background-color: rgba(255, 255, 255, 0.1);
  143. }
  144. #search-app-box.hidden {
  145. left: -8px;
  146. }
  147. #search-app-box.dragging {
  148. transition: none;
  149. }
  150. `;
  151. document.head.appendChild(style);
  152. }
  153.  
  154. function createSearchBox() {
  155. const div = document.createElement('div');
  156. div.id = 'search-app-box';
  157.  
  158. const icon = document.createElement('div');
  159. icon.id = 'search-app-icon';
  160. icon.innerText = '🔍';
  161. div.appendChild(icon);
  162.  
  163. const listContainer = document.createElement('div');
  164. listContainer.id = 'search-engine-list';
  165.  
  166. for (const item of urlMapping) {
  167. const a = document.createElement('a');
  168. a.href = item.searchUrl + encodeURIComponent(getKeywords());
  169. a.innerText = item.name;
  170. a.addEventListener('click', (e) => {
  171. e.preventDefault();
  172. window.location.href = a.href;
  173. });
  174. listContainer.appendChild(a);
  175. }
  176.  
  177. div.appendChild(listContainer);
  178. document.body.appendChild(div);
  179.  
  180. let hideTimeout;
  181.  
  182. function showSearchBox() {
  183. div.classList.remove('hidden');
  184. clearTimeout(hideTimeout);
  185. }
  186.  
  187. function hideSearchBox() {
  188. div.classList.add('hidden');
  189. }
  190.  
  191. function resetHideTimer() {
  192. clearTimeout(hideTimeout);
  193. hideTimeout = setTimeout(hideSearchBox, AUTO_HIDE_DELAY);
  194. }
  195.  
  196. div.addEventListener('mouseenter', () => {
  197. showSearchBox();
  198. listContainer.style.opacity = '1';
  199. listContainer.style.visibility = 'visible';
  200. listContainer.style.transform = 'translateX(0)';
  201. });
  202.  
  203. div.addEventListener('mouseleave', () => {
  204. listContainer.style.opacity = '0';
  205. listContainer.style.visibility = 'hidden';
  206. listContainer.style.transform = 'translateX(-10px)';
  207. resetHideTimer();
  208. });
  209.  
  210. // 拖拽功能
  211. let isDragging = false;
  212. let startX, startY, startLeft, startTop;
  213.  
  214. div.addEventListener('mousedown', (e) => {
  215. isDragging = true;
  216. startX = e.clientX;
  217. startY = e.clientY;
  218. startLeft = div.offsetLeft;
  219. startTop = div.offsetTop;
  220. showSearchBox();
  221. e.preventDefault();
  222. div.classList.add('dragging');
  223. });
  224.  
  225. document.addEventListener('mousemove', (e) => {
  226. if (!isDragging) return;
  227. const dx = e.clientX - startX;
  228. const dy = e.clientY - startY;
  229. div.style.left = `${startLeft + dx}px`;
  230. div.style.top = `${startTop + dy}px`;
  231. });
  232.  
  233. document.addEventListener('mouseup', () => {
  234. isDragging = false;
  235. resetHideTimer();
  236. div.classList.remove('dragging');
  237. });
  238.  
  239. // 初始化隐藏定时器
  240. resetHideTimer();
  241. }
  242.  
  243. function updateTheme() {
  244. document.body.classList.toggle('dark-mode', isDarkMode());
  245. }
  246.  
  247. function init() {
  248. createStyle();
  249. createSearchBox();
  250. updateTheme();
  251.  
  252. // 监听主题变化
  253. window.matchMedia('(prefers-color-scheme: dark)').addListener(updateTheme);
  254. }
  255.  
  256. // 使用 MutationObserver 来确保脚本在动态加载的页面上也能正常工作
  257. const observer = new MutationObserver((mutations, obs) => {
  258. const body = document.querySelector('body');
  259. if (body) {
  260. init();
  261. obs.disconnect();
  262. }
  263. });
  264.  
  265. observer.observe(document.documentElement, {
  266. childList: true,
  267. subtree: true
  268. });
  269. })();