GitHub Profile Icon

Adds a clickable profile icon to identify personal or organizational accounts.

目前為 2025-01-01 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         GitHub Profile Icon
// @description  Adds a clickable profile icon to identify personal or organizational accounts.
// @icon         https://github.githubassets.com/favicons/favicon-dark.svg
// @version      1.4
// @author       afkarxyz
// @namespace    https://github.com/afkarxyz/misc-scripts/
// @supportURL   https://github.com/afkarxyz/misc-scripts/issues
// @license      MIT
// @match        https://github.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function() {
    'use strict';
    
    const style = document.createElement('style');
    style.textContent = `
        .icon-wrapper {
            position: relative !important;
            display: inline-block !important;
            margin-left: 4px !important;
        }
        .profile-icon-tooltip {
            visibility: hidden;
            position: fixed !important;
            background: #212830 !important;
            color: white !important;
            padding: 4px 8px !important;
            border-radius: 6px !important;
            font-size: 12px !important;
            white-space: nowrap !important;
            z-index: 9999 !important;
            pointer-events: none !important;
            transform: translateX(-50%) !important;
        }
        .profile-icon-tooltip::after {
            content: '';
            position: absolute !important;
            top: 100% !important;
            left: 50% !important;
            transform: translateX(-50%) !important;
            border: 5px solid transparent !important;
            border-top-color: #212830 !important;
        }
        .icon-wrapper:hover .profile-icon-tooltip {
            visibility: visible !important;
        }
        .fork-icon {
            width: 10px !important;
            height: 10px !important;
            opacity: 1 !important;
        }
        .non-fork-icon {
            opacity: 0.575 !important;
        }
        .fork-wrapper {
            margin-left: 8px !important;
        }
        .search-title {
            display: flex !important;
            align-items: flex-start !important;
        }
        .search-title .icon-wrapper {
            margin-left: 8px !important;
            display: inline-flex !important;
            align-items: center !important;
            margin-top: 3px !important;
        }
    `;
    document.head.appendChild(style);

    const ICONS = {
        user: "M11.1,8.7c2.5,1.2,4.1,3.6,4.2,6.3c0,0.5-0.3,0.9-0.9,1c-0.5,0-0.9-0.3-1-0.9c0,0,0,0,0,0c-0.1-3.1-2.7-5.4-5.8-5.3c-2.9,0.1-5.1,2.4-5.3,5.3c0,0.5-0.5,0.9-1,0.9c-0.5,0-0.9-0.4-0.9-0.9c0.1-2.7,1.8-5.2,4.2-6.3C2.8,7,2.5,3.9,4.2,1.8s4.8-2.4,6.9-0.6s2.4,4.8,0.6,6.9C11.6,8.3,11.4,8.5,11.1,8.7z M11.1,4.9c0-1.7-1.4-3.1-3.1-3.1S4.9,3.2,4.9,4.9S6.3,8,8,8S11.1,6.6,11.1,4.9z",
        organization: "M1.75 16A1.75 1.75 0 0 1 0 14.25V1.75C0 .784.784 0 1.75 0h8.5C11.216 0 12 .784 12 1.75v12.5c0 .085-.006.168-.018.25h2.268a.25.25 0 0 0 .25-.25V8.285a.25.25 0 0 0-.111-.208l-1.055-.703a.749.749 0 1 1 .832-1.248l1.055.703c.487.325.779.871.779 1.456v5.965A1.75 1.75 0 0 1 14.25 16h-3.5a.766.766 0 0 1-.197-.026c-.099.017-.2.026-.303.026h-3a.75.75 0 0 1-.75-.75V14h-1v1.25a.75.75 0 0 1-.75.75Zm-.25-1.75c0 .138.112.25.25.25H4v-1.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 .75.75v1.25h2.25a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25ZM3.75 6h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5ZM3 3.75A.75.75 0 0 1 3.75 3h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 3 3.75Zm4 3A.75.75 0 0 1 7.75 6h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 7 6.75ZM7.75 3h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5ZM3 9.75A.75.75 0 0 1 3.75 9h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 3 9.75ZM7.75 9h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5Z"
    };

    function getCachedUserType(username) {
        const cache = GM_getValue('userTypeCache', {});
        const cachedData = cache[username];
        if (cachedData) {
            const now = Date.now();
            if (now - cachedData.timestamp < 24 * 60 * 60 * 1000) {
                return cachedData.type;
            }
            delete cache[username];
            GM_setValue('userTypeCache', cache);
        }
        return null;
    }

    function cacheUserType(username, type) {
        const cache = GM_getValue('userTypeCache', {});
        cache[username] = {
            type: type,
            timestamp: Date.now()
        };
        GM_setValue('userTypeCache', cache);
    }

    async function checkUserType(username) {
        const cachedType = getCachedUserType(username);
        if (cachedType) {
            return cachedType;
        }

        try {
            const response = await fetch(`https://api.github.com/users/${username}`);
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            const data = await response.json();
            const type = data.type?.toLowerCase() === 'organization' ? 'organization' : 'user';
            cacheUserType(username, type);
            return type;
        } catch (error) {
            cacheUserType(username, 'user');
            return 'user';
        }
    }

    async function createIcon(username, wrapper, isFork = false) {
        const type = await checkUserType(username);
        
        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
        svg.setAttribute("viewBox", "0 0 16 16");
        svg.style.cssText = `width:${isFork ? '10px' : '14px'};height:${isFork ? '10px' : '14px'};cursor:pointer;fill:currentColor;transition:transform .1s`;
        
        if (isFork) {
            svg.classList.add('fork-icon');
            wrapper.classList.add('fork-wrapper');
        } else {
            svg.classList.add('non-fork-icon');
        }

        const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
        path.setAttribute("d", ICONS[type]);
        
        const tooltip = document.createElement('div');
        tooltip.className = 'profile-icon-tooltip';
        tooltip.textContent = username;

        wrapper.addEventListener('mouseenter', (e) => {
            svg.style.transform = 'scale(1.1)';
            const rect = wrapper.getBoundingClientRect();
            tooltip.style.left = `${rect.left + (rect.width / 2)}px`;
            tooltip.style.top = `${rect.top - 35}px`;
        });
        
        wrapper.addEventListener('mouseleave', () => {
            svg.style.transform = 'scale(1)';
        });
        
        wrapper.addEventListener('mousemove', (e) => {
            const rect = wrapper.getBoundingClientRect();
            tooltip.style.left = `${rect.left + (rect.width / 2)}px`;
            tooltip.style.top = `${rect.top - 35}px`;
        });

        wrapper.addEventListener('click', () => window.open(`https://github.com/${username}`, '_blank'));

        svg.appendChild(path);
        wrapper.appendChild(svg);
        wrapper.appendChild(tooltip);
    }

    async function addGitHubIcons() {
        const tasks = [];
        
        const isSearchPage = window.location.pathname === '/search' || window.location.pathname.startsWith('/search/');
        
        if (isSearchPage) {
            document.querySelectorAll('.search-title').forEach(titleDiv => {
                if (titleDiv.querySelector('.icon-wrapper')) return;
                const link = titleDiv.querySelector('a');
                if (!link) return;
                const href = link.getAttribute('href');
                if (!href) return;
                const username = href.split('/').filter(Boolean)[0];
                const wrapper = document.createElement('div');
                wrapper.className = 'icon-wrapper';
                titleDiv.appendChild(wrapper);
                tasks.push(createIcon(username, wrapper, false));
            });
        } else {
            const repoNav = document.querySelector('#repository-container-header');
            
            document.querySelectorAll('h3:not(.search-title)').forEach(h3 => {
                if (h3.closest('#readme') || h3.closest('article')) return;
                
                if (repoNav && !h3.closest('#repository-container-header')) return;
                
                if (h3.querySelector('.icon-wrapper')) return;
                const link = h3.querySelector('a');
                if (!link) return;
                const href = link.getAttribute('href');
                if (!href || !href.startsWith('/')) return;
                const username = href.split('/').filter(Boolean)[0];
                const wrapper = document.createElement('div');
                wrapper.className = 'icon-wrapper';
                h3.appendChild(wrapper);
                tasks.push(createIcon(username, wrapper, false));
            });

            document.querySelectorAll('.f6.color-fg-muted.mb-1').forEach(forkInfo => {
                if (forkInfo.querySelector('.icon-wrapper')) return;
                const link = forkInfo.querySelector('a.Link--muted');
                if (!link || !link.href.includes('/')) return;
                const username = link.getAttribute('href').split('/').filter(Boolean)[0];
                const wrapper = document.createElement('div');
                wrapper.className = 'icon-wrapper';
                link.insertAdjacentElement('afterend', wrapper);
                tasks.push(createIcon(username, wrapper, true));
            });
        }

        await Promise.all(tasks);
    }

    addGitHubIcons();

    const observer = new MutationObserver(mutations => {
        if (mutations.some(m => m.addedNodes.length)) addGitHubIcons();
    });

    observer.observe(document.body, { childList: true, subtree: true });

    const pushState = history.pushState;
    const replaceState = history.replaceState;
    
    history.pushState = function() {
        pushState.apply(history, arguments);
        addGitHubIcons();
    };

    history.replaceState = function() {
        replaceState.apply(history, arguments);
        addGitHubIcons();
    };

    window.addEventListener('popstate', addGitHubIcons);
})();