GitHub Join Date

Displays user's join date/time/age.

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

  1. // ==UserScript==
  2. // @name GitHub Join Date
  3. // @description Displays user's join date/time/age.
  4. // @icon https://github.githubassets.com/favicons/favicon-dark.svg
  5. // @version 1.0
  6. // @author afkarxyz
  7. // @namespace https://github.com/afkarxyz/misc-scripts/
  8. // @supportURL https://github.com/afkarxyz/misc-scripts/issues
  9. // @license MIT
  10. // @match https://github.com/*
  11. // @grant none
  12. // @run-at document-idle
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. const ELEMENT_ID = 'userscript-join-date-display';
  19. const CACHE_KEY = 'githubUserJoinDatesCache_v1';
  20. let isProcessing = false;
  21. let observerDebounceTimeout = null;
  22.  
  23. 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>`;
  24.  
  25. function readCache() {
  26. try {
  27. const cachedData = localStorage.getItem(CACHE_KEY);
  28. return cachedData ? JSON.parse(cachedData) : {};
  29. } catch (e) {
  30. return {};
  31. }
  32. }
  33.  
  34. function writeCache(cacheData) {
  35. try {
  36. localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
  37. } catch (e) {
  38.  
  39. }
  40. }
  41.  
  42. function getRelativeTime(dateString) {
  43. 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`;
  44. }
  45.  
  46. async function getGitHubJoinDate(user) {
  47. 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; }
  48. }
  49.  
  50. function removeExistingElement() {
  51. const existingElement = document.getElementById(ELEMENT_ID);
  52. if (existingElement) {
  53. existingElement.remove();
  54. }
  55. }
  56.  
  57. async function addOrUpdateJoinDateElement() {
  58. if (document.getElementById(ELEMENT_ID) && !isProcessing) { return; }
  59. if (isProcessing) { return; }
  60.  
  61. const pathParts = window.location.pathname.split('/').filter(part => part);
  62. if (pathParts.length < 1 || pathParts.length > 2 || (pathParts.length === 2 && !['sponsors', 'followers', 'following'].includes(pathParts[1]))) {
  63. removeExistingElement(); return;
  64. }
  65. const profileSidebar = document.querySelector('.vcard');
  66. const mainContentArea = document.querySelector('div[itemtype="http://schema.org/Person"]');
  67. if (!profileSidebar && !mainContentArea) {
  68. removeExistingElement(); return;
  69. }
  70. const username = pathParts[0].toLowerCase();
  71.  
  72. isProcessing = true;
  73. let joinElement = document.getElementById(ELEMENT_ID);
  74. let createdAtISO = null;
  75. let fromCache = false;
  76.  
  77. try {
  78. const cache = readCache();
  79. if (cache[username]) {
  80. createdAtISO = cache[username];
  81. fromCache = true;
  82. }
  83.  
  84. if (!joinElement) {
  85. joinElement = document.createElement('div');
  86. joinElement.id = ELEMENT_ID;
  87. joinElement.innerHTML = fromCache ? `${svgIcon} ...` : `${svgIcon} Loading...`;
  88. joinElement.style.marginTop = '8px'; joinElement.style.marginBottom = '8px'; joinElement.style.color = 'var(--color-fg-muted)'; joinElement.style.fontSize = '14px';
  89.  
  90. const editableArea = mainContentArea?.querySelector('.js-profile-editable-area') || profileSidebar?.querySelector('.js-profile-editable-area');
  91. if (!editableArea) {
  92. const detailsList = profileSidebar?.querySelector('ul.vcard-details');
  93. 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); }
  94. else { isProcessing = false; return; }
  95. } else {
  96. 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); }
  97. }
  98. }
  99.  
  100. if (!fromCache) {
  101. createdAtISO = await getGitHubJoinDate(username);
  102. joinElement = document.getElementById(ELEMENT_ID);
  103. if (!joinElement) { return; }
  104. if (createdAtISO) {
  105. const currentCache = readCache();
  106. currentCache[username] = createdAtISO;
  107. writeCache(currentCache);
  108. } else {
  109. removeExistingElement(); return;
  110. }
  111. }
  112.  
  113. if (createdAtISO && joinElement) {
  114. const joinDate = new Date(createdAtISO);
  115. const dateOptions = { year: 'numeric', month: 'long', day: 'numeric' };
  116. const formattedDate = joinDate.toLocaleDateString('en-US', dateOptions);
  117. const hours = joinDate.getHours().toString().padStart(2, '0');
  118. const minutes = joinDate.getMinutes().toString().padStart(2, '0');
  119. const formattedTime = `${hours}:${minutes}`;
  120. const relativeTimeString = getRelativeTime(createdAtISO);
  121. joinElement.innerHTML = `${svgIcon} ${formattedDate} - ${formattedTime} (${relativeTimeString})`;
  122. } else if (!createdAtISO && joinElement) {
  123. removeExistingElement();
  124. }
  125.  
  126. } catch (error) {
  127. removeExistingElement();
  128. } finally {
  129. isProcessing = false;
  130. }
  131. }
  132.  
  133. function handlePotentialPageChange() {
  134. clearTimeout(observerDebounceTimeout);
  135. observerDebounceTimeout = setTimeout(() => {
  136. addOrUpdateJoinDateElement();
  137. }, 600);
  138. }
  139.  
  140. addOrUpdateJoinDateElement();
  141.  
  142. const observer = new MutationObserver((mutationsList) => {
  143. let potentiallyRelevantChange = false;
  144. for (const mutation of mutationsList) {
  145. if (mutation.type === 'childList' && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)) {
  146. const targetNode = mutation.target;
  147. if (targetNode && (targetNode.matches?.('main, main *, .Layout-sidebar, .Layout-sidebar *, body'))) {
  148. let onlySelfChange = false;
  149. if ((mutation.addedNodes.length === 1 && mutation.addedNodes[0].id === ELEMENT_ID && mutation.removedNodes.length === 0) ||
  150. (mutation.removedNodes.length === 1 && mutation.removedNodes[0].id === ELEMENT_ID && mutation.addedNodes.length === 0)) {
  151. onlySelfChange = true;
  152. }
  153. if (!onlySelfChange) { potentiallyRelevantChange = true; break; }
  154. }
  155. }
  156. }
  157. if(potentiallyRelevantChange) { handlePotentialPageChange(); }
  158. });
  159. observer.observe(document.body, { childList: true, subtree: true });
  160.  
  161. })();