GitHub Repo Age

Displays repository creation date/time/age.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         GitHub Repo Age
// @description  Displays repository creation date/time/age.
// @icon         https://github.githubassets.com/favicons/favicon-dark.svg
// @version      1.5
// @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
// @require      https://cdn.jsdelivr.net/npm/[email protected]/cdn.min.js
// ==/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 datePart = dateFns.format(createdDate, "dd MMM yyyy")
    
    const timePart = dateFns.format(createdDate, "HH:mm")
    
    const diffYears = dateFns.differenceInYears(now, createdDate)
    const tempDate = dateFns.addYears(createdDate, diffYears)
    
    const diffMonths = dateFns.differenceInMonths(now, tempDate)
    const tempDate2 = dateFns.addMonths(tempDate, diffMonths)
    
    const diffDays = dateFns.differenceInDays(now, tempDate2)
    const tempDate3 = dateFns.addDays(tempDate2, diffDays)
    
    const diffHours = dateFns.differenceInHours(now, tempDate3)
    const tempDate4 = dateFns.addHours(tempDate3, diffHours)
    
    const diffMinutes = dateFns.differenceInMinutes(now, tempDate4)
    
    let ageText = ""
    
    if (diffYears > 0) {
      ageText = `${diffYears} year${diffYears !== 1 ? "s" : ""}`
      if (diffMonths > 0) {
        ageText += ` ${diffMonths} month${diffMonths !== 1 ? "s" : ""}`
      }
    } else if (dateFns.differenceInMonths(now, createdDate) > 0) {
      const totalMonths = dateFns.differenceInMonths(now, createdDate)
      ageText = `${totalMonths} month${totalMonths !== 1 ? "s" : ""}`
      if (diffDays > 0) {
        ageText += ` ${diffDays} day${diffDays !== 1 ? "s" : ""}`
      }
    } else if (dateFns.differenceInDays(now, createdDate) > 0) {
      const totalDays = dateFns.differenceInDays(now, createdDate)
      ageText = `${totalDays} day${totalDays !== 1 ? "s" : ""}`
      if (diffHours > 0 && totalDays < 7) {
        ageText += ` ${diffHours} hour${diffHours !== 1 ? "s" : ""}`
      }
    } else if (dateFns.differenceInHours(now, createdDate) > 0) {
      const totalHours = dateFns.differenceInHours(now, createdDate)
      ageText = `${totalHours} hour${totalHours !== 1 ? "s" : ""}`
      if (diffMinutes > 0) {
        ageText += ` ${diffMinutes} minute${diffMinutes !== 1 ? "s" : ""}`
      }
    } else {
      const totalMinutes = dateFns.differenceInMinutes(now, createdDate)
      ageText = `${totalMinutes} minute${totalMinutes !== 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 cachedValue
      } catch (err) {
        return null
      }
    },

    set: function (user, repo, value) {
      try {
        const key = this.getKey(user, repo)
        localStorage.setItem(key, value)
      } catch (err) {

      }
    },
  }

  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, createdAt)
          insertedCount++
          break
        }
      }
    }

    return insertedCount > 0
  }

  function insertDateElement(targetElement, formattedDate, view, isoDateStr) {
    const p = document.createElement("p")
    p.className = `f6 color-fg-muted repo-created-date repo-created-${view}`
    p.dataset.createdAt = isoDateStr
    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 updateAges() {
    document.querySelectorAll(".repo-created-date").forEach((el) => {
      const createdAt = el.dataset.createdAt
      if (createdAt) {
        const formattedDate = formatDate(createdAt)
        const strongElement = el.querySelector("strong")
        if (strongElement) {
          el.innerHTML = `<strong>Created</strong> ${formattedDate}`
        } else {
          el.innerHTML = formattedDate
        }
      }
    })
  }

  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 })
  
  setInterval(updateAges, 60000)
})()