GitHub Repo Status

GitHub Repository Status (Creation date and popular forks)

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         GitHub Repo Status
// @namespace    https://blog.xlab.app/
// @supportURL   https://github.com/ttttmr/UserJS
// @version      0.1
// @description  GitHub Repository Status (Creation date and popular forks)
// @author       tmr
// @match        https://github.com/*/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=github.com
// @grant        GM_xmlhttpRequest
// @connect      api.github.com
// ==/UserScript==

(function() {
    'use strict';

    function getRepoInfo() {
        const pathParts = window.location.pathname.split('/').filter(Boolean);
        if (pathParts.length < 2) return null;
        return {
            owner: pathParts[0],
            repo: pathParts[1]
        };
    }

    async function fetchHighStarForks(owner, repo) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: `https://api.github.com/repos/${owner}/${repo}/forks?sort=stargazers&per_page=3`,
                onload: function(response) {
                    if (response.status === 200) {
                        try {
                            resolve(JSON.parse(response.responseText));
                        } catch (e) {
                            resolve([]);
                        }
                    } else {
                        resolve([]);
                    }
                },
                onerror: () => resolve([])
            });
        });
    }

    function createIcon(svgPath, size = 16) {
        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        svg.setAttribute("aria-hidden", "true");
        svg.setAttribute("height", size);
        svg.setAttribute("viewBox", "0 0 16 16");
        svg.setAttribute("version", "1.1");
        svg.setAttribute("width", size);
        svg.classList.add("octicon", "color-fg-muted", "mr-2");
        
        const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
        path.setAttribute("d", svgPath);
        svg.appendChild(path);
        return svg;
    }

    async function injectStatus() {
        if (document.getElementById('github-repo-status-extra')) return;

        const repoInfo = getRepoInfo();
        if (!repoInfo) return;

        try {
            const scriptTag = document.querySelector('script[data-target="react-app.embeddedData"]');
            if (!scriptTag) return;

            const data = JSON.parse(scriptTag.textContent);
            
            const findKey = (obj, key) => {
                if (!obj || typeof obj !== 'object') return null;
                if (obj[key] !== undefined) return obj[key];
                for (const k in obj) {
                    if (Object.prototype.hasOwnProperty.call(obj, k)) {
                        const result = findKey(obj[k], key);
                        if (result) return result;
                    }
                }
                return null;
            };

            const createdAt = findKey(data, 'createdAt');
            if (!createdAt) return;

            const date = new Date(createdAt);
            const formattedDate = date.toLocaleDateString(undefined, {
                year: 'numeric',
                month: 'long',
                day: 'numeric'
            });

            const aboutH2 = Array.from(document.querySelectorAll('.BorderGrid-row h2'))
                .find(h2 => h2.textContent.trim() === 'About');
            if (!aboutH2) return;

            const aboutRow = aboutH2.closest('.BorderGrid-row');
            if (!aboutRow) return;

            // Root status row
            const statusRow = document.createElement('div');
            statusRow.className = 'BorderGrid-row';
            statusRow.id = 'github-repo-status-extra';

            const borderGridCell = document.createElement('div');
            borderGridCell.className = 'BorderGrid-cell';
            statusRow.appendChild(borderGridCell);

            const title = document.createElement('h2');
            title.className = 'h4 mb-3';
            title.textContent = 'Status';
            borderGridCell.appendChild(title);

            const list = document.createElement('ul');
            list.className = 'list-style-none';
            borderGridCell.appendChild(list);

            // Created Date Item
            const dateLi = document.createElement('li');
            dateLi.className = 'mt-3 d-flex flex-items-center';
            const calendarIcon = createIcon("M4.75 0a.75.75 0 0 1 .75.75V2h5V.75a.75.75 0 0 1 1.5 0V2h1.25c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 13.25 16H2.75A1.75 1.75 0 0 1 1 14.25V3.75C1 2.784 1.784 2 2.75 2H4V.75A.75.75 0 0 1 4.75 0ZM2.5 7.5v6.75c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V7.5Zm10.75-4H2.75a.25.25 0 0 0-.25.25V6h11V3.75a.25.25 0 0 0-.25-.25Z");
            const dateSpan = document.createElement('span');
            dateSpan.className = 'color-fg-muted';
            dateSpan.textContent = `Created on ${formattedDate}`;
            dateLi.appendChild(calendarIcon);
            dateLi.appendChild(dateSpan);
            list.appendChild(dateLi);

            // High star forks container
            const forksContainer = document.createElement('li');
            forksContainer.id = 'github-high-star-forks';
            list.appendChild(forksContainer);

            aboutRow.parentNode.insertBefore(statusRow, aboutRow.nextSibling);

            // Fetch and inject high star forks
            const forks = await fetchHighStarForks(repoInfo.owner, repoInfo.repo);
            if (forks && forks.length > 0) {
                forks.forEach(fork => {
                    if (fork.stargazers_count > 0) {
                        const forkDiv = document.createElement('div');
                        forkDiv.className = 'mt-3 d-flex flex-items-center';

                        const forkIcon = createIcon("M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 10a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z");
                        
                        const link = document.createElement('a');
                        link.href = fork.html_url;
                        link.className = 'Link--muted no-underline d-flex flex-items-center';

                        const ownerSpan = document.createElement('span');
                        ownerSpan.className = 'color-fg-default mr-1';
                        ownerSpan.textContent = fork.owner.login;

                        const nameSpan = document.createElement('span');
                        nameSpan.className = 'color-fg-muted';
                        nameSpan.textContent = `/ ${fork.name}`;

                        const starSpan = document.createElement('span');
                        starSpan.className = 'd-flex flex-items-center ml-2 color-fg-muted';
                        const starIcon = createIcon("M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.145L6.711 5.046a.75.75 0 0 1-.564.41l-3.099.45 2.242 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.242-2.184-3.1-.45a.75.75 0 0 1-.563-.41L8 2.395Z", 14);
                        starIcon.classList.remove('mr-2');
                        starIcon.classList.add('mr-1');

                        const starCount = document.createTextNode(fork.stargazers_count.toString());
                        
                        starSpan.appendChild(starIcon);
                        starSpan.appendChild(starCount);

                        link.appendChild(ownerSpan);
                        link.appendChild(nameSpan);
                        link.appendChild(starSpan);

                        forkDiv.appendChild(forkIcon);
                        forkDiv.appendChild(link);
                        forksContainer.appendChild(forkDiv);
                    }
                });
            }

        } catch (e) {
            console.error('[GitHub Status Extra] Error:', e);
        }
    }

    injectStatus();
    // GitHub uses Turbo for navigation
    document.addEventListener('turbo:render', injectStatus);
    document.addEventListener('turbo:load', injectStatus);

    // Some cases might need a small delay or observation if Turbo events fire too early
    const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
            if (mutation.addedNodes.length > 0) {
                if (document.querySelector('.BorderGrid-row') && !document.getElementById('github-repo-status-extra')) {
                    injectStatus();
                    break;
                }
            }
        }
    });

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