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