您需要先安装一个扩展,例如 篡改猴、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.8 // @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 // @grant GM_getValue // @grant GM_setValue // @require https://update.greatest.deepsurf.us/scripts/524693/1525919/Any%20Hackernews%20Link%20Utils.js // @license MIT // ==/UserScript== (function() { 'use strict'; // Fallback implementation for Safari if (typeof GM_addStyle === 'undefined') { window.GM_addStyle = function(css) { const style = document.createElement('style'); style.textContent = css; document.head.appendChild(style); return style; }; } // Fallback implementations for GM storage functions if (typeof GM_getValue === 'undefined') { window.GM_getValue = function(key, defaultValue) { const value = localStorage.getItem('GM_' + key); return value === null ? defaultValue : JSON.parse(value); }; } if (typeof GM_setValue === 'undefined') { window.GM_setValue = function(key, value) { localStorage.setItem('GM_' + key, JSON.stringify(value)); }; } /** * Constants */ const POSITIONS = { BOTTOM_LEFT: { bottom: '20px', left: '20px', top: 'auto', right: 'auto' }, BOTTOM_RIGHT: { bottom: '20px', right: '20px', top: 'auto', left: 'auto' }, TOP_LEFT: { top: '20px', left: '20px', bottom: 'auto', right: 'auto' }, TOP_RIGHT: { top: '20px', right: '20px', bottom: 'auto', left: 'auto' } }; /** * 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: move; user-select: none; 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); } } `; /** * 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); // Apply saved position const savedPosition = GM_getValue('hnPosition', 'BOTTOM_LEFT'); this.applyPosition(div, POSITIONS[savedPosition]); // Add drag functionality this.addDragHandlers(div); return div; }, /** * Update the floating element with HN data * @param {Object|null} data - HN post data or null if not found */ applyPosition(element, position) { Object.assign(element.style, position); }, getClosestPosition(x, y) { const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const isTop = y < viewportHeight / 2; const isLeft = x < viewportWidth / 2; if (isTop) { return isLeft ? 'TOP_LEFT' : 'TOP_RIGHT'; } else { return isLeft ? 'BOTTOM_LEFT' : 'BOTTOM_RIGHT'; } }, addDragHandlers(element) { let isDragging = false; let currentX; let currentY; let initialX; let initialY; element.addEventListener('mousedown', e => { if (e.target.tagName === 'A') return; // Don't drag when clicking links isDragging = true; element.style.transition = 'none'; initialX = e.clientX - element.offsetLeft; initialY = e.clientY - element.offsetTop; }); document.addEventListener('mousemove', e => { if (!isDragging) return; e.preventDefault(); currentX = e.clientX - initialX; currentY = e.clientY - initialY; // Keep the element within viewport bounds currentX = Math.max(0, Math.min(currentX, window.innerWidth - element.offsetWidth)); currentY = Math.max(0, Math.min(currentY, window.innerHeight - element.offsetHeight)); element.style.left = `${currentX}px`; element.style.top = `${currentY}px`; element.style.bottom = 'auto'; element.style.right = 'auto'; }); document.addEventListener('mouseup', e => { if (!isDragging) return; isDragging = false; element.style.transition = 'all 0.2s ease'; const position = this.getClosestPosition(currentX + element.offsetWidth / 2, currentY + element.offsetHeight / 2); this.applyPosition(element, POSITIONS[position]); // Save position GM_setValue('hnPosition', position); }); }, 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'; // Make icon clickable iconDiv.style.cursor = 'pointer'; iconDiv.onclick = (e) => { e.stopPropagation(); window.open(data.link, '_blank'); }; // 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); } }; /** * 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, UI.updateFloatingElement); } // Start the script init(); })();