GitHub Top Languages

Display top programming languages on GitHub profiles.

  1. // ==UserScript==
  2. // @name GitHub Top Languages
  3. // @description Display top programming languages on GitHub profiles.
  4. // @icon https://github.githubassets.com/favicons/favicon-dark.svg
  5. // @version 1.3
  6. // @author afkarxyz
  7. // @namespace https://github.com/afkarxyz/userscripts/
  8. // @supportURL https://github.com/afkarxyz/userscripts/issues
  9. // @license MIT
  10. // @match https://github.com/*
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. // Hardcode: let GITHUB_TOKEN = "your_github_personal_access_token";
  18. let GITHUB_TOKEN = localStorage.getItem("gh_token") || "";
  19. const CACHE_DURATION = 60 * 60 * 1000;
  20.  
  21. window.setGitHubToken = function(token) {
  22. GITHUB_TOKEN = token;
  23. localStorage.setItem("gh_token", token);
  24. console.log("GitHub token has been set successfully!");
  25. console.log("Refresh the page to see the changes.");
  26. };
  27.  
  28. window.clearGitHubToken = function() {
  29. GITHUB_TOKEN = "";
  30. localStorage.removeItem("gh_token");
  31. console.log("GitHub token has been cleared!");
  32. };
  33.  
  34. function getCachedData(key) {
  35. const cachedItem = localStorage.getItem(key);
  36. if (!cachedItem) return null;
  37.  
  38. try {
  39. const { data, timestamp } = JSON.parse(cachedItem);
  40. if (Date.now() - timestamp < CACHE_DURATION) {
  41. return data;
  42. }
  43. localStorage.removeItem(key);
  44. return null;
  45. } catch (e) {
  46. console.error("Error parsing cached data:", e);
  47. localStorage.removeItem(key);
  48. return null;
  49. }
  50. }
  51.  
  52. function setCachedData(key, data) {
  53. const cacheItem = {
  54. data,
  55. timestamp: Date.now()
  56. };
  57. localStorage.setItem(key, JSON.stringify(cacheItem));
  58. }
  59.  
  60. window.clearLanguageCache = function() {
  61. const keysToRemove = [];
  62. for (let i = 0; i < localStorage.length; i++) {
  63. const key = localStorage.key(i);
  64. if (key.startsWith('gh_langs_') || key.startsWith('gh_colors')) {
  65. keysToRemove.push(key);
  66. }
  67. }
  68. keysToRemove.forEach(key => localStorage.removeItem(key));
  69. console.log("Language cache has been cleared!");
  70. };
  71.  
  72. const COLORS_URL = "https://raw.githubusercontent.com/afkarxyz/userscripts/refs/heads/main/assets/github/colors.json";
  73. let lastUsername = null;
  74.  
  75. async function getLanguageColors() {
  76. const cachedColors = getCachedData('gh_colors');
  77. if (cachedColors) {
  78. return cachedColors;
  79. }
  80.  
  81. try {
  82. const res = await fetch(COLORS_URL);
  83. const colors = await res.json();
  84. setCachedData('gh_colors', colors);
  85. return colors;
  86. } catch (e) {
  87. console.error("Failed to fetch language colors:", e);
  88. return {};
  89. }
  90. }
  91.  
  92. async function fetchLanguagesGraphQL(username, isOrg = false) {
  93. const cacheKey = `gh_langs_${username}_${isOrg ? 'org' : 'user'}`;
  94.  
  95. const cachedLangs = getCachedData(cacheKey);
  96. if (cachedLangs) {
  97. console.log(`Using cached language data for ${username}`);
  98. return cachedLangs;
  99. }
  100.  
  101. if (!GITHUB_TOKEN) {
  102. console.warn("GitHub GraphQL API requires a token. Please set one using window.setGitHubToken()");
  103. return [];
  104. }
  105.  
  106. console.log(`Fetching fresh language data for ${username} using GraphQL`);
  107.  
  108. const query = isOrg ? `
  109. query OrgRepoLanguages($orgName: String!, $cursor: String) {
  110. organization(login: $orgName) {
  111. repositories(first: 100, after: $cursor, privacy: PUBLIC, isFork: false) {
  112. pageInfo {
  113. hasNextPage
  114. endCursor
  115. }
  116. nodes {
  117. languages(first: 100, orderBy: {field: SIZE, direction: DESC}) {
  118. edges {
  119. size
  120. node {
  121. name
  122. color
  123. }
  124. }
  125. totalSize
  126. }
  127. }
  128. }
  129. }
  130. }
  131. ` : `
  132. query UserRepoLanguages($login: String!, $cursor: String) {
  133. user(login: $login) {
  134. repositories(first: 100, after: $cursor, privacy: PUBLIC, ownerAffiliations: OWNER, isFork: false) {
  135. pageInfo {
  136. hasNextPage
  137. endCursor
  138. }
  139. nodes {
  140. languages(first: 100, orderBy: {field: SIZE, direction: DESC}) {
  141. edges {
  142. size
  143. node {
  144. name
  145. color
  146. }
  147. }
  148. totalSize
  149. }
  150. }
  151. }
  152. }
  153. }
  154. `;
  155.  
  156. const allLanguages = {};
  157. let hasNextPage = true;
  158. let cursor = null;
  159.  
  160. try {
  161. while (hasNextPage) {
  162. const variables = isOrg
  163. ? { orgName: username, cursor }
  164. : { login: username, cursor };
  165.  
  166. const response = await fetch("https://api.github.com/graphql", {
  167. method: "POST",
  168. headers: {
  169. "Authorization": `Bearer ${GITHUB_TOKEN}`,
  170. "Content-Type": "application/json"
  171. },
  172. body: JSON.stringify({ query, variables })
  173. });
  174.  
  175. if (!response.ok) {
  176. throw new Error(`GitHub API error: ${response.status}`);
  177. }
  178.  
  179. const data = await response.json();
  180.  
  181. if (data.errors) {
  182. console.error("GraphQL errors:", data.errors);
  183. break;
  184. }
  185.  
  186. const entityData = isOrg ? data.data?.organization : data.data?.user;
  187. if (!entityData) break;
  188.  
  189. const repositories = entityData.repositories;
  190. const pageInfo = repositories.pageInfo;
  191.  
  192. repositories.nodes.forEach(repo => {
  193. if (!repo.languages.edges) return;
  194. repo.languages.edges.forEach(edge => {
  195. const { name, color } = edge.node;
  196. const size = edge.size;
  197. if (!allLanguages[name]) {
  198. allLanguages[name] = {
  199. size: 0,
  200. color: color
  201. };
  202. }
  203. allLanguages[name].size += size;
  204. });
  205. });
  206.  
  207. hasNextPage = pageInfo.hasNextPage;
  208. cursor = pageInfo.endCursor;
  209. }
  210.  
  211. const totalSize = Object.values(allLanguages).reduce((sum, lang) => sum + lang.size, 0);
  212. const result = Object.entries(allLanguages)
  213. .map(([lang, data]) => ({
  214. lang,
  215. color: data.color,
  216. count: data.size,
  217. percent: ((data.size / totalSize) * 100).toFixed(2)
  218. }))
  219. .sort((a, b) => b.count - a.count);
  220.  
  221. setCachedData(cacheKey, result);
  222. return result;
  223. } catch (e) {
  224. console.error("Error fetching languages with GraphQL:", e);
  225. return [];
  226. }
  227. }
  228.  
  229. function createLanguageBar(languages, colorMap) {
  230. const container = document.createElement("div");
  231. container.style.marginTop = "16px";
  232. container.style.width = "100%";
  233.  
  234. const barContainer = document.createElement("div");
  235. barContainer.style.display = "flex";
  236. barContainer.style.height = "8px";
  237. barContainer.style.width = "100%";
  238. barContainer.style.borderRadius = "4px";
  239. barContainer.style.overflow = "hidden";
  240. barContainer.style.marginBottom = "8px";
  241.  
  242. const legendContainer = document.createElement("div");
  243. legendContainer.style.display = "flex";
  244. legendContainer.style.flexWrap = "wrap";
  245. legendContainer.style.fontSize = "12px";
  246.  
  247. languages.forEach((langData) => {
  248. const { lang, percent, color: langColor } = langData;
  249. const percentNum = parseFloat(percent);
  250.  
  251. const color = langColor || (colorMap[lang] && colorMap[lang].color) || "#ccc";
  252.  
  253. const segment = document.createElement("div");
  254. segment.style.backgroundColor = color;
  255. segment.style.width = `${percentNum}%`;
  256. segment.style.height = "100%";
  257. barContainer.appendChild(segment);
  258.  
  259. const legendItem = document.createElement("div");
  260. legendItem.style.display = "flex";
  261. legendItem.style.alignItems = "center";
  262. legendItem.style.marginRight = "16px";
  263. legendItem.style.marginBottom = "4px";
  264.  
  265. const colorDot = document.createElement("span");
  266. colorDot.style.display = "inline-block";
  267. colorDot.style.width = "8px";
  268. colorDot.style.height = "8px";
  269. colorDot.style.backgroundColor = color;
  270. colorDot.style.borderRadius = "50%";
  271. colorDot.style.marginRight = "6px";
  272.  
  273. const langNameSpan = document.createElement("span");
  274. langNameSpan.textContent = lang;
  275. langNameSpan.style.fontWeight = "600";
  276. const percentSpan = document.createElement("span");
  277. percentSpan.textContent = ` ${percent}%`;
  278. percentSpan.style.fontWeight = "400";
  279. const langName = document.createElement("span");
  280. langName.appendChild(langNameSpan);
  281. langName.appendChild(percentSpan);
  282.  
  283. legendItem.appendChild(colorDot);
  284. legendItem.appendChild(langName);
  285. legendContainer.appendChild(legendItem);
  286. });
  287.  
  288. container.appendChild(barContainer);
  289. container.appendChild(legendContainer);
  290.  
  291. return container;
  292. }
  293.  
  294. async function insertLanguageStats() {
  295. const match = window.location.pathname.match(/^\/([^\/]+)$/);
  296. if (!match) return;
  297.  
  298. const username = match[1];
  299. if (username === lastUsername) return;
  300. lastUsername = username;
  301.  
  302. try {
  303. const userContainer = document.querySelector('.vcard-names-container');
  304. const orgContainer = document.querySelector('.h2.lh-condensed')?.closest('.flex-1.d-flex.flex-column');
  305. const container = userContainer || orgContainer;
  306.  
  307. if (!container) return;
  308. const isOrg = !userContainer;
  309.  
  310. if (container.querySelector('#gh-lang-stats')) return;
  311.  
  312. const loadingEl = document.createElement("div");
  313. loadingEl.id = "lang-stats-loading";
  314. loadingEl.textContent = "Loading...";
  315. loadingEl.style.marginTop = "12px";
  316. loadingEl.style.fontSize = "13px";
  317. loadingEl.style.color = "#666";
  318. container.appendChild(loadingEl);
  319.  
  320. if (!GITHUB_TOKEN) {
  321. loadingEl.textContent = "GitHub API token required for language statistics";
  322. const tokenNotice = document.createElement("div");
  323. tokenNotice.style.fontSize = "12px";
  324. tokenNotice.style.color = "#666";
  325. tokenNotice.style.marginTop = "4px";
  326. tokenNotice.innerHTML = "Set token with <code>window.setGitHubToken('your_token')</code> in console";
  327. loadingEl.appendChild(tokenNotice);
  328. return;
  329. }
  330.  
  331. const [langs, colors] = await Promise.all([
  332. fetchLanguagesGraphQL(username, isOrg),
  333. getLanguageColors()
  334. ]);
  335.  
  336. const loadingIndicator = document.getElementById("lang-stats-loading");
  337. if (loadingIndicator) loadingIndicator.remove();
  338.  
  339. if (langs.length === 0) {
  340. return;
  341. }
  342.  
  343. const statsWrapper = document.createElement("div");
  344. statsWrapper.id = "gh-lang-stats";
  345.  
  346. const topLangs = langs.slice(0, 10);
  347.  
  348. const langBar = createLanguageBar(topLangs, colors);
  349. statsWrapper.appendChild(langBar);
  350.  
  351. container.appendChild(statsWrapper);
  352. } catch (error) {
  353. console.error("Error inserting language stats:", error);
  354. }
  355. }
  356.  
  357. let currentPath = location.pathname;
  358. const observer = new MutationObserver(() => {
  359. if (location.pathname !== currentPath) {
  360. currentPath = location.pathname;
  361. setTimeout(insertLanguageStats, 800);
  362. }
  363. });
  364.  
  365. observer.observe(document.body, { childList: true, subtree: true });
  366.  
  367. setTimeout(insertLanguageStats, 500);
  368. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址