您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Displays repository creation date/time/age.
当前为
// ==UserScript== // @name GitHub Repo Age // @description Displays repository creation date/time/age. // @icon https://github.githubassets.com/favicons/favicon-dark.svg // @version 1.3 // @author afkarxyz // @namespace https://github.com/afkarxyz/userscripts/ // @supportURL https://github.com/afkarxyz/userscripts/issues // @license MIT // @match https://github.com/*/* // @grant GM_xmlhttpRequest // @connect api.codetabs.com // @connect api.github.com // ==/UserScript== (function () { 'use strict'; const githubApiBase = 'https://api.github.com/repos/'; const fallbackApiBase = 'https://api.codetabs.com/v1/proxy/?quest=https://api.github.com/repos/'; const CACHE_KEY_PREFIX = 'github_repo_created_'; const selectors = { desktop: [ '.BorderGrid-cell .hide-sm.hide-md .f4.my-3', '.BorderGrid-cell' ], mobile: [ '.d-block.d-md-none.mb-2.px-3.px-md-4.px-lg-5 .f4.mb-3.color-fg-muted', '.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', '.d-block.d-md-none.mb-2.px-3.px-md-4.px-lg-5' ] }; let currentRepoPath = ''; function formatDate(isoDateStr) { const createdDate = new Date(isoDateStr); const now = new Date(); const diffTime = Math.abs(now - createdDate); const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); const diffHours = Math.floor((diffTime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const diffMinutes = Math.floor((diffTime % (1000 * 60 * 60)) / (1000 * 60)); const diffMonths = Math.floor(diffDays / 30.44); const diffYears = Math.floor(diffMonths / 12); const remainingMonths = diffMonths % 12; const remainingDays = Math.floor(diffDays % 30.44); const datePart = createdDate.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }); const timePart = createdDate.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', hour12: false }); let ageText = ''; if (diffYears > 0) { ageText = `${diffYears} year${diffYears !== 1 ? 's' : ''}`; if (remainingMonths > 0) { ageText += ` ${remainingMonths} month${remainingMonths !== 1 ? 's' : ''}`; } } else if (diffMonths > 0) { ageText = `${diffMonths} month${diffMonths !== 1 ? 's' : ''}`; if (remainingDays > 0) { ageText += ` ${remainingDays} day${remainingDays !== 1 ? 's' : ''}`; } } else if (diffDays > 0) { ageText = `${diffDays} day${diffDays !== 1 ? 's' : ''}`; if (diffHours > 0) { ageText += ` ${diffHours} hour${diffHours !== 1 ? 's' : ''}`; } } else if (diffHours > 0) { ageText = `${diffHours} hour${diffHours !== 1 ? 's' : ''}`; if (diffMinutes > 0) { ageText += ` ${diffMinutes} minute${diffMinutes !== 1 ? 's' : ''}`; } } else { ageText = `${diffMinutes} minute${diffMinutes !== 1 ? 's' : ''}`; } return `${datePart} - ${timePart} (${ageText} ago)`; } const cache = { getKey: function(user, repo) { return `${CACHE_KEY_PREFIX}${user}_${repo}`; }, get: function(user, repo) { try { const key = this.getKey(user, repo); const cachedValue = localStorage.getItem(key); if (!cachedValue) return null; return JSON.parse(cachedValue); } catch (err) { return null; } }, set: function(user, repo, value) { try { const key = this.getKey(user, repo); localStorage.setItem(key, JSON.stringify(value)); } catch (err) { } } }; async function fetchFromGitHubApi(user, repo) { const apiUrl = `${githubApiBase}${user}/${repo}`; return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: apiUrl, headers: { 'Accept': 'application/vnd.github.v3+json' }, onload: function(response) { if (response.status === 200) { try { const data = JSON.parse(response.responseText); const createdAt = data.created_at; if (createdAt) { resolve({ success: true, data: createdAt }); } else { resolve({ success: false, error: 'Missing creation date' }); } } catch (e) { resolve({ success: false, error: 'JSON parse error' }); } } else { resolve({ success: false, error: `Status ${response.status}`, useProxy: response.status === 403 || response.status === 429 }); } }, onerror: function() { resolve({ success: false, error: 'Network error', useProxy: true }); }, ontimeout: function() { resolve({ success: false, error: 'Timeout', useProxy: true }); } }); }); } async function fetchFromProxyApi(user, repo) { const apiUrl = `${fallbackApiBase}${user}/${repo}`; return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: apiUrl, onload: function(response) { if (response.status >= 200 && response.status < 300) { try { const data = JSON.parse(response.responseText); const createdAt = data.created_at; if (createdAt) { resolve({ success: true, data: createdAt }); } else { resolve({ success: false, error: 'Missing creation date' }); } } catch (e) { resolve({ success: false, error: 'JSON parse error' }); } } else { resolve({ success: false, error: `Status ${response.status}` }); } }, onerror: function() { resolve({ success: false, error: 'Network error' }); }, ontimeout: function() { resolve({ success: false, error: 'Timeout' }); } }); }); } async function getRepoCreationDate(user, repo) { const cachedDate = cache.get(user, repo); if (cachedDate) { return cachedDate; } const directResult = await fetchFromGitHubApi(user, repo); if (directResult.success) { cache.set(user, repo, directResult.data); return directResult.data; } if (directResult.useProxy) { console.log('GitHub Repo Age: Use Proxy'); const proxyResult = await fetchFromProxyApi(user, repo); if (proxyResult.success) { cache.set(user, repo, proxyResult.data); return proxyResult.data; } } return null; } async function insertCreatedDate() { const match = window.location.pathname.match(/^\/([^\/]+)\/([^\/]+)/); if (!match) return false; const [_, user, repo] = match; const repoPath = `${user}/${repo}`; currentRepoPath = repoPath; const createdAt = await getRepoCreationDate(user, repo); if (!createdAt) return false; const formattedDate = formatDate(createdAt); let insertedCount = 0; document.querySelectorAll('.repo-created-date').forEach(el => el.remove()); for (const [view, selectorsList] of Object.entries(selectors)) { for (const selector of selectorsList) { const element = document.querySelector(selector); if (element && !element.querySelector(`.repo-created-${view}`)) { insertDateElement(element, formattedDate, view); insertedCount++; break; } } } return insertedCount > 0; } function insertDateElement(targetElement, formattedDate, view) { const p = document.createElement('p'); p.className = `f6 color-fg-muted repo-created-date repo-created-${view}`; p.style.marginTop = '4px'; p.style.marginBottom = '8px'; p.innerHTML = `<strong>Created</strong> ${formattedDate}`; if (view === 'mobile') { const flexWrap = targetElement.querySelector('.flex-wrap'); if (flexWrap) { flexWrap.parentNode.insertBefore(p, flexWrap.nextSibling); return; } const dFlex = targetElement.querySelector('.d-flex'); if (dFlex) { dFlex.parentNode.insertBefore(p, dFlex.nextSibling); return; } } targetElement.insertBefore(p, targetElement.firstChild); } function checkAndInsertWithRetry(retryCount = 0, maxRetries = 5) { insertCreatedDate().then(inserted => { if (!inserted && retryCount < maxRetries) { const delay = Math.pow(2, retryCount) * 500; setTimeout(() => checkAndInsertWithRetry(retryCount + 1, maxRetries), delay); } }); } function checkForRepoChange() { const match = window.location.pathname.match(/^\/([^\/]+)\/([^\/]+)/); if (!match) return; const [_, user, repo] = match; const repoPath = `${user}/${repo}`; if (repoPath !== currentRepoPath) { checkAndInsertWithRetry(); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => checkAndInsertWithRetry()); } else { checkAndInsertWithRetry(); } const originalPushState = history.pushState; history.pushState = function() { originalPushState.apply(this, arguments); setTimeout(checkForRepoChange, 100); }; const originalReplaceState = history.replaceState; history.replaceState = function() { originalReplaceState.apply(this, arguments); setTimeout(checkForRepoChange, 100); }; window.addEventListener('popstate', () => { setTimeout(checkForRepoChange, 100); }); const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList' && (mutation.target.id === 'js-repo-pjax-container' || mutation.target.id === 'repository-container-header')) { setTimeout(checkForRepoChange, 100); break; } } }); observer.observe(document.body, { childList: true, subtree: true }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址