您需要先安装一个扩展,例如 篡改猴、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 });
- })();