您需要先安装一个扩展,例如 篡改猴、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.4 // @description Check if current page has been posted to Hacker News // @author RoCry // @icon https://news.ycombinator.com/favicon.ico // @match https://*/* // @exclude https://news.ycombinator.com/* // @exclude https://hn.algolia.com/* // @exclude https://*.google.com/* // @exclude https://mail.yahoo.com/* // @exclude https://outlook.com/* // @exclude https://proton.me/* // @exclude https://localhost/* // @exclude https://127.0.0.1/* // @exclude https://192.168.*.*/* // @exclude https://10.*.*.*/* // @exclude https://172.16.*.*/* // @exclude https://web.whatsapp.com/* // @exclude https://*.facebook.com/messages/* // @exclude https://*.twitter.com/messages/* // @exclude https://*.linkedin.com/messaging/* // @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', // Additional domains to ignore that couldn't be handled by @exclude IGNORED_DOMAINS: [ 'gmail.com', 'accounts.google.com', 'accounts.youtube.com', 'signin.', 'login.', 'auth.', 'oauth.', ], // 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' ], // Minimum ratio of ASCII characters to consider content as English MIN_ASCII_RATIO: 0.9, // Number of characters to check for language detection CHARS_TO_CHECK: 300 }; /** * 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; display: flex; align-items: center; height: 40px; box-sizing: border-box; } #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; line-height: 1; padding-bottom: 1px; } #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: -4px; right: -4px; background: linear-gradient(135deg, #3b82f6, #2563eb); color: white; border-radius: 8px; min-width: 14px; height: 14px; font-size: 10px; display: flex; align-items: center; justify-content: center; padding: 0 3px; font-weight: 600; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); border: 1.5px 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; width: 0; flex: 0; } #hn-float:hover .hn-info { opacity: 1; width: auto; flex: 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 remaining 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 tab parameter for all hosts // https://github.com/HackerNews/API?tab=readme-ov-file -> https://github.com/HackerNews/API urlObj.searchParams.delete('tab'); // 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; } } }; /** * Content Utilities */ const ContentUtils = { /** * Check if text is primarily English by checking ASCII ratio * @param {string} text - Text to analyze * @returns {boolean} - True if content is likely English */ isEnglishContent() { try { // Get text from title and first paragraph or relevant content const title = document.title || ''; const firstParagraphs = Array.from(document.getElementsByTagName('p')) .slice(0, 3) .map(p => p.textContent) .join(' '); const textToAnalyze = (title + ' ' + firstParagraphs) .slice(0, CONFIG.CHARS_TO_CHECK) .replace(/\s+/g, ' ') .trim(); if (!textToAnalyze) return true; // If no text found, assume English // Count ASCII characters (excluding spaces and common punctuation) const asciiChars = textToAnalyze.replace(/[\s\.,\-_'"!?()]/g, '') .split('') .filter(char => char.charCodeAt(0) <= 127).length; const totalChars = textToAnalyze.replace(/[\s\.,\-_'"!?()]/g, '').length; if (totalChars === 0) return true; const asciiRatio = asciiChars / totalChars; console.log('🈂️ ASCII Ratio:', asciiRatio.toFixed(2)); return asciiRatio >= CONFIG.MIN_ASCII_RATIO; } catch (e) { console.error('Error checking content language:', e); return true; // Default to allowing English in case of error } } }; /** * 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'; // Create icon element const iconDiv = document.createElement('div'); iconDiv.className = 'hn-icon loading'; iconDiv.textContent = 'Y'; // Create info element const infoDiv = document.createElement('div'); infoDiv.className = 'hn-info'; infoDiv.textContent = 'Checking HN...'; // Append children div.appendChild(iconDiv); div.appendChild(infoDiv); 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.textContent = 'Y'; infoDiv.textContent = 'Not found on HN'; return; } iconDiv.classList.remove('not-found'); iconDiv.classList.add('found'); // Clear existing content iconDiv.textContent = 'Y'; // Add badge if there are comments if (data.comments > 0) { const badge = document.createElement('span'); badge.className = 'badge'; badge.textContent = data.comments > 999 ? '999+' : data.comments.toString(); iconDiv.appendChild(badge); } // Clear and rebuild info content infoDiv.textContent = ''; const titleDiv = document.createElement('div'); const titleLink = document.createElement('a'); titleLink.href = data.link; titleLink.target = '_blank'; titleLink.textContent = data.title; titleDiv.appendChild(titleLink); const statsDiv = document.createElement('div'); statsDiv.className = 'hn-stats'; statsDiv.textContent = `${data.points} points | ${data.comments} comments | ${data.posted}`; infoDiv.appendChild(titleDiv); infoDiv.appendChild(statsDiv); } }; /** * 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() { // Skip if we're in an iframe if (window.top !== window.self) { console.log('📌 Skipping execution in iframe'); return; } // Skip if document is hidden (like background tabs or invisible frames) if (document.hidden) { console.log('📌 Skipping execution in hidden document'); // Add listener for when the tab becomes visible document.addEventListener('visibilitychange', function onVisible() { if (!document.hidden) { init(); document.removeEventListener('visibilitychange', onVisible); } }); return; } const currentUrl = window.location.href; // Check if the floating element already exists if (document.getElementById('hn-float')) { console.log('📌 HN float already exists, skipping'); return; } if (URLUtils.shouldIgnoreUrl(currentUrl)) { console.log('🚫 Ignored URL:', currentUrl); return; } // Check if content is primarily English if (!ContentUtils.isEnglishContent()) { console.log('🈂️ Non-English content detected, skipping'); return; } GM_addStyle(STYLES); const normalizedUrl = URLUtils.normalizeUrl(currentUrl); console.log('🔗 Normalized URL:', normalizedUrl); UI.createFloatingElement(); HNApi.checkHackerNews(normalizedUrl); } // Start the script init(); })();