您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Displays user's join date/time/age.
当前为
// ==UserScript== // @name GitHub Join Date // @description Displays user's join date/time/age. // @icon https://github.githubassets.com/favicons/favicon-dark.svg // @version 1.2 // @author afkarxyz // @namespace https://github.com/afkarxyz/userscripts/ // @supportURL https://github.com/afkarxyz/userscripts/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; 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`; } function getAbbreviatedMonth(date) { const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; return months[date.getMonth()]; } 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 usernameElement = document.querySelector('.p-nickname.vcard-username') || document.querySelector('h1.h2.lh-condensed'); if (!usernameElement) { 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 ? "..." : "Loading..."; joinElement.style.color = 'var(--color-fg-muted)'; joinElement.style.fontSize = '14px'; joinElement.style.fontWeight = 'normal'; if (usernameElement.classList.contains('h2')) { joinElement.style.marginTop = '0px'; const colorFgMuted = usernameElement.nextElementSibling?.classList.contains('color-fg-muted') ? usernameElement.nextElementSibling : null; if (colorFgMuted) { const innerDiv = colorFgMuted.querySelector('div') || colorFgMuted; innerDiv.appendChild(joinElement); } else { usernameElement.insertAdjacentElement('afterend', joinElement); } } else { joinElement.style.marginTop = '8px'; usernameElement.insertAdjacentElement('afterend', 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 day = joinDate.getDate(); const month = getAbbreviatedMonth(joinDate); const year = joinDate.getFullYear(); 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 = `<strong>Joined</strong> <span style="font-weight: normal;">${day} ${month} ${year} - ${formattedTime} (${relativeTimeString})</span>`; } 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 }); })();