Add labels to GitHub notifications

Use API calls to get the labels of all issues and pull requests from the notification list.

目前为 2023-08-03 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Add labels to GitHub notifications
  3. // @namespace https://greasyfork.org/en/users/668659-denvercoder1
  4. // @match https://github.com/notifications
  5. // @grant none
  6. // @license MIT
  7. // @version 1.0.2
  8. // @author Jonah Lawrence
  9. // @description Use API calls to get the labels of all issues and pull requests from the notification list.
  10. // ==/UserScript==
  11.  
  12. /*
  13. * Get more GitHub API requests and enable private repos with a personal access token:
  14. * localStorage.setItem("gh_token", "YOUR_TOKEN_HERE");
  15. *
  16. * To get a personal access token go to https://github.com/settings/tokens/new
  17. * To enable private repos, you will need to enable the repos scope for the token.
  18. *
  19. * Manually clear cache by running the following in the console:
  20. * localStorage.setItem("labels", "{}");
  21. */
  22.  
  23. (() => {
  24. function hexToRgb(hex) {
  25. const bigint = parseInt(hex, 16);
  26. const r = (bigint >> 16) & 255;
  27. const g = (bigint >> 8) & 255;
  28. const b = bigint & 255;
  29. return [r, g, b];
  30. }
  31.  
  32. function hexToHsl(hex) {
  33. let [r, g, b] = hexToRgb(hex);
  34. r /= 255;
  35. g /= 255;
  36. b /= 255;
  37. const l = Math.max(r, g, b);
  38. const s = l - Math.min(r, g, b);
  39. const h = s ? (l === r ? (g - b) / s : l === g ? 2 + (b - r) / s : 4 + (r - g) / s) : 0;
  40. return [
  41. 60 * h < 0 ? 60 * h + 360 : 60 * h,
  42. 100 * (s ? (l <= 0.5 ? s / (2 * l - s) : s / (2 - (2 * l - s))) : 0),
  43. (100 * (2 * l - s)) / 2,
  44. ];
  45. }
  46.  
  47. function addLabels(labels, container) {
  48. // if there are already labels, do nothing
  49. if (container.querySelector(".js-issue-labels")) {
  50. return;
  51. }
  52. // append colored labels to the notification list
  53. const labelContainer = document.createElement("div");
  54. labelContainer.className = "js-issue-labels d-flex flex-wrap";
  55. labelContainer.style.marginTop = "10px";
  56. labelContainer.style.maxHeight = "20px";
  57. labels.forEach((label) => {
  58. const labelElement = document.createElement("span");
  59. labelElement.className = "IssueLabel hx_IssueLabel width-fit mb-1 mr-1 d-inline-flex";
  60. const [r, g, b] = hexToRgb(label.color);
  61. const [h, s, l] = hexToHsl(label.color);
  62. labelElement.setAttribute(
  63. "style",
  64. `--label-r:${r};--label-g:${g};--label-b:${b};--label-h:${h};--label-s:${s};--label-l:${l}; cursor:pointer;`,
  65. );
  66. labelElement.innerText = label.name;
  67. labelElement.addEventListener("click", (e) => {
  68. e.stopPropagation();
  69. window.open(label.filterUrl);
  70. });
  71. labelContainer.appendChild(labelElement);
  72. });
  73. container.appendChild(labelContainer);
  74. }
  75.  
  76. function run() {
  77. const notificationLinks = [...document.querySelectorAll(".notification-list-item-link:not(.added-notifications)")];
  78. if (notificationLinks.length === 0) {
  79. return;
  80. }
  81. const cachedLabels = JSON.parse(localStorage.getItem("labels") || "{}");
  82. notificationLinks.map((a) => {
  83. a.classList.add("added-notifications");
  84. const url = a.href;
  85. if (cachedLabels[url]) {
  86. console.info("cached", url, cachedLabels[url]);
  87. addLabels(cachedLabels[url], a.parentElement);
  88. return;
  89. }
  90. const issueRegex = /https:\/\/github.com\/(.*)\/(.*)\/(issues|pull)\/(\d+)/;
  91. const match = url.match(issueRegex);
  92. if (match) {
  93. const [, owner, repo, , number] = match;
  94. const apiUrl = `https://api.github.com/repos/${owner}/${repo}/issues/${number}`;
  95. const headers = {
  96. Accept: "application/vnd.github.v3+json",
  97. };
  98. const token = localStorage.getItem("gh_token") || "";
  99. if (token) {
  100. headers.Authorization = `token ${token}`;
  101. }
  102. fetch(apiUrl, {
  103. headers,
  104. })
  105. .then((response) => response.json())
  106. .then((data) => {
  107. const labels = data.labels || [];
  108. console.info("fetched", apiUrl, labels);
  109. cachedLabels[url] = labels.map((label) => ({
  110. date: new Date(),
  111. name: label.name,
  112. color: label.color,
  113. filterUrl: `https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues?q=is%3Aopen+label%3A"${encodeURIComponent(label.name)}"`,
  114. }));
  115. localStorage.setItem("labels", JSON.stringify(cachedLabels));
  116. addLabels(cachedLabels[url], a.parentElement);
  117. })
  118. .catch((error) => console.error(error));
  119. }
  120. });
  121. }
  122.  
  123. function init() {
  124. // clear cache older than 2 hours
  125. const cachedLabels = JSON.parse(localStorage.getItem("labels") || "{}");
  126. Object.keys(cachedLabels).forEach((url) => {
  127. const { date } = cachedLabels[url];
  128. if (new Date() - new Date(date) > 2 * 60 * 60 * 1000) {
  129. delete cachedLabels[url];
  130. }
  131. });
  132. localStorage.setItem("labels", JSON.stringify(cachedLabels));
  133.  
  134. // run every 500ms
  135. setInterval(run, 500);
  136. }
  137.  
  138. // run init when the page loads or if it has already loaded
  139. if (document.readyState === "loading") {
  140. document.addEventListener("DOMContentLoaded", init);
  141. } else {
  142. init();
  143. }
  144. })();