您需要先安装一个扩展,例如 篡改猴、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.4 // @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.cors.lol // @connect api.allorigins.win // @connect everyorigin.jwvbremen.nl // @connect api.github.com // ==/UserScript== ;(() => { const CACHE_KEY_PREFIX = "github_repo_created_" const proxyServices = [ { name: "Direct GitHub API", url: "https://api.github.com/repos/", parseResponse: (response) => { return JSON.parse(response) }, }, { name: "CodeTabs Proxy", url: "https://api.codetabs.com/v1/proxy/?quest=https://api.github.com/repos/", parseResponse: (response) => { return JSON.parse(response) }, }, { name: "CORS.lol Proxy", url: "https://api.cors.lol/?url=https://api.github.com/repos/", parseResponse: (response) => { return JSON.parse(response) }, }, { name: "AllOrigins Proxy", url: "https://api.allorigins.win/get?url=https://api.github.com/repos/", parseResponse: (response) => { const parsed = JSON.parse(response) return JSON.parse(parsed.contents) }, }, { name: "EveryOrigin Proxy", url: "https://everyorigin.jwvbremen.nl/api/get?url=https://api.github.com/repos/", parseResponse: (response) => { const parsed = JSON.parse(response) return JSON.parse(parsed.html) }, }, ] 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: (user, repo) => `${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) { // Storage error - continue without caching } }, } async function fetchFromApi(proxyService, user, repo) { const apiUrl = `${proxyService.url}${user}/${repo}` return new Promise((resolve) => { if (typeof GM_xmlhttpRequest === "undefined") { resolve({ success: false, error: "GM_xmlhttpRequest is not defined" }) return } GM_xmlhttpRequest({ method: "GET", url: apiUrl, headers: { Accept: "application/vnd.github.v3+json", }, onload: (response) => { if (response.responseText.includes("limit") && response.responseText.includes("API")) { resolve({ success: false, error: "Rate limit exceeded", isRateLimit: true, }) return } if (response.status >= 200 && response.status < 300) { try { const data = proxyService.parseResponse(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: () => { resolve({ success: false, error: "Network error" }) }, ontimeout: () => { resolve({ success: false, error: "Timeout" }) }, }) }) } async function getRepoCreationDate(user, repo) { const cachedDate = cache.get(user, repo) if (cachedDate) { return cachedDate } for (let i = 0; i < proxyServices.length; i++) { const proxyService = proxyServices[i] const result = await fetchFromApi(proxyService, user, repo) if (result.success) { cache.set(user, repo, result.data) return result.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或关注我们的公众号极客氢云获取最新地址