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.1
  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 tokenCreationUrl = new URL('https://github.com/settings/tokens/new')
  170. tokenCreationUrl.searchParams.append('description', 'GitHub Repo Size Display')
  171. tokenCreationUrl.searchParams.append('scopes', 'repo')
  172.  
  173. const modalHTML = `
  174. <div class="modal-overlay">
  175. <div class="modal-content">
  176. <h2 class="modal-title">Set GitHub Token</h2>
  177. <p class="modal-description">
  178. Enter your GitHub personal access token with "repo" scope.
  179. <a href="${tokenCreationUrl.toString()}" target="_blank" rel="noopener noreferrer">
  180. Click here to create a new token
  181. </a>
  182. </p>
  183. <input type="text" id="github-token-input" placeholder="Enter your GitHub personal access token">
  184. <button id="save-token">Save</button>
  185. <button id="cancel-token" class="cancel">Cancel</button>
  186. </div>
  187. </div>
  188. `
  189.  
  190. const modalContainer = document.createElement('div')
  191. modalContainer.innerHTML = modalHTML
  192. document.body.appendChild(modalContainer)
  193.  
  194. const input = document.getElementById('github-token-input')
  195. input.value = GM_getValue('github_token', '')
  196.  
  197. document.getElementById('save-token').addEventListener('click', () => {
  198. const token = input.value.trim()
  199. if (token) {
  200. GM_setValue('github_token', token)
  201. modalContainer.remove()
  202. location.reload()
  203. }
  204. })
  205.  
  206. document.getElementById('cancel-token').addEventListener('click', () => modalContainer.remove())
  207. }
  208.  
  209. function observePageChanges() {
  210. if (observer) {
  211. observer.disconnect()
  212. }
  213.  
  214. const targetNode = document.body
  215. const config = { childList: true, subtree: true }
  216.  
  217. observer = new MutationObserver((mutationsList, observer) => {
  218. for (const mutation of mutationsList) {
  219. if (mutation.type === 'childList') {
  220. repoTitleComponent = document.querySelector('#repo-title-component')
  221. if (repoTitleComponent && !repoTitleComponent.querySelector('.repo-size-label')) {
  222. fetchRepoSize()
  223. observer.disconnect()
  224. break
  225. }
  226. }
  227. }
  228. })
  229.  
  230. observer.observe(targetNode, config)
  231. }
  232.  
  233. function init() {
  234. fetchRepoSize()
  235. observePageChanges()
  236. }
  237.  
  238. // Register the menu command to set the GitHub token
  239. GM_registerMenuCommand('Set GitHub Token', createModal)
  240.  
  241. // Execute on page load
  242. window.addEventListener('load', init)
  243.  
  244. // Listen for GitHub's pjax:end event to handle updates during internal navigation
  245. document.addEventListener('pjax:end', init)
  246. })()