Instant Search Switcher

Changes search engine from one site to another without deleting search query. Draggable UI.

  1. // ==UserScript==
  2. // @name Instant Search Switcher
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.6
  5. // @description Changes search engine from one site to another without deleting search query. Draggable UI.
  6. // @author Faisal Bhuiyan
  7. // @match *://*.bing.com/*search*
  8. // @match *://*.bing.com/chat*
  9. // @match https://www.google.com/search*
  10. // @match https://yandex.com/*search*
  11. // @match https://search.brave.com/*search*
  12. // @match https://searx.fmhy.net/*
  13. // @license MIT
  14. // @grant GM_setValue
  15. // @grant GM_getValue
  16. // ==/UserScript==
  17.  
  18. (function () {
  19. 'use strict';
  20.  
  21. const searchEngines = [
  22. {
  23. name: 'Google',
  24. url: 'https://www.google.com/search',
  25. param: 'q'
  26. },
  27. {
  28. name: 'Bing',
  29. url: 'https://www.bing.com/search',
  30. param: 'q'
  31. },
  32. {
  33. name: 'Copilot',
  34. url: 'https://www.bing.com/chat',
  35. param: 'q',
  36. additionalParams: '&sendquery=1&FORM=SCCODX'
  37. },
  38. {
  39. name: 'Brave',
  40. url: 'https://search.brave.com/search',
  41. param: 'q'
  42. },
  43. {
  44. name: 'Yandex',
  45. url: 'https://yandex.com/search',
  46. param: 'text'
  47. },
  48. {
  49. name: 'Perplexity',
  50. url: 'https://www.perplexity.ai/search',
  51. param: 'q'
  52. },
  53. {
  54. name: 'SearX',
  55. url: 'https://searx.fmhy.net/',
  56. param: 'q'
  57. },
  58. {
  59. name: 'Scira',
  60. url: 'https://scira.ai/',
  61. param: 'q'
  62. },
  63. {
  64. name: 'ChatGPT',
  65. url: 'https://chatgpt.com/',
  66. param: 'q'
  67. }
  68. ];
  69.  
  70. // Default position - define this as a constant
  71. const defaultPosition = { top: '20px', right: '5rem', left: 'auto' };
  72.  
  73. // Try to get saved position or use default
  74. let position;
  75. try {
  76. const savedPosition = GM_getValue('switcherPosition');
  77. // Only use saved position if it exists and has been manually set
  78. position = (savedPosition && GM_getValue('positionModified')) ? savedPosition : defaultPosition;
  79. } catch (e) {
  80. // If GM_getValue is not available, use defaultPosition
  81. position = defaultPosition;
  82. }
  83.  
  84. // Create container for draggable functionality
  85. const container = document.createElement('div');
  86. container.style.position = 'fixed';
  87. container.style.top = position.top;
  88. container.style.right = position.right;
  89. container.style.left = position.left;
  90. container.style.zIndex = '9999';
  91. container.style.cursor = 'move';
  92. container.style.userSelect = 'none';
  93. container.style.touchAction = 'none';
  94. container.style.display = 'flex';
  95. container.style.flexDirection = 'row';
  96. container.style.alignItems = 'center';
  97. container.style.padding = '4px';
  98. container.style.backgroundColor = 'rgba(255, 255, 255, 0.8)';
  99. container.style.borderRadius = '4px';
  100. container.style.boxShadow = '0 2px 5px rgba(0, 0, 0, 0.2)';
  101.  
  102. // Create a handle for dragging
  103. const dragHandle = document.createElement('div');
  104. dragHandle.innerHTML = '⋮⋮'; // vertical dots as drag indicator
  105. dragHandle.style.marginRight = '5px';
  106. dragHandle.style.cursor = 'move';
  107. dragHandle.style.color = '#555';
  108. dragHandle.style.fontSize = '16px';
  109. dragHandle.style.paddingRight = '5px';
  110. dragHandle.title = 'Drag to move';
  111.  
  112. // Create reset button
  113. const resetButton = document.createElement('div');
  114. resetButton.innerHTML = '↺'; // reset icon
  115. resetButton.style.marginLeft = '5px';
  116. resetButton.style.cursor = 'pointer';
  117. resetButton.style.color = '#555';
  118. resetButton.style.fontSize = '16px';
  119. resetButton.style.padding = '0 5px';
  120. resetButton.title = 'Reset position';
  121. resetButton.style.display = 'flex';
  122. resetButton.style.alignItems = 'center';
  123. resetButton.style.justifyContent = 'center';
  124.  
  125. // Create the floating select box
  126. const selectBox = document.createElement('select');
  127. selectBox.style.fontSize = '16px';
  128. selectBox.style.padding = '5px';
  129. selectBox.style.borderRadius = '4px';
  130. selectBox.style.backgroundColor = '#fff';
  131. selectBox.style.color = '#000'; // Force black text
  132. selectBox.style.border = '1px solid #ccc';
  133. selectBox.style.outline = 'none';
  134. selectBox.style.cursor = 'pointer';
  135.  
  136. // Add CSS to ensure text remains visible in both light and dark themes
  137. const style = document.createElement('style');
  138. style.textContent = `
  139. .search-switcher-select {
  140. color: #000 !important;
  141. -webkit-text-fill-color: #000 !important;
  142. }
  143. .search-switcher-select option {
  144. background-color: #fff !important;
  145. color: #000 !important;
  146. -webkit-text-fill-color: #000 !important;
  147. }
  148. `;
  149. document.head.appendChild(style);
  150.  
  151. selectBox.className = 'search-switcher-select';
  152.  
  153. // Add an empty option as the first element
  154. const emptyOption = document.createElement('option');
  155. emptyOption.value = '';
  156. emptyOption.textContent = 'Select';
  157. emptyOption.disabled = true;
  158. emptyOption.selected = true;
  159. selectBox.appendChild(emptyOption);
  160.  
  161. // Add search engines to the select box
  162. searchEngines.forEach(engine => {
  163. const option = document.createElement('option');
  164. option.value = engine.url;
  165. option.textContent = engine.name;
  166. selectBox.appendChild(option);
  167. });
  168.  
  169. // Add elements to container
  170. container.appendChild(dragHandle);
  171. container.appendChild(selectBox);
  172. container.appendChild(resetButton);
  173.  
  174. // Append the container to the body
  175. document.body.appendChild(container);
  176.  
  177. // Reset button functionality
  178. resetButton.addEventListener('click', (e) => {
  179. e.stopPropagation();
  180.  
  181. // Apply default position
  182. container.style.top = defaultPosition.top;
  183. container.style.right = defaultPosition.right;
  184. container.style.left = defaultPosition.left;
  185.  
  186. // Save the default settings
  187. try {
  188. GM_setValue('switcherPosition', defaultPosition);
  189. GM_setValue('positionModified', false);
  190. } catch (e) {
  191. // Silently fail if GM_setValue is not available
  192. }
  193. });
  194.  
  195. // Detect changes to the select box
  196. selectBox.addEventListener('change', () => {
  197. const selectedEngine = searchEngines[selectBox.selectedIndex - 1];
  198. // Try to get query parameter from either 'q' or 'text' depending on current search engine
  199. const currentQuery = new URLSearchParams(window.location.search).get('q') ||
  200. new URLSearchParams(window.location.search).get('text');
  201.  
  202. if (currentQuery && selectedEngine) {
  203. const additionalParams = selectedEngine.additionalParams || '';
  204. window.location.href = `${selectedEngine.url}?${selectedEngine.param}=${encodeURIComponent(currentQuery)}${additionalParams}`;
  205. }
  206. });
  207.  
  208. // Make the container draggable
  209. let isDragging = false;
  210. let dragOffsetX, dragOffsetY;
  211.  
  212. // Function to start dragging
  213. function startDrag(clientX, clientY) {
  214. isDragging = true;
  215.  
  216. // Calculate the offset of the mouse within the container
  217. const rect = container.getBoundingClientRect();
  218. dragOffsetX = clientX - rect.left;
  219. dragOffsetY = clientY - rect.top;
  220.  
  221. // Prevent text selection during drag
  222. document.body.style.userSelect = 'none';
  223. }
  224.  
  225. // Function to handle dragging
  226. function drag(clientX, clientY) {
  227. if (!isDragging) return;
  228.  
  229. // Calculate new position based on mouse/touch position and offset
  230. const newLeft = clientX - dragOffsetX;
  231. const newTop = clientY - dragOffsetY;
  232.  
  233. // Ensure the element stays within viewport bounds
  234. const maxX = window.innerWidth - container.offsetWidth;
  235. const maxY = window.innerHeight - container.offsetHeight;
  236.  
  237. const boundedLeft = Math.max(0, Math.min(newLeft, maxX));
  238. const boundedTop = Math.max(0, Math.min(newTop, maxY));
  239.  
  240. // Set new position
  241. container.style.left = boundedLeft + 'px';
  242. container.style.top = boundedTop + 'px';
  243. container.style.right = 'auto'; // Clear right positioning to avoid conflicts
  244. }
  245.  
  246. // Function to end dragging
  247. function endDrag() {
  248. if (isDragging) {
  249. isDragging = false;
  250. document.body.style.userSelect = '';
  251.  
  252. // Save the new position and mark as modified
  253. try {
  254. GM_setValue('switcherPosition', {
  255. top: container.style.top,
  256. right: 'auto',
  257. left: container.style.left
  258. });
  259. GM_setValue('positionModified', true);
  260. } catch (e) {
  261. // Silently fail if GM_setValue is not available
  262. }
  263. }
  264. }
  265.  
  266. // Mouse event listeners
  267. dragHandle.addEventListener('mousedown', (e) => {
  268. e.preventDefault();
  269. startDrag(e.clientX, e.clientY);
  270. });
  271.  
  272. document.addEventListener('mousemove', (e) => {
  273. drag(e.clientX, e.clientY);
  274. });
  275.  
  276. document.addEventListener('mouseup', endDrag);
  277.  
  278. // Touch event listeners
  279. dragHandle.addEventListener('touchstart', (e) => {
  280. if (e.touches.length === 1) {
  281. e.preventDefault();
  282. const touch = e.touches[0];
  283. startDrag(touch.clientX, touch.clientY);
  284. }
  285. });
  286.  
  287. document.addEventListener('touchmove', (e) => {
  288. if (isDragging && e.touches.length === 1) {
  289. e.preventDefault();
  290. const touch = e.touches[0];
  291. drag(touch.clientX, touch.clientY);
  292. }
  293. });
  294.  
  295. document.addEventListener('touchend', endDrag);
  296. document.addEventListener('touchcancel', endDrag);
  297.  
  298. // Prevent selectbox from triggering drag
  299. selectBox.addEventListener('mousedown', (e) => {
  300. e.stopPropagation();
  301. });
  302.  
  303. selectBox.addEventListener('touchstart', (e) => {
  304. e.stopPropagation();
  305. });
  306. })();