// ==UserScript== // @name Hypothes.is Search // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description Search user's Hypothes.is annotations across multiple search engines // @author MA Junyi // @match https://www.google.com/search* // @match https://www.bing.com/search* // @match https://duckduckgo.com/* // @match https://www.baidu.com/s* // @match https://search.brave.com/search* // @match https://yandex.com/search* // @match https://presearch.com/search* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @connect api.hypothes.is // @license GPL-3.0 // @downloadURL none // ==/UserScript== (function () { 'use strict'; const ITEMS_PER_PAGE = 10; let currentPage = 1; let totalAnnotations = []; const API_URL = 'https://api.hypothes.is/api/search'; const styles = ` :root { --md-primary: #1976d2; --md-primary-dark: #1565c0; --md-surface: #ffffff; --md-on-surface: #1f1f1f; --md-outline: rgba(0, 0, 0, 0.12); --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); --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); } #hypothesis-panel { position: fixed !important; top: 100px !important; right: 20px !important; width: 360px !important; min-height: 100px !important; max-height: 80vh !important; background: var(--md-surface) !important; border-radius: 8px !important; box-shadow: var(--md-shadow-1) !important; padding: 16px !important; overflow-y: auto !important; z-index: 99999 !important; font-family: Roboto, Arial, sans-serif !important; transition: box-shadow 0.3s ease !important; } #hypothesis-panel:hover { box-shadow: var(--md-shadow-2) !important; } #hypothesis-panel h3 { color: var(--md-on-surface) !important; font-size: 20px !important; font-weight: 500 !important; margin: 0 0 16px 0 !important; padding-right: 24px !important; } .gear-icon { position: absolute !important; top: 16px !important; right: 16px !important; cursor: pointer !important; color: var(--md-on-surface) !important; opacity: 0.54 !important; transition: opacity 0.2s ease !important; padding: 8px !important; border-radius: 50% !important; background: transparent !important; } .gear-icon:hover { opacity: 0.87 !important; background: rgba(0, 0, 0, 0.04) !important; } .annotation-div { margin-bottom: 16px !important; padding: 12px !important; border-radius: 4px !important; border: 1px solid var(--md-outline) !important; transition: all 0.2s ease !important; } .annotation-div:hover { border-color: var(--md-primary) !important; box-shadow: 0 1px 3px rgba(0,0,0,0.12) !important; } .annotation-div strong { display: block !important; font-size: 16px !important; color: var(--md-on-surface) !important; margin-bottom: 8px !important; } .annotation-div div:nth-child(2) { font-size: 14px !important; color: rgba(0, 0, 0, 0.87) !important; line-height: 1.5 !important; margin-bottom: 8px !important; } .annotation-div a { color: var(--md-primary) !important; text-decoration: none !important; font-size: 14px !important; font-weight: 500 !important; text-transform: uppercase !important; letter-spacing: 0.5px !important; transition: color 0.2s ease !important; } .annotation-div a:hover { color: var(--md-primary-dark) !important; } .pagination { display: flex !important; justify-content: space-between !important; align-items: center !important; margin-top: 16px !important; padding: 8px 0 !important; border-top: 1px solid var(--md-outline) !important; } .pagination button { background: transparent !important; color: var(--md-primary) !important; border: none !important; padding: 8px 16px !important; border-radius: 4px !important; font-size: 14px !important; font-weight: 500 !important; text-transform: uppercase !important; cursor: pointer !important; transition: background-color 0.2s ease !important; } .pagination button:hover:not(:disabled) { background: rgba(25, 118, 210, 0.04) !important; } .pagination button:disabled { color: rgba(0, 0, 0, 0.38) !important; cursor: default !important; } .page-info { color: rgba(0, 0, 0, 0.6) !important; font-size: 14px !important; } #settings-panel { position: fixed !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%) !important; background: var(--md-surface) !important; border-radius: 8px !important; box-shadow: var(--md-shadow-2) !important; padding: 24px !important; z-index: 100001 !important; min-width: 320px !important; max-width: 400px !important; } #settings-panel h3 { color: var(--md-on-surface) !important; font-size: 20px !important; font-weight: 500 !important; margin: 0 0 24px 0 !important; } #settings-panel label { color: rgba(0, 0, 0, 0.87) !important; font-size: 14px !important; margin-bottom: 4px !important; display: block !important; } #settings-panel input { width: 100% !important; padding: 8px 12px !important; margin: 4px 0 16px 0 !important; border: 1px solid var(--md-outline) !important; border-radius: 4px !important; font-size: 16px !important; transition: border-color 0.2s ease !important; box-sizing: border-box; } #settings-panel input:focus { outline: none !important; border-color: var(--md-primary) !important; } #settings-panel button { background: var(--md-primary) !important; color: white !important; border: none !important; padding: 8px 16px !important; border-radius: 4px !important; font-size: 14px !important; font-weight: 500 !important; text-transform: uppercase !important; cursor: pointer !important; margin-left: 8px !important; transition: background-color 0.2s ease !important; } #settings-panel button:hover { background: var(--md-primary-dark) !important; } #settings-panel button:first-child { margin-left: 0 !important; } #settings-panel button#closeSettings { background: transparent !important; color: var(--md-primary) !important; } #settings-panel button#closeSettings:hover { background: rgba(25, 118, 210, 0.04) !important; } .checkbox-container { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; } .checkbox-container label { margin: 0; } `; const getQueryParameter = (param) => new URLSearchParams(window.location.search).get(param); const searchEngines = { 'google.com': { getQuery: () => getQueryParameter('q') }, 'bing.com': { getQuery: () => getQueryParameter('q') }, 'duckduckgo.com': { getQuery: () => getQueryParameter('q') }, 'baidu.com': { getQuery: () => getQueryParameter('wd') }, 'brave.com': { getQuery: () => getQueryParameter('q') }, 'yandex.com': { getQuery: () => getQueryParameter('text') }, 'presearch.com': { getQuery: () => getQueryParameter('q') } }; GM_addStyle(styles); const getCurrentSearchQuery = () => { const currentDomain = Object.keys(searchEngines).find(domain => window.location.hostname.includes(domain)); return currentDomain ? searchEngines[currentDomain].getQuery() : null; }; const getSettings = () => ({ username: GM_getValue('hypothesisUsername', ''), apiToken: GM_getValue('hypothesisApiToken', ''), excludeTags: GM_getValue('hypothesisExcludeTags', ''), mergeByUri: GM_getValue('mergeByUri', true) }); const saveSettings = (username, apiToken, excludeTags, mergeByUri) => { GM_setValue('hypothesisUsername', username); GM_setValue('hypothesisApiToken', apiToken); GM_setValue('hypothesisExcludeTags', excludeTags); GM_setValue('mergeByUri', mergeByUri); }; const addSettingsIcon = () => { const panel = document.getElementById('hypothesis-panel'); if (!panel) return; let gear = panel.querySelector('.gear-icon'); if (!gear) { gear = document.createElement('span'); gear.className = 'gear-icon'; gear.textContent = '⚙️'; gear.title = 'Settings'; gear.addEventListener('click', openSettingsPanel); panel.appendChild(gear); } }; const openSettingsPanel = () => { let settingsPanel = document.getElementById('settings-panel'); if (settingsPanel) return; const settings = getSettings(); settingsPanel = document.createElement('div'); settingsPanel.id = 'settings-panel'; settingsPanel.innerHTML = `
No annotations found for this query.
'; } }; const fetchAnnotations = (query) => { const settings = getSettings(); if (!settings.username || !settings.apiToken) { const contentDiv = document.getElementById('annotations-content'); if (contentDiv) { contentDiv.innerHTML = 'Please configure your Hypothes.is username and API token.
'; } openSettingsPanel(); return; } GM_xmlhttpRequest({ method: 'GET', url: `${API_URL}?user=acct:${settings.username}@hypothes.is&limit=200&any=${encodeURIComponent(query)}`, headers: { 'Authorization': `Bearer ${settings.apiToken}` }, onload: function (response) { const data = JSON.parse(response.responseText); const excludedTags = settings.excludeTags.split(',').map(tag => tag.trim()).filter(Boolean); const uniqueUri = new Map(); data.rows.forEach(annotation => { const hasExcludedTag = annotation.tags && excludedTags.some(excludeTag => annotation.tags.includes(excludeTag)); if (!hasExcludedTag) { if (settings.mergeByUri) { uniqueUri.set(annotation.uri, annotation); } else { totalAnnotations.push(annotation); } } }); totalAnnotations = settings.mergeByUri ? Array.from(uniqueUri.values()) : totalAnnotations; displayAnnotations(); }, onerror: function (err) { console.error('Failed to fetch annotations', err); const contentDiv = document.getElementById('annotations-content'); if (contentDiv) { contentDiv.innerHTML = 'Failed to fetch annotations. Please check your settings and try again.
'; } } }); }; const query = getCurrentSearchQuery(); if (query) { fetchAnnotations(query); } })();