Greasy Fork is available in English.

🔍 聚合快搜

为Via设计的第三方聚合搜索栏

  1. // ==UserScript==
  2. // @name 🔍 聚合快搜
  3. // @namespace https://ez118.github.io/
  4. // @version 0.4.3
  5. // @description 为Via设计的第三方聚合搜索栏
  6. // @author ZZY_WISU
  7. // @match *://*/*
  8. // @license GPLv3
  9. // @icon data:image/svg+xml;charset=utf-8;base64,PHN2ZyB2ZXJzaW9uPSIxLjIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDEyOCAxMjgiPjxjaXJjbGUgY3g9IjY0IiBjeT0iNjQiIHI9IjY0IiBzdHlsZT0iZmlsbDojZjJmN2ZmIi8+PGNpcmNsZSBjeD0iMzUiIGN5PSI2MCIgcj0iMjAiIHN0eWxlPSJmaWxsOiMwMGE1ZjYiLz48Y2lyY2xlIGN4PSI2MCIgY3k9Ijc5IiByPSIyNSIgc3R5bGU9ImZpbGw6I2ZmM2UwMCIvPjxjaXJjbGUgY3g9Ijg4LjQiIGN5PSI1MCIgcj0iMzAuMiIgc3R5bGU9ImZpbGw6I2ZmYjcwMCIvPjwvc3ZnPg==
  10. // @run-at document-start
  11. // @grant GM_registerMenuCommand
  12. // @grant GM_addStyle
  13. // @grant GM_setValue
  14. // @grant GM_getValue
  15. // @grant GM_addElement
  16. // @grant window.onurlchange
  17. // @require https://unpkg.com/jquery@3.7.1/dist/jquery.min.js
  18. // ==/UserScript==
  19.  
  20.  
  21.  
  22. /* =====[ 用户自定义 ]===== */
  23.  
  24. var searchEngine = [
  25. {
  26. "id":"google",
  27. "name":"Google",
  28. "link":"https://www.google.com/search?q=%s"
  29. },
  30. {
  31. "id":"bing",
  32. "name":"Bing",
  33. "link":"https://www.bing.com/search?q=%s"
  34. },
  35. {
  36. "id":"cnbing",
  37. "name":"必应",
  38. "link":"https://cn.bing.com/search?q=%s"
  39. },
  40. {
  41. "id":"duckduckgo",
  42. "name":"DuckDuckGo",
  43. "link":"https://duckduckgo.com/?t=h_&q=%s"
  44. },
  45. {
  46. "id":"github",
  47. "name":"Github",
  48. "link":"https://github.com/search?q=%s&type=repositories"
  49. },
  50. {
  51. "id":"fdroid",
  52. "name":"F-Droid",
  53. "link":"https://search.f-droid.org/?q=%s&lang=zh_Hans"
  54. },
  55. {
  56. "id":"baidu",
  57. "name":"百度",
  58. "link":"https://www.baidu.com/s?wd=%s"
  59. }
  60. ];
  61.  
  62.  
  63.  
  64. /* =====[ 变量存储 ]===== */
  65.  
  66. const ICONS = {
  67. 'settings': '<svg viewBox="0 0 24 24" width="20px" height="20px"><path d="m19.44 12.99-.01.02c.04-.33.08-.67.08-1.01 0-.34-.03-.66-.07-.99l.01.02 2.44-1.92-2.43-4.22-2.87 1.16.01.01c-.52-.4-1.09-.74-1.71-1h.01L14.44 2H9.57l-.44 3.07h.01c-.62.26-1.19.6-1.71 1l.01-.01-2.88-1.17-2.44 4.22 2.44 1.92.01-.02c-.04.33-.07.65-.07.99 0 .34.03.68.08 1.01l-.01-.02-2.1 1.65-.33.26 2.43 4.2 2.88-1.15-.02-.04c.53.41 1.1.75 1.73 1.01h-.03L9.58 22h4.85s.03-.18.06-.42l.38-2.65h-.01c.62-.26 1.2-.6 1.73-1.01l-.02.04 2.88 1.15 2.43-4.2s-.14-.12-.33-.26zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5"></path></svg>',
  68. 'up': '<svg viewBox="0 0 24 24" width="20px" height="20px"><path d="m7 14 5-5 5 5z"></path></svg>',
  69. 'del': '<svg viewBox="0 0 24 24" width="20px" height="20px"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path></svg>'
  70. };
  71.  
  72. var currentEngine = null; /* 存储当前正在使用的搜索引擎 */
  73. var currentQuery = null; /* 存储当前检索词 */
  74.  
  75. /* =====[ 计算反色 ]===== */
  76.  
  77. function invertHex(hex) {
  78. hex = hex.replace("#", "");
  79.  
  80. // 补全短的 hex 颜色代码
  81. if (hex.length === 3) {
  82. hex = hex.split('').map(c => c + c).join('');
  83. }
  84.  
  85. // 确保 hex 长度为 6
  86. if (hex.length !== 6) {
  87. throw new Error("Invalid HEX color.");
  88. }
  89.  
  90. // 计算反色
  91. let inverted = (Number(`0x${hex}`) ^ 0xFFFFFF).toString(16).toUpperCase();
  92.  
  93. // 补全不足位数的 0
  94. inverted = ("000000" + inverted).slice(-6);
  95.  
  96. return `#${inverted}`;
  97. }
  98.  
  99. function invertRgb(rgb) {
  100. // 匹配 rgb(r, g, b) 格式
  101. const match = rgb.match(/^rgb\((\d{1,3}), (\d{1,3}), (\d{1,3})\)$/);
  102. if (!match) {
  103. throw new Error("Invalid RGB color.");
  104. }
  105.  
  106. const r = 255 - parseInt(match[1], 10);
  107. const g = 255 - parseInt(match[2], 10);
  108. const b = 255 - parseInt(match[3], 10);
  109.  
  110. return `rgb(${r}, ${g}, ${b})`;
  111. }
  112.  
  113. function invertRgba(rgba) {
  114. // 匹配 rgba(r, g, b, a) 格式
  115. const match = rgba.match(/^rgba\((\d{1,3}), (\d{1,3}), (\d{1,3}), (0|0?\.\d+|1(\.0)?)\)$/);
  116. if (!match) {
  117. throw new Error("Invalid RGBA color.");
  118. }
  119.  
  120. const r = 255 - parseInt(match[1], 10);
  121. const g = 255 - parseInt(match[2], 10);
  122. const b = 255 - parseInt(match[3], 10);
  123.  
  124. return `rgba(${r}, ${g}, ${b}, 1)`;
  125. }
  126. function invertColor(color) {
  127. if (color.startsWith("#")) {
  128. return invertHex(color);
  129. } else if (color.startsWith("rgb(")) {
  130. return invertRgb(color);
  131. } else if (color.startsWith("rgba(")) {
  132. return invertRgba(color);
  133. } else {
  134. throw new Error("Invalid color format.");
  135. }
  136. }
  137.  
  138. /* ================ */
  139.  
  140.  
  141.  
  142.  
  143. function hash(str) {
  144. let hash = 5381;
  145. for (let i = 0; i < str.length; i++) {
  146. hash = (hash * 33) ^ str.charCodeAt(i);
  147. }
  148. return hash >>> 0;
  149. }
  150.  
  151. function findByKeyValue(array, key, value) {
  152. /* 在JSON中,以键值匹配项 */
  153. return array.findIndex(item => item[key] === value);
  154. }
  155.  
  156. function isSearchEngine() {
  157. /* 判断是否是被记录的搜索页 */
  158. const currentUrl = new URL(window.location.href);
  159. return searchEngine.some(engine => {
  160. const prefix = engine.link.split('%s')[0];
  161. const engineUrl = new URL(prefix);
  162. return currentUrl.hostname === engineUrl.hostname && currentUrl.pathname.startsWith(engineUrl.pathname);
  163. });
  164. }
  165.  
  166. function showOption() {
  167. if($("#userscript-optDlg").length > 0) { return; }
  168.  
  169. let newEngineList = searchEngine;
  170. let origEngineList = searchEngine.slice();
  171.  
  172. var $optDlg = $('<div>', {
  173. class: 'userscript-optDlg',
  174. id: 'userscript-optDlg',
  175. style: 'display:none;'
  176. }).appendTo('body');
  177.  
  178. $optDlg.hide();
  179. $optDlg.fadeIn(100);
  180.  
  181. var listHtml = '';
  182. $.each(newEngineList, (index, item) => {
  183. listHtml += `
  184. <div class="list-item" seid="${item.id}">
  185. <p class="item-title">${item.name}</p>
  186. <p class="item-movebtn" seid="${item.id}" title="上移">${ICONS.up}</p>
  187. <p class="item-delbtn" seid="${item.id}" title="移除">${ICONS.del}</p>
  188. </div>`;
  189. });
  190.  
  191. $optDlg.html(`
  192. <div style="height:fit-content; max-height:calc(80vh - 60px); overflow-x:hidden; overflow-y:auto;">
  193. <h3>设置</h3>
  194. <div style="height:fit-content; margin:5px;">
  195. <p class="subtitle">搜索引擎:</p>
  196. ` + listHtml + `
  197. </div>
  198. </div>
  199. <div align="right">
  200. <input type="button" value="取消" class="ctrlbtn" id="userscript-cancelBtn">
  201. <input type="button" value="添加" class="ctrlbtn" id="userscript-addBtn">
  202. <input type="button" value="保存" class="ctrlbtn" id="userscript-saveBtn">
  203. </div>
  204. `);
  205.  
  206. $(document).on('click', '.list-item>.item-movebtn', function(e) {
  207. let seid = $(e.target).parent().attr("seid");
  208. const index = findByKeyValue(newEngineList, 'id', seid);
  209. if (index > 0) {
  210. [newEngineList[index - 1], newEngineList[index]] = [newEngineList[index], newEngineList[index - 1]];
  211. // 交换DOM元素位置
  212. const currentItem = $(`.list-item[seid="${seid}"]`);
  213. const previousItem = currentItem.prev();
  214. if (previousItem.length) {
  215. currentItem.insertBefore(previousItem);
  216. }
  217. }
  218. });
  219.  
  220. $(document).on('click', '.list-item>.item-delbtn', function(e) {
  221. let seid = $(e.target).parent().attr("seid");
  222. const index = findByKeyValue(newEngineList, 'id', seid);
  223. if (index !== -1) {
  224. newEngineList.splice(index, 1);
  225. $(`.list-item[seid="${seid}"]`).remove();
  226. }
  227. });
  228.  
  229. $(document).on('click', '#userscript-cancelBtn', function(e) {
  230. /* 取消按钮 */
  231. newEngineList = origEngineList;
  232.  
  233. $(document).off('click', '#userscript-addBtn');
  234. $(document).off('click', '#userscript-saveBtn');
  235. $(document).off('click', '.list-item>.item-movebtn');
  236. $(document).off('click', '.list-item>.item-delbtn');
  237.  
  238. let $optDlg = $("#userscript-optDlg");
  239. $optDlg.fadeOut(100);
  240. setTimeout(() => {
  241. $optDlg.remove();
  242.  
  243. $(document).off('click', '#userscript-cancelBtn');
  244.  
  245. location.reload();
  246. }, 110);
  247. });
  248.  
  249. $(document).on('click', '#userscript-addBtn', function(e) {
  250. let url = prompt("【添加搜索引擎】\n请在此处填写搜索引擎的链接。(用“%s”代替搜索字词)", "https://");
  251. if(!url){ return; }
  252. if(!url.includes("%s") || !url.includes("://")) { alert("【未添加】不合法的链接。"); return; }
  253. let name = prompt("【添加搜索引擎】\n请为该条目命名。", "")
  254. if(!name){ return; }
  255. if(name.length > 100) { alert("【未添加】过长的名称。"); return; }
  256.  
  257. newEngineList.push({
  258. "id": hash(url + name).toString(),
  259. "name": name,
  260. "link": url
  261. });
  262.  
  263. GM_setValue('search_engine', newEngineList);
  264. alert("【已添加】页面即将刷新");
  265. location.reload();
  266. });
  267.  
  268. $(document).on('click', '#userscript-saveBtn', function(e) {
  269. /* 保存按钮 */
  270. GM_setValue('search_engine', newEngineList);
  271. alert("【已保存】请刷新页面以应用更改");
  272.  
  273. $(document).off('click', '#userscript-addBtn');
  274. $(document).off('click', '#userscript-cancelBtn');
  275. $(document).off('click', '.list-item>.item-movebtn');
  276. $(document).off('click', '.list-item>.item-delbtn');
  277.  
  278. let $optDlg = $("#userscript-optDlg");
  279. $optDlg.fadeOut(100);
  280. setTimeout(() => {
  281. $optDlg.remove();
  282.  
  283. $(document).off('click', '#userscript-saveBtn');
  284. }, 110);
  285. });
  286. }
  287.  
  288.  
  289. function initEle() {
  290. // 创建搜索栏元素并添加到页面
  291. var $quickSearchBar = $('<div>', {
  292. class: 'userscript-quickSearchBar',
  293. id: 'userscript-quickSearchBar'
  294. }).appendTo('body');
  295.  
  296. let html = '';
  297. const currentURL = window.location.href;
  298.  
  299. $.each(searchEngine, (index, item) => {
  300. const link = item.link;
  301. const splitLink = link.split('%s');
  302. let prefix = null;
  303. let suffix = null;
  304.  
  305. if (splitLink.length === 2) {
  306. prefix = splitLink[0];
  307. suffix = splitLink[1];
  308. } else if (splitLink.length > 2) {
  309. prefix = splitLink[0];
  310. suffix = splitLink.slice(1).join('%s');
  311. }
  312.  
  313. if (currentURL.startsWith(prefix) && currentURL.endsWith(suffix)) {
  314. currentEngine = item;
  315. currentQuery = currentURL.slice(prefix.length, currentURL.length - suffix.length);
  316. html += `<div class="item active" seid="${item.id}">${item.name}</div>`;
  317. } else {
  318. html += `<div class="item" seid="${item.id}">${item.name}</div>`;
  319. }
  320. });
  321.  
  322. // 清理 HTML 内容并插入到搜索栏
  323. $quickSearchBar.append(html + `
  324. <div id="userscript-optBtn">` + ICONS.settings + `</div>
  325. <div class="blank"></div>
  326. `);
  327.  
  328. // 添加点击事件监听器
  329. $(document).on('click', '#userscript-quickSearchBar>.item', function(e) {
  330. var seid = $(e.target).attr("seid");
  331. var seIndex = findByKeyValue(searchEngine, "id", seid);
  332. location.href = searchEngine[seIndex].link.replace("%s", currentQuery);
  333. });
  334.  
  335.  
  336. $("#userscript-optBtn").click(() => {
  337. showOption();
  338. });
  339. }
  340.  
  341. /* =====[ 菜单注册 ]===== */
  342. var menu_ = GM_registerMenuCommand('⚙️ 脚本设置', function () { showOption(); }, 'o');
  343.  
  344. (function () {
  345. 'use strict';
  346.  
  347. if(GM_getValue('search_engine') == null || GM_getValue('search_engine') == "" || GM_getValue('search_engine') == undefined){ GM_setValue('search_engine', searchEngine); }
  348. else { searchEngine = GM_getValue('search_engine'); }
  349.  
  350. var websiteThemeColor = $('body').css('background-color') || "#FFF";
  351. var websiteFontColor = invertColor(websiteThemeColor);
  352. websiteThemeColor = invertColor(websiteFontColor);
  353.  
  354. GM_addStyle(`
  355. body{ -webkit-appearance:none!important; }
  356.  
  357. .userscript-quickSearchBar{ user-select:none; background-color:` + websiteThemeColor + `; color:` + websiteFontColor + `; padding:6px 10px; font-size:12px; line-height:20px; width:100vw; height:30px; border-top:1px solid #99999999; position:fixed; bottom:-1px; left:0; right:0; display:flex; flex-direction:row; overflow-x:auto; overflow-y:hidden; box-sizing:initial; z-index:100000; font-family:"Hiragino Sans GB","Microsoft YaHei","WenQuanYi Micro Hei",sans-serif; }
  358. .userscript-quickSearchBar>.item{ margin:0px 5px; border-radius:20px; border:1px solid ` + websiteFontColor + `; padding:5px 9px; width:fit-content; flex-basis:fit-content; flex-shrink:0; cursor:pointer; background-color:transparent; }
  359. .userscript-quickSearchBar>.active{ border:1px solid #6d7fb4; color:#6d7fb4; }
  360. .userscript-quickSearchBar>.blank{ flex-basis:25px; flex-shrink:0; width:25px; }
  361. #userscript-optBtn{ margin:0px 5px; flex-shrink:0; flex-basis:fit-content; width:fit-content; border-radius:20px; border:1px solid ` + websiteFontColor + `; padding:4px 6px; cursor:pointer; background-color:transparent; }
  362. #userscript-optBtn svg{ fill:` + websiteFontColor + `; }
  363.  
  364. .userscript-optDlg{ user-select:none; background-color:` + websiteThemeColor + `; color:` + websiteFontColor + `; border:1px solid #99999999; position:fixed; top:50%; height:fit-content; left:50%; transform:translateX(-50%) translateY(-50%); width:92vw; max-width:300px; padding:15px; border-radius:15px; box-sizing:initial; z-index:100000; box-shadow:0 1px 10px #00000088; font-family:"Hiragino Sans GB","Microsoft YaHei","WenQuanYi Micro Hei",sans-serif; }
  365. .userscript-optDlg .ctrlbtn{ border:none; background-color:transparent; padding:8px; margin:0; color:#6d7fb4; cursor:pointer; overflow:hidden; }
  366. .userscript-optDlg h3{ margin:5px; margin-bottom:15px; font-size:24px; }
  367. .userscript-optDlg .subtitle{ margin:5px 1px; font-size:16px; font-weight:400; }
  368.  
  369. .userscript-optDlg .list-item{ width:calc(100% - 10px); padding:10px 5px; margin:0; display:flex; flex-direction:row; vertical-align:middle; box-sizing:initial; }
  370. .userscript-optDlg .list-item:hover{ background-color:#55555555; }
  371. .userscript-optDlg .list-item>p{ padding:0; margin:0; font-size:16px; }
  372. .userscript-optDlg .list-item>.item-title{ flex-grow:1; margin-left:5px; }
  373.  
  374. .userscript-optDlg .list-item>.item-movebtn{ cursor:pointer; width:25px; }
  375. .userscript-optDlg .list-item>.item-movebtn svg{ fill:` + websiteFontColor + `; height:100%; min-height:16px; }
  376. .userscript-optDlg .list-item>.item-delbtn{ cursor:pointer; width:25px; }
  377. .userscript-optDlg .list-item>.item-delbtn svg{ fill:` + websiteFontColor + `; height:100%; min-height:16px; }
  378. `);
  379.  
  380. if(isSearchEngine()){
  381. /* 存储初始化 */
  382.  
  383. console.log("【提示】匹配到已保存的搜素引擎");
  384. try{
  385. initEle();
  386. }catch(e){
  387. console.log(e)
  388. }
  389.  
  390. setTimeout(() => {
  391. if($("#userscript-quickSearchBar").length == 0){
  392. /* 如果在页面加载时未完成初始化 */
  393. initEle();
  394. }
  395. }, 600);
  396.  
  397. setTimeout(()=>{
  398. if($("#userscript-quickSearchBar").length == 0){
  399. /* 如果在页面加载时未完成初始化 */
  400. initEle();
  401. }
  402. }, 1200)
  403. }
  404. })();