GitHub 仓库备注工具

为 GitHub 仓库添加本地备注

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

  1. // ==UserScript==
  2. // @name GitHub Repo Notes
  3. // @name:zh-CN GitHub 仓库备注工具
  4. // @namespace http://tampermonkey.net/
  5. // @version 1.1
  6. // @description Add local notes to GitHub repository
  7. // @description:zh-CN 为 GitHub 仓库添加本地备注
  8. // @author Ivans
  9. // @match https://github.com/*
  10. // @grant none
  11. // @icon https://cdn.simpleicons.org/github/808080
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. const NOTE_KEY_PREFIX = 'gh_repo_note_';
  19.  
  20. // Get the full name of the repository
  21. function getRepoFullName(card) {
  22. const link = card.querySelector('h3 a[itemprop="name codeRepository"]') ||
  23. card.querySelector('h3 a') ||
  24. card.querySelector('.search-title a') ||
  25. card.querySelector('a.Link--primary.Link.text-bold[data-hovercard-type="repository"]');
  26. if (!link) return null;
  27. const href = link.getAttribute('href');
  28. if (!href) return null;
  29. return href.substring(1);
  30. }
  31.  
  32. // Get the note from local storage
  33. function getNote(repoFullName) {
  34. // Convert to lowercase
  35. return localStorage.getItem(NOTE_KEY_PREFIX + repoFullName.toLowerCase()) || '';
  36. }
  37. // Set the note to local storage
  38. function setNote(repoFullName, note) {
  39. if (note) {
  40. localStorage.setItem(NOTE_KEY_PREFIX + repoFullName.toLowerCase(), note);
  41. } else {
  42. localStorage.removeItem(NOTE_KEY_PREFIX + repoFullName.toLowerCase());
  43. }
  44. }
  45.  
  46. // Create the note button
  47. function createNoteButton(repoFullName, note, onClick) {
  48. const btn = document.createElement('button');
  49. const icon = document.createElement('span');
  50. 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));">
  51. <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>
  52. </svg>`;
  53. btn.appendChild(icon);
  54. btn.appendChild(document.createTextNode(note ? 'Edit' : 'Add'));
  55. btn.style.margin = '4px';
  56. btn.style.borderRadius = '6px';
  57. btn.style.padding = '2px 8px';
  58. btn.style.fontSize = '12px';
  59. btn.style.cursor = 'pointer';
  60. btn.style.fontFamily = 'var(--fontStack-sansSerif)';
  61. btn.style.lineHeight = '20px';
  62. btn.style.fontWeight = '600';
  63. btn.style.color = 'var(--button-default-fgColor-rest, var(--color-btn-text))';
  64. btn.style.backgroundColor = 'var(--button-default-bgColor-rest, var(--color-btn-bg))';
  65. btn.style.border = '1px solid var(--button-default-borderColor-rest,var(--color-btn-border))';
  66. btn.style.display = 'flex';
  67. btn.style.alignItems = 'center';
  68. btn.style.height = 'var(--control-small-size,1.75rem)';
  69. btn.addEventListener('click', onClick);
  70. return btn;
  71. }
  72.  
  73. // Create the note display
  74. function createNoteDisplay(note) {
  75. const div = document.createElement('div');
  76. const icon = document.createElement('span');
  77. 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));">
  78. <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>
  79. </svg>`;
  80. div.appendChild(icon);
  81. div.appendChild(document.createTextNode(note));
  82. div.style.borderRadius = '6px';
  83. div.style.padding = '4px 8px';
  84. div.style.marginTop = '2px';
  85. div.style.marginBottom = '6px';
  86. div.style.fontSize = '13px';
  87. div.style.fontFamily = 'var(--fontStack-sansSerif)';
  88. div.style.lineHeight = '20px';
  89. div.style.display = 'flex';
  90. div.style.alignItems = 'center';
  91. return div;
  92. }
  93.  
  94. // Prompt the user to input the note
  95. function promptNote(oldNote) {
  96. let note = prompt('Please input your notes (leave blank to be deleted):', oldNote || '');
  97. if (note === null) return undefined;
  98. note = note.trim();
  99. return note;
  100. }
  101.  
  102. // Insert the button and note on the card
  103. function enhanceCard(card) {
  104. if (card.dataset.noteEnhanced) return; // Avoid duplicate
  105. const repoFullName = getRepoFullName(card);
  106. if (!repoFullName) return;
  107. card.dataset.noteEnhanced = '1';
  108. // Find the star button
  109. let starBtn = card.querySelector('.js-toggler-container.js-social-container.starring-container, .Box-sc-g0xbh4-0.fvaNTI');
  110. if (!starBtn) return;
  111. // Create the button
  112. let note = getNote(repoFullName);
  113. let btn = createNoteButton(repoFullName, note, function() {
  114. let newNote = promptNote(note);
  115. if (typeof newNote === 'undefined') return;
  116. setNote(repoFullName, newNote);
  117. // Re-render
  118. card.dataset.noteEnhanced = '';
  119. enhanceCard(card);
  120. });
  121. // Insert the button
  122. if (starBtn.parentElement) {
  123. // Avoid duplicate insertion
  124. let oldBtn = starBtn.parentElement.querySelector('.gh-note-btn');
  125. if (oldBtn) oldBtn.remove();
  126. btn.classList.add('gh-note-btn');
  127. starBtn.parentElement.appendChild(btn);
  128. }
  129. // Note display
  130. let oldNoteDiv = card.querySelector('.gh-note-display');
  131. if (oldNoteDiv) oldNoteDiv.remove();
  132. if (note) {
  133. let noteDiv = createNoteDisplay(note);
  134. noteDiv.classList.add('gh-note-display');
  135. // Put it before the data bar
  136. 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');
  137. if (dataInfo && dataInfo.parentElement) {
  138. dataInfo.parentElement.insertBefore(noteDiv, dataInfo);
  139. }
  140. }
  141. }
  142.  
  143. // Select all repository cards
  144. function findAllRepoCards() {
  145. // Adapt to multiple card structures
  146. let cards = Array.from(document.querySelectorAll(`
  147. .col-12.d-block.width-full.py-4.border-bottom.color-border-muted,
  148. li.col-12.d-flex.flex-justify-between.width-full.py-4.border-bottom.color-border-muted,
  149. .Box-sc-g0xbh4-0.iwUbcA,
  150. .Box-sc-g0xbh4-0.flszRz,
  151. .Box-sc-g0xbh4-0.jbaXRR,
  152. .Box-sc-g0xbh4-0.bmHqGc,
  153. .Box-sc-g0xbh4-0.hFxojJ,
  154. section[aria-label="card content"]
  155. `));
  156. // Filter out cards without a repository full name
  157. return cards.filter(card => getRepoFullName(card));
  158. }
  159.  
  160. // Determine if it is a repository page
  161. function isRepoPage() {
  162. const path = window.location.pathname;
  163. const parts = path.split('/').filter(Boolean);
  164. return (parts.length === 2 || (parts.length === 4 && parts[2] === 'tree')) &&
  165. !path.includes('/blob/') &&
  166. !path.includes('/issues/') &&
  167. !path.includes('/pulls/');
  168. }
  169.  
  170. // Repository page processing function
  171. function enhanceRepoPage() {
  172. if (document.body.dataset.noteEnhanced) return; // Avoid duplicate
  173. // Get the repository name from the link
  174. const path = window.location.pathname;
  175. const parts = path.split('/').filter(Boolean);
  176. const repoFullName = parts.slice(0, 2).join('/');
  177. if (!repoFullName) return;
  178. document.body.dataset.noteEnhanced = '1';
  179.  
  180. // Create the first button
  181. let note = getNote(repoFullName);
  182. let btn = createNoteButton(repoFullName, note, function() {
  183. let newNote = promptNote(note);
  184. if (typeof newNote === 'undefined') return;
  185. setNote(repoFullName, newNote);
  186. // Re-render
  187. document.body.dataset.noteEnhanced = '';
  188. enhanceRepoPage();
  189. });
  190.  
  191. // Find the button bar
  192. let actionsList = document.querySelector('.pagehead-actions');
  193. if (actionsList) {
  194. // Create a new li element
  195. let li = document.createElement('li');
  196. li.appendChild(btn);
  197.  
  198. // Avoid duplicate insertion
  199. let oldLi = actionsList.querySelector('.gh-note-li');
  200. if (oldLi) oldLi.remove();
  201. li.classList.add('gh-note-li');
  202.  
  203. // Add to the button bar
  204. actionsList.appendChild(li);
  205. }
  206.  
  207. // Create the second button
  208. let btn2 = createNoteButton(repoFullName, note, function() {
  209. let newNote = promptNote(note);
  210. if (typeof newNote === 'undefined') return;
  211. setNote(repoFullName, newNote);
  212. // Re-render
  213. document.body.dataset.noteEnhanced = '';
  214. enhanceRepoPage();
  215. });
  216.  
  217. // Find the container
  218. let container = document.querySelector('.container-xl:not(.d-flex):not(.clearfix)');
  219. if (container) {
  220. // Find the star button
  221. let starBtn = container.querySelector('div[data-view-component="true"].js-toggler-container.starring-container');
  222. if (starBtn && starBtn.parentElement) {
  223. // Avoid duplicate insertion
  224. let oldBtn = starBtn.parentElement.querySelector('.gh-note-btn');
  225. if (oldBtn) oldBtn.remove();
  226. let newBtn = btn2;
  227. newBtn.classList.add('gh-note-btn');
  228. starBtn.parentElement.appendChild(newBtn);
  229. }
  230. }
  231.  
  232. // Remove the old note display
  233. let oldNoteDiv = document.querySelector('.gh-note-display');
  234. if (oldNoteDiv) oldNoteDiv.remove();
  235. let oldNoteDiv2 = document.querySelector('.gh-note-display2');
  236. if (oldNoteDiv2) oldNoteDiv2.remove();
  237. if (note) {
  238. // Create the first note display
  239. let noteDiv = createNoteDisplay(note);
  240. noteDiv.classList.add('gh-note-display');
  241. // Find the description
  242. let description = document.querySelector('.f4.my-3, .f4.my-3.color-fg-muted.text-italic');
  243. if (description && description.parentElement) {
  244. description.parentElement.insertBefore(noteDiv, description.nextSibling);
  245. }
  246.  
  247. // Create the second note display
  248. let noteDiv2 = createNoteDisplay(note);
  249. noteDiv2.classList.add('gh-note-display2');
  250.  
  251. if (container) {
  252. // Try finding the description
  253. let newDescription = container.querySelector('p.f4.mb-3.color-fg-muted');
  254. if (newDescription && newDescription.parentElement) {
  255. newDescription.parentElement.insertBefore(noteDiv2, newDescription.nextSibling);
  256. } else {
  257. // If there is no description, find the flex container
  258. let flexContainer = container.querySelector('div.d-flex.gap-2.mt-n3.mb-3.flex-wrap');
  259. if (flexContainer && flexContainer.parentElement) {
  260. flexContainer.parentElement.insertBefore(noteDiv2, flexContainer.nextSibling);
  261. }
  262. }
  263. }
  264. }
  265. }
  266.  
  267. // Initial processing
  268. function enhanceAll() {
  269. if (isRepoPage()) {
  270. enhanceRepoPage();
  271. } else {
  272. findAllRepoCards().forEach(enhanceCard);
  273. }
  274. }
  275.  
  276. // Listen for DOM changes to adapt to dynamic loading
  277. const observer = new MutationObserver(() => {
  278. enhanceAll();
  279. });
  280. observer.observe(document.body, {childList: true, subtree: true});
  281.  
  282. // First load
  283. enhanceAll();
  284. })();