Fast Search

Quickly search various sites using custom shortcuts with an improved UI.

  1. // ==UserScript==
  2. // @name Fast Search
  3. // @namespace fast-search
  4. // @version 0.1.6
  5. // @description Quickly search various sites using custom shortcuts with an improved UI.
  6. // @author JJJ
  7. // @icon https://th.bing.com/th/id/OUG.FC606EBD21BF6D1E0D5ABF01EACD594E?rs=1&pid=ImgDetMain
  8. // @match *://*/*
  9. // @exclude https://www.youtube.com/*/videos
  10. // @grant GM_addStyle
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_registerMenuCommand
  14. // @grant window.focus
  15. // @run-at document-end
  16. // @require https://unpkg.com/react@17/umd/react.production.min.js
  17. // @require https://unpkg.com/react-dom@17/umd/react-dom.production.min.js
  18. // @license MIT
  19. // ==/UserScript==
  20.  
  21. (function () {
  22. 'use strict';
  23.  
  24. // ===================================================
  25. // CONFIGURATION
  26. // ===================================================
  27. const SEARCH_ENGINES = {
  28. // Search
  29. a: { name: "Amazon", url: "https://www.amazon.com/s?k=" },
  30. g: { name: "Google", url: "https://www.google.com/search?q=" },
  31. b: { name: "Bing", url: "https://www.bing.com/search?q=" },
  32. d: { name: "DuckDuckGo", url: "https://duckduckgo.com/?q=" },
  33. gs: { name: "Google Scholar", url: "https://scholar.google.com/scholar?q=" },
  34. gi: { name: "Google Images", url: "https://www.google.com/search?tbm=isch&q=" },
  35. ar: { name: "Internet Archive", url: "https://archive.org/search.php?query=" },
  36. way: { name: "Wayback Machine", url: "https://web.archive.org/web/*/" },
  37. w: { name: "Wikipedia", url: "https://en.wikipedia.org/w/index.php?search=" },
  38. p: { name: "Perplexity", url: "https://www.perplexity.ai/?q=" },
  39.  
  40. // Coding
  41. gf: { name: "Greasy Fork", url: "https://greasyfork.org/en/scripts?q=" },
  42. gh: { name: "GitHub", url: "https://github.com/search?q=" },
  43. so: { name: "Stack Overflow", url: "https://stackoverflow.com/search?q=" },
  44.  
  45. // Social
  46. r: { name: "Reddit", url: "https://www.reddit.com/search/?q=" },
  47. li: { name: "LinkedIn", url: "https://www.linkedin.com/search/results/all/?keywords=" },
  48. t: { name: "Twitch", url: "https://www.twitch.tv/search?term=" },
  49. x: { name: "Twitter", url: "https://twitter.com/search?q=" },
  50. f: { name: "Facebook", url: "https://www.facebook.com/search/top/?q=" },
  51. i: { name: "Instagram", url: "https://www.instagram.com/explore/tags/" },
  52. pi: { name: "Pinterest", url: "https://www.pinterest.com/search/pins/?q=" },
  53. tu: { name: "Tumblr", url: "https://www.tumblr.com/search/" },
  54. q: { name: "Quora", url: "https://www.quora.com/search?q=" },
  55. sc: { name: "SoundCloud", url: "https://soundcloud.com/search?q=" },
  56. y: { name: "YouTube", url: "https://www.youtube.com/results?search_query=" },
  57. tk: { name: "TikTok", url: "https://www.tiktok.com/search?q=" },
  58. fi: { name: "Find That Meme", url: "https://findthatmeme.com/?search=" },
  59. sp: { name: "Spotify", url: "https://open.spotify.com/search/" },
  60.  
  61. // Gaming
  62. steam: { name: "Steam", url: "https://store.steampowered.com/search/?term=" },
  63. epic: { name: "Epic Games", url: "https://store.epicgames.com/en-US/browse?q=" },
  64. gog: { name: "GOG", url: "https://www.gog.com/games?search=" },
  65. ubi: { name: "Ubisoft", url: "https://store.ubi.com/us/search?q=" },
  66. g2: { name: "G2A", url: "https://www.g2a.com/search?query=" },
  67. cd: { name: "CDKeys", url: "https://www.cdkeys.com/catalogsearch/result/?q=" },
  68. ori: { name: "Origin", url: "https://www.origin.com/search?searchString=" },
  69. bat: { name: "Battle.net", url: "https://shop.battle.net/search?q=" },
  70.  
  71. // Movies and TV Shows
  72. c: { name: "Cuevana", url: "https://wow.cuevana3.nu/search?s=" },
  73. lm: { name: "LookMovie (Movies)", url: "https://www.lookmovie2.to/movies/search/?q=" },
  74. ls: { name: "LookMovie (Shows)", url: "https://www.lookmovie2.to/shows/search/?q=" },
  75. };
  76.  
  77. // ===================================================
  78. // UTILITY FUNCTIONS
  79. // ===================================================
  80. const Utils = {
  81. /**
  82. * Check if the focus is in an editable element
  83. * @returns {boolean} True if focus is in editable element
  84. */
  85. isFocusInEditable: () => {
  86. const el = document.activeElement;
  87. return el.isContentEditable || ['input', 'textarea'].includes(el.tagName.toLowerCase());
  88. },
  89.  
  90. /**
  91. * Construct search URL from shortcut and query
  92. * @param {string} shortcut - The search engine shortcut
  93. * @param {string} query - The search query
  94. * @returns {string} The constructed search URL
  95. */
  96. constructSearchUrl: (shortcut, query) => {
  97. const engine = SEARCH_ENGINES[shortcut] || SEARCH_ENGINES.g;
  98. if (!query.trim()) {
  99. // Extract base domain using regex
  100. const match = engine.url.match(/^https?:\/\/([\w.-]+\.[a-z]{2,})/);
  101. return match ? `https://${match[1]}/` : engine.url;
  102. }
  103. let baseUrl = engine.url;
  104. if (shortcut === 'epic') {
  105. baseUrl += `${encodeURIComponent(query)}&sortBy=relevancy&sortDir=DESC&count=40`;
  106. } else {
  107. baseUrl += encodeURIComponent(query);
  108. }
  109. return baseUrl;
  110. },
  111.  
  112. /**
  113. * Get selected text from the page
  114. * @returns {string} The currently selected text
  115. */
  116. getSelectedText: () => {
  117. return window.getSelection().toString().trim();
  118. },
  119.  
  120. /**
  121. * Filter search engines based on input
  122. * @param {string} input - The user input
  123. * @returns {Array} Array of matching engine options
  124. */
  125. filterSearchEngines: (input) => {
  126. if (!input) return [];
  127. const searchTerm = input.toLowerCase();
  128. return Object.entries(SEARCH_ENGINES)
  129. .filter(([shortcut, engine]) => {
  130. return shortcut.toLowerCase().includes(searchTerm) ||
  131. engine.name.toLowerCase().includes(searchTerm);
  132. })
  133. .slice(0, 6) // Limit to 6 suggestions
  134. .map(([shortcut, engine]) => ({
  135. shortcut,
  136. name: engine.name
  137. }));
  138. },
  139.  
  140. /**
  141. * Safely remove event listeners
  142. * @param {Element} element - DOM element
  143. * @param {string} eventType - Event type
  144. * @param {Function} handler - Event handler
  145. */
  146. safeRemoveEventListener: (element, eventType, handler) => {
  147. if (element && typeof element.removeEventListener === 'function') {
  148. element.removeEventListener(eventType, handler);
  149. }
  150. }
  151. };
  152.  
  153. // ===================================================
  154. // SEARCH FUNCTIONS
  155. // ===================================================
  156. const SearchActions = {
  157. /**
  158. * Open search URL based on openMode setting
  159. * @param {string} url - The URL to open
  160. * @param {string} openMode - The mode to open the URL ('currenttab' or 'newwindow')
  161. */
  162. openSearch: (url, openMode) => {
  163. if (openMode === 'currenttab') {
  164. window.location.href = url;
  165. } else {
  166. window.open(url, '', 'width=800,height=600,noopener');
  167. }
  168. },
  169.  
  170. /**
  171. * Search multiple gaming platforms
  172. * @param {string} query - The search query
  173. * @param {string} openMode - The mode to open the URLs
  174. */
  175. searchMultipleGamingPlatforms: (query, openMode) => {
  176. const platforms = ['g2', 'cd'];
  177. platforms.forEach(platform => {
  178. const searchUrl = Utils.constructSearchUrl(platform, query);
  179. SearchActions.openSearch(searchUrl, openMode);
  180. });
  181. }
  182. };
  183.  
  184. // ===================================================
  185. // REACT COMPONENTS
  186. // ===================================================
  187.  
  188. /**
  189. * EngineSuggestions Component - Display search engine suggestions
  190. */
  191. const EngineSuggestions = React.memo(({
  192. suggestions,
  193. selectedIndex,
  194. onSelectSuggestion
  195. }) => {
  196. if (!suggestions || suggestions.length === 0) return null;
  197.  
  198. return React.createElement('div', {
  199. className: 'absolute left-0 right-0 top-full mt-1 bg-custom-darker rounded-md shadow-lg z-10 max-h-64 overflow-y-auto'
  200. },
  201. React.createElement('ul', { className: 'py-1' },
  202. suggestions.map((suggestion, index) =>
  203. React.createElement('li', {
  204. key: suggestion.shortcut,
  205. className: `px-3 py-2 cursor-pointer hover:bg-blue-600 text-white ${index === selectedIndex ? 'bg-blue-600' : ''}`,
  206. onClick: () => onSelectSuggestion(suggestion.shortcut)
  207. },
  208. React.createElement('span', { className: 'inline-block min-w-[40px] font-mono text-blue-400' }, suggestion.shortcut),
  209. ': ',
  210. suggestion.name
  211. )
  212. )
  213. )
  214. );
  215. });
  216.  
  217. /**
  218. * SearchInput Component - Handles user input for search with keyboard navigation
  219. */
  220. const SearchInput = React.memo(({
  221. input,
  222. setInput,
  223. handleSearch,
  224. currentEngine,
  225. engineOptions = []
  226. }) => {
  227. const inputRef = React.useRef(null);
  228. const [showSuggestions, setShowSuggestions] = React.useState(false);
  229. const [selectedIndex, setSelectedIndex] = React.useState(-1);
  230. const [suggestions, setSuggestions] = React.useState([]);
  231.  
  232. // Generate engine suggestions based on input
  233. React.useEffect(() => {
  234. const engineSuggestions = Utils.filterSearchEngines(input);
  235. setSuggestions(engineSuggestions);
  236. // Reset selection when suggestions change
  237. setSelectedIndex(-1);
  238.  
  239. // Cleanup unnecessary references
  240. return () => {
  241. setSuggestions([]);
  242. };
  243. }, [input]);
  244.  
  245. React.useEffect(() => {
  246. if (inputRef.current) {
  247. inputRef.current.focus();
  248. }
  249.  
  250. // Cleanup function to help garbage collection
  251. return () => {
  252. inputRef.current = null;
  253. };
  254. }, []);
  255.  
  256. const handleKeyDown = React.useCallback((e) => {
  257. // Handle arrow navigation for engine suggestions
  258. if (e.key === 'ArrowDown') {
  259. e.preventDefault();
  260. setShowSuggestions(true);
  261. setSelectedIndex(prev =>
  262. prev < suggestions.length - 1 ? prev + 1 : 0
  263. );
  264. }
  265. else if (e.key === 'ArrowUp') {
  266. e.preventDefault();
  267. setShowSuggestions(true);
  268. setSelectedIndex(prev =>
  269. prev > 0 ? prev - 1 : suggestions.length - 1
  270. );
  271. }
  272. else if (e.key === 'Tab') {
  273. // Cycle through common engine shortcuts with Tab
  274. e.preventDefault();
  275. const commonShortcuts = ['g', 'y', 'w', 'r', 'a'];
  276. const currentShortcut = input.split(' ')[0];
  277. const currentIndex = commonShortcuts.indexOf(currentShortcut);
  278. const nextShortcut = commonShortcuts[(currentIndex + 1) % commonShortcuts.length];
  279.  
  280. // Replace the current shortcut or add a new one
  281. if (currentIndex >= 0) {
  282. const rest = input.substring(currentShortcut.length);
  283. setInput(nextShortcut + rest);
  284. } else {
  285. setInput(nextShortcut + ' ' + input);
  286. }
  287. }
  288. else if (e.key === 'Enter') {
  289. // Apply selected suggestion or perform search
  290. if (selectedIndex >= 0 && selectedIndex < suggestions.length) {
  291. const selectedShortcut = suggestions[selectedIndex].shortcut;
  292. setInput(selectedShortcut + ' ');
  293. setSelectedIndex(-1);
  294. setShowSuggestions(false);
  295. } else {
  296. handleSearch();
  297. }
  298. }
  299. else if (e.key === 'Escape') {
  300. // Close suggestions panel
  301. setShowSuggestions(false);
  302. setSelectedIndex(-1);
  303. }
  304. // Reset selection when typing regular characters
  305. else if (e.key.length === 1) {
  306. setShowSuggestions(true);
  307. }
  308. }, [input, suggestions, selectedIndex, setInput, handleSearch]);
  309.  
  310. const handleSelectSuggestion = React.useCallback((shortcut) => {
  311. setInput(shortcut + ' ');
  312. setShowSuggestions(false);
  313. setSelectedIndex(-1);
  314. setTimeout(() => inputRef.current?.focus(), 10);
  315. }, [setInput]);
  316.  
  317. return React.createElement('div', { className: 'flex flex-col mb-4 relative' },
  318. React.createElement('div', { className: 'flex gap-2 items-center' },
  319. currentEngine && React.createElement('div', {
  320. className: 'bg-blue-600 text-white text-sm px-2 py-1 rounded'
  321. }, currentEngine.name),
  322. React.createElement('input', {
  323. ref: inputRef,
  324. type: 'text',
  325. value: input,
  326. onChange: (e) => setInput(e.target.value),
  327. onKeyDown: handleKeyDown,
  328. onFocus: () => setShowSuggestions(true),
  329. onBlur: () => setTimeout(() => setShowSuggestions(false), 200),
  330. placeholder: 'Enter search command...',
  331. className: 'flex-1 px-3 py-2 bg-custom-darker border-0 rounded-md text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500'
  332. })
  333. ),
  334. showSuggestions && suggestions.length > 0 && React.createElement(EngineSuggestions, {
  335. suggestions,
  336. selectedIndex,
  337. onSelectSuggestion: handleSelectSuggestion
  338. })
  339. );
  340. });
  341.  
  342. /**
  343. * ModeSwitcher Component - Toggles between current tab and new window modes
  344. */
  345. const ModeSwitcher = React.memo(({ openMode, setOpenMode }) => {
  346. const toggleMode = React.useCallback(() => {
  347. setOpenMode(openMode === 'currenttab' ? 'newwindow' : 'currenttab');
  348. }, [openMode, setOpenMode]);
  349.  
  350. return React.createElement('div', { className: 'mb-4 flex items-center justify-between' },
  351. React.createElement('div', { className: 'flex items-center gap-3' },
  352. React.createElement('button', {
  353. onClick: toggleMode,
  354. className: 'toggle-button-switch flex items-center justify-start'
  355. },
  356. React.createElement('div', {
  357. className: `toggle-slider ${openMode === 'currenttab' ? 'active' : ''}`
  358. })
  359. ),
  360. React.createElement('span', { className: 'text-gray-300 text-sm leading-none' },
  361. openMode === 'newwindow' ? 'New Window' : 'Current Tab'
  362. )
  363. )
  364. );
  365. });
  366.  
  367. /**
  368. * SearchResults Component - Displays search results
  369. */
  370. const SearchResults = React.memo(({ results }) => {
  371. return React.createElement('div', { className: 'space-y-2' },
  372. results.map((result, index) =>
  373. React.createElement('div', { key: index, className: 'text-sm' },
  374. result.type === 'link'
  375. ? React.createElement('a', {
  376. href: result.url,
  377. target: '_blank',
  378. rel: 'noopener noreferrer',
  379. className: 'text-blue-400 hover:text-blue-300 hover:underline'
  380. }, result.message)
  381. : React.createElement('span', {
  382. className: 'text-gray-300'
  383. }, result.message)
  384. )
  385. )
  386. );
  387. });
  388.  
  389. /**
  390. * HelpContent Component - Shows the help modal content
  391. */
  392. const HelpContent = React.memo(({ onClose }) => {
  393. return React.createElement('div', {
  394. className: 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[2147483647]',
  395. onClick: onClose
  396. },
  397. React.createElement('div', {
  398. className: 'bg-custom-dark p-6 rounded-lg max-w-4xl max-h-[80vh] overflow-y-auto text-white w-full mx-4',
  399. onClick: e => e.stopPropagation()
  400. },
  401. React.createElement('div', { className: 'flex justify-between items-center mb-4' },
  402. React.createElement('h3', { className: 'text-lg font-bold' }, 'Fast Search Help'),
  403. React.createElement('button', {
  404. onClick: onClose,
  405. className: 'text-gray-400 hover:text-white text-xl'
  406. }, '×')
  407. ),
  408. React.createElement('div', { className: 'grid grid-cols-2 gap-6' },
  409. // Left column - Shortcuts
  410. React.createElement('div', null,
  411. React.createElement('h4', { className: 'text-blue-400 font-bold mb-3' }, 'Search Shortcuts'),
  412. Object.entries({
  413. 'Search': ['a', 'g', 'b', 'd', 'gs', 'gi', 'ar', 'way', 'w', 'p'],
  414. 'Coding': ['gf', 'gh', 'so'],
  415. 'Social': ['r', 'li', 't', 'x', 'f', 'i', 'pi', 'tu', 'q', 'sc', 'y', 'tk', 'fi', 'sp'],
  416. 'Gaming': ['steam', 'epic', 'gog', 'ubi', 'g2', 'cd', 'ori', 'bat'],
  417. 'Movies and TV Shows': ['c', 'lm', 'ls']
  418. }).map(([category, shortcuts]) =>
  419. React.createElement('div', { key: category, className: 'mb-4' },
  420. React.createElement('h5', { className: 'text-gray-300 font-bold mb-2 text-sm' }, category),
  421. React.createElement('ul', { className: 'space-y-1' },
  422. shortcuts.map(shortcut =>
  423. React.createElement('li', { key: shortcut, className: 'text-sm' },
  424. React.createElement('code', { className: 'bg-custom-darker px-1 rounded' }, shortcut),
  425. ': ',
  426. SEARCH_ENGINES[shortcut].name
  427. )
  428. )
  429. )
  430. )
  431. )
  432. ),
  433. // Right column - Usage & Options
  434. React.createElement('div', null,
  435. React.createElement('div', { className: 'mb-6' },
  436. React.createElement('h4', { className: 'text-blue-400 font-bold mb-3' }, 'Opening Options'),
  437. React.createElement('div', { className: 'bg-custom-darker p-4 rounded-lg' },
  438. React.createElement('ul', { className: 'space-y-3' },
  439. React.createElement('li', { className: 'text-sm' },
  440. React.createElement('span', { className: 'text-blue-400 font-bold' }, 'New Window: '),
  441. 'Opens search in a popup window'
  442. ),
  443. React.createElement('li', { className: 'text-sm' },
  444. React.createElement('span', { className: 'text-blue-400 font-bold' }, 'Current Tab: '),
  445. 'Replaces current page with search'
  446. )
  447. )
  448. )
  449. ),
  450. React.createElement('div', { className: 'mb-6' },
  451. React.createElement('h4', { className: 'text-blue-400 font-bold mb-3' }, 'Usage Tips'),
  452. React.createElement('ul', { className: 'space-y-2 text-sm' },
  453. React.createElement('li', null, '• Press ',
  454. React.createElement('code', { className: 'bg-custom-darker px-1 rounded' }, 'Insert'),
  455. ' to open Fast Search'
  456. ),
  457. React.createElement('li', null, '• Type shortcut followed by search terms'),
  458. React.createElement('li', null, '• Press ',
  459. React.createElement('code', { className: 'bg-custom-darker px-1 rounded' }, 'Enter'),
  460. ' to search'
  461. ),
  462. React.createElement('li', null, '• Press ',
  463. React.createElement('code', { className: 'bg-custom-darker px-1 rounded' }, 'Esc'),
  464. ' to close'
  465. ),
  466. React.createElement('li', null, '• Type shortcut only to visit site homepage')
  467. )
  468. )
  469. )
  470. )
  471. )
  472. );
  473. });
  474.  
  475. /**
  476. * BotInterface Component - Main component for the search interface
  477. */
  478. const BotInterface = React.memo(({ onClose, initialQuery = '' }) => {
  479. const [input, setInput] = React.useState(initialQuery);
  480. const [results, setResults] = React.useState([]);
  481. const [currentEngine, setCurrentEngine] = React.useState(null);
  482. const [openMode, setOpenMode] = React.useState(() => {
  483. return GM_getValue('fastsearch_openmode', 'newwindow');
  484. });
  485. const [showHelp, setShowHelp] = React.useState(false);
  486.  
  487. // Track previous active element to restore focus when unmounting
  488. const previousActiveElement = React.useRef(document.activeElement);
  489.  
  490. // Create a list of engine shortcuts for keyboard navigation
  491. const engineOptions = React.useMemo(() => {
  492. return Object.keys(SEARCH_ENGINES);
  493. }, []);
  494.  
  495. // Save openMode changes
  496. React.useEffect(() => {
  497. GM_setValue('fastsearch_openmode', openMode);
  498. }, [openMode]);
  499.  
  500. // Handle escape key to close
  501. React.useEffect(() => {
  502. const handleEscape = (e) => {
  503. if (e.key === 'Escape') {
  504. onClose();
  505. }
  506. };
  507.  
  508. document.addEventListener('keydown', handleEscape);
  509. return () => {
  510. Utils.safeRemoveEventListener(document, 'keydown', handleEscape);
  511.  
  512. // Restore focus to previous element when unmounting
  513. if (previousActiveElement.current) {
  514. try {
  515. previousActiveElement.current.focus();
  516. } catch (e) {
  517. // Ignore focus errors
  518. }
  519. }
  520. };
  521. }, [onClose]);
  522.  
  523. // Update current engine based on input
  524. React.useEffect(() => {
  525. const [shortcut] = input.trim().split(/\s+/);
  526. const engine = SEARCH_ENGINES[shortcut.toLowerCase()];
  527. setCurrentEngine(engine || null);
  528.  
  529. // Clear references on unmount
  530. return () => {
  531. setCurrentEngine(null);
  532. };
  533. }, [input]);
  534.  
  535. // Memoized search handler
  536. const handleSearch = React.useCallback(() => {
  537. const [rawShortcut, ...queryParts] = input.trim().split(/\s+/);
  538. const shortcut = rawShortcut.toLowerCase();
  539. const query = queryParts.join(" ");
  540.  
  541. let newResults = [];
  542.  
  543. if (shortcut === 'sg') {
  544. newResults.push({ type: 'info', message: 'Searching multiple gaming platforms...' });
  545. SearchActions.searchMultipleGamingPlatforms(query, openMode);
  546. } else if (SEARCH_ENGINES.hasOwnProperty(shortcut)) {
  547. const searchUrl = Utils.constructSearchUrl(shortcut, query || '');
  548. const siteName = SEARCH_ENGINES[shortcut].name;
  549. newResults.push({ type: 'link', url: searchUrl, message: `Searching ${siteName} for "${query}"` });
  550. SearchActions.openSearch(searchUrl, openMode);
  551. } else {
  552. const searchUrl = SEARCH_ENGINES.g.url + encodeURIComponent(input);
  553. newResults.push({ type: 'link', url: searchUrl, message: `Searching Google for "${input}"` });
  554. SearchActions.openSearch(searchUrl, openMode);
  555. }
  556.  
  557. setResults(prevResults => [...newResults, ...prevResults]);
  558. setInput('');
  559.  
  560. // Close the UI after performing the search
  561. setTimeout(() => {
  562. onClose();
  563. }, 100);
  564. }, [input, openMode, onClose]);
  565.  
  566. // Toggle help dialog
  567. const toggleHelp = React.useCallback(() => {
  568. setShowHelp(prev => !prev);
  569. }, []);
  570.  
  571. return React.createElement('div', { className: 'fixed top-4 right-4 min-w-[20rem] max-w-[30rem] w-[90vw] bg-custom-dark shadow-lg rounded-lg overflow-hidden' },
  572. React.createElement('div', { className: 'p-4 relative' },
  573. // Header
  574. React.createElement('div', { className: 'flex justify-between items-center mb-4' },
  575. React.createElement('h2', { className: 'text-lg font-bold text-white' }, 'Fast Search'),
  576. React.createElement('button', {
  577. onClick: onClose,
  578. className: 'text-gray-400 hover:text-gray-200'
  579. }, '×')
  580. ),
  581. // Search input
  582. React.createElement(SearchInput, {
  583. input,
  584. setInput,
  585. handleSearch,
  586. currentEngine,
  587. engineOptions
  588. }),
  589. // Mode switcher and help button
  590. React.createElement('div', { className: 'mb-4 flex items-center justify-between' },
  591. React.createElement(ModeSwitcher, {
  592. openMode,
  593. setOpenMode
  594. }),
  595. React.createElement('button', {
  596. onClick: toggleHelp,
  597. className: 'bg-custom-darker text-white px-3 py-1 rounded hover:bg-blue-600 transition-colors'
  598. }, '❔')
  599. ),
  600. // Search results
  601. React.createElement(SearchResults, { results }),
  602. // Help modal
  603. showHelp && React.createElement(HelpContent, { onClose: toggleHelp })
  604. )
  605. );
  606. });
  607.  
  608. // ===================================================
  609. // MAIN APP INITIALIZATION
  610. // ===================================================
  611. const App = {
  612. botContainer: null,
  613. observer: null,
  614. eventListeners: [],
  615.  
  616. /**
  617. * Register event listener with automatic cleanup
  618. * @param {Element} element - DOM element
  619. * @param {string} eventType - Event type
  620. * @param {Function} handler - Event handler
  621. * @param {boolean|object} options - Event listener options
  622. */
  623. registerEventListener: (element, eventType, handler, options = false) => {
  624. if (!element || !eventType || !handler) return;
  625.  
  626. element.addEventListener(eventType, handler, options);
  627. App.eventListeners.push({ element, eventType, handler, options });
  628. },
  629.  
  630. /**
  631. * Clean up resources to prevent memory leaks
  632. */
  633. cleanup: () => {
  634. // Clean up React components properly
  635. if (App.botContainer) {
  636. ReactDOM.unmountComponentAtNode(App.botContainer);
  637. App.botContainer.remove();
  638. App.botContainer = null;
  639. }
  640.  
  641. // Disconnect mutation observer if it exists
  642. if (App.observer) {
  643. App.observer.disconnect();
  644. App.observer = null;
  645. }
  646.  
  647. // Remove all registered event listeners
  648. App.eventListeners.forEach(({ element, eventType, handler, options }) => {
  649. Utils.safeRemoveEventListener(element, eventType, handler, options);
  650. });
  651. App.eventListeners = [];
  652. },
  653.  
  654. /**
  655. * Show the search interface with optional initial query
  656. * @param {string} initialQuery - Text to prefill in search input
  657. */
  658. showBot: (initialQuery = '') => {
  659. // Clean up any existing instances first to prevent duplicates
  660. App.cleanup();
  661.  
  662. App.botContainer = document.createElement('div');
  663. document.body.appendChild(App.botContainer);
  664.  
  665. ReactDOM.render(
  666. React.createElement(BotInterface, {
  667. onClose: () => {
  668. App.cleanup();
  669. },
  670. initialQuery: initialQuery
  671. }),
  672. App.botContainer
  673. );
  674.  
  675. // Set up mutation observer to detect if our container gets removed
  676. App.observer = new MutationObserver((mutations) => {
  677. if (!document.body.contains(App.botContainer) && App.botContainer !== null) {
  678. App.cleanup();
  679. }
  680. });
  681.  
  682. // Watch for changes to document.body
  683. App.observer.observe(document.body, {
  684. childList: true,
  685. subtree: true
  686. });
  687. },
  688.  
  689. /**
  690. * Initialize the application
  691. */
  692. init: () => {
  693. // Event listener for Insert key
  694. App.registerEventListener(document, 'keydown', event => {
  695. if (event.key === 'Insert' && !Utils.isFocusInEditable()) {
  696. event.preventDefault();
  697.  
  698. // Use selected text as initial query if available
  699. const selectedText = Utils.getSelectedText();
  700. App.showBot(selectedText);
  701. }
  702. }, true);
  703.  
  704. // Register context menu command
  705. GM_registerMenuCommand("Fast Search", () => {
  706. const selectedText = Utils.getSelectedText();
  707. App.showBot(selectedText);
  708. });
  709.  
  710. // Add context menu functionality for right-clicking on selected text
  711. App.registerEventListener(document, 'mousedown', event => {
  712. // Only handle right-click events
  713. if (event.button === 2) {
  714. const selectedText = Utils.getSelectedText();
  715. if (selectedText) {
  716. // Store the selected text so we can use it later if the context menu command is chosen
  717. GM_setValue('fastsearch_selected_text', selectedText);
  718. }
  719. }
  720. });
  721.  
  722. // Cleanup on page unload
  723. App.registerEventListener(window, 'beforeunload', App.cleanup);
  724.  
  725. // Cleanup on page visibility change (helps with some browsers/scenarios)
  726. App.registerEventListener(document, 'visibilitychange', () => {
  727. if (document.visibilityState === 'hidden') {
  728. // Perform partial cleanup when page is hidden
  729. if (App.botContainer) {
  730. ReactDOM.unmountComponentAtNode(App.botContainer);
  731. }
  732. }
  733. });
  734. }
  735. };
  736.  
  737. // Add styles
  738. GM_addStyle(`
  739. .fixed { position: fixed; }
  740. .top-4 { top: 1rem; }
  741. .right-4 { right: 1rem; }
  742. .w-80 { width: 20rem; }
  743. .bg-custom-dark { background-color: #030d22; }
  744. .bg-custom-darker { background-color: #15132a; }
  745. .shadow-lg { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2); }
  746. .rounded-lg { border-radius: 0.5rem; }
  747. .overflow-hidden { overflow: hidden; }
  748. /* Add very high z-index to ensure it's above everything */
  749. .fixed.top-4.right-4 { z-index: 2147483647; }
  750. .p-4 { padding: 1rem; }
  751. .flex { display: flex; }
  752. .justify-between { justify-content: space-between; }
  753. .items-center { align-items: center; }
  754. .mb-4 { margin-bottom: 1rem; }
  755. .text-lg { font-size: 1.125rem; }
  756. .font-bold { font-weight: 700; }
  757. .text-white { color: white; }
  758. .text-gray-200 { color: #e5e7eb; }
  759. .text-gray-300 { color: #d1d5db; }
  760. .text-gray-400 { color: #9ca3af; }
  761. .hover\\:text-gray-200:hover { color: #e5e7eb; }
  762. .w-full { width: 100%; }
  763. .px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
  764. .py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
  765. .rounded-l-md { border-top-left-radius: 0.375rem; border-bottom-left-radius: 0.375rem; }
  766. .focus\\:outline-none:focus { outline: 2px solid transparent; outline-offset: 2px; }
  767. .focus\\:ring-2:focus { --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); }
  768. .focus\\:ring-blue-500:focus { --tw-ring-opacity: 1; --tw-ring-color: rgba(59, 130, 246, var(--tw-ring-opacity)); }
  769. .px-4 { padding-left: 1rem; padding-right: 1rem; }
  770. .bg-blue-600 { background-color: #2563eb; }
  771. .hover\\:bg-blue-700:hover { background-color: #1d4ed8; }
  772. .text-blue-400 { color: #60a5fa; }
  773. .hover\\:text-blue-300:hover { color: #93c5fd; }
  774. .rounded-r-md { border-top-right-radius: 0.375rem; border-bottom-right-radius: 0.375rem; }
  775. .space-y-2 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); margin-bottom: calc(0.5rem * var(--tw-space-y-reverse)); }
  776. .text-sm { font-size: 0.875rem; }
  777. .hover\\:underline:hover { text-decoration: underline; }
  778. .placeholder-gray-400::placeholder { color: #9ca3af; }
  779. .relative { position: relative; }
  780. .transform { transform: var(--tw-transform); }
  781. .-translate-y-1/2 { --tw-translate-y: -50%; transform: var(--tw-transform); }
  782. .text-xs { font-size: 0.75rem; line-height: 1rem; }
  783. .pl-24 { padding-left: 6rem; }
  784. .top-1/2 { top: 50%; }
  785. .left-2 { left: 0.5rem; }
  786. .min-w-\\[20rem\\] { min-width: 20rem; }
  787. .max-w-\\[30rem\\] { max-width: 30rem; }
  788. .w-\\[90vw\\] { width: 90vw; }
  789. .gap-2 { gap: 0.5rem; }
  790. .flex-1 { flex: 1 1 0%; }
  791. .flex-shrink-0 { flex-shrink: 0; }
  792. .whitespace-nowrap { white-space: nowrap; }
  793. .rounded-md { border-radius: 0.375rem; }
  794. .min-w-\\[80px\\] { min-width: 80px; }
  795. .-translate-y-6 { --tw-translate-y: -1.5rem; }
  796. .toggle-switch {
  797. position: relative;
  798. display: inline-block;
  799. width: 50px;
  800. height: 24px;
  801. }
  802. .toggle-checkbox {
  803. opacity: 0;
  804. width: 0;
  805. height: 0;
  806. }
  807. .toggle-label {
  808. position: absolute;
  809. cursor: pointer;
  810. top: 0;
  811. left: 0;
  812. right: 0;
  813. bottom: 0;
  814. background-color: #15132a;
  815. transition: .4s;
  816. border-radius: 24px;
  817. }
  818. .toggle-button {
  819. position: absolute;
  820. height: 20px;
  821. width: 20px;
  822. left: 2px;
  823. bottom: 2px;
  824. background-color: #2563eb;
  825. transition: .4s;
  826. border-radius: 50%;
  827. }
  828. .toggle-checkbox:checked + .toggle-label .toggle-button {
  829. transform: translateX(26px);
  830. }
  831. .grid { display: grid; }
  832. .grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  833. .gap-6 { gap: 1.5rem; }
  834. .bg-custom-darker { background-color: #15132a; }
  835. .p-6 { padding: 1.5rem; }
  836. .max-w-4xl { max-width: 56rem; }
  837. .max-h-\\[80vh\\] { max-height: 80vh; }
  838. .overflow-y-auto { overflow-y: auto; }
  839. .space-y-3 > :not([hidden]) ~ :not([hidden]) { margin-top: 0.75rem; }
  840. .toggle-button-switch {
  841. position: relative;
  842. width: 50px;
  843. height: 24px;
  844. background-color: #15132a;
  845. border-radius: 24px;
  846. padding: 2px;
  847. border: none;
  848. cursor: pointer;
  849. outline: none;
  850. display: flex;
  851. align-items: center;
  852. }
  853. .toggle-slider {
  854. position: absolute;
  855. height: 20px;
  856. width: 20px;
  857. background-color: #2563eb;
  858. border-radius: 50%;
  859. transition: transform 0.3s;
  860. }
  861. .toggle-slider.active {
  862. transform: translateX(26px);
  863. }
  864. .gap-3 {
  865. gap: 0.75rem;
  866. }
  867. .leading-none {
  868. line-height: 1;
  869. }
  870. .hover\\:bg-blue-600:hover {
  871. background-color: #2563eb;
  872. }
  873. .transition-colors {
  874. transition-property: color, background-color, border-color;
  875. transition-duration: 0.15s;
  876. transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  877. }
  878. /* New styles for engine suggestions */
  879. .flex-col {
  880. flex-direction: column;
  881. }
  882. .top-full {
  883. top: 100%;
  884. }
  885. .mt-1 {
  886. margin-top: 0.25rem;
  887. }
  888. .py-1 {
  889. padding-top: 0.25rem;
  890. padding-bottom: 0.25rem;
  891. }
  892. .max-h-64 {
  893. max-height: 16rem;
  894. }
  895. .overflow-y-auto {
  896. overflow-y: auto;
  897. }
  898. .cursor-pointer {
  899. cursor: pointer;
  900. }
  901. .font-mono {
  902. font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  903. }
  904. .min-w-\\[40px\\] {
  905. min-width: 40px;
  906. }
  907. .inline-block {
  908. display: inline-block;
  909. }
  910. .z-10 {
  911. z-index: 10;
  912. }
  913. `);
  914.  
  915. // Start the app
  916. App.init();
  917. })();