您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Displays user's join date/time/age.
当前为
- // ==UserScript==
- // @name GitHub Join Date
- // @description Displays user's join 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
- // @run-at document-idle
- // ==/UserScript==
- (function() {
- 'use strict';
- const ELEMENT_ID = 'userscript-join-date-display';
- const CACHE_KEY = 'githubUserJoinDatesCache_v1';
- const GITHUB_API_BASE = 'https://api.github.com/users/';
- const FALLBACK_API_BASE = 'https://api.codetabs.com/v1/proxy/?quest=https://api.github.com/users/';
- let isProcessing = false;
- let observerDebounceTimeout = null;
- function readCache() {
- try {
- const cachedData = localStorage.getItem(CACHE_KEY);
- return cachedData ? JSON.parse(cachedData) : {};
- } catch (e) {
- return {};
- }
- }
- function writeCache(cacheData) {
- try {
- localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
- } catch (e) {
- }
- }
- function getRelativeTime(dateString) {
- 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`;
- }
- function getAbbreviatedMonth(date) {
- const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
- return months[date.getMonth()];
- }
- async function fetchFromGitHubApi(username) {
- const apiUrl = `${GITHUB_API_BASE}${username}`;
- 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 userData = JSON.parse(response.responseText);
- const createdAt = userData.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(username) {
- const apiUrl = `${FALLBACK_API_BASE}${username}`;
- return new Promise((resolve) => {
- GM_xmlhttpRequest({
- method: 'GET',
- url: apiUrl,
- onload: function(response) {
- if (response.status >= 200 && response.status < 300) {
- try {
- const userData = JSON.parse(response.responseText);
- const createdAt = userData.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 getGitHubJoinDate(username) {
- const directResult = await fetchFromGitHubApi(username);
- if (directResult.success) {
- return directResult.data;
- }
- if (directResult.useProxy) {
- console.log('GitHub Join Date: Use Proxy');
- const proxyResult = await fetchFromProxyApi(username);
- if (proxyResult.success) {
- return proxyResult.data;
- }
- }
- return null;
- }
- function removeExistingElement() {
- const existingElement = document.getElementById(ELEMENT_ID);
- if (existingElement) {
- existingElement.remove();
- }
- }
- async function addOrUpdateJoinDateElement() {
- if (document.getElementById(ELEMENT_ID) && !isProcessing) { return; }
- if (isProcessing) { return; }
- const pathParts = window.location.pathname.split('/').filter(part => part);
- if (pathParts.length < 1 || pathParts.length > 2 || (pathParts.length === 2 && !['sponsors', 'followers', 'following'].includes(pathParts[1]))) {
- removeExistingElement();
- return;
- }
- const usernameElement = document.querySelector('.p-nickname.vcard-username') ||
- document.querySelector('h1.h2.lh-condensed');
- if (!usernameElement) {
- removeExistingElement();
- return;
- }
- const username = pathParts[0].toLowerCase();
- isProcessing = true;
- let joinElement = document.getElementById(ELEMENT_ID);
- let createdAtISO = null;
- let fromCache = false;
- try {
- const cache = readCache();
- if (cache[username]) {
- createdAtISO = cache[username];
- fromCache = true;
- }
- if (!joinElement) {
- joinElement = document.createElement('div');
- joinElement.id = ELEMENT_ID;
- joinElement.innerHTML = fromCache ? "..." : "Loading...";
- joinElement.style.color = 'var(--color-fg-muted)';
- joinElement.style.fontSize = '14px';
- joinElement.style.fontWeight = 'normal';
- if (usernameElement.classList.contains('h2')) {
- joinElement.style.marginTop = '0px';
- const colorFgMuted = usernameElement.nextElementSibling?.classList.contains('color-fg-muted') ?
- usernameElement.nextElementSibling : null;
- if (colorFgMuted) {
- const innerDiv = colorFgMuted.querySelector('div') || colorFgMuted;
- innerDiv.appendChild(joinElement);
- } else {
- usernameElement.insertAdjacentElement('afterend', joinElement);
- }
- } else {
- joinElement.style.marginTop = '8px';
- usernameElement.insertAdjacentElement('afterend', joinElement);
- }
- }
- if (!fromCache) {
- createdAtISO = await getGitHubJoinDate(username);
- joinElement = document.getElementById(ELEMENT_ID);
- if (!joinElement) { return; }
- if (createdAtISO) {
- const currentCache = readCache();
- currentCache[username] = createdAtISO;
- writeCache(currentCache);
- } else {
- removeExistingElement();
- return;
- }
- }
- if (createdAtISO && joinElement) {
- const joinDate = new Date(createdAtISO);
- const day = joinDate.getDate();
- const month = getAbbreviatedMonth(joinDate);
- const year = joinDate.getFullYear();
- const hours = joinDate.getHours().toString().padStart(2, '0');
- const minutes = joinDate.getMinutes().toString().padStart(2, '0');
- const formattedTime = `${hours}:${minutes}`;
- const relativeTimeString = getRelativeTime(createdAtISO);
- joinElement.innerHTML = `<strong>Joined</strong> <span style="font-weight: normal;">${day} ${month} ${year} - ${formattedTime} (${relativeTimeString})</span>`;
- } else if (!createdAtISO && joinElement) {
- removeExistingElement();
- }
- } catch (error) {
- removeExistingElement();
- } finally {
- isProcessing = false;
- }
- }
- function handlePotentialPageChange() {
- clearTimeout(observerDebounceTimeout);
- observerDebounceTimeout = setTimeout(() => {
- addOrUpdateJoinDateElement();
- }, 600);
- }
- addOrUpdateJoinDateElement();
- const observer = new MutationObserver((mutationsList) => {
- let potentiallyRelevantChange = false;
- for (const mutation of mutationsList) {
- if (mutation.type === 'childList' && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0)) {
- const targetNode = mutation.target;
- if (targetNode && (targetNode.matches?.('main, main *, .Layout-sidebar, .Layout-sidebar *, body'))) {
- let onlySelfChange = false;
- if ((mutation.addedNodes.length === 1 && mutation.addedNodes[0].id === ELEMENT_ID && mutation.removedNodes.length === 0) ||
- (mutation.removedNodes.length === 1 && mutation.removedNodes[0].id === ELEMENT_ID && mutation.addedNodes.length === 0)) {
- onlySelfChange = true;
- }
- if (!onlySelfChange) {
- potentiallyRelevantChange = true;
- break;
- }
- }
- }
- }
- if(potentiallyRelevantChange) {
- handlePotentialPageChange();
- }
- });
- observer.observe(document.body, { childList: true, subtree: true });
- })();