GitHub Repo Size Display

Display repository size on GitHub repo pages

As of 2024-08-16. See the latest version.

  1. // ==UserScript==
  2. // @name GitHub Repo Size Display
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description Display repository size on GitHub repo pages
  6. // @license MIT
  7. // @author Lainbo
  8. // @match https://github.com/*
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_registerMenuCommand
  12. // @grant GM_addStyle
  13. // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMTQgMTQiPjxnIGZpbGw9Im5vbmUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHdpZHRoPSIxMSIgaGVpZ2h0PSIxMyIgeD0iMS41IiB5PSIuNSIgcng9IjEiLz48cGF0aCBkPSJtNC41IDEwLjVsMS43NS0yLjc1TTQuMDkgN0EyLjkzIDIuOTMgMCAwIDEgNCA1LjE2YTMgMyAwIDEgMSA0LjY3IDMuMjdNNy41IDExSDEwIi8+PC9nPjwvc3ZnPg==
  14. // ==/UserScript==
  15.  
  16. (function () {
  17. 'use strict'
  18.  
  19. let observer
  20. let repoTitleComponent
  21.  
  22. GM_addStyle(`
  23. .modal-overlay {
  24. position: fixed;
  25. top: 0;
  26. left: 0;
  27. right: 0;
  28. bottom: 0;
  29. background-color: rgba(0, 0, 0, 0.5);
  30. display: flex;
  31. justify-content: center;
  32. align-items: center;
  33. z-index: 9999;
  34. }
  35. .modal-content {
  36. background-color: #f6f8fa;
  37. padding: 24px;
  38. border-radius: 6px;
  39. width: 340px;
  40. box-shadow: 0 8px 24px rgba(140,149,159,0.2);
  41. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  42. }
  43. .modal-title {
  44. font-size: 20px;
  45. font-weight: 600;
  46. margin-bottom: 16px;
  47. color: #24292f;
  48. }
  49. .modal-description {
  50. font-size: 14px;
  51. color: #57606a;
  52. margin-bottom: 16px;
  53. line-height: 1.5;
  54. }
  55. .modal-content input {
  56. width: 100%;
  57. margin-bottom: 16px;
  58. padding: 5px 12px;
  59. font-size: 14px;
  60. line-height: 20px;
  61. color: #24292f;
  62. vertical-align: middle;
  63. background-color: #ffffff;
  64. background-repeat: no-repeat;
  65. background-position: right 8px center;
  66. border: 1px solid #d0d7de;
  67. border-radius: 6px;
  68. box-shadow: inset 0 1px 0 rgba(208,215,222,0.2);
  69. }
  70. .modal-content button {
  71. color: #ffffff;
  72. background-color: #2da44e;
  73. padding: 5px 16px;
  74. font-size: 14px;
  75. font-weight: 500;
  76. line-height: 20px;
  77. white-space: nowrap;
  78. vertical-align: middle;
  79. cursor: pointer;
  80. border: 1px solid;
  81. border-radius: 6px;
  82. appearance: none;
  83. user-select: none;
  84. margin-right: 8px;
  85. }
  86. .modal-content button.cancel {
  87. color: #24292f;
  88. background-color: #f6f8fa;
  89. border-color: rgba(27,31,36,0.15);
  90. }
  91. .modal-content button:hover {
  92. background-color: #2c974b;
  93. }
  94. .modal-content button.cancel:hover {
  95. background-color: #f3f4f6;
  96. }
  97. .repo-size-label.error {
  98. background-color: #ffdce0;
  99. color: #cf222e;
  100. }
  101. `)
  102.  
  103. function formatSize(sizeInKB) {
  104. if (sizeInKB >= 1024 * 1024) {
  105. return `${(sizeInKB / (1024 * 1024)).toFixed(2)} GB`
  106. }
  107. else if (sizeInKB >= 1024) {
  108. return `${(sizeInKB / 1024).toFixed(2)} MB`
  109. }
  110. else {
  111. return `${sizeInKB} KB`
  112. }
  113. }
  114.  
  115. function updateRepoSize(content, isError = false) {
  116. if (!repoTitleComponent) {
  117. repoTitleComponent = document.querySelector('#repo-title-component')
  118. if (!repoTitleComponent) { return }
  119. }
  120.  
  121. let sizeElement = repoTitleComponent.querySelector('.repo-size-label')
  122. if (!sizeElement) {
  123. sizeElement = document.createElement('span')
  124. sizeElement.className = 'Label Label--secondary v-align-middle ml-1 repo-size-label'
  125. repoTitleComponent.appendChild(sizeElement)
  126. }
  127.  
  128. sizeElement.textContent = content
  129. sizeElement.classList.toggle('error', isError)
  130. }
  131.  
  132. function fetchRepoSize() {
  133. const repoRegex = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/?$/
  134. const match = location.href.match(repoRegex)
  135.  
  136. if (!match) { return }
  137.  
  138. const [, owner, repo] = match
  139. const apiUrl = `https://api.github.com/repos/${owner}/${repo}`
  140. const token = GM_getValue('github_token', '')
  141.  
  142. if (!token) {
  143. updateRepoSize('Token not set', true)
  144. return
  145. }
  146.  
  147. fetch(apiUrl, {
  148. headers: { Authorization: `token ${token}` },
  149. })
  150. .then((response) => {
  151. if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) }
  152. return response.json()
  153. })
  154. .then((data) => {
  155. if (data.size !== undefined) {
  156. updateRepoSize(formatSize(data.size))
  157. }
  158. else {
  159. throw new Error('Size information not available')
  160. }
  161. })
  162. .catch((error) => {
  163. console.error('Error fetching repo size:', error)
  164. updateRepoSize('Size fetch failed', true)
  165. })
  166. }
  167.  
  168. function createModal() {
  169. const modalHTML = `
  170. <div class="modal-overlay">
  171. <div class="modal-content">
  172. <h2 class="modal-title">Set GitHub Token</h2>
  173. <p class="modal-description">Enter your GitHub personal access token with "repo" scope.</p>
  174. <input type="text" id="github-token-input" placeholder="Enter your GitHub personal access token">
  175. <button id="save-token">Save</button>
  176. <button id="cancel-token" class="cancel">Cancel</button>
  177. </div>
  178. </div>
  179. `
  180.  
  181. const modalContainer = document.createElement('div')
  182. modalContainer.innerHTML = modalHTML
  183. document.body.appendChild(modalContainer)
  184.  
  185. const input = document.getElementById('github-token-input')
  186. input.value = GM_getValue('github_token', '')
  187.  
  188. document.getElementById('save-token').addEventListener('click', () => {
  189. const token = input.value.trim()
  190. if (token) {
  191. GM_setValue('github_token', token)
  192. modalContainer.remove()
  193. location.reload()
  194. }
  195. })
  196.  
  197. document.getElementById('cancel-token').addEventListener('click', () => modalContainer.remove())
  198. }
  199.  
  200. function observePageChanges() {
  201. if (observer) {
  202. observer.disconnect()
  203. }
  204.  
  205. const targetNode = document.body
  206. const config = { childList: true, subtree: true }
  207.  
  208. observer = new MutationObserver((mutationsList, observer) => {
  209. for (const mutation of mutationsList) {
  210. if (mutation.type === 'childList') {
  211. repoTitleComponent = document.querySelector('#repo-title-component')
  212. if (repoTitleComponent && !repoTitleComponent.querySelector('.repo-size-label')) {
  213. fetchRepoSize()
  214. observer.disconnect()
  215. break
  216. }
  217. }
  218. }
  219. })
  220.  
  221. observer.observe(targetNode, config)
  222. }
  223.  
  224. function init() {
  225. fetchRepoSize()
  226. observePageChanges()
  227. }
  228.  
  229. // Register the menu command to set the GitHub token
  230. GM_registerMenuCommand('Set GitHub Token', createModal)
  231.  
  232. // Execute on page load
  233. window.addEventListener('load', init)
  234.  
  235. // Listen for GitHub's pjax:end event to handle updates during internal navigation
  236. document.addEventListener('pjax:end', init)
  237. })()