- // ==UserScript==
- // @name GitHub Repo Size
- // @description Displays repository size.
- // @icon https://github.githubassets.com/favicons/favicon-dark.svg
- // @version 1.1
- // @author afkarxyz
- // @namespace https://github.com/afkarxyz/userscripts/
- // @supportURL https://github.com/afkarxyz/userscripts/issues
- // @license MIT
- // @match https://github.com/*/*
- // @grant GM_xmlhttpRequest
- // @grant GM_setValue
- // @grant GM_getValue
- // @connect api.codetabs.com
- // @connect api.cors.lol
- // @connect api.allorigins.win
- // @connect everyorigin.jwvbremen.nl
- // @connect api.github.com
- // ==/UserScript==
-
- ;(() => {
- let isRequestInProgress = false
- let debounceTimer = null
- const CACHE_DURATION = 10 * 60 * 1000
-
- const databaseIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="16" height="16" class="octicon mr-2" fill="currentColor" aria-hidden="true" style="vertical-align: text-bottom;">
- <path d="M400 86l0 88.7c-13.3 7.2-31.6 14.2-54.8 19.9C311.3 203 269.5 208 224 208s-87.3-5-121.2-13.4C79.6 188.9 61.3 182 48 174.7L48 86l.6-.5C53.9 81 64.5 74.8 81.8 68.6C115.9 56.5 166.2 48 224 48s108.1 8.5 142.2 20.6c17.3 6.2 27.8 12.4 33.2 16.9l.6 .5zm0 141.5l0 75.2c-13.3 7.2-31.6 14.2-54.8 19.9C311.3 331 269.5 336 224 336s-87.3-5-121.2-13.4C79.6 316.9 61.3 310 48 302.7l0-75.2c13.3 5.3 27.9 9.9 43.3 13.7C129.5 250.6 175.2 256 224 256s94.5-5.4 132.7-14.8c15.4-3.8 30-8.3 43.3-13.7zM48 426l0-70.4c13.3 5.3 27.9 9.9 43.3 13.7C129.5 378.6 175.2 384 224 384s94.5-5.4 132.7-14.8c15.4-3.8 30-8.3 43.3-13.7l0 70.4-.6 .5c-5.3 4.5-15.9 10.7-33.2 16.9C332.1 455.5 281.8 464 224 464s-108.1-8.5-142.2-20.6c-17.3-6.2-27.8-12.4-33.2-16.9L48 426z"/>
- </svg>`
-
- 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)
- },
- },
- ]
-
- function extractRepoInfo() {
- const match = window.location.pathname.match(/^\/([^/]+)\/([^/]+)(\/|$)/)
- if (!match) return null
-
- return {
- owner: match[1],
- repo: match[2],
- }
- }
-
- function formatSize(bytes) {
- const units = ["B", "KB", "MB", "GB", "TB"]
- let i = 0
- while (bytes >= 1024 && i < units.length - 1) {
- bytes /= 1024
- i++
- }
- return {
- value: bytes.toFixed(1),
- unit: units[i],
- }
- }
-
- function injectSize({ value, unit }, downloadURL) {
- const existingSizeDivs = document.querySelectorAll(".gh-repo-size-display")
- existingSizeDivs.forEach(div => div.remove())
-
- injectSizeDesktop({ value, unit }, downloadURL)
-
- injectSizeMobile({ value, unit }, downloadURL)
- }
-
- function injectSizeDesktop({ value, unit }, downloadURL) {
- if (document.querySelector(".gh-repo-size-display")) {
- return
- }
-
- const forksHeader = Array.from(document.querySelectorAll("h3.sr-only")).find(
- (el) => el.textContent.trim() === "Forks",
- )
- if (!forksHeader) return
-
- const forksContainer = forksHeader.nextElementSibling
- if (!forksContainer || !forksContainer.classList.contains("mt-2")) return
-
- const existingLink = document.querySelector(".Link--muted .octicon-repo-forked")
- if (existingLink) {
- const parentLinkElement = existingLink.closest("a")
-
- const sizeDiv = document.createElement("div")
- sizeDiv.className = "mt-2 gh-repo-size-display"
-
- const downloadLink = document.createElement("a")
- downloadLink.className = parentLinkElement.className
- downloadLink.href = downloadURL
- downloadLink.style.cursor = "pointer"
-
- downloadLink.innerHTML = `
- ${databaseIcon}
- <strong>${value}</strong> ${unit}`
-
- sizeDiv.appendChild(downloadLink)
-
- forksContainer.insertAdjacentElement("afterend", sizeDiv)
- } else {
- const sizeDiv = document.createElement("div")
- sizeDiv.className = "mt-2 gh-repo-size-display"
-
- sizeDiv.innerHTML = `
- <a class="Link Link--muted" href="${downloadURL}" style="cursor: pointer;">
- ${databaseIcon}
- <strong>${value}</strong> ${unit}
- </a>`
-
- forksContainer.insertAdjacentElement("afterend", sizeDiv)
- }
- }
-
- function injectSizeMobile({ value, unit }, downloadURL) {
- if (document.querySelector(".d-block.d-md-none .gh-repo-size-display")) {
- return
- }
-
- const mobileContainer = document.querySelector(".d-block.d-md-none.mb-2")
- if (!mobileContainer) return
-
- let targetContainer = null
-
- const publicRepoElement = Array.from(mobileContainer.querySelectorAll('.color-fg-muted span')).find(
- (el) => el.textContent.trim() === "Public repository"
- )
-
- if (publicRepoElement) {
- targetContainer = publicRepoElement.closest('.mb-2.d-flex')
- }
-
- if (!targetContainer) {
- const forkedElement = mobileContainer.querySelector('.color-fg-muted span a[href*="/"]')
- if (forkedElement) {
- targetContainer = forkedElement.closest('.mb-2.d-flex')
- }
- }
-
- if (!targetContainer) {
- targetContainer = mobileContainer.querySelector('.mb-2.d-flex')
- }
-
- if (!targetContainer) return
-
- const sizeDivMobile = document.createElement("div")
- sizeDivMobile.className = "mb-2 d-flex color-fg-muted gh-repo-size-display"
-
- sizeDivMobile.innerHTML = `
- <div class="d-flex flex-items-center" style="height: 21px">
- ${databaseIcon}
- </div>
- <a href="${downloadURL}" class="flex-auto min-width-0 width-fit" style="color:inherit">
- <strong>${value}</strong> ${unit}
- </a>`
-
- targetContainer.insertAdjacentElement("afterend", sizeDivMobile)
- }
-
- function getCacheKey(owner, repo) {
- return `gh_repo_size_${owner}_${repo}`
- }
-
- function getFromCache(owner, repo) {
- try {
- const cacheKey = getCacheKey(owner, repo)
- const cachedData = GM_getValue(cacheKey)
-
- if (!cachedData) return null
-
- const { data, timestamp } = cachedData
- const now = Date.now()
-
- if (now - timestamp < CACHE_DURATION) {
- return data
- }
-
- return null
- } catch (error) {
- console.error('Error getting from cache:', error)
- return null
- }
- }
-
- function saveToCache(owner, repo, data) {
- try {
- const cacheKey = getCacheKey(owner, repo)
- GM_setValue(cacheKey, {
- data,
- timestamp: Date.now()
- })
- } catch (error) {
- console.error('Error saving to cache:', error)
- }
- }
-
- async function fetchFromApi(proxyService, owner, repo) {
- const apiUrl = `${proxyService.url}${owner}/${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)
- resolve({ success: true, data: data })
- } 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 fetchRepoInfo(owner, repo) {
- if (isRequestInProgress) {
- return
- }
-
- const cachedData = getFromCache(owner, repo)
- if (cachedData) {
- processRepoData(cachedData)
- return
- }
-
- isRequestInProgress = true
- let fetchSuccessful = false
-
- try {
- for (let i = 0; i < proxyServices.length; i++) {
- const proxyService = proxyServices[i]
- const result = await fetchFromApi(proxyService, owner, repo)
-
- if (result.success) {
- saveToCache(owner, repo, result.data)
- processRepoData(result.data)
- fetchSuccessful = true
- break
- }
- }
-
- if (!fetchSuccessful) {
- console.warn('All proxy attempts failed for', owner, repo)
- }
- } finally {
- isRequestInProgress = false
- }
- }
-
- function processRepoData(data) {
- if (data && data.size != null) {
- const repoInfo = extractRepoInfo()
- if (!repoInfo) return
-
- const formatted = formatSize(data.size * 1024)
-
- let defaultBranch = "master"
- if (data.default_branch) {
- defaultBranch = data.default_branch
- }
-
- const downloadURL = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/archive/refs/heads/${defaultBranch}.zip`
- injectSize(formatted, downloadURL)
- }
- }
-
- let lastProcessedRepo = ''
-
- function checkAndInsertWithRetry(retryCount = 0, maxRetries = 5) {
- const repoInfo = extractRepoInfo()
- if (!repoInfo) return
-
- const currentRepo = `${repoInfo.owner}/${repoInfo.repo}`
-
- if (currentRepo === lastProcessedRepo && document.querySelector(".gh-repo-size-display")) {
- return
- }
-
- lastProcessedRepo = currentRepo
-
- fetchRepoInfo(repoInfo.owner, repoInfo.repo).catch(() => {
- if (retryCount < maxRetries) {
- const delay = Math.pow(2, retryCount) * 500
- setTimeout(() => checkAndInsertWithRetry(retryCount + 1, maxRetries), delay)
- }
- })
- }
-
- let isHandlingRouteChange = false
-
- function handleRouteChange() {
- if (isHandlingRouteChange) return
- isHandlingRouteChange = true
-
- const repoInfo = extractRepoInfo()
- if (!repoInfo) {
- isHandlingRouteChange = false
- return
- }
-
- const pathParts = window.location.pathname.split("/").filter(Boolean)
- if (pathParts.length !== 2) {
- isHandlingRouteChange = false
- return
- }
-
- if (debounceTimer) {
- clearTimeout(debounceTimer)
- }
-
- debounceTimer = setTimeout(() => {
- checkAndInsertWithRetry()
- isHandlingRouteChange = false
- }, 300)
- }
-
- let lastUrl = location.href
-
- const observer = new MutationObserver(() => {
- if (lastUrl !== location.href) {
- lastUrl = location.href
- handleRouteChange()
- }
- })
-
- observer.observe(document.body, { childList: true, subtree: true })
-
- ;(() => {
- const origPushState = history.pushState
- const origReplaceState = history.replaceState
- let lastPath = location.pathname
-
- function checkPathChange() {
- if (location.pathname !== lastPath) {
- lastPath = location.pathname
- setTimeout(handleRouteChange, 300)
- }
- }
-
- history.pushState = function (...args) {
- origPushState.apply(this, args)
- checkPathChange()
- }
-
- history.replaceState = function (...args) {
- origReplaceState.apply(this, args)
- checkPathChange()
- }
-
- window.addEventListener("popstate", checkPathChange)
- })()
-
- if (document.readyState === "loading") {
- document.addEventListener("DOMContentLoaded", handleRouteChange)
- } else {
- handleRouteChange()
- }
- })()