GitHub Repo Size

Displays repository size.

当前为 2025-05-14 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Repo Size
  3. // @description Displays repository size.
  4. // @icon https://github.githubassets.com/favicons/favicon-dark.svg
  5. // @version 1.0
  6. // @author afkarxyz
  7. // @namespace https://github.com/afkarxyz/userscripts/
  8. // @supportURL https://github.com/afkarxyz/userscripts/issues
  9. // @license MIT
  10. // @match https://github.com/*/*
  11. // @grant GM_xmlhttpRequest
  12. // @grant GM_setValue
  13. // @grant GM_getValue
  14. // @connect api.codetabs.com
  15. // @connect api.cors.lol
  16. // @connect api.allorigins.win
  17. // @connect everyorigin.jwvbremen.nl
  18. // @connect api.github.com
  19. // ==/UserScript==
  20.  
  21. ;(() => {
  22. let isRequestInProgress = false
  23. let debounceTimer = null
  24. const CACHE_DURATION = 10 * 60 * 1000
  25. 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;">
  26. <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"/>
  27. </svg>`
  28. const proxyServices = [
  29. {
  30. name: "Direct GitHub API",
  31. url: "https://api.github.com/repos/",
  32. parseResponse: (response) => {
  33. return JSON.parse(response)
  34. },
  35. },
  36. {
  37. name: "CodeTabs Proxy",
  38. url: "https://api.codetabs.com/v1/proxy/?quest=https://api.github.com/repos/",
  39. parseResponse: (response) => {
  40. return JSON.parse(response)
  41. },
  42. },
  43. {
  44. name: "CORS.lol Proxy",
  45. url: "https://api.cors.lol/?url=https://api.github.com/repos/",
  46. parseResponse: (response) => {
  47. return JSON.parse(response)
  48. },
  49. },
  50. {
  51. name: "AllOrigins Proxy",
  52. url: "https://api.allorigins.win/get?url=https://api.github.com/repos/",
  53. parseResponse: (response) => {
  54. const parsed = JSON.parse(response)
  55. return JSON.parse(parsed.contents)
  56. },
  57. },
  58. {
  59. name: "EveryOrigin Proxy",
  60. url: "https://everyorigin.jwvbremen.nl/api/get?url=https://api.github.com/repos/",
  61. parseResponse: (response) => {
  62. const parsed = JSON.parse(response)
  63. return JSON.parse(parsed.html)
  64. },
  65. },
  66. ]
  67. function extractRepoInfo() {
  68. const match = window.location.pathname.match(/^\/([^/]+)\/([^/]+)(\/|$)/)
  69. if (!match) return null
  70. return {
  71. owner: match[1],
  72. repo: match[2],
  73. }
  74. }
  75. function formatSize(bytes) {
  76. const units = ["B", "KB", "MB", "GB", "TB"]
  77. let i = 0
  78. while (bytes >= 1024 && i < units.length - 1) {
  79. bytes /= 1024
  80. i++
  81. }
  82. return {
  83. value: bytes.toFixed(1),
  84. unit: units[i],
  85. }
  86. }
  87. function injectSize({ value, unit }, downloadURL) {
  88. const existingSizeDiv = document.querySelector(".gh-repo-size-display")
  89. if (existingSizeDiv) {
  90. existingSizeDiv.remove()
  91. }
  92. const forksHeader = Array.from(document.querySelectorAll("h3.sr-only")).find(
  93. (el) => el.textContent.trim() === "Forks",
  94. )
  95. if (!forksHeader) return
  96. const forksContainer = forksHeader.nextElementSibling
  97. if (!forksContainer || !forksContainer.classList.contains("mt-2")) return
  98. const existingLink = document.querySelector(".Link--muted .octicon-repo-forked")
  99. if (existingLink) {
  100. const parentLinkElement = existingLink.closest("a")
  101. const sizeDiv = document.createElement("div")
  102. sizeDiv.className = "mt-2 gh-repo-size-display"
  103. const downloadLink = document.createElement("a")
  104. downloadLink.className = parentLinkElement.className
  105. downloadLink.href = downloadURL
  106. downloadLink.style.cursor = "pointer"
  107. downloadLink.title = "Click to download repository as ZIP"
  108. downloadLink.innerHTML = `
  109. ${databaseIcon}
  110. <strong>${value}</strong> ${unit}`
  111. sizeDiv.appendChild(downloadLink)
  112. forksContainer.insertAdjacentElement("afterend", sizeDiv)
  113. } else {
  114. const sizeDiv = document.createElement("div")
  115. sizeDiv.className = "mt-2 gh-repo-size-display"
  116. sizeDiv.innerHTML = `
  117. <a class="Link Link--muted" href="${downloadURL}" title="Click to download repository as ZIP" style="cursor: pointer;">
  118. ${databaseIcon}
  119. <strong>${value}</strong> ${unit}
  120. </a>`
  121. forksContainer.insertAdjacentElement("afterend", sizeDiv)
  122. }
  123. }
  124. function getCacheKey(owner, repo) {
  125. return `gh_repo_size_${owner}_${repo}`
  126. }
  127. function getFromCache(owner, repo) {
  128. try {
  129. const cacheKey = getCacheKey(owner, repo)
  130. const cachedData = GM_getValue(cacheKey)
  131. if (!cachedData) return null
  132. const { data, timestamp } = cachedData
  133. const now = Date.now()
  134. if (now - timestamp < CACHE_DURATION) {
  135. return data
  136. }
  137. return null
  138. } catch (error) {
  139. console.error('Error getting from cache:', error)
  140. return null
  141. }
  142. }
  143. function saveToCache(owner, repo, data) {
  144. try {
  145. const cacheKey = getCacheKey(owner, repo)
  146. GM_setValue(cacheKey, {
  147. data,
  148. timestamp: Date.now()
  149. })
  150. } catch (error) {
  151. console.error('Error saving to cache:', error)
  152. }
  153. }
  154. async function fetchFromApi(proxyService, owner, repo) {
  155. const apiUrl = `${proxyService.url}${owner}/${repo}`
  156. return new Promise((resolve) => {
  157. if (typeof GM_xmlhttpRequest === "undefined") {
  158. resolve({ success: false, error: "GM_xmlhttpRequest is not defined" })
  159. return
  160. }
  161. GM_xmlhttpRequest({
  162. method: "GET",
  163. url: apiUrl,
  164. headers: {
  165. Accept: "application/vnd.github.v3+json",
  166. },
  167. onload: (response) => {
  168. if (response.responseText.includes("limit") && response.responseText.includes("API")) {
  169. resolve({
  170. success: false,
  171. error: "Rate limit exceeded",
  172. isRateLimit: true,
  173. })
  174. return
  175. }
  176. if (response.status >= 200 && response.status < 300) {
  177. try {
  178. const data = proxyService.parseResponse(response.responseText)
  179. resolve({ success: true, data: data })
  180. } catch (e) {
  181. resolve({ success: false, error: "JSON parse error" })
  182. }
  183. } else {
  184. resolve({
  185. success: false,
  186. error: `Status ${response.status}`,
  187. })
  188. }
  189. },
  190. onerror: () => {
  191. resolve({ success: false, error: "Network error" })
  192. },
  193. ontimeout: () => {
  194. resolve({ success: false, error: "Timeout" })
  195. },
  196. })
  197. })
  198. }
  199. async function fetchRepoInfo(owner, repo) {
  200. if (isRequestInProgress) {
  201. return
  202. }
  203. const cachedData = getFromCache(owner, repo)
  204. if (cachedData) {
  205. processRepoData(cachedData)
  206. return
  207. }
  208. isRequestInProgress = true
  209. let fetchSuccessful = false
  210. try {
  211. for (let i = 0; i < proxyServices.length; i++) {
  212. const proxyService = proxyServices[i]
  213. const result = await fetchFromApi(proxyService, owner, repo)
  214. if (result.success) {
  215. saveToCache(owner, repo, result.data)
  216. processRepoData(result.data)
  217. fetchSuccessful = true
  218. break
  219. }
  220. }
  221. if (!fetchSuccessful) {
  222. console.warn('All proxy attempts failed for', owner, repo)
  223. }
  224. } finally {
  225. isRequestInProgress = false
  226. }
  227. }
  228. function processRepoData(data) {
  229. if (data && data.size != null) {
  230. const repoInfo = extractRepoInfo()
  231. if (!repoInfo) return
  232. const formatted = formatSize(data.size * 1024)
  233. let defaultBranch = "master"
  234. if (data.default_branch) {
  235. defaultBranch = data.default_branch
  236. }
  237. const downloadURL = `https://github.com/${repoInfo.owner}/${repoInfo.repo}/archive/refs/heads/${defaultBranch}.zip`
  238. injectSize(formatted, downloadURL)
  239. }
  240. }
  241. function checkAndInsertWithRetry(retryCount = 0, maxRetries = 5) {
  242. const repoInfo = extractRepoInfo()
  243. if (!repoInfo) return
  244. fetchRepoInfo(repoInfo.owner, repoInfo.repo).catch((err) => {
  245. if (retryCount < maxRetries) {
  246. const delay = Math.pow(2, retryCount) * 500
  247. setTimeout(() => checkAndInsertWithRetry(retryCount + 1, maxRetries), delay)
  248. }
  249. })
  250. }
  251. function handleRouteChange() {
  252. const repoInfo = extractRepoInfo()
  253. if (!repoInfo) return
  254. const pathParts = window.location.pathname.split("/").filter(Boolean)
  255. if (pathParts.length !== 2) return
  256. if (debounceTimer) {
  257. clearTimeout(debounceTimer)
  258. }
  259. debounceTimer = setTimeout(() => {
  260. checkAndInsertWithRetry()
  261. }, 300)
  262. }
  263. const observer = new MutationObserver(() => {
  264. handleRouteChange()
  265. })
  266. observer.observe(document.body, { childList: true, subtree: true })
  267. ;(() => {
  268. const origPushState = history.pushState
  269. const origReplaceState = history.replaceState
  270. let lastPath = location.pathname
  271. function checkPathChange() {
  272. if (location.pathname !== lastPath) {
  273. lastPath = location.pathname
  274. setTimeout(handleRouteChange, 300)
  275. }
  276. }
  277. history.pushState = function (...args) {
  278. origPushState.apply(this, args)
  279. checkPathChange()
  280. }
  281. history.replaceState = function (...args) {
  282. origReplaceState.apply(this, args)
  283. checkPathChange()
  284. }
  285. window.addEventListener("popstate", checkPathChange)
  286. })()
  287. if (document.readyState === "loading") {
  288. document.addEventListener("DOMContentLoaded", handleRouteChange)
  289. } else {
  290. handleRouteChange()
  291. }
  292. })()