GitHub Profile Icon

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

От 31.12.2024. Виж последната версия.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като 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.0
// @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: #24292e !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: #24292e !important;
        }
        .icon-wrapper:hover .profile-icon-tooltip {
            visibility: visible !important;
        }
        .fork-icon {
            width: 12px !important;
            height: 12px !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: "M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512l388.6 0c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304l-91.4 0z",
        organization: "M48 0C21.5 0 0 21.5 0 48L0 464c0 26.5 21.5 48 48 48l96 0 0-80c0-26.5 21.5-48 48-48s48 21.5 48 48l0 80 96 0c26.5 0 48-21.5 48-48l0-416c0-26.5-21.5-48-48-48L48 0zM64 240c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32zm112-16l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32c0-8.8 7.2-16 16-16zm80 16c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32zM80 96l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32c0-8.8 7.2-16 16-16zm80 16c0-8.8 7.2-16 16-16l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32zM272 96l32 0c8.8 0 16 7.2 16 16l0 32c0 8.8-7.2 16-16 16l-32 0c-8.8 0-16-7.2-16-16l0-32c0-8.8 7.2-16 16-16z"
    };

    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 448 512");
        svg.style.cssText = `width:${isFork ? '12px' : '16px'};height:${isFork ? '12px' : '16px'};cursor:pointer;fill:currentColor;transition:transform .1s`;
        
        if (isFork) {
            svg.classList.add('fork-icon');
            wrapper.classList.add('fork-wrapper');
        }

        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 {
            document.querySelectorAll('h3:not(.search-title)').forEach(h3 => {
                if (h3.querySelector('.icon-wrapper')) return;
                const link = h3.querySelector('a');
                if (!link) return;
                const href = link.getAttribute('href');
                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);
})();