Any Hackernews Link

Check if current page has been posted to Hacker News

当前为 2025-02-06 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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.greasyfork.org/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();
})();