GitHub Repo Notes

Add local notes to GitHub repository

目前為 2025-05-14 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name GitHub Repo Notes
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description Add local notes to GitHub repository
  6. // @author Ivans
  7. // @match https://github.com/*
  8. // @grant none
  9. // @icon https://cdn.simpleicons.org/github/808080
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. const NOTE_KEY_PREFIX = 'gh_repo_note_';
  17.  
  18. // Get the full name of the repository
  19. function getRepoFullName(card) {
  20. const link = card.querySelector('h3 a[itemprop="name codeRepository"]') ||
  21. card.querySelector('h3 a') ||
  22. card.querySelector('.search-title a') ||
  23. card.querySelector('a.Link--primary.Link.text-bold[data-hovercard-type="repository"]');
  24. if (!link) return null;
  25. const href = link.getAttribute('href');
  26. if (!href) return null;
  27. return href.substring(1);
  28. }
  29.  
  30. // Get the note from local storage
  31. function getNote(repoFullName) {
  32. // Convert to lowercase
  33. return localStorage.getItem(NOTE_KEY_PREFIX + repoFullName.toLowerCase()) || '';
  34. }
  35. // Set the note to local storage
  36. function setNote(repoFullName, note) {
  37. if (note) {
  38. localStorage.setItem(NOTE_KEY_PREFIX + repoFullName.toLowerCase(), note);
  39. } else {
  40. localStorage.removeItem(NOTE_KEY_PREFIX + repoFullName.toLowerCase());
  41. }
  42. }
  43.  
  44. // Create the note button
  45. function createNoteButton(repoFullName, note, onClick) {
  46. const btn = document.createElement('button');
  47. const icon = document.createElement('span');
  48. icon.innerHTML = `<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" style="vertical-align: text-bottom; margin-right: 8px; fill: var(--fgColor-muted,var(--color-fg-muted));">
  49. <path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Z"></path>
  50. </svg>`;
  51. btn.appendChild(icon);
  52. btn.appendChild(document.createTextNode(note ? 'Exit' : 'Add'));
  53. btn.style.margin = '4px';
  54. btn.style.borderRadius = '6px';
  55. btn.style.padding = '2px 8px';
  56. btn.style.fontSize = '12px';
  57. btn.style.cursor = 'pointer';
  58. btn.style.fontFamily = 'var(--fontStack-sansSerif)';
  59. btn.style.lineHeight = '20px';
  60. btn.style.fontWeight = '600';
  61. btn.style.color = 'var(--button-default-fgColor-rest, var(--color-btn-text))';
  62. btn.style.backgroundColor = 'var(--button-default-bgColor-rest, var(--color-btn-bg))';
  63. btn.style.border = '1px solid var(--button-default-borderColor-rest,var(--color-btn-border))';
  64. btn.style.display = 'flex';
  65. btn.style.alignItems = 'center';
  66. btn.style.height = 'var(--control-small-size,1.75rem)';
  67. btn.addEventListener('click', onClick);
  68. return btn;
  69. }
  70.  
  71. // Create the note display
  72. function createNoteDisplay(note) {
  73. const div = document.createElement('div');
  74. const icon = document.createElement('span');
  75. icon.innerHTML = `<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" style="vertical-align: text-bottom; margin-right: 8px; fill: var(--fgColor-muted,var(--color-fg-muted));">
  76. <path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Z"></path>
  77. </svg>`;
  78. div.appendChild(icon);
  79. div.appendChild(document.createTextNode(note));
  80. div.style.borderRadius = '6px';
  81. div.style.padding = '4px 8px';
  82. div.style.marginTop = '2px';
  83. div.style.marginBottom = '6px';
  84. div.style.fontSize = '13px';
  85. div.style.fontFamily = 'var(--fontStack-sansSerif)';
  86. div.style.lineHeight = '20px';
  87. div.style.display = 'flex';
  88. div.style.alignItems = 'center';
  89. return div;
  90. }
  91.  
  92. // Prompt the user to input the note
  93. function promptNote(oldNote) {
  94. let note = prompt('Please input your notes (leave blank to be deleted):', oldNote || '');
  95. if (note === null) return undefined;
  96. note = note.trim();
  97. return note;
  98. }
  99.  
  100. // Insert the button and note on the card
  101. function enhanceCard(card) {
  102. if (card.dataset.noteEnhanced) return; // Avoid duplicate
  103. const repoFullName = getRepoFullName(card);
  104. if (!repoFullName) return;
  105. card.dataset.noteEnhanced = '1';
  106. // Find the star button
  107. let starBtn = card.querySelector('.js-toggler-container.js-social-container.starring-container, .Box-sc-g0xbh4-0.fvaNTI');
  108. if (!starBtn) return;
  109. // Create the button
  110. let note = getNote(repoFullName);
  111. let btn = createNoteButton(repoFullName, note, function() {
  112. let newNote = promptNote(note);
  113. if (typeof newNote === 'undefined') return;
  114. setNote(repoFullName, newNote);
  115. // Re-render
  116. card.dataset.noteEnhanced = '';
  117. enhanceCard(card);
  118. });
  119. // Insert the button
  120. if (starBtn.parentElement) {
  121. // Avoid duplicate insertion
  122. let oldBtn = starBtn.parentElement.querySelector('.gh-note-btn');
  123. if (oldBtn) oldBtn.remove();
  124. btn.classList.add('gh-note-btn');
  125. starBtn.parentElement.appendChild(btn);
  126. }
  127. // Note display
  128. let oldNoteDiv = card.querySelector('.gh-note-display');
  129. if (oldNoteDiv) oldNoteDiv.remove();
  130. if (note) {
  131. let noteDiv = createNoteDisplay(note);
  132. noteDiv.classList.add('gh-note-display');
  133. // Put it before the data bar
  134. let dataInfo = card.querySelector('.f6.color-fg-muted.mt-0.mb-0.width-full, .f6.color-fg-muted.mt-2, .Box-sc-g0xbh4-0.bZkODq');
  135. if (dataInfo && dataInfo.parentElement) {
  136. dataInfo.parentElement.insertBefore(noteDiv, dataInfo);
  137. }
  138. }
  139. }
  140.  
  141. // Select all repository cards
  142. function findAllRepoCards() {
  143. // Adapt to multiple card structures
  144. let cards = Array.from(document.querySelectorAll(`
  145. .col-12.d-block.width-full.py-4.border-bottom.color-border-muted,
  146. li.col-12.d-flex.flex-justify-between.width-full.py-4.border-bottom.color-border-muted,
  147. .Box-sc-g0xbh4-0.iwUbcA,
  148. .Box-sc-g0xbh4-0.flszRz,
  149. .Box-sc-g0xbh4-0.jbaXRR,
  150. .Box-sc-g0xbh4-0.bmHqGc,
  151. .Box-sc-g0xbh4-0.hFxojJ,
  152. section[aria-label="card content"]
  153. `));
  154. // Filter out cards without a repository full name
  155. return cards.filter(card => getRepoFullName(card));
  156. }
  157.  
  158. // Determine if it is a repository page
  159. function isRepoPage() {
  160. const path = window.location.pathname;
  161. const parts = path.split('/').filter(Boolean);
  162. return (parts.length === 2 || (parts.length === 4 && parts[2] === 'tree')) &&
  163. !path.includes('/blob/') &&
  164. !path.includes('/issues/') &&
  165. !path.includes('/pulls/');
  166. }
  167.  
  168. // Repository page processing function
  169. function enhanceRepoPage() {
  170. if (document.body.dataset.noteEnhanced) return; // Avoid duplicate
  171. // Get the repository name from the link
  172. const path = window.location.pathname;
  173. const parts = path.split('/').filter(Boolean);
  174. const repoFullName = parts.slice(0, 2).join('/');
  175. if (!repoFullName) return;
  176. document.body.dataset.noteEnhanced = '1';
  177.  
  178. // Find the button bar
  179. let actionsList = document.querySelector('.pagehead-actions');
  180. if (!actionsList) return;
  181.  
  182. // Create the button
  183. let note = getNote(repoFullName);
  184. let btn = createNoteButton(repoFullName, note, function() {
  185. let newNote = promptNote(note);
  186. if (typeof newNote === 'undefined') return;
  187. setNote(repoFullName, newNote);
  188. // Re-render
  189. document.body.dataset.noteEnhanced = '';
  190. enhanceRepoPage();
  191. });
  192.  
  193. // Create a new li element
  194. let li = document.createElement('li');
  195. li.appendChild(btn);
  196.  
  197. // Avoid duplicate insertion
  198. let oldLi = actionsList.querySelector('.gh-note-li');
  199. if (oldLi) oldLi.remove();
  200. li.classList.add('gh-note-li');
  201.  
  202. // Add to the button bar
  203. actionsList.appendChild(li);
  204.  
  205. // Note display
  206. let oldNoteDiv = document.querySelector('.gh-note-display');
  207. if (oldNoteDiv) oldNoteDiv.remove();
  208. if (note) {
  209. let noteDiv = createNoteDisplay(note);
  210. noteDiv.classList.add('gh-note-display');
  211. // Find the description
  212. let description = document.querySelector('.f4.my-3, .f4.my-3.color-fg-muted.text-italic');
  213. if (description && description.parentElement) {
  214. description.parentElement.insertBefore(noteDiv, description.nextSibling);
  215. }
  216. }
  217. }
  218.  
  219. // Initial processing
  220. function enhanceAll() {
  221. if (isRepoPage()) {
  222. enhanceRepoPage();
  223. } else {
  224. findAllRepoCards().forEach(enhanceCard);
  225. }
  226. }
  227.  
  228. // Listen for DOM changes to adapt to dynamic loading
  229. const observer = new MutationObserver(() => {
  230. enhanceAll();
  231. });
  232. observer.observe(document.body, {childList: true, subtree: true});
  233.  
  234. // First load
  235. enhanceAll();
  236. })();