DuckDuckGo优化

便捷返回顶部/跨引擎一键搜

当前为 2025-04-21 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name DuckDuckGo Optimization
  3. // @name:zh-CN DuckDuckGo优化
  4. // @name:zh-TW DuckDuckGo優化
  5. // @description Double Click To Return The Top / Shortcuts To Other Search Engines
  6. // @description:zh-CN 便捷返回顶部/跨引擎一键搜
  7. // @description:zh-TW 便捷返回頂部/跨引擎一鍵搜
  8. // @version 1.1.1
  9. // @icon https://raw.githubusercontent.com/MiPoNianYou/UserScripts/refs/heads/main/Icons/DuckDuckGoOptimizationIcon.svg
  10. // @author 念柚
  11. // @namespace https://github.com/MiPoNianYou/UserScripts
  12. // @supportURL https://github.com/MiPoNianYou/UserScripts/issues
  13. // @license GPL-3.0
  14. // @match https://duckduckgo.com/*
  15. // @grant GM_addStyle
  16. // ==/UserScript==
  17.  
  18. (function () {
  19. "use strict";
  20.  
  21. const RightAreaRatio = 0.2;
  22. const InteractiveElementsSelector =
  23. 'a, button, input, select, textarea, [role="button"], [tabindex]:not([tabindex="-1"])';
  24. const SearchFormSelector = "#search_form";
  25. const SearchInputSelector = "#search_form_input";
  26. const SearchBtnGroupClass = "SearchBtnGroup";
  27. const SearchBtnClass = "SearchBtn";
  28. const DebounceDelay = 250;
  29.  
  30. document.addEventListener(
  31. "dblclick",
  32. function (Event) {
  33. const WindowWidth = window.innerWidth;
  34. const TriggerBoundary = WindowWidth * (1 - RightAreaRatio);
  35. if (
  36. Event.clientX > TriggerBoundary &&
  37. !Event.target.closest(InteractiveElementsSelector)
  38. ) {
  39. window.scrollTo({
  40. top: 0,
  41. behavior: "smooth",
  42. });
  43. }
  44. },
  45. { passive: true }
  46. );
  47.  
  48. const SearchButtonStyle = `
  49. .${SearchBtnGroupClass} {
  50. display: flex;
  51. flex-wrap: wrap;
  52. gap: 10px;
  53. margin: 12px auto;
  54. padding: 0 10px;
  55. max-width: 800px;
  56. justify-content: center;
  57. }
  58. .${SearchBtnClass} {
  59. padding: 8px 16px;
  60. border-radius: 8px;
  61. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  62. font-size: 14px;
  63. font-weight: 500;
  64. cursor: pointer;
  65. transition: background-color 0.2s ease, border-color 0.2s ease;
  66. min-width: 110px;
  67. text-align: center;
  68. flex-grow: 1;
  69. flex-basis: 110px;
  70. box-sizing: border-box;
  71. border: 1px solid transparent;
  72. background-color: rgba(60, 60, 60, 0.8);
  73. color: #f5f5f7;
  74. border-color: rgba(85, 85, 85, 0.9);
  75. }
  76. .${SearchBtnClass}:hover {
  77. background-color: rgba(75, 75, 75, 0.9);
  78. border-color: rgba(100, 100, 100, 0.9);
  79. }
  80. `;
  81. GM_addStyle(SearchButtonStyle);
  82.  
  83. const EngineList = [
  84. { Name: "Google", Url: "https://www.google.com/search?q=" },
  85. { Name: "Bing", Url: "https://www.bing.com/search?q=" },
  86. { Name: "Baidu", Url: "https://www.baidu.com/s?wd=" },
  87. ];
  88.  
  89. function Debounce(Func, Wait) {
  90. let Timeout;
  91. return function ExecutedFunction(...Args) {
  92. const Later = () => {
  93. clearTimeout(Timeout);
  94. Func(...Args);
  95. };
  96. clearTimeout(Timeout);
  97. Timeout = setTimeout(Later, Wait);
  98. };
  99. }
  100.  
  101. function CreateSearchButtons() {
  102. const SearchForm = document.querySelector(SearchFormSelector);
  103. const SearchInput = SearchForm?.querySelector(SearchInputSelector);
  104.  
  105. if (
  106. !SearchForm ||
  107. !SearchInput ||
  108. document.querySelector(`.${SearchBtnGroupClass}`)
  109. ) {
  110. return;
  111. }
  112.  
  113. const BtnGroup = document.createElement("div");
  114. BtnGroup.className = SearchBtnGroupClass;
  115.  
  116. EngineList.forEach((Engine) => {
  117. const Button = document.createElement("button");
  118. Button.className = SearchBtnClass;
  119. Button.textContent = `使用 ${Engine.Name} 搜索`;
  120. Button.type = "button";
  121.  
  122. Button.addEventListener("click", (Event) => {
  123. Event.preventDefault();
  124. const Query = SearchInput.value.trim();
  125. if (Query) {
  126. const SearchUrl = `${Engine.Url}${encodeURIComponent(Query)}`;
  127. window.open(SearchUrl, "_blank", "noopener,noreferrer");
  128. }
  129. });
  130. BtnGroup.appendChild(Button);
  131. });
  132.  
  133. SearchForm.parentNode.insertBefore(BtnGroup, SearchForm.nextSibling);
  134. }
  135.  
  136. const DebouncedCreateSearchButtons = Debounce(
  137. CreateSearchButtons,
  138. DebounceDelay
  139. );
  140.  
  141. function CheckAndMaybeCreateButtons() {
  142. const SearchFormExists = document.querySelector(SearchFormSelector);
  143. const ButtonsExist = document.querySelector(`.${SearchBtnGroupClass}`);
  144.  
  145. if (SearchFormExists && !ButtonsExist) {
  146. DebouncedCreateSearchButtons();
  147. }
  148. }
  149.  
  150. const Observer = new MutationObserver(CheckAndMaybeCreateButtons);
  151.  
  152. Observer.observe(document.body, {
  153. childList: true,
  154. subtree: true,
  155. });
  156.  
  157. if (document.readyState === "loading") {
  158. window.addEventListener("DOMContentLoaded", CheckAndMaybeCreateButtons);
  159. } else {
  160. CheckAndMaybeCreateButtons();
  161. }
  162. })();