Hypothes.is Search

Search user's Hypothes.is annotations across multiple search engines

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

QingJ © 2025

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