搜索引擎重定向插件,搜索引擎管理助手

将搜索请求从一个搜索引擎重定向到另一个(支持多个搜索引擎),并管理搜索引擎重定向规则

  1. // ==UserScript==
  2. // @name Search Engine Redirector, Search Engine Manager
  3. // @name:zh-CN 搜索引擎重定向插件,搜索引擎管理助手
  4. // @name:zh-TW 搜尋引擎重定向插件,搜尋引擎管理助手
  5. // @name:en Search Engine Redirector, Search Engine Manager
  6. // @name:ja 検索エンジンリダイレクター・検索エンジン管理
  7. // @name:ko 검색 엔진 리디렉터, 검색 엔진 관리자
  8. // @name:ru Перенаправление поисковых систем, Менеджер поисковых систем
  9. // @name:fr Redirection de moteur de recherche, Gestionnaire de moteurs de recherche
  10. // @name:es Redireccionador de motores de búsqueda, Gestor de motores de búsqueda
  11. // @name:de Suchmaschinen-Umleiter, Suchmaschinen-Manager
  12. // @name:pt-BR Redirecionador de Mecanismos de Busca, Gerenciador de Mecanismos de Busca
  13. // @name:it Reindirizzatore motore di ricerca, Gestore motore di ricerca
  14. // @name:tr Arama Motoru Yönlendirici, Arama Motoru Yöneticisi
  15. // @name:vi Trình chuyển hướng công cụ tìm kiếm, Trình quản lý công cụ tìm kiếm
  16. // @name:pl Przekierowywacz wyszukiwarek, Menedżer wyszukiwarek
  17. // @name:uk Перенаправлення пошукових систем, Менеджер пошукових систем
  18. // @name:ar معيد توجيه محرك البحث، مدير محرك البحث
  19. // @name:hi सर्च इंजन रीडायरेक्टर, सर्च इंजन मैनेजर
  20. // @description Redirect search requests from one engine to another (supports multiple engines) and manage search engine redirection rules
  21. // @description:zh-CN 将搜索请求从一个搜索引擎重定向到另一个(支持多个搜索引擎),并管理搜索引擎重定向规则
  22. // @description:zh-TW 將搜尋請求從一個搜尋引擎重定向到另一個(支援多個搜尋引擎),並管理搜尋引擎重定向規則
  23. // @description:en Redirect search requests from one engine to another (supports multiple engines) and manage search engine redirection rules
  24. // @description:ja 検索リクエストを別の検索エンジンにリダイレクト(複数エンジン対応)、リダイレクトルールを管理
  25. // @description:ko 검색 요청을 다른 검색 엔진으로 리디렉션(여러 엔진 지원), 검색 엔진 리디렉션 규칙 관리
  26. // @description:ru Перенаправляет поисковые запросы с одной системы на другую (поддержка нескольких систем) и управляет правилами перенаправления
  27. // @description:fr Redirige les requêtes de recherche d'un moteur à un autre (plusieurs moteurs pris en charge) et gère les règles de redirection
  28. // @description:es Redirige las búsquedas de un motor a otro (soporta múltiples motores) y gestiona reglas de redirección
  29. // @description:de Leitet Suchanfragen von einer Suchmaschine zu einer anderen um (unterstützt mehrere Suchmaschinen) und verwaltet Umleitungsregeln
  30. // @description:pt-BR Redireciona buscas de um mecanismo para outro (suporta múltiplos mecanismos) e gerencia regras de redirecionamento
  31. // @description:it Reindirizza le ricerche da un motore all'altro (supporta più motori) e gestisce le regole di reindirizzamento
  32. // @description:tr Arama isteklerini bir motordan diğerine yönlendirir (birden fazla motor desteklenir) ve yönlendirme kurallarını yönetir
  33. // @description:vi Chuyển hướng tìm kiếm từ một công cụ sang công cụ khác (hỗ trợ nhiều công cụ), quản lý quy tắc chuyển hướng
  34. // @description:pl Przekierowuje zapytania z jednej wyszukiwarki do innej (obsługuje wiele wyszukiwarek) i zarządza regułami przekierowań
  35. // @description:uk Перенаправляє пошукові запити з однієї системи на іншу (підтримка декількох систем) і керує правилами перенаправлення
  36. // @description:ar يعيد توجيه طلبات البحث من محرك إلى آخر (يدعم عدة محركات) ويدير قواعد إعادة التوجيه (يدعم الصينية والإنجليزية)
  37. // @description:hi खोज अनुरोधों को एक इंजन से दूसरे में रीडायरेक्ट करें (कई इंजन समर्थित), रीडायरेक्शन नियम प्रबंधित करें
  38. // @namespace https://github.com/r6hk/search-engine-redirect/
  39. // @homepage https://github.com/r6hk/search-engine-redirect/
  40. // @supportURL https://github.com/r6hk/search-engine-redirect/
  41. // @version 1.0.1
  42. // @author r6hk
  43. // @match *://*/*
  44. // @grant GM_registerMenuCommand
  45. // @grant GM_setValue
  46. // @grant GM_getValue
  47. // @grant GM_getResourceText
  48. // @grant GM_addStyle
  49. // @run-at document-start
  50. // @license MIT
  51. // ==/UserScript==
  52.  
  53. (function() {
  54. 'use strict';
  55.  
  56. // 搜索引擎信息
  57. const SEARCH_ENGINES = {
  58. Google: {
  59. prefix: 'https://www.google.com/search',
  60. param: 'q'
  61. },
  62. Bing: {
  63. prefix: 'https://www.bing.com/search',
  64. param: 'q'
  65. },
  66. DuckDuckGo: {
  67. prefix: 'https://duckduckgo.com/',
  68. param: 'q'
  69. },
  70. Yandex: {
  71. prefix: 'https://yandex.com/search',
  72. param: 'text'
  73. },
  74. 'Brave Search': {
  75. prefix: 'https://search.brave.com/search',
  76. param: 'q'
  77. },
  78. Startpage: {
  79. prefix: 'https://www.startpage.com/do/search',
  80. param: 'q'
  81. },
  82. Ecosia: {
  83. prefix: 'https://www.ecosia.org/search',
  84. param: 'q'
  85. }
  86. };
  87.  
  88. // 存储键名
  89. const STORAGE_KEYS = {
  90. REDIRECT_LIST: 'redirect_list',
  91. RULES: 'rules'
  92. };
  93.  
  94. // 国际化文本
  95. const i18n = {
  96. en: {
  97. title: "Search Engine Redirector Settings",
  98. redirectLabel: "Search engines I want to redirect:",
  99. enabledRules: "Enabled Rules",
  100. disabledRules: "Disabled Rules",
  101. addButton: "Add",
  102. name: "Name",
  103. keyword: "Keyword",
  104. urlFormat: "URL Format (use %s for query)",
  105. actions: "Actions",
  106. setDefault: "Set Default",
  107. disable: "Disable",
  108. enable: "Enable",
  109. delete: "Delete",
  110. save: "Save",
  111. cancel: "Cancel",
  112. addRule: "Add Rule",
  113. ruleName: "Rule Name",
  114. ruleKeyword: "Shortcut Keyword",
  115. ruleUrl: "URL Format",
  116. required: "Required",
  117. invalidUrl: "URL must contain '%s'",
  118. defaultSet: "Default set",
  119. ruleAdded: "Rule added",
  120. ruleDeleted: "Rule deleted",
  121. ruleEnabled: "Rule enabled",
  122. ruleDisabled: "Rule disabled",
  123. settingsSaved: "Settings saved"
  124. },
  125. zh: {
  126. title: "搜索引擎重定向设置",
  127. redirectLabel: "我希望重定向的搜索引擎:",
  128. enabledRules: "已启用规则",
  129. disabledRules: "已禁用规则",
  130. addButton: "添加",
  131. name: "名称",
  132. keyword: "快捷字词",
  133. urlFormat: "网址格式(用\"%s\"代替搜索字词)",
  134. actions: "操作",
  135. setDefault: "设为默认",
  136. disable: "禁用",
  137. enable: "启用",
  138. delete: "删除",
  139. save: "保存",
  140. cancel: "取消",
  141. addRule: "添加规则",
  142. ruleName: "规则名称",
  143. ruleKeyword: "快捷字词",
  144. ruleUrl: "网址格式",
  145. required: "必填",
  146. invalidUrl: "URL必须包含'%s'",
  147. defaultSet: "已设为默认",
  148. ruleAdded: "规则已添加",
  149. ruleDeleted: "规则已删除",
  150. ruleEnabled: "规则已启用",
  151. ruleDisabled: "规则已禁用",
  152. settingsSaved: "设置已保存"
  153. }
  154. };
  155.  
  156. // 获取语言
  157. const lang = navigator.language.startsWith('zh') ? 'zh' : 'en';
  158. const text = i18n[lang];
  159.  
  160. // 初始化存储
  161. function initializeStorage() {
  162. if (GM_getValue(STORAGE_KEYS.REDIRECT_LIST) === undefined) {
  163. GM_setValue(STORAGE_KEYS.REDIRECT_LIST, []);
  164. }
  165.  
  166. if (GM_getValue(STORAGE_KEYS.RULES) === undefined) {
  167. GM_setValue(STORAGE_KEYS.RULES, [
  168. { id: 1, name: "Brave", keyword: "br", url: "https://search.brave.com/search?q=%s&source=desktop", enabled: true, isDefault: false },
  169. { id: 2, name: "DuckDuckGo", keyword: "d", url: "https://duckduckgo.com/?q=%s&t=brave", enabled: true, isDefault: false }
  170. ]);
  171. }
  172. }
  173.  
  174. // 主重定向逻辑
  175. function performRedirect() {
  176. const redirectList = GM_getValue(STORAGE_KEYS.REDIRECT_LIST, []);
  177. const rules = GM_getValue(STORAGE_KEYS.RULES, []);
  178. const currentUrl = window.location.href;
  179. if (/([&?])redirect=false(?!\w)/.test(currentUrl)) return;
  180. let matchedEngine = null;
  181. for (const engineName of redirectList) {
  182. const engine = SEARCH_ENGINES[engineName];
  183. if (engine && currentUrl.startsWith(engine.prefix)) {
  184. matchedEngine = engine;
  185. break;
  186. }
  187. }
  188.  
  189. if (!matchedEngine) return;
  190.  
  191. // 提取搜索关键词
  192. const urlObj = new URL(currentUrl);
  193. let query = urlObj.searchParams.get(matchedEngine.param);
  194. if (!query) return;
  195.  
  196. // 分割关键词
  197. const words = query.trim().split(/\s+/);
  198. const firstWord = words[0];
  199. let remainingQuery = words.slice(1).join(' ');
  200.  
  201. // 查找匹配规则
  202. let matchedRule = null;
  203. let defaultRule = null;
  204. for (const rule of rules) {
  205. if (!rule.enabled) continue;
  206.  
  207. if (rule.keyword === firstWord) {
  208. matchedRule = rule;
  209. break;
  210. }
  211.  
  212. if (rule.isDefault) {
  213. defaultRule = rule;
  214. }
  215. }
  216.  
  217. // 使用默认规则(如果存在)
  218. if (!matchedRule && defaultRule) {
  219. matchedRule = defaultRule;
  220. remainingQuery = query; // 使用完整查询
  221. }
  222.  
  223. if (matchedRule) {
  224. let targetUrl = matchedRule.url.replace('%s', encodeURIComponent(remainingQuery));
  225. if (targetUrl.includes('?')) {
  226. targetUrl += '&redirect=false';
  227. } else {
  228. targetUrl += '?redirect=false';
  229. }
  230. window.location.replace(targetUrl);
  231. }
  232. }
  233.  
  234. // 创建设置UI
  235. function createSettingsUI() {
  236. const style = `
  237. .redirector-settings {
  238. font-family: system-ui, -apple-system, sans-serif;
  239. position: fixed;
  240. top: 0;
  241. left: 0;
  242. width: 100%;
  243. height: 100%;
  244. background: rgba(0,0,0,0.7);
  245. display: flex;
  246. align-items: center;
  247. justify-content: center;
  248. z-index: 9999;
  249. overflow-y: auto;
  250. padding: 20px;
  251. box-sizing: border-box;
  252. }
  253.  
  254. .settings-container {
  255. background: white;
  256. border-radius: 12px;
  257. box-shadow: 0 10px 30px rgba(0,0,0,0.2);
  258. width: 100%;
  259. max-width: 900px;
  260. max-height: 90vh;
  261. overflow-y: auto;
  262. }
  263.  
  264. .settings-header {
  265. padding: 20px;
  266. border-bottom: 1px solid #eaeaea;
  267. display: flex;
  268. justify-content: space-between;
  269. align-items: center;
  270. }
  271.  
  272. .settings-title {
  273. font-size: 1.5rem;
  274. font-weight: 600;
  275. color: #333;
  276. margin: 0;
  277. }
  278.  
  279. .close-btn {
  280. background: none;
  281. border: none;
  282. font-size: 1.5rem;
  283. cursor: pointer;
  284. color: #666;
  285. padding: 5px;
  286. }
  287.  
  288. .settings-body {
  289. padding: 20px;
  290. }
  291.  
  292. .section {
  293. margin-bottom: 30px;
  294. }
  295.  
  296. .section-title {
  297. font-size: 1.2rem;
  298. margin: 0 0 15px 0;
  299. color: #444;
  300. font-weight: 600;
  301. }
  302.  
  303. .engines-grid {
  304. display: grid;
  305. grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  306. gap: 12px;
  307. margin-bottom: 20px;
  308. }
  309.  
  310. .engine-item {
  311. display: flex;
  312. align-items: center;
  313. }
  314.  
  315. .engine-item input {
  316. margin-right: 8px;
  317. }
  318.  
  319. .rules-container {
  320. display: flex;
  321. flex-direction: column;
  322. gap: 20px;
  323. }
  324.  
  325. .rules-table {
  326. width: 100%;
  327. border-collapse: collapse;
  328. }
  329.  
  330. .rules-table th {
  331. background: #f8f9fa;
  332. text-align: left;
  333. padding: 12px 15px;
  334. font-weight: 600;
  335. color: #495057;
  336. border-bottom: 1px solid #dee2e6;
  337. }
  338.  
  339. .rules-table td {
  340. padding: 12px 15px;
  341. border-bottom: 1px solid #eaeaea;
  342. }
  343.  
  344. .rules-table tr:nth-child(even) {
  345. background-color: #f9f9f9;
  346. }
  347.  
  348. .rules-table tr:hover {
  349. background-color: #f0f7ff;
  350. }
  351.  
  352. .action-btn {
  353. background: none;
  354. border: 1px solid #d1d5db;
  355. border-radius: 4px;
  356. padding: 5px 10px;
  357. margin: 0 3px;
  358. cursor: pointer;
  359. font-size: 0.85rem;
  360. transition: all 0.2s;
  361. }
  362.  
  363. .action-btn:hover {
  364. background: #f0f7ff;
  365. border-color: #3b82f6;
  366. color: #3b82f6;
  367. }
  368.  
  369. .add-btn {
  370. background: #3b82f6;
  371. color: white;
  372. border: none;
  373. border-radius: 6px;
  374. padding: 8px 16px;
  375. cursor: pointer;
  376. font-weight: 500;
  377. display: flex;
  378. align-items: center;
  379. gap: 5px;
  380. margin-top: 10px;
  381. transition: background 0.2s;
  382. }
  383.  
  384. .add-btn:hover {
  385. background: #2563eb;
  386. }
  387.  
  388. .modal {
  389. position: fixed;
  390. top: 0;
  391. left: 0;
  392. width: 100%;
  393. height: 100%;
  394. background: rgba(0,0,0,0.5);
  395. display: flex;
  396. align-items: center;
  397. justify-content: center;
  398. z-index: 10000;
  399. }
  400.  
  401. .modal-content {
  402. background: white;
  403. border-radius: 10px;
  404. width: 90%;
  405. max-width: 500px;
  406. padding: 25px;
  407. box-shadow: 0 10px 25px rgba(0,0,0,0.2);
  408. }
  409.  
  410. .modal-header {
  411. display: flex;
  412. justify-content: space-between;
  413. align-items: center;
  414. margin-bottom: 20px;
  415. }
  416.  
  417. .modal-title {
  418. font-size: 1.3rem;
  419. font-weight: 600;
  420. margin: 0;
  421. }
  422.  
  423. .form-group {
  424. margin-bottom: 20px;
  425. }
  426.  
  427. .form-label {
  428. display: block;
  429. margin-bottom: 8px;
  430. font-weight: 500;
  431. color: #444;
  432. }
  433.  
  434. .form-input {
  435. width: 100%;
  436. padding: 10px 12px;
  437. border: 1px solid #d1d5db;
  438. border-radius: 6px;
  439. font-size: 1rem;
  440. box-sizing: border-box;
  441. }
  442.  
  443. .form-input:focus {
  444. outline: none;
  445. border-color: #3b82f6;
  446. box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
  447. }
  448.  
  449. .error-message {
  450. color: #e53e3e;
  451. font-size: 0.85rem;
  452. margin-top: 5px;
  453. display: none;
  454. }
  455.  
  456. .modal-footer {
  457. display: flex;
  458. justify-content: flex-end;
  459. gap: 10px;
  460. margin-top: 20px;
  461. }
  462.  
  463. .btn {
  464. padding: 10px 20px;
  465. border-radius: 6px;
  466. font-weight: 500;
  467. cursor: pointer;
  468. transition: all 0.2s;
  469. }
  470.  
  471. .btn-primary {
  472. background: #3b82f6;
  473. color: white;
  474. border: none;
  475. }
  476.  
  477. .btn-primary:hover {
  478. background: #2563eb;
  479. }
  480.  
  481. .btn-secondary {
  482. background: #f3f4f6;
  483. color: #4b5563;
  484. border: 1px solid #d1d5db;
  485. }
  486.  
  487. .btn-secondary:hover {
  488. background: #e5e7eb;
  489. }
  490.  
  491. .toast {
  492. position: fixed;
  493. bottom: 20px;
  494. right: 20px;
  495. background: #333;
  496. color: white;
  497. padding: 12px 20px;
  498. border-radius: 6px;
  499. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  500. z-index: 10001;
  501. opacity: 0;
  502. transform: translateY(20px);
  503. transition: all 0.3s;
  504. }
  505.  
  506. .toast.show {
  507. opacity: 1;
  508. transform: translateY(0);
  509. }
  510. `;
  511.  
  512. GM_addStyle(style);
  513.  
  514. const settingsContainer = document.createElement('div');
  515. settingsContainer.className = 'redirector-settings';
  516. settingsContainer.innerHTML = `
  517. <div class="settings-container">
  518. <div class="settings-header">
  519. <h2 class="settings-title">${text.title}</h2>
  520. <button class="close-btn">&times;</button>
  521. </div>
  522. <div class="settings-body">
  523. <div class="section">
  524. <h3 class="section-title">${text.redirectLabel}</h3>
  525. <div class="engines-grid" id="engines-grid"></div>
  526. </div>
  527.  
  528. <div class="rules-container">
  529. <div class="section">
  530. <div style="display: flex; justify-content: space-between; align-items: center;">
  531. <h3 class="section-title">${text.enabledRules}</h3>
  532. <button class="add-btn" id="add-enabled-btn">+ ${text.addButton}</button>
  533. </div>
  534. <table class="rules-table" id="enabled-table">
  535. <thead>
  536. <tr>
  537. <th>${text.name}</th>
  538. <th>${text.keyword}</th>
  539. <th>${text.urlFormat}</th>
  540. <th>${text.actions}</th>
  541. </tr>
  542. </thead>
  543. <tbody id="enabled-rules-body"></tbody>
  544. </table>
  545. </div>
  546.  
  547. <div class="section">
  548. <div style="display: flex; justify-content: space-between; align-items: center;">
  549. <h3 class="section-title">${text.disabledRules}</h3>
  550. <button class="add-btn" id="add-disabled-btn">+ ${text.addButton}</button>
  551. </div>
  552. <table class="rules-table" id="disabled-table">
  553. <thead>
  554. <tr>
  555. <th>${text.name}</th>
  556. <th>${text.keyword}</th>
  557. <th>${text.urlFormat}</th>
  558. <th>${text.actions}</th>
  559. </tr>
  560. </thead>
  561. <tbody id="disabled-rules-body"></tbody>
  562. </table>
  563. </div>
  564. </div>
  565. </div>
  566. </div>
  567. `;
  568.  
  569. document.body.appendChild(settingsContainer);
  570.  
  571. // 关闭按钮
  572. settingsContainer.querySelector('.close-btn').addEventListener('click', () => {
  573. document.body.removeChild(settingsContainer);
  574. });
  575.  
  576. // 渲染设置
  577. renderSettings();
  578.  
  579. // 添加规则按钮
  580. document.getElementById('add-enabled-btn').addEventListener('click', () => {
  581. showAddRuleModal(true);
  582. });
  583.  
  584. document.getElementById('add-disabled-btn').addEventListener('click', () => {
  585. showAddRuleModal(false);
  586. });
  587. }
  588.  
  589. // 渲染设置
  590. function renderSettings() {
  591. const redirectList = GM_getValue(STORAGE_KEYS.REDIRECT_LIST, []);
  592. const rules = GM_getValue(STORAGE_KEYS.RULES, []);
  593.  
  594. // 渲染搜索引擎选择
  595. const enginesGrid = document.getElementById('engines-grid');
  596. enginesGrid.innerHTML = '';
  597.  
  598. Object.keys(SEARCH_ENGINES).forEach(engine => {
  599. const isChecked = redirectList.includes(engine);
  600. const engineItem = document.createElement('label');
  601. engineItem.className = 'engine-item';
  602. engineItem.innerHTML = `
  603. <input type="checkbox" value="${engine}" ${isChecked ? 'checked' : ''}>
  604. ${engine}
  605. `;
  606. enginesGrid.appendChild(engineItem);
  607. });
  608.  
  609. // 为复选框添加事件
  610. enginesGrid.querySelectorAll('input').forEach(checkbox => {
  611. checkbox.addEventListener('change', () => {
  612. const newRedirectList = [...enginesGrid.querySelectorAll('input:checked')].map(cb => cb.value);
  613. GM_setValue(STORAGE_KEYS.REDIRECT_LIST, newRedirectList);
  614. });
  615. });
  616.  
  617. // 渲染规则表
  618. renderRulesTable('enabled-rules-body', rules.filter(r => r.enabled));
  619. renderRulesTable('disabled-rules-body', rules.filter(r => !r.enabled));
  620. }
  621.  
  622. // 渲染规则表
  623. function renderRulesTable(tableId, rules) {
  624. const tableBody = document.getElementById(tableId);
  625. tableBody.innerHTML = '';
  626.  
  627. if (rules.length === 0) {
  628. const row = document.createElement('tr');
  629. row.innerHTML = `<td colspan="4" style="text-align: center; padding: 20px; color: #777;">${lang === 'zh' ? '没有规则' : 'No rules'}</td>`;
  630. tableBody.appendChild(row);
  631. return;
  632. }
  633. rules.forEach(rule => {
  634. const isDefault = rule.isDefault;
  635. const row = document.createElement('tr');
  636. row.innerHTML = `
  637. <td>${rule.name} ${isDefault ? '<span style="color:#3b82f6;font-size:0.8em;">(默认)</span>' : ''}</td>
  638. <td>${rule.keyword}</td>
  639. <td style="max-width: 300px; word-break: break-all;">${rule.url}</td>
  640. <td>
  641. ${rule.enabled ?
  642. `<button class="action-btn set-default-btn" data-id="${rule.id}">${text.setDefault}</button>
  643. <button class="action-btn disable-btn" data-id="${rule.id}" ${isDefault ? 'disabled style="opacity:0.5;cursor:not-allowed;"' : ''}>${text.disable}</button>` :
  644. `<button class="action-btn enable-btn" data-id="${rule.id}">${text.enable}</button>`
  645. }
  646. <button class="action-btn delete-btn" data-id="${rule.id}" ${isDefault ? 'disabled style="opacity:0.5;cursor:not-allowed;"' : ''}>${text.delete}</button>
  647. </td>
  648. `;
  649. tableBody.appendChild(row);
  650. });
  651. // 事件处理
  652. tableBody.querySelectorAll('.set-default-btn').forEach(btn => {
  653. btn.addEventListener('click', () => setDefaultRule(btn.dataset.id));
  654. });
  655.  
  656. tableBody.querySelectorAll('.disable-btn').forEach(btn => {
  657. if (btn.hasAttribute('disabled')) return;
  658. btn.addEventListener('click', () => toggleRule(btn.dataset.id, false));
  659. });
  660.  
  661. tableBody.querySelectorAll('.enable-btn').forEach(btn => {
  662. btn.addEventListener('click', () => toggleRule(btn.dataset.id, true));
  663. });
  664.  
  665. tableBody.querySelectorAll('.delete-btn').forEach(btn => {
  666. if (btn.hasAttribute('disabled')) return;
  667. btn.addEventListener('click', () => deleteRule(btn.dataset.id));
  668. });
  669. }
  670.  
  671. // 设置默认规则
  672. function setDefaultRule(ruleId) {
  673. const rules = GM_getValue(STORAGE_KEYS.RULES, []);
  674. const updatedRules = rules.map(rule => ({
  675. ...rule,
  676. isDefault: rule.id.toString() === ruleId
  677. }));
  678.  
  679. GM_setValue(STORAGE_KEYS.RULES, updatedRules);
  680. renderSettings();
  681. showToast(text.defaultSet);
  682. }
  683.  
  684. // 切换规则状态
  685. function toggleRule(ruleId, enabled) {
  686. const rules = GM_getValue(STORAGE_KEYS.RULES, []);
  687. const updatedRules = rules.map(rule =>
  688. rule.id.toString() === ruleId ? {...rule, enabled} : rule
  689. );
  690.  
  691. GM_setValue(STORAGE_KEYS.RULES, updatedRules);
  692. renderSettings();
  693. showToast(enabled ? text.ruleEnabled : text.ruleDisabled);
  694. }
  695.  
  696. // 删除规则
  697. function deleteRule(ruleId) {
  698. const rules = GM_getValue(STORAGE_KEYS.RULES, []);
  699. const updatedRules = rules.filter(rule => rule.id.toString() !== ruleId);
  700.  
  701. GM_setValue(STORAGE_KEYS.RULES, updatedRules);
  702. renderSettings();
  703. showToast(text.ruleDeleted);
  704. }
  705.  
  706. // 显示添加规则模态框
  707. function showAddRuleModal(enabled) {
  708. const modal = document.createElement('div');
  709. modal.className = 'modal';
  710. modal.innerHTML = `
  711. <div class="modal-content">
  712. <div class="modal-header">
  713. <h3 class="modal-title">${text.addRule}</h3>
  714. <button class="close-btn">&times;</button>
  715. </div>
  716. <div class="form-group">
  717. <label class="form-label">${text.ruleName} <span style="color:red">*</span></label>
  718. <input type="text" class="form-input" id="rule-name">
  719. <div class="error-message" id="name-error">${text.required}</div>
  720. </div>
  721. <div class="form-group">
  722. <label class="form-label">${text.ruleKeyword} <span style="color:red">*</span></label>
  723. <input type="text" class="form-input" id="rule-keyword">
  724. <div class="error-message" id="keyword-error">${text.required}</div>
  725. </div>
  726. <div class="form-group">
  727. <label class="form-label">${text.ruleUrl} <span style="color:red">*</span></label>
  728. <input type="text" class="form-input" id="rule-url" placeholder="https://example.com/search?q=%s">
  729. <div class="error-message" id="url-error">${text.invalidUrl.replace('%s', '%s')}</div>
  730. </div>
  731. <div class="modal-footer">
  732. <button class="btn btn-secondary" id="cancel-btn">${text.cancel}</button>
  733. <button class="btn btn-primary" id="save-btn">${text.save}</button>
  734. </div>
  735. </div>
  736. `;
  737.  
  738. document.body.appendChild(modal);
  739.  
  740. // 关闭按钮
  741. modal.querySelector('.close-btn').addEventListener('click', () => {
  742. document.body.removeChild(modal);
  743. });
  744.  
  745. // 取消按钮
  746. modal.querySelector('#cancel-btn').addEventListener('click', () => {
  747. document.body.removeChild(modal);
  748. });
  749.  
  750. // 保存按钮
  751. modal.querySelector('#save-btn').addEventListener('click', () => {
  752. const name = modal.querySelector('#rule-name').value.trim();
  753. const keyword = modal.querySelector('#rule-keyword').value.trim();
  754. const url = modal.querySelector('#rule-url').value.trim();
  755.  
  756. // 验证输入
  757. let valid = true;
  758.  
  759. if (!name) {
  760. modal.querySelector('#name-error').style.display = 'block';
  761. valid = false;
  762. } else {
  763. modal.querySelector('#name-error').style.display = 'none';
  764. }
  765.  
  766. if (!keyword) {
  767. modal.querySelector('#keyword-error').style.display = 'block';
  768. valid = false;
  769. } else {
  770. modal.querySelector('#keyword-error').style.display = 'none';
  771. }
  772.  
  773. if (!url || !url.includes('%s')) {
  774. modal.querySelector('#url-error').style.display = 'block';
  775. valid = false;
  776. } else {
  777. modal.querySelector('#url-error').style.display = 'none';
  778. }
  779.  
  780. if (!valid) return;
  781.  
  782. // 保存规则
  783. const rules = GM_getValue(STORAGE_KEYS.RULES, []);
  784. const newId = rules.length > 0 ? Math.max(...rules.map(r => r.id)) + 1 : 1;
  785.  
  786. rules.push({
  787. id: newId,
  788. name,
  789. keyword,
  790. url,
  791. enabled,
  792. isDefault: false
  793. });
  794.  
  795. GM_setValue(STORAGE_KEYS.RULES, rules);
  796. document.body.removeChild(modal);
  797. renderSettings();
  798. showToast(text.ruleAdded);
  799. });
  800. }
  801.  
  802. // 显示提示消息
  803. function showToast(message) {
  804. let toast = document.querySelector('.toast');
  805. if (!toast) {
  806. toast = document.createElement('div');
  807. toast.className = 'toast';
  808. document.body.appendChild(toast);
  809. }
  810.  
  811. toast.textContent = message;
  812. toast.classList.add('show');
  813.  
  814. setTimeout(() => {
  815. toast.classList.remove('show');
  816. }, 3000);
  817. }
  818.  
  819. // 初始化
  820. initializeStorage();
  821.  
  822. // 注册菜单命令
  823. GM_registerMenuCommand(text.title, createSettingsUI);
  824.  
  825. // 执行重定向
  826. performRedirect();
  827. })();