GitHub Join Date

Displays user's join date/time/age.

当前为 2025-03-27 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         GitHub Join Date
// @description  Displays user's join date/time/age.
// @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        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const ELEMENT_ID = 'userscript-join-date-display';
    const CACHE_KEY = 'githubUserJoinDatesCache_v1';
    let isProcessing = false;
    let observerDebounceTimeout = null;

    const svgIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" fill="currentColor" style="width: 1em; height: 1em; vertical-align: middle; margin-right: 0.25em; position: relative; top: -0.08em;" aria-hidden="true"><path d="M128 0c13.3 0 24 10.7 24 24l0 40 144 0 0-40c0-13.3 10.7-24 24-24s24 10.7 24 24l0 40 40 0c35.3 0 64 28.7 64 64l0 16 0 48-16 0-32 0-112 0L48 192l0 256c0 8.8 7.2 16 16 16l220.5 0c12.3 18.8 28 35.1 46.3 48L64 512c-35.3 0-64-28.7-64-64L0 192l0-48 0-16C0 92.7 28.7 64 64 64l40 0 0-40c0-13.3 10.7-24 24-24zM288 368a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-80c-8.8 0-16 7.2-16 16l0 64c0 8.8 7.2 16 16 16l48 0c8.8 0 16-7.2 16-16s-7.2-16-16-16l-32 0 0-48c0-8.8-7.2-16-16-16z"/></svg>`;

    function readCache() {
        try {
            const cachedData = localStorage.getItem(CACHE_KEY);
            return cachedData ? JSON.parse(cachedData) : {};
        } catch (e) {
            return {};
        }
    }

    function writeCache(cacheData) {
        try {
            localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
        } catch (e) {

        }
    }

    function getRelativeTime(dateString) {
        const joinDate = new Date(dateString); const now = new Date(); const diffInSeconds = Math.round((now - joinDate) / 1000); const minute = 60, hour = 3600, day = 86400, month = 2592000, year = 31536000; if (diffInSeconds < minute) return `less than a minute ago`; if (diffInSeconds < hour) { const m = Math.floor(diffInSeconds / minute); return `${m} ${m === 1 ? 'minute' : 'minutes'} ago`; } if (diffInSeconds < day) { const h = Math.floor(diffInSeconds / hour); return `${h} ${h === 1 ? 'hour' : 'hours'} ago`; } if (diffInSeconds < month) { const d = Math.floor(diffInSeconds / day); return `${d} ${d === 1 ? 'day' : 'days'} ago`; } if (diffInSeconds < year) { const mo = Math.floor(diffInSeconds / month); return `${mo} ${mo === 1 ? 'month' : 'months'} ago`; } const y = Math.floor(diffInSeconds / year); return `${y} ${y === 1 ? 'year' : 'years'} ago`;
    }

    async function getGitHubJoinDate(user) {
         const apiUrl = `https://api.github.com/users/${user}`; try { const response = await fetch(apiUrl); if (!response.ok) { return null; } const userData = await response.json(); return userData.created_at; } catch (error) { return null; }
    }

    function removeExistingElement() {
        const existingElement = document.getElementById(ELEMENT_ID);
        if (existingElement) {
            existingElement.remove();
        }
    }

    async function addOrUpdateJoinDateElement() {
        if (document.getElementById(ELEMENT_ID) && !isProcessing) { return; }
        if (isProcessing) { return; }

        const pathParts = window.location.pathname.split('/').filter(part => part);
        if (pathParts.length < 1 || pathParts.length > 2 || (pathParts.length === 2 && !['sponsors', 'followers', 'following'].includes(pathParts[1]))) {
             removeExistingElement(); return;
        }
        const profileSidebar = document.querySelector('.vcard');
        const mainContentArea = document.querySelector('div[itemtype="http://schema.org/Person"]');
         if (!profileSidebar && !mainContentArea) {
             removeExistingElement(); return;
        }
        const username = pathParts[0].toLowerCase();

        isProcessing = true;
        let joinElement = document.getElementById(ELEMENT_ID);
        let createdAtISO = null;
        let fromCache = false;

        try {
            const cache = readCache();
            if (cache[username]) {
                createdAtISO = cache[username];
                fromCache = true;
            }

             if (!joinElement) {
                joinElement = document.createElement('div');
                joinElement.id = ELEMENT_ID;
                joinElement.innerHTML = fromCache ? `${svgIcon} ...` : `${svgIcon} Loading...`;
                joinElement.style.marginTop = '8px'; joinElement.style.marginBottom = '8px'; joinElement.style.color = 'var(--color-fg-muted)'; joinElement.style.fontSize = '14px';

                const editableArea = mainContentArea?.querySelector('.js-profile-editable-area') || profileSidebar?.querySelector('.js-profile-editable-area');
                if (!editableArea) {
                     const detailsList = profileSidebar?.querySelector('ul.vcard-details');
                     if (detailsList) { const listItem = document.createElement('li'); listItem.classList.add('vcard-detail', 'pt-1'); listItem.appendChild(joinElement); joinElement.style.marginTop = '0'; joinElement.style.marginBottom = '0'; detailsList.appendChild(listItem); }
                     else { isProcessing = false; return; }
                } else {
                    const originalBioElement = editableArea.querySelector('.user-profile-bio'); const editButtonContainer = editableArea.querySelector('div.mb-3:has(> button.js-profile-editable-edit-button)'); if (originalBioElement && originalBioElement.offsetParent !== null) { originalBioElement.insertAdjacentElement('afterend', joinElement); } else if (editButtonContainer) { editButtonContainer.insertAdjacentElement('beforebegin', joinElement); } else { editableArea.prepend(joinElement); }
                }
            }

            if (!fromCache) {
                createdAtISO = await getGitHubJoinDate(username);
                joinElement = document.getElementById(ELEMENT_ID);
                 if (!joinElement) { return; }
                if (createdAtISO) {
                    const currentCache = readCache();
                    currentCache[username] = createdAtISO;
                    writeCache(currentCache);
                } else {
                    removeExistingElement(); return;
                }
            }

             if (createdAtISO && joinElement) {
                const joinDate = new Date(createdAtISO);
                const dateOptions = { year: 'numeric', month: 'long', day: 'numeric' };
                const formattedDate = joinDate.toLocaleDateString('en-US', dateOptions);
                const hours = joinDate.getHours().toString().padStart(2, '0');
                const minutes = joinDate.getMinutes().toString().padStart(2, '0');
                const formattedTime = `${hours}:${minutes}`;
                const relativeTimeString = getRelativeTime(createdAtISO);
                joinElement.innerHTML = `${svgIcon} ${formattedDate} - ${formattedTime} (${relativeTimeString})`;
             } else if (!createdAtISO && joinElement) {
                 removeExistingElement();
             }

        } catch (error) {
             removeExistingElement();
        } finally {
            isProcessing = false;
        }
    }

    function handlePotentialPageChange() {
        clearTimeout(observerDebounceTimeout);
        observerDebounceTimeout = setTimeout(() => {
             addOrUpdateJoinDateElement();
        }, 600);
    }

    addOrUpdateJoinDateElement();

    const observer = new MutationObserver((mutationsList) => {
        let potentiallyRelevantChange = false;
        for (const mutation of mutationsList) {
             if (mutation.type === 'childList' && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)) {
                 const targetNode = mutation.target;
                 if (targetNode && (targetNode.matches?.('main, main *, .Layout-sidebar, .Layout-sidebar *, body'))) {
                      let onlySelfChange = false;
                      if ((mutation.addedNodes.length === 1 && mutation.addedNodes[0].id === ELEMENT_ID && mutation.removedNodes.length === 0) ||
                          (mutation.removedNodes.length === 1 && mutation.removedNodes[0].id === ELEMENT_ID && mutation.addedNodes.length === 0)) {
                         onlySelfChange = true;
                      }
                      if (!onlySelfChange) { potentiallyRelevantChange = true; break; }
                 }
             }
        }
        if(potentiallyRelevantChange) { handlePotentialPageChange(); }
    });
    observer.observe(document.body, { childList: true, subtree: true });

})();