GreenWall - 查看历年 GitHub 的贡献图 ⬜🟩

在 GitHub 中查看用户历年的贡献图。

  1. // ==UserScript==
  2. // @name GreenWall: View all contribution graphs in GitHub ⬜🟩
  3. // @description View a graph of users' contributions over the years in GitHub.
  4. // @name:zh-CN GreenWall - 查看历年 GitHub 的贡献图 ⬜🟩
  5. // @description:zh-CN 在 GitHub 中查看用户历年的贡献图。
  6. // @version 1.2.0
  7. // @namespace https://green-wall.leoku.dev
  8. // @author LeoKu(https://leoku.dev)
  9. // @match https://github.com/*
  10. // @run-at document-start
  11. // @icon https://green-wall.leoku.dev/favicon.svg
  12. // @grant GM.xmlHttpRequest
  13. // @homepageURL https://github.com/Codennnn/Green-Wall
  14. // @license MIT
  15. // ==/UserScript==
  16.  
  17. var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
  18. if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
  19. if (ar || !(i in from)) {
  20. if (!ar) ar = Array.prototype.slice.call(from, 0, i);
  21. ar[i] = from[i];
  22. }
  23. }
  24. return to.concat(ar || Array.prototype.slice.call(from));
  25. };
  26. function addHistoryEvent(type) {
  27. var originalMethod = window.history[type];
  28. return function () {
  29. var args = [];
  30. for (var _i = 0; _i < arguments.length; _i++) {
  31. args[_i] = arguments[_i];
  32. }
  33. originalMethod.apply(window.history, args);
  34. var ev = new Event(type);
  35. window.dispatchEvent(ev);
  36. };
  37. }
  38. window.history.replaceState = addHistoryEvent('replaceState');
  39. var handler = function () {
  40. var _a, _b, _c;
  41. var githubUserPageRegex = /^https:\/\/github\.com\/[a-zA-Z0-9-]+(?=\/?$)/;
  42. var isProfile = githubUserPageRegex.test(window.location.href);
  43. if (isProfile) {
  44. var ORIGIN_1 = 'https://green-wall.leoku.dev';
  45. var produceData_1 = function (_a) {
  46. var data = _a.data;
  47. var contributionCalendars = data.contributionCalendars.map(function (cur) {
  48. var rows = [[], [], [], [], [], [], []];
  49. var nullDay = { count: 0, date: '', level: 'Null' };
  50. cur.weeks.forEach(function (_a) {
  51. var days = _a.days;
  52. if (days.length !== 7) {
  53. var newDays = __spreadArray([], days, true);
  54. for (var i = 0; i <= 6; i++) {
  55. var theDay = newDays.at(i);
  56. var weekday = i;
  57. if (theDay && typeof theDay.weekday === 'number') {
  58. if (theDay.weekday === weekday) {
  59. rows[theDay.weekday].push(theDay);
  60. }
  61. else {
  62. newDays.splice(i, 0, nullDay);
  63. rows[i].push(nullDay);
  64. }
  65. }
  66. else {
  67. rows[i].push(nullDay);
  68. }
  69. }
  70. }
  71. else {
  72. days.forEach(function (day) {
  73. if (typeof day.weekday === 'number') {
  74. rows[day.weekday].push(day);
  75. }
  76. });
  77. }
  78. });
  79. var calendar = {
  80. total: cur.total,
  81. year: cur.year,
  82. rows: rows,
  83. };
  84. return calendar;
  85. });
  86. return {
  87. contributionCalendars: contributionCalendars,
  88. };
  89. };
  90. var isHalloween_1 = false;
  91. var createGraph_1 = function (params) {
  92. var year = params.year, total = params.total, rows = params.rows;
  93. var table = document.createElement('table');
  94. table.classList.add('ContributionCalendar-grid');
  95. table.style.borderSpacing = '3px';
  96. table.style.overflow = 'hidden';
  97. table.style.position = 'relative';
  98. var tbody = document.createElement('tbody');
  99. var tr = document.createElement('tr');
  100. tr.style.height = '10px';
  101. rows.forEach(function (row) {
  102. var clonedTr = tr.cloneNode();
  103. var htmlStr = '';
  104. row.forEach(function (col, idx) {
  105. var td = '<td></td>';
  106. if (col.level !== "Null" /* ContributionLevel.Null */) {
  107. var level = col.level === "NONE" /* ContributionLevel.NONE */
  108. ? 0
  109. : col.level === "FIRST_QUARTILE" /* ContributionLevel.FIRST_QUARTILE */
  110. ? 1
  111. : col.level === "SECOND_QUARTILE" /* ContributionLevel.SECOND_QUARTILE */
  112. ? 2
  113. : col.level === "THIRD_QUARTILE" /* ContributionLevel.THIRD_QUARTILE */
  114. ? 3
  115. : 4;
  116. td = "\n <td\n title=\"".concat(col.count === 0 ? 'No' : col.count, " contributions in ").concat(col.date, "\"\n tabindex=\"-1\"\n data-ix=\"").concat(idx, "\"\n style=\"width: 10px\"\n data-level=\"").concat(level, "\"\n class=\"ContributionCalendar-day\"\n data-date=\"").concat(col.level, "\"\n aria-selected=\"false\"\n role=\"gridcell\"\n ></td>\n ");
  117. }
  118. htmlStr += td;
  119. });
  120. if (clonedTr instanceof HTMLTableRowElement) {
  121. clonedTr.innerHTML = htmlStr;
  122. tbody.append(clonedTr);
  123. }
  124. });
  125. table.appendChild(tbody);
  126. var graphItem = document.createElement('div');
  127. var countText = document.createElement('div');
  128. countText.style.marginBottom = '5px';
  129. countText.textContent = "".concat(total, " contributions in ").concat(year);
  130. graphItem.append(countText, table);
  131. if (isHalloween_1) {
  132. graphItem.classList.add('ContributionCalendar');
  133. graphItem.setAttribute('data-holiday', 'halloween');
  134. }
  135. return { graphItem: graphItem };
  136. };
  137. var createDialog = function (params) {
  138. var username = params.username;
  139. var dialog = document.createElement('dialog');
  140. dialog.id = 'green-wall-dialog';
  141. dialog.classList.add('Overlay', 'Overlay-whenNarrow', 'Overlay--size-medium-portrait', 'Overlay--motion-scaleFadeOverlay', 'Overlay-whenNarrow', 'Overlay--size-medium-portrait', 'Overlay--motion-scaleFade');
  142. dialog.style.minWidth = '720px';
  143. dialog.style.maxHeight = 'calc(100vh - 50px)';
  144. dialog.addEventListener('close', function () {
  145. document.body.classList.remove('has-modal');
  146. });
  147. var mouseDownTarget;
  148. var mouseDownHandler = function (ev) {
  149. if (ev.target instanceof HTMLElement) {
  150. mouseDownTarget = ev.target;
  151. }
  152. };
  153. var mouseUpHandler = function (ev) {
  154. if (ev.target instanceof HTMLDialogElement &&
  155. ev.target === mouseDownTarget &&
  156. ev.target === dialog) {
  157. dialog.close();
  158. }
  159. };
  160. dialog.addEventListener('mousedown', mouseDownHandler);
  161. dialog.addEventListener('mouseup', mouseUpHandler);
  162. // ---
  163. var wrap = document.createElement('div');
  164. wrap.style.display = 'flex';
  165. wrap.style.flexDirection = 'column';
  166. wrap.style.overflow = 'hidden';
  167. // ---
  168. var dialogHeader = document.createElement('div');
  169. dialogHeader.classList.add('Overlay-header');
  170. var contentWrap = document.createElement('div');
  171. contentWrap.classList.add('Overlay-headerContentWrap');
  172. var titleWrap = document.createElement('div');
  173. titleWrap.classList.add('Overlay-titleWrap');
  174. var title = document.createElement('h1');
  175. title.classList.add('Overlay-title');
  176. title.textContent = "".concat(username, "'s GreenWall");
  177. var actionWrap = document.createElement('div');
  178. actionWrap.classList.add('Overlay-actionWrap');
  179. var actionButton = document.createElement('button');
  180. actionButton.classList.add('close-button', 'Overlay-closeButton');
  181. actionButton.setAttribute('type', 'button');
  182. actionButton.innerHTML = "\n <svg aria-hidden=\"true\" height=\"16\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" data-view-component=\"true\" class=\"octicon octicon-x\">\n <path d=\"M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z\"></path>\n </svg>\n ";
  183. actionButton.addEventListener('click', function (ev) {
  184. ev.stopPropagation();
  185. dialog.close();
  186. });
  187. // ---
  188. var dialogBody = document.createElement('div');
  189. dialogBody.classList.add('Overlay-body');
  190. dialogBody.style.overflowY = 'auto';
  191. var dialogContent = document.createElement('div');
  192. dialogContent.style.display = 'flex';
  193. dialogContent.style.flexDirection = 'column';
  194. dialogContent.style.rowGap = '10px';
  195. dialogContent.style.alignItems = 'center';
  196. dialogContent.style.padding = 'var(--stack-padding-normal, 1rem)';
  197. // ---
  198. var dialogFooter = document.createElement('div');
  199. dialogFooter.classList.add('Overlay-footer', 'Overlay-footer--alignEnd', 'Overlay-footer--divided');
  200. var openExtrnalBtn = document.createElement('button');
  201. var btnContent = document.createElement('span');
  202. btnContent.classList.add('Button-label');
  203. btnContent.textContent = 'Open in Green Wall';
  204. openExtrnalBtn.classList.add('Button', 'Button--primary', 'Button--medium');
  205. openExtrnalBtn.addEventListener('click', function () {
  206. window.open("".concat(ORIGIN_1, "/user/").concat(username), '_blank');
  207. });
  208. titleWrap.append(title);
  209. actionWrap.append(actionButton);
  210. contentWrap.append(titleWrap, actionWrap);
  211. openExtrnalBtn.append(btnContent);
  212. dialogHeader.append(contentWrap);
  213. dialogBody.append(dialogContent);
  214. dialogFooter.append(openExtrnalBtn);
  215. wrap.append(dialogHeader, dialogBody, dialogFooter);
  216. dialog.append(wrap);
  217. document.body.append(dialog);
  218. return { dialog: dialog, dialogContent: dialogContent };
  219. };
  220. var profileArea = document.querySelector('.Layout-sidebar .h-card .js-profile-editable-replace');
  221. var refNode = (_b = (_a = document.querySelector('.js-profile-editable-replace > .d-flex.flex-column')) === null || _a === void 0 ? void 0 : _a.nextSibling) === null || _b === void 0 ? void 0 : _b.nextSibling;
  222. if (profileArea instanceof HTMLElement && refNode instanceof HTMLElement) {
  223. var username_1 = (_c = document
  224. .querySelector('meta[name="octolytics-dimension-user_login"]')) === null || _c === void 0 ? void 0 : _c.getAttribute('content');
  225. if (username_1) {
  226. var exists = !!document.querySelector('#green-wall-block');
  227. if (!exists) {
  228. var block = document.createElement('div');
  229. block.setAttribute('id', 'green-wall-block');
  230. block.classList.add('border-top', 'color-border-muted', 'pt-3', 'mt-3', 'clearfix', 'hide-sm', 'hide-md');
  231. var title = document.createElement('h2');
  232. title.classList.add('h4', 'mb-2');
  233. title.textContent = 'Green Wall';
  234. var openBtn = document.createElement('button');
  235. openBtn.classList.add('btn');
  236. openBtn.textContent = ' ⬜🟩 View All Green';
  237. block.appendChild(title);
  238. block.appendChild(openBtn);
  239. profileArea.insertBefore(block, refNode);
  240. var _d = createDialog({ username: username_1 }), dialog_1 = _d.dialog, dialogContent_1 = _d.dialogContent;
  241. var hasLoaded_1 = false;
  242. var handleLoadError_1 = function () {
  243. dialogContent_1.innerHTML = '';
  244. var errorBlock = document.createElement('div');
  245. errorBlock.style.display = 'flex';
  246. errorBlock.style.flexDirection = 'column';
  247. errorBlock.style.alignItems = 'center';
  248. var tip = document.createElement('p');
  249. tip.textContent = 'The process of obtaining data has an exception.';
  250. var retryBtn = document.createElement('button');
  251. retryBtn.classList.add('btn');
  252. retryBtn.textContent = 'Retry';
  253. retryBtn.addEventListener('click', function () {
  254. // eslint-disable-next-line @typescript-eslint/no-use-before-define
  255. handleLoadData_1();
  256. });
  257. errorBlock.append(tip, retryBtn);
  258. dialogContent_1.append(errorBlock);
  259. };
  260. var handleLoadData_1 = function () {
  261. var loading = "\n <svg aria-label=\"Loading\" style=\"box-sizing: content-box; color: var(--color-icon-primary);\" width=\"32\" height=\"32\" viewBox=\"0 0 16 16\" fill=\"none\" data-view-component=\"true\" class=\"anim-rotate\">\n <circle cx=\"8\" cy=\"8\" r=\"7\" stroke=\"currentColor\" stroke-opacity=\"0.25\" stroke-width=\"2\" vector-effect=\"non-scaling-stroke\" fill=\"none\"></circle>\n <path d=\"M15 8a7.002 7.002 0 00-7-7\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" vector-effect=\"non-scaling-stroke\"></path>\n </svg>\n ";
  262. dialogContent_1.innerHTML = loading;
  263. GM.xmlHttpRequest({
  264. method: 'GET',
  265. url: "".concat(ORIGIN_1, "/api/contribution/").concat(username_1),
  266. onload: function (response) {
  267. try {
  268. dialogContent_1.innerHTML = '';
  269. var data = JSON.parse(response.responseText);
  270. var xData = produceData_1(data);
  271. xData.contributionCalendars.forEach(function (calendar) {
  272. var graphItem = createGraph_1(calendar).graphItem;
  273. dialogContent_1.append(graphItem);
  274. });
  275. hasLoaded_1 = true;
  276. }
  277. catch (_a) {
  278. handleLoadError_1();
  279. }
  280. },
  281. onerror: function (err) {
  282. console.error('[Green Wall]: ', err);
  283. handleLoadError_1();
  284. },
  285. });
  286. };
  287. var handleDialogOpen_1 = function () {
  288. var _a;
  289. dialog_1.showModal();
  290. document.body.classList.add('has-modal');
  291. if (!hasLoaded_1) {
  292. isHalloween_1 =
  293. ((_a = document.querySelector('.ContributionCalendar')) === null || _a === void 0 ? void 0 : _a.getAttribute('data-holiday')) ===
  294. 'halloween';
  295. handleLoadData_1();
  296. }
  297. };
  298. openBtn.addEventListener('click', function () {
  299. handleDialogOpen_1();
  300. });
  301. }
  302. }
  303. }
  304. else {
  305. console.warn('[Green Wall]: Target node not found.');
  306. }
  307. }
  308. };
  309. window.addEventListener('replaceState', handler);
  310.  
  311.