您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Check if current page has been posted to Hacker News
当前为
// ==UserScript== // @name Any Hackernews Link // @namespace http://tampermonkey.net/ // @version 0.1 // @description Check if current page has been posted to Hacker News // @author RoCry // @match *://*/* // @grant GM_xmlhttpRequest // @connect hn.algolia.com // @grant GM_addStyle // @license MIT // ==/UserScript== (function() { 'use strict'; /** * Configuration */ const CONFIG = { // HN API endpoint API_URL: 'https://hn.algolia.com/api/v1/search', // List of domains to ignore IGNORED_DOMAINS: [ 'news.ycombinator.com', 'hn.algolia.com', 'mail.google.com', 'gmail.com', 'outlook.com', 'yahoo.com', 'proton.me', 'localhost', 'accounts.google.com', 'drive.google.com', 'docs.google.com', 'calendar.google.com', 'meet.google.com', 'chat.google.com', 'web.whatsapp.com', 'twitter.com/messages', 'facebook.com/messages', 'linkedin.com/messaging' ], // Patterns that indicate a search page SEARCH_PATTERNS: [ '/search', '/webhp', '/results', '?q=', '?query=', '?search=', '?s=' ], // URL parameters to remove during normalization TRACKING_PARAMS: [ 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', '_ga', 'ref', 'source' ] }; /** * Styles */ const STYLES = ` @keyframes fadeIn { 0% { opacity: 0; transform: translateY(10px); } 100% { opacity: 1; transform: translateY(0); } } @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.6; } 100% { opacity: 1; } } #hn-float { position: fixed; bottom: 20px; left: 20px; z-index: 9999; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; display: flex; align-items: center; gap: 12px; background: rgba(255, 255, 255, 0.98); padding: 8px 12px; border-radius: 12px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.05); cursor: pointer; transition: all 0.2s ease; max-width: 50px; overflow: hidden; opacity: 0.95; height: 40px; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); animation: fadeIn 0.3s ease forwards; will-change: transform, max-width, box-shadow; color: #111827; } #hn-float:hover { max-width: 600px; opacity: 1; transform: translateY(-2px); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.05); } #hn-float .hn-icon { min-width: 24px; width: 24px; height: 24px; background: linear-gradient(135deg, #ff6600, #ff7f33); color: white; display: flex; align-items: center; justify-content: center; font-weight: bold; border-radius: 6px; flex-shrink: 0; position: relative; font-size: 13px; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); transition: transform 0.2s ease; } #hn-float:hover .hn-icon { transform: scale(1.05); } #hn-float .hn-icon.not-found { background: #9ca3af; } #hn-float .hn-icon.found { background: linear-gradient(135deg, #ff6600, #ff7f33); } #hn-float .hn-icon.loading { background: #6b7280; animation: pulse 1.5s infinite; } #hn-float .hn-icon .badge { position: absolute; top: -6px; right: -6px; background: linear-gradient(135deg, #3b82f6, #2563eb); color: white; border-radius: 10px; min-width: 18px; height: 18px; font-size: 11px; display: flex; align-items: center; justify-content: center; padding: 0 4px; font-weight: 600; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); border: 2px solid white; } #hn-float .hn-info { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.4; font-size: 13px; opacity: 0; transition: opacity 0.2s ease; } #hn-float:hover .hn-info { opacity: 1; } #hn-float .hn-info a { color: inherit; font-weight: 500; text-decoration: none; } #hn-float .hn-info a:hover { text-decoration: underline; } #hn-float .hn-stats { color: #6b7280; font-size: 12px; margin-top: 2px; } @media (prefers-color-scheme: dark) { #hn-float { background: rgba(17, 24, 39, 0.95); color: #e5e7eb; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.1); } #hn-float:hover { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1); } #hn-float .hn-stats { color: #9ca3af; } #hn-float .hn-icon .badge { border-color: rgba(17, 24, 39, 0.95); } } `; /** * URL Utilities */ const URLUtils = { /** * Check if a URL should be ignored based on domain or search patterns * @param {string} url - URL to check * @returns {boolean} - True if URL should be ignored */ shouldIgnoreUrl(url) { try { const urlObj = new URL(url); // Check ignored domains if (CONFIG.IGNORED_DOMAINS.some(domain => urlObj.hostname.includes(domain))) { return true; } // Check if it's a search page if (CONFIG.SEARCH_PATTERNS.some(pattern => urlObj.pathname.includes(pattern) || urlObj.search.includes(pattern))) { return true; } return false; } catch (e) { console.error('Error checking URL:', e); return false; } }, /** * Normalize URL by removing tracking parameters and standardizing format * @param {string} url - URL to normalize * @returns {string} - Normalized URL */ normalizeUrl(url) { try { const urlObj = new URL(url); // Remove tracking parameters CONFIG.TRACKING_PARAMS.forEach(param => urlObj.searchParams.delete(param)); // Remove hash urlObj.hash = ''; // Remove trailing slash for consistency let normalizedUrl = urlObj.toString(); if (normalizedUrl.endsWith('/')) { normalizedUrl = normalizedUrl.slice(0, -1); } return normalizedUrl; } catch (e) { console.error('Error normalizing URL:', e); return url; } }, /** * Compare two URLs for equality after normalization * @param {string} url1 - First URL * @param {string} url2 - Second URL * @returns {boolean} - True if URLs match */ urlsMatch(url1, url2) { try { const u1 = new URL(this.normalizeUrl(url1)); const u2 = new URL(this.normalizeUrl(url2)); return u1.hostname.toLowerCase() === u2.hostname.toLowerCase() && u1.pathname.toLowerCase() === u2.pathname.toLowerCase() && u1.search === u2.search; } catch (e) { console.error('Error comparing URLs:', e); return false; } } }; /** * UI Component */ const UI = { /** * Create and append the floating element to the page * @returns {HTMLElement} - The created element */ createFloatingElement() { const div = document.createElement('div'); div.id = 'hn-float'; div.innerHTML = ` <div class="hn-icon loading">Y</div> <div class="hn-info">Checking HN...</div> `; document.body.appendChild(div); return div; }, /** * Update the floating element with HN data * @param {Object|null} data - HN post data or null if not found */ updateFloatingElement(data) { const iconDiv = document.querySelector('#hn-float .hn-icon'); const infoDiv = document.querySelector('#hn-float .hn-info'); iconDiv.classList.remove('loading'); if (!data) { iconDiv.classList.add('not-found'); iconDiv.classList.remove('found'); iconDiv.innerHTML = 'Y'; infoDiv.textContent = 'Not found on HN'; return; } iconDiv.classList.remove('not-found'); iconDiv.classList.add('found'); iconDiv.innerHTML = `Y${data.comments > 0 ? `<span class="badge">${data.comments > 999 ? '999+' : data.comments}</span>` : ''}`; infoDiv.innerHTML = ` <div><a href="${data.link}" target="_blank">${data.title}</a></div> <div class="hn-stats"> ${data.points} points | ${data.comments} comments | ${data.posted} </div> `; } }; /** * HackerNews API Handler */ const HNApi = { /** * Search for a URL on HackerNews * @param {string} normalizedUrl - URL to search for */ checkHackerNews(normalizedUrl) { const apiUrl = `${CONFIG.API_URL}?query=${encodeURIComponent(normalizedUrl)}&restrictSearchableAttributes=url`; GM_xmlhttpRequest({ method: 'GET', url: apiUrl, onload: (response) => this.handleApiResponse(response, normalizedUrl), onerror: (error) => { console.error('Error fetching from HN API:', error); UI.updateFloatingElement(null); } }); }, /** * Handle the API response * @param {Object} response - API response * @param {string} normalizedUrl - Original normalized URL */ handleApiResponse(response, normalizedUrl) { try { const data = JSON.parse(response.responseText); const matchingHits = data.hits.filter(hit => URLUtils.urlsMatch(hit.url, normalizedUrl)); if (matchingHits.length === 0) { console.log('🔍 URL not found on Hacker News'); UI.updateFloatingElement(null); return; } const topHit = matchingHits.sort((a, b) => (b.points || 0) - (a.points || 0))[0]; const result = { title: topHit.title, points: topHit.points || 0, comments: topHit.num_comments || 0, link: `https://news.ycombinator.com/item?id=${topHit.objectID}`, posted: new Date(topHit.created_at).toLocaleDateString() }; console.log('📰 Found on Hacker News:', result); UI.updateFloatingElement(result); } catch (e) { console.error('Error parsing HN API response:', e); UI.updateFloatingElement(null); } } }; /** * Initialize the script */ function init() { const currentUrl = window.location.href; if (URLUtils.shouldIgnoreUrl(currentUrl)) { return; } GM_addStyle(STYLES); const normalizedUrl = URLUtils.normalizeUrl(currentUrl); console.log('🔗 Normalized URL:', normalizedUrl); UI.createFloatingElement(); HNApi.checkHackerNews(normalizedUrl); } // Start the script init(); })();