GitHub Profile Icon

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

Verze ze dne 31. 12. 2024. Zobrazit nejnovější verzi.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

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