GitHub Repo Age

Displays repository creation date/time/age.

当前为 2025-05-11 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Repo Age
  3. // @description Displays repository creation date/time/age.
  4. // @icon https://github.githubassets.com/favicons/favicon-dark.svg
  5. // @version 1.1
  6. // @author afkarxyz
  7. // @namespace https://github.com/afkarxyz/userscripts/
  8. // @supportURL https://github.com/afkarxyz/userscripts/issues
  9. // @license MIT
  10. // @match https://github.com/*/*
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16. const apiBase = 'https://api.github.com/repos/';
  17. const CACHE_KEY_PREFIX = 'github_repo_created_';
  18. const CACHE_EXPIRY = 7 * 24 * 60 * 60 * 1000;
  19. const selectors = {
  20. desktop: [
  21. '.BorderGrid-cell .hide-sm.hide-md .f4.my-3',
  22. '.BorderGrid-cell'
  23. ],
  24. mobile: [
  25. '.d-block.d-md-none.mb-2.px-3.px-md-4.px-lg-5 .f4.mb-3.color-fg-muted',
  26. '.d-block.d-md-none.mb-2.px-3.px-md-4.px-lg-5 .d-flex.gap-2.mt-n3.mb-3.flex-wrap',
  27. '.d-block.d-md-none.mb-2.px-3.px-md-4.px-lg-5'
  28. ]
  29. };
  30. let currentRepoPath = '';
  31. function formatDate(isoDateStr) {
  32. const createdDate = new Date(isoDateStr);
  33. const now = new Date();
  34. const diffTime = Math.abs(now - createdDate);
  35. const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
  36. const diffHours = Math.floor((diffTime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
  37. const diffMinutes = Math.floor((diffTime % (1000 * 60 * 60)) / (1000 * 60));
  38. const diffMonths = Math.floor(diffDays / 30.44);
  39. const diffYears = Math.floor(diffMonths / 12);
  40. const remainingMonths = diffMonths % 12;
  41. const remainingDays = Math.floor(diffDays % 30.44);
  42. const datePart = createdDate.toLocaleDateString('en-GB', {
  43. day: '2-digit',
  44. month: 'short',
  45. year: 'numeric'
  46. });
  47. const timePart = createdDate.toLocaleTimeString('en-GB', {
  48. hour: '2-digit',
  49. minute: '2-digit',
  50. hour12: false
  51. });
  52. let ageText = '';
  53. if (diffYears > 0) {
  54. ageText = `${diffYears} year${diffYears !== 1 ? 's' : ''}`;
  55. if (remainingMonths > 0) {
  56. ageText += ` ${remainingMonths} month${remainingMonths !== 1 ? 's' : ''}`;
  57. }
  58. } else if (diffMonths > 0) {
  59. ageText = `${diffMonths} month${diffMonths !== 1 ? 's' : ''}`;
  60. if (remainingDays > 0) {
  61. ageText += ` ${remainingDays} day${remainingDays !== 1 ? 's' : ''}`;
  62. }
  63. } else if (diffDays > 0) {
  64. ageText = `${diffDays} day${diffDays !== 1 ? 's' : ''}`;
  65. if (diffHours > 0) {
  66. ageText += ` ${diffHours} hour${diffHours !== 1 ? 's' : ''}`;
  67. }
  68. } else if (diffHours > 0) {
  69. ageText = `${diffHours} hour${diffHours !== 1 ? 's' : ''}`;
  70. if (diffMinutes > 0) {
  71. ageText += ` ${diffMinutes} minute${diffMinutes !== 1 ? 's' : ''}`;
  72. }
  73. } else {
  74. ageText = `${diffMinutes} minute${diffMinutes !== 1 ? 's' : ''}`;
  75. }
  76. return `${datePart} - ${timePart} (${ageText} ago)`;
  77. }
  78. const cache = {
  79. getKey: function(user, repo) {
  80. return `${CACHE_KEY_PREFIX}${user}_${repo}`;
  81. },
  82. get: function(user, repo) {
  83. try {
  84. const key = this.getKey(user, repo);
  85. const cachedData = localStorage.getItem(key);
  86. if (!cachedData) return null;
  87. const { value, expiry } = JSON.parse(cachedData);
  88. if (expiry && expiry < Date.now()) {
  89. console.log('Cache expired, removing item');
  90. localStorage.removeItem(key);
  91. return null;
  92. }
  93. console.log('Using cached data for ' + user + '/' + repo);
  94. return value;
  95. } catch (err) {
  96. console.error('Error reading from cache:', err);
  97. return null;
  98. }
  99. },
  100. set: function(user, repo, value) {
  101. try {
  102. const key = this.getKey(user, repo);
  103. const data = {
  104. value: value,
  105. expiry: Date.now() + CACHE_EXPIRY
  106. };
  107. localStorage.setItem(key, JSON.stringify(data));
  108. console.log('Saved to cache: ' + user + '/' + repo);
  109. } catch (err) {
  110. console.error('Error writing to cache:', err);
  111. }
  112. },
  113. cleanup: function() {
  114. try {
  115. const now = Date.now();
  116. for (let i = 0; i < localStorage.length; i++) {
  117. const key = localStorage.key(i);
  118. if (key && key.startsWith(CACHE_KEY_PREFIX)) {
  119. try {
  120. const data = JSON.parse(localStorage.getItem(key));
  121. if (data.expiry && data.expiry < now) {
  122. localStorage.removeItem(key);
  123. console.log('Cleaned up expired cache item:', key);
  124. }
  125. } catch (e) {
  126. localStorage.removeItem(key);
  127. }
  128. }
  129. }
  130. } catch (err) {
  131. console.error('Error cleaning cache:', err);
  132. }
  133. }
  134. };
  135. async function getRepoCreationDate(user, repo) {
  136. const cachedDate = cache.get(user, repo);
  137. if (cachedDate) {
  138. return cachedDate;
  139. }
  140. const apiUrl = `${apiBase}${user}/${repo}`;
  141. try {
  142. const res = await fetch(apiUrl);
  143. if (res.status === 403) {
  144. console.warn('GitHub API rate limit exceeded. Try again later.');
  145. return null;
  146. }
  147. if (!res.ok) {
  148. console.error(`API error: ${res.status} - ${res.statusText}`);
  149. return null;
  150. }
  151. const data = await res.json();
  152. const createdAt = data.created_at;
  153. if (createdAt) {
  154. cache.set(user, repo, createdAt);
  155. return createdAt;
  156. }
  157. return null;
  158. } catch (err) {
  159. console.error('Failed to fetch repo creation date:', err);
  160. return null;
  161. }
  162. }
  163. async function insertCreatedDate() {
  164. const match = window.location.pathname.match(/^\/([^\/]+)\/([^\/]+)/);
  165. if (!match) return false;
  166. const [_, user, repo] = match;
  167. const repoPath = `${user}/${repo}`;
  168. currentRepoPath = repoPath;
  169. const createdAt = await getRepoCreationDate(user, repo);
  170. if (!createdAt) return false;
  171. const formattedDate = formatDate(createdAt);
  172. let insertedCount = 0;
  173. document.querySelectorAll('.repo-created-date').forEach(el => el.remove());
  174. for (const [view, selectorsList] of Object.entries(selectors)) {
  175. for (const selector of selectorsList) {
  176. const element = document.querySelector(selector);
  177. if (element && !element.querySelector(`.repo-created-${view}`)) {
  178. insertDateElement(element, formattedDate, view);
  179. insertedCount++;
  180. console.log(`Added creation date to ${view} view using selector: ${selector}`);
  181. break;
  182. }
  183. }
  184. }
  185. return insertedCount > 0;
  186. }
  187. function insertDateElement(targetElement, formattedDate, view) {
  188. const p = document.createElement('p');
  189. p.className = `f6 color-fg-muted repo-created-date repo-created-${view}`;
  190. p.style.marginTop = '4px';
  191. p.style.marginBottom = '8px';
  192. p.innerHTML = `<strong>Created</strong> ${formattedDate}`;
  193. if (view === 'mobile') {
  194. const flexWrap = targetElement.querySelector('.flex-wrap');
  195. if (flexWrap) {
  196. flexWrap.parentNode.insertBefore(p, flexWrap.nextSibling);
  197. return;
  198. }
  199. const dFlex = targetElement.querySelector('.d-flex');
  200. if (dFlex) {
  201. dFlex.parentNode.insertBefore(p, dFlex.nextSibling);
  202. return;
  203. }
  204. }
  205. targetElement.insertBefore(p, targetElement.firstChild);
  206. }
  207. function checkAndInsertWithRetry(retryCount = 0, maxRetries = 5) {
  208. insertCreatedDate().then(inserted => {
  209. if (!inserted && retryCount < maxRetries) {
  210. const delay = Math.pow(2, retryCount) * 500;
  211. console.log(`Retrying in ${delay}ms (attempt ${retryCount + 1}/${maxRetries})`);
  212. setTimeout(() => checkAndInsertWithRetry(retryCount + 1, maxRetries), delay);
  213. }
  214. });
  215. }
  216. function checkForRepoChange() {
  217. const match = window.location.pathname.match(/^\/([^\/]+)\/([^\/]+)/);
  218. if (!match) return;
  219. const [_, user, repo] = match;
  220. const repoPath = `${user}/${repo}`;
  221. if (repoPath !== currentRepoPath) {
  222. console.log(`Repository changed: ${currentRepoPath} -> ${repoPath}`);
  223. checkAndInsertWithRetry();
  224. }
  225. }
  226. cache.cleanup();
  227. if (document.readyState === 'loading') {
  228. document.addEventListener('DOMContentLoaded', () => checkAndInsertWithRetry());
  229. } else {
  230. checkAndInsertWithRetry();
  231. }
  232. const originalPushState = history.pushState;
  233. history.pushState = function() {
  234. originalPushState.apply(this, arguments);
  235. setTimeout(checkForRepoChange, 100);
  236. };
  237. const originalReplaceState = history.replaceState;
  238. history.replaceState = function() {
  239. originalReplaceState.apply(this, arguments);
  240. setTimeout(checkForRepoChange, 100);
  241. };
  242. window.addEventListener('popstate', () => {
  243. setTimeout(checkForRepoChange, 100);
  244. });
  245. const observer = new MutationObserver((mutations) => {
  246. for (const mutation of mutations) {
  247. if (mutation.type === 'childList' &&
  248. (mutation.target.id === 'js-repo-pjax-container' ||
  249. mutation.target.id === 'repository-container-header')) {
  250. setTimeout(checkForRepoChange, 100);
  251. break;
  252. }
  253. }
  254. });
  255. observer.observe(document.body, { childList: true, subtree: true });
  256. })();