GitHub Repo Size Display

Display repository size on GitHub repo pages

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