// ==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 // @downloadURL none // ==/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 = `