GitHub Repo Star Time

Display the time stared at for GitHub repositories on the repository page.

  1. // ==UserScript==
  2. // @name GitHub Repo Star Time
  3. // @namespace http://github.com/qbosen/tm-gh-star-time
  4. // @version 0.3
  5. // @description Display the time stared at for GitHub repositories on the repository page.
  6. // @author qbosen
  7. // @match https://github.com/*/*
  8. // @connect api.github.com
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_registerMenuCommand
  12. // @grant GM_xmlhttpRequest
  13. // @license MIT
  14. // ==/UserScript==
  15.  
  16.  
  17. (function () {
  18. 'use strict';
  19.  
  20. let githubToken = GM_getValue('githubToken', '');
  21.  
  22. // 注册菜单命令,用于设置 Token
  23. GM_registerMenuCommand("Setup GitHub Token", function () {
  24. const token = prompt("Input GitHub Personal Access Token\n(Permission: 'Metadata' repository permissions (read) and 'Starring' user permissions (read)):", githubToken);
  25. if (token !== null) {
  26. GM_setValue('githubToken', token);
  27. githubToken = token;
  28. alert("GitHub Token Saved");
  29. }
  30. });
  31. // 清理 缓存 命令
  32. GM_registerMenuCommand("Clean Cache", function () {
  33. GM_setValue('starredRepos', '{"repos": [], "timestamp": 0}');
  34. alert("Cache Cleaned");
  35. });
  36.  
  37. if (!githubToken) {
  38. console.warn("请先设置 GitHub Token。");
  39. return; // 没有 Token,不执行后续操作
  40. }
  41.  
  42. // 检查是否 star 过
  43. function checkStarred(owner, repo, callback) {
  44. // https://docs.github.com/en/rest/activity/starring?apiVersion=2022-11-28#check-if-a-repository-is-starred-by-the-authenticated-user
  45. GM_xmlhttpRequest({
  46. method: "GET",
  47. url: `https://api.github.com/user/starred/${owner}/${repo}`,
  48. headers: {
  49. "Authorization": `token ${githubToken}`,
  50. "Accept": "application/vnd.github.v3+json",
  51. "X-GitHub-Api-Version": "2022-11-28"
  52. },
  53. onload: function (response) {
  54. if (response.status === 204) {
  55. callback(true);
  56. } else if (response.status === 404) {
  57. callback(false);
  58. } else {
  59. console.error(`检查 star 状态失败: ${response.status} ${response.statusText}`);
  60. callback(false);
  61. }
  62. },
  63. onerror: function (error) {
  64. console.error("检查 star 状态错误:", error);
  65. callback(false);
  66. }
  67. });
  68. }
  69.  
  70. // 获取用户所有star过的仓库
  71. function getAllStarredRepos(username, callback) {
  72. let allRepos = [];
  73. let page = 1;
  74. const perPage = 100; // 每页 100 个
  75.  
  76. function fetchPage(page) {
  77. GM_xmlhttpRequest({
  78. method: "GET",
  79. url: `https://api.github.com/users/${username}/starred?per_page=${perPage}&page=${page}`,
  80. headers: {
  81. "Authorization": `token ${githubToken}`,
  82. "Accept": "application/vnd.github.star+json",
  83. "X-GitHub-Api-Version": "2022-11-28",
  84. },
  85. onload: function (response) {
  86. if (response.status >= 200 && response.status < 300) {
  87. try {
  88. const repos = JSON.parse(response.responseText);
  89. // 只提取需要的字段
  90. const simplifiedRepos = repos.map(repo => ({
  91. full_name: repo.repo.full_name,
  92. starred_at: repo.starred_at
  93. }));
  94. allRepos = allRepos.concat(simplifiedRepos);
  95.  
  96. if (repos.length === perPage) {
  97. fetchPage(page + 1);
  98. } else {
  99. callback(allRepos);
  100. }
  101. } catch (error) {
  102. console.error("解析 JSON 失败:", error);
  103. callback(null);
  104. }
  105. } else {
  106. console.error(`获取 star 列表失败: ${response.status} ${response.statusText}`);
  107. callback(null);
  108. }
  109. },
  110. onerror: function (error) {
  111. console.error("获取 star 列表错误:", error);
  112. callback(null);
  113. }
  114. });
  115. }
  116.  
  117. fetchPage(page);
  118. }
  119.  
  120.  
  121. const pathParts = window.location.pathname.split('/');
  122. if (pathParts.length < 3) return;
  123. const owner = pathParts[1];
  124. const repo = pathParts[2];
  125.  
  126. const username = document.querySelector('meta[name="user-login"]').content;
  127.  
  128. checkStarred(owner, repo, function (isStarred) {
  129. if (isStarred) {
  130. let cachedStarredRepos = JSON.parse(GM_getValue('starredRepos', '{"repos": [], "timestamp": 0}'));
  131. const now = Date.now();
  132. const oneDay = 24 * 60 * 60 * 1000;
  133.  
  134. if (now - cachedStarredRepos.timestamp > oneDay || cachedStarredRepos.repos.length === 0) { // 修改判断条件
  135. getAllStarredRepos(username, function (repos) {
  136. if (repos) {
  137. GM_setValue('starredRepos', JSON.stringify({ repos: repos, timestamp: now }));
  138. cachedStarredRepos = { repos: repos, timestamp: now };
  139. displayStarTime(cachedStarredRepos, owner, repo);
  140. }
  141. });
  142. } else {
  143. displayStarTime(cachedStarredRepos, owner, repo);
  144. }
  145. }
  146. });
  147.  
  148. function displayStarTime(cachedStarredRepos, owner, repo) {
  149. const fullName = `${owner}/${repo}`;
  150. const starredRepo = cachedStarredRepos.repos.find(r => r.full_name === fullName);
  151.  
  152. if (starredRepo) {
  153. const starredAt = new Date(starredRepo.starred_at);
  154. const timeString = starredAt.toLocaleString('zh-CN', {
  155. year: 'numeric',
  156. month: '2-digit',
  157. day: '2-digit',
  158. hour: '2-digit',
  159. minute: '2-digit',
  160. second: '2-digit',
  161. hour12: false // 使用 24 小时制
  162. })
  163.  
  164. const detailsElement = document.querySelector('.BorderGrid .BorderGrid-cell');
  165. if (detailsElement) {
  166. const timeElement = document.createElement('div');
  167. timeElement.textContent = `Starred at ${timeString}`;
  168. detailsElement.appendChild(timeElement);
  169. } else {
  170. console.log(`star time: ${timeString}`);
  171. }
  172. }
  173. }
  174. })();