Linkwarden Search

Search user's Linkwarden bookmarks across multiple search engines

目前為 2025-03-01 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Linkwarden Search
  3. // @namespace https://mjyai.com
  4. // @version 1.3.0
  5. // @description Search user's Linkwarden bookmarks across multiple search engines
  6. // @author MA Junyi
  7. // @match https://www.google.com/search*
  8. // @match https://www.bing.com/search*
  9. // @match https://duckduckgo.com/*
  10. // @match https://www.baidu.com/s*
  11. // @match https://search.brave.com/search*
  12. // @match https://yandex.com/search*
  13. // @match https://presearch.com/search*
  14. // @match *://*/search*
  15. // @grant GM_xmlhttpRequest
  16. // @grant GM_addStyle
  17. // @grant GM_setValue
  18. // @grant GM_getValue
  19. // @connect cloud.linkwarden.app
  20. // @license GPL-3.0
  21. // ==/UserScript==
  22.  
  23. (function () {
  24. 'use strict';
  25.  
  26. const ITEMS_PER_PAGE = 10;
  27. let currentPage = 1;
  28. let totalItems = [];
  29.  
  30. const styles = `
  31. :root {
  32. --md-primary: #1976d2;
  33. --md-primary-dark: #1565c0;
  34. --md-surface: #ffffff;
  35. --md-on-surface: #1f1f1f;
  36. --md-outline: rgba(0, 0, 0, 0.12);
  37. --md-text-primary: rgba(0, 0, 0, 0.87);
  38. --md-text-secondary: rgba(0, 0, 0, 0.6);
  39. --md-text-disabled: rgba(0, 0, 0, 0.38);
  40. --md-hover-overlay: rgba(0, 0, 0, 0.04);
  41. --md-shadow-1: 0 2px 4px -1px rgba(0,0,0,.2), 0 4px 5px 0 rgba(0,0,0,.14), 0 1px 10px 0 rgba(0,0,0,.12);
  42. --md-shadow-2: 0 5px 5px -3px rgba(0,0,0,.2), 0 8px 10px 1px rgba(0,0,0,.14), 0 3px 14px 2px rgba(0,0,0,.12);
  43. }
  44.  
  45. @media (prefers-color-scheme: dark) {
  46. :root {
  47. --md-primary: #90caf9;
  48. --md-primary-dark: #82b1ff;
  49. --md-surface: #1e1e1e;
  50. --md-on-surface: #ffffff;
  51. --md-outline: rgba(255, 255, 255, 0.12);
  52. --md-text-primary: rgba(255, 255, 255, 0.87);
  53. --md-text-secondary: rgba(255, 255, 255, 0.6);
  54. --md-text-disabled: rgba(255, 255, 255, 0.38);
  55. --md-hover-overlay: rgba(255, 255, 255, 0.04);
  56. --md-shadow-1: 0 2px 4px -1px rgba(0,0,0,.4), 0 4px 5px 0 rgba(0,0,0,.34), 0 1px 10px 0 rgba(0,0,0,.32);
  57. --md-shadow-2: 0 5px 5px -3px rgba(0,0,0,.4), 0 8px 10px 1px rgba(0,0,0,.34), 0 3px 14px 2px rgba(0,0,0,.32);
  58. }
  59. }
  60.  
  61. #linkwarden-panel {
  62. position: fixed !important;
  63. top: 100px !important;
  64. right: 20px !important;
  65. width: 360px !important;
  66. min-height: 100px !important;
  67. max-height: 80vh !important;
  68. background: var(--md-surface) !important;
  69. border-radius: 8px !important;
  70. box-shadow: var(--md-shadow-1) !important;
  71. padding: 16px !important;
  72. overflow-y: auto !important;
  73. z-index: 99999 !important;
  74. font-family: Roboto, Arial, sans-serif !important;
  75. transition: box-shadow 0.3s ease !important;
  76. opacity: 0.95 !important;
  77. color: var(--md-text-primary) !important;
  78. }
  79.  
  80. #linkwarden-panel:hover {
  81. opacity: 1 !important;
  82. box-shadow: var(--md-shadow-2) !important;
  83. }
  84.  
  85. #linkwarden-panel h3 {
  86. color: var(--md-on-surface) !important;
  87. font-size: 20px !important;
  88. font-weight: 500 !important;
  89. margin: 0 0 16px 0 !important;
  90. padding-right: 24px !important;
  91. }
  92.  
  93. .gear-icon {
  94. position: absolute !important;
  95. top: 16px !important;
  96. right: 16px !important;
  97. cursor: pointer !important;
  98. color: var(--md-text-secondary) !important;
  99. opacity: 0.87 !important;
  100. transition: opacity 0.2s ease !important;
  101. padding: 8px !important;
  102. border-radius: 50% !important;
  103. background: transparent !important;
  104. }
  105.  
  106. .gear-icon:hover {
  107. opacity: 1 !important;
  108. background: var(--md-hover-overlay) !important;
  109. }
  110.  
  111. .item-div {
  112. margin-bottom: 16px !important;
  113. padding: 12px !important;
  114. border-radius: 4px !important;
  115. border: 1px solid var(--md-outline) !important;
  116. transition: all 0.2s ease !important;
  117. color: var(--md-text-primary) !important;
  118. cursor: pointer !important;
  119. text-decoration: none !important;
  120. display: block !important;
  121. }
  122.  
  123. .item-div:hover {
  124. border-color: var(--md-primary) !important;
  125. box-shadow: 0 1px 3px rgba(0,0,0,0.12) !important;
  126. background: var(--md-hover-overlay) !important;
  127. }
  128.  
  129. .item-div strong {
  130. display: block !important;
  131. font-size: 16px !important;
  132. color: var(--md-primary) !important;
  133. margin-bottom: 8px !important;
  134. }
  135.  
  136. .item-div div:nth-child(2) {
  137. font-size: 14px !important;
  138. color: var(--md-text-primary) !important;
  139. line-height: 1.5 !important;
  140. margin-bottom: 8px !important;
  141. }
  142.  
  143. .pagination {
  144. display: flex !important;
  145. justify-content: space-between !important;
  146. align-items: center !important;
  147. margin-top: 16px !important;
  148. padding: 8px 0 !important;
  149. border-top: 1px solid var(--md-outline) !important;
  150. }
  151.  
  152. .pagination button {
  153. background: transparent !important;
  154. color: var(--md-primary) !important;
  155. border: none !important;
  156. padding: 8px 16px !important;
  157. border-radius: 4px !important;
  158. font-size: 14px !important;
  159. font-weight: 500 !important;
  160. text-transform: uppercase !important;
  161. cursor: pointer !important;
  162. transition: background-color 0.2s ease !important;
  163. }
  164.  
  165. .pagination button:hover:not(:disabled) {
  166. background: var(--md-hover-overlay) !important;
  167. }
  168.  
  169. .pagination button:disabled {
  170. color: var(--md-text-disabled) !important;
  171. cursor: default !important;
  172. }
  173.  
  174. .page-info {
  175. color: var(--md-text-secondary) !important;
  176. font-size: 14px !important;
  177. }
  178.  
  179. #settings-panel {
  180. position: fixed !important;
  181. top: 50% !important;
  182. left: 50% !important;
  183. transform: translate(-50%, -50%) !important;
  184. background: var(--md-surface) !important;
  185. border-radius: 8px !important;
  186. box-shadow: var(--md-shadow-2) !important;
  187. padding: 24px !important;
  188. z-index: 100001 !important;
  189. min-width: 320px !important;
  190. max-width: 400px !important;
  191. color: var(--md-text-primary) !important;
  192. }
  193.  
  194. #settings-panel h3 {
  195. color: var(--md-on-surface) !important;
  196. font-size: 20px !important;
  197. font-weight: 500 !important;
  198. margin: 0 0 24px 0 !important;
  199. }
  200.  
  201. #settings-panel label {
  202. color: var(--md-text-primary) !important;
  203. font-size: 14px !important;
  204. margin-bottom: 4px !important;
  205. display: block !important;
  206. }
  207.  
  208. #settings-panel input {
  209. width: 100% !important;
  210. padding: 8px 12px !important;
  211. margin: 4px 0 16px 0 !important;
  212. border: 1px solid var(--md-outline) !important;
  213. border-radius: 4px !important;
  214. font-size: 16px !important;
  215. transition: border-color 0.2s ease !important;
  216. box-sizing: border-box;
  217. background: var(--md-surface) !important;
  218. color: var(--md-text-primary) !important;
  219. }
  220.  
  221. #settings-panel input:focus {
  222. outline: none !important;
  223. border-color: var(--md-primary) !important;
  224. }
  225.  
  226. #settings-panel button {
  227. background: var(--md-primary) !important;
  228. color: var(--md-surface) !important;
  229. border: none !important;
  230. padding: 8px 16px !important;
  231. border-radius: 4px !important;
  232. font-size: 14px !important;
  233. font-weight: 500 !important;
  234. text-transform: uppercase !important;
  235. cursor: pointer !important;
  236. margin-left: 8px !important;
  237. transition: background-color 0.2s ease !important;
  238. }
  239.  
  240. #settings-panel button:hover {
  241. background: var(--md-primary-dark) !important;
  242. }
  243.  
  244. #settings-panel button:first-child {
  245. margin-left: 0 !important;
  246. }
  247.  
  248. #settings-panel button#closeSettings {
  249. background: transparent !important;
  250. color: var(--md-primary) !important;
  251. }
  252.  
  253. #settings-panel button#closeSettings:hover {
  254. background: var(--md-hover-overlay) !important;
  255. }
  256.  
  257. .checkbox-container {
  258. display: flex !important;
  259. align-items: center !important;
  260. gap: 8px !important;
  261. margin-bottom: 16px !important;
  262. padding: 8px 0 !important;
  263. }
  264.  
  265. .checkbox-container input[type="checkbox"] {
  266. width: auto !important;
  267. margin: 0 !important;
  268. }
  269.  
  270. .checkbox-container label {
  271. display: inline !important;
  272. margin: 0 !important;
  273. cursor: pointer !important;
  274. }
  275. `;
  276.  
  277. const getQueryParameter = (param) => new URLSearchParams(window.location.search).get(param);
  278.  
  279. const isSearxNG = () => {
  280. return (
  281. document.querySelector('meta[name="generator"][content*="searxng"]') !== null ||
  282. document.querySelector('a[href*="searx/preferences"]') !== null ||
  283. document.querySelector('a[href*="searx/settings"]') !== null ||
  284. document.querySelector('label[for="time_range"]') !== null
  285. );
  286. };
  287.  
  288. const searchEngines = {
  289. 'google.com': { getQuery: () => getQueryParameter('q') },
  290. 'bing.com': { getQuery: () => getQueryParameter('q') },
  291. 'duckduckgo.com': { getQuery: () => getQueryParameter('q') },
  292. 'baidu.com': { getQuery: () => getQueryParameter('wd') },
  293. 'brave.com': { getQuery: () => getQueryParameter('q') },
  294. 'yandex.com': { getQuery: () => getQueryParameter('text') },
  295. 'presearch.com': { getQuery: () => getQueryParameter('q') }
  296. };
  297.  
  298. GM_addStyle(styles);
  299.  
  300. const getCurrentSearchQuery = () => {
  301. const searxngEnabled = GM_getValue('searxngEnabled', false);
  302.  
  303. if (searxngEnabled && isSearxNG()) {
  304. return getQueryParameter('q');
  305. }
  306.  
  307. const currentDomain = Object.keys(searchEngines).find(domain =>
  308. window.location.hostname.includes(domain));
  309. return currentDomain ? searchEngines[currentDomain].getQuery() : null;
  310. };
  311.  
  312. const getSettings = () => ({
  313. baseUrl: GM_getValue('linkwardenUrl', 'https://cloud.linkwarden.app'),
  314. apiToken: GM_getValue('linkwardenApiToken', ''),
  315. searxngEnabled: GM_getValue('searxngEnabled', false)
  316. });
  317.  
  318. const saveSettings = (baseUrl, apiToken) => {
  319. GM_setValue('linkwardenUrl', baseUrl);
  320. GM_setValue('linkwardenApiToken', apiToken);
  321. GM_setValue('searxngEnabled', searxngEnabled);
  322. };
  323.  
  324. const addSettingsIcon = () => {
  325. const panel = document.getElementById('linkwarden-panel');
  326. if (!panel) return;
  327.  
  328. let gear = panel.querySelector('.gear-icon');
  329. if (!gear) {
  330. gear = document.createElement('span');
  331. gear.className = 'gear-icon';
  332. gear.textContent = '⚙️';
  333. gear.title = 'Settings';
  334. gear.addEventListener('click', openSettingsPanel);
  335. panel.appendChild(gear);
  336. }
  337. };
  338.  
  339. const openSettingsPanel = () => {
  340. let settingsPanel = document.getElementById('settings-panel');
  341. if (settingsPanel) return;
  342.  
  343. const settings = getSettings();
  344. settingsPanel = document.createElement('div');
  345. settingsPanel.id = 'settings-panel';
  346.  
  347. settingsPanel.innerHTML = `
  348. <h3>Linkwarden Settings</h3>
  349. <label for="baseUrl">Url(*):</label><br>
  350. <input type="text" id="baseUrl"><br>
  351. <label for="apiToken">API Token(*):</label><br>
  352. <input type="text" id="apiToken"><br>
  353. <div class="checkbox-container">
  354. <label for="searxngEnabled">SearXNG Support</label>
  355. <input type="checkbox" id="searxngEnabled" ${settings.searxngEnabled ? 'checked' : ''}>
  356. </div>
  357. <button id="saveSettings">Save</button>
  358. <button id="closeSettings">Close</button>
  359. `;
  360.  
  361. document.body.appendChild(settingsPanel);
  362.  
  363. document.getElementById('baseUrl').value = settings.baseUrl;
  364. document.getElementById('apiToken').value = settings.apiToken;
  365. document.getElementById('searxngEnabled').checked = settings.searxngEnabled;
  366.  
  367. document.getElementById('saveSettings').onclick = () => {
  368. const baseUrl = document.getElementById('baseUrl').value;
  369. const apiToken = document.getElementById('apiToken').value;
  370. const searxngEnabled = document.getElementById('searxngEnabled').checked;
  371. saveSettings(baseUrl, apiToken, searxngEnabled);
  372. alert('Settings saved!');
  373. document.body.removeChild(settingsPanel);
  374. fetchItems(getCurrentSearchQuery());
  375. };
  376.  
  377. document.getElementById('closeSettings').onclick = () => {
  378. document.body.removeChild(settingsPanel);
  379. };
  380. };
  381.  
  382. const createLinkwardenPanel = () => {
  383. const panel = document.createElement('div');
  384. panel.id = 'linkwarden-panel';
  385. panel.innerHTML = '<h3>Linkwarden Bookmarks</h3><div id="items-content"></div>';
  386. document.body.appendChild(panel);
  387. addSettingsIcon();
  388. return panel;
  389. };
  390.  
  391. const displayItems = () => {
  392. let panel = document.getElementById('linkwarden-panel');
  393. if (!panel) {
  394. panel = createLinkwardenPanel();
  395. }
  396.  
  397. const contentDiv = document.getElementById('items-content');
  398. if (!contentDiv) {
  399. console.error('Content div not found');
  400. return;
  401. }
  402.  
  403. contentDiv.innerHTML = '';
  404.  
  405. if (totalItems.length > 0) {
  406. const startIdx = (currentPage - 1) * ITEMS_PER_PAGE;
  407. const endIdx = Math.min(startIdx + ITEMS_PER_PAGE, totalItems.length);
  408. const currentItems = totalItems.slice(startIdx, endIdx);
  409.  
  410. currentItems.forEach(item => {
  411. const itemDiv = document.createElement('a');
  412. itemDiv.className = 'item-div';
  413. itemDiv.href = item.url;
  414. itemDiv.target = '_blank';
  415. itemDiv.innerHTML = `
  416. <div><strong>${item.name || 'Untitled'}</strong></div>
  417. <div>${item.description ? item.description : ''}</div>
  418. `;
  419. contentDiv.appendChild(itemDiv);
  420. });
  421.  
  422. const totalPages = Math.ceil(totalItems.length / ITEMS_PER_PAGE);
  423. const paginationDiv = document.createElement('div');
  424. paginationDiv.className = 'pagination';
  425.  
  426. const prevButton = document.createElement('button');
  427. prevButton.textContent = 'Previous';
  428. prevButton.disabled = currentPage === 1;
  429. prevButton.onclick = () => {
  430. if (currentPage > 1) {
  431. currentPage--;
  432. displayItems();
  433. }
  434. };
  435.  
  436. const nextButton = document.createElement('button');
  437. nextButton.textContent = 'Next';
  438. nextButton.disabled = currentPage >= totalPages;
  439. nextButton.onclick = () => {
  440. if (currentPage < totalPages) {
  441. currentPage++;
  442. displayItems();
  443. }
  444. };
  445.  
  446. const pageInfo = document.createElement('span');
  447. pageInfo.className = 'page-info';
  448. pageInfo.textContent = `Page ${currentPage} of ${totalPages}`;
  449.  
  450. paginationDiv.appendChild(prevButton);
  451. paginationDiv.appendChild(pageInfo);
  452. paginationDiv.appendChild(nextButton);
  453. contentDiv.appendChild(paginationDiv);
  454. } else {
  455. contentDiv.innerHTML = '<p>No bookmarks found for this query.</p>';
  456. }
  457. };
  458.  
  459. const fetchItems = (query) => {
  460. const settings = getSettings();
  461. if (!settings.baseUrl || !settings.apiToken) {
  462. const contentDiv = document.getElementById('items-content');
  463. if (contentDiv) {
  464. contentDiv.innerHTML = '<p>Please configure your Linkwarden baseUrl and API token.</p>';
  465. }
  466. openSettingsPanel();
  467. return;
  468. }
  469.  
  470. GM_xmlhttpRequest({
  471. method: 'GET',
  472. url: `${settings.baseUrl}/api/v1/links?searchByName=true&searchByUrl=true&searchByDescription=true&searchByTextContent=true&searchByTags=true&searchQueryString=${encodeURIComponent(query)}`,
  473. headers: { 'Authorization': `Bearer ${settings.apiToken}` },
  474. onload: function (response) {
  475. const data = JSON.parse(response.responseText);
  476.  
  477. data.response.forEach(item => {
  478. totalItems.push(item);
  479. });
  480.  
  481. displayItems();
  482. },
  483. onerror: function (err) {
  484. console.error('Failed to fetch bookmarks', err);
  485. const contentDiv = document.getElementById('items-content');
  486. if (contentDiv) {
  487. contentDiv.innerHTML = '<p>Failed to fetch bookmarks. Please check your settings and try again.</p>';
  488. }
  489. }
  490. });
  491. };
  492.  
  493. const query = getCurrentSearchQuery();
  494. if (query) {
  495. fetchItems(query);
  496. }
  497. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址