GitHub Repo Age

Displays repository creation date/time/age.

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

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

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==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)
})()