GitHub Profile Icon

Adds a clickable profile icon to identify personal or organizational accounts.

  1. // ==UserScript==
  2. // @name GitHub Profile Icon
  3. // @description Adds a clickable profile icon to identify personal or organizational accounts.
  4. // @icon https://github.githubassets.com/favicons/favicon-dark.svg
  5. // @version 1.5
  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 GM_getValue
  12. // @grant GM_setValue
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17. const style = document.createElement('style');
  18. style.textContent = `
  19. .icon-wrapper {
  20. position: relative !important;
  21. display: inline-block !important;
  22. margin-left: 4px !important;
  23. }
  24. .profile-icon-tooltip {
  25. visibility: hidden;
  26. position: fixed !important;
  27. background: #212830 !important;
  28. color: white !important;
  29. padding: 4px 8px !important;
  30. border-radius: 6px !important;
  31. font-size: 12px !important;
  32. white-space: nowrap !important;
  33. z-index: 9999 !important;
  34. pointer-events: none !important;
  35. transform: translateX(-50%) !important;
  36. }
  37. .profile-icon-tooltip::after {
  38. content: '';
  39. position: absolute !important;
  40. top: 100% !important;
  41. left: 50% !important;
  42. transform: translateX(-50%) !important;
  43. border: 5px solid transparent !important;
  44. border-top-color: #212830 !important;
  45. }
  46. .icon-wrapper:hover .profile-icon-tooltip {
  47. visibility: visible !important;
  48. }
  49. .fork-icon {
  50. width: 10px !important;
  51. height: 10px !important;
  52. opacity: 1 !important;
  53. }
  54. .non-fork-icon {
  55. opacity: 0.575 !important;
  56. }
  57. .fork-wrapper {
  58. margin-left: 8px !important;
  59. }
  60. .search-title {
  61. display: flex !important;
  62. align-items: flex-start !important;
  63. }
  64. .search-title .icon-wrapper {
  65. margin-left: 8px !important;
  66. display: inline-flex !important;
  67. align-items: center !important;
  68. margin-top: 3px !important;
  69. }
  70. `;
  71. document.head.appendChild(style);
  72.  
  73. const ICONS = {
  74. user: "M11.1,8.7c2.5,1.2,4.1,3.6,4.2,6.3c0,0.5-0.3,0.9-0.9,1c-0.5,0-0.9-0.3-1-0.9c0,0,0,0,0,0c-0.1-3.1-2.7-5.4-5.8-5.3c-2.9,0.1-5.1,2.4-5.3,5.3c0,0.5-0.5,0.9-1,0.9c-0.5,0-0.9-0.4-0.9-0.9c0.1-2.7,1.8-5.2,4.2-6.3C2.8,7,2.5,3.9,4.2,1.8s4.8-2.4,6.9-0.6s2.4,4.8,0.6,6.9C11.6,8.3,11.4,8.5,11.1,8.7z M11.1,4.9c0-1.7-1.4-3.1-3.1-3.1S4.9,3.2,4.9,4.9S6.3,8,8,8S11.1,6.6,11.1,4.9z",
  75. organization: "M1.75 16A1.75 1.75 0 0 1 0 14.25V1.75C0 .784.784 0 1.75 0h8.5C11.216 0 12 .784 12 1.75v12.5c0 .085-.006.168-.018.25h2.268a.25.25 0 0 0 .25-.25V8.285a.25.25 0 0 0-.111-.208l-1.055-.703a.749.749 0 1 1 .832-1.248l1.055.703c.487.325.779.871.779 1.456v5.965A1.75 1.75 0 0 1 14.25 16h-3.5a.766.766 0 0 1-.197-.026c-.099.017-.2.026-.303.026h-3a.75.75 0 0 1-.75-.75V14h-1v1.25a.75.75 0 0 1-.75.75Zm-.25-1.75c0 .138.112.25.25.25H4v-1.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 .75.75v1.25h2.25a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25ZM3.75 6h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5ZM3 3.75A.75.75 0 0 1 3.75 3h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 3 3.75Zm4 3A.75.75 0 0 1 7.75 6h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 7 6.75ZM7.75 3h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5ZM3 9.75A.75.75 0 0 1 3.75 9h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 3 9.75ZM7.75 9h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5Z"
  76. };
  77.  
  78. function getCachedUserType(username) {
  79. const cache = GM_getValue('userTypeCache', {});
  80. const cachedData = cache[username];
  81. if (cachedData) {
  82. const now = Date.now();
  83. if (now - cachedData.timestamp < 24 * 60 * 60 * 1000) {
  84. return cachedData.type;
  85. }
  86. delete cache[username];
  87. GM_setValue('userTypeCache', cache);
  88. }
  89. return null;
  90. }
  91.  
  92. function cacheUserType(username, type) {
  93. const cache = GM_getValue('userTypeCache', {});
  94. cache[username] = {
  95. type: type,
  96. timestamp: Date.now()
  97. };
  98. GM_setValue('userTypeCache', cache);
  99. }
  100.  
  101. async function checkUserType(username) {
  102. const cachedType = getCachedUserType(username);
  103. if (cachedType) {
  104. return cachedType;
  105. }
  106.  
  107. try {
  108. const response = await fetch(`https://api.github.com/users/${username}`);
  109. if (!response.ok) {
  110. throw new Error(`HTTP error! status: ${response.status}`);
  111. }
  112. const data = await response.json();
  113. const type = data.type?.toLowerCase() === 'organization' ? 'organization' : 'user';
  114. cacheUserType(username, type);
  115. return type;
  116. } catch (error) {
  117. cacheUserType(username, 'user');
  118. return 'user';
  119. }
  120. }
  121.  
  122. async function createIcon(username, wrapper, isFork = false) {
  123. const type = await checkUserType(username);
  124. const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  125. svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
  126. svg.setAttribute("viewBox", "0 0 16 16");
  127. svg.style.cssText = `width:${isFork ? '10px' : '14px'};height:${isFork ? '10px' : '14px'};cursor:pointer;fill:currentColor;transition:transform .1s`;
  128. if (isFork) {
  129. svg.classList.add('fork-icon');
  130. wrapper.classList.add('fork-wrapper');
  131. } else {
  132. svg.classList.add('non-fork-icon');
  133. }
  134.  
  135. const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
  136. path.setAttribute("d", ICONS[type]);
  137. const tooltip = document.createElement('div');
  138. tooltip.className = 'profile-icon-tooltip';
  139. tooltip.textContent = username;
  140.  
  141. wrapper.addEventListener('mouseenter', (e) => {
  142. svg.style.transform = 'scale(1.1)';
  143. const rect = wrapper.getBoundingClientRect();
  144. tooltip.style.left = `${rect.left + (rect.width / 2)}px`;
  145. tooltip.style.top = `${rect.top - 35}px`;
  146. });
  147. wrapper.addEventListener('mouseleave', () => {
  148. svg.style.transform = 'scale(1)';
  149. });
  150. wrapper.addEventListener('mousemove', (e) => {
  151. const rect = wrapper.getBoundingClientRect();
  152. tooltip.style.left = `${rect.left + (rect.width / 2)}px`;
  153. tooltip.style.top = `${rect.top - 35}px`;
  154. });
  155.  
  156. wrapper.addEventListener('click', () => window.open(`https://github.com/${username}`, '_blank'));
  157.  
  158. svg.appendChild(path);
  159. wrapper.appendChild(svg);
  160. wrapper.appendChild(tooltip);
  161. }
  162.  
  163. async function addGitHubIcons() {
  164. const tasks = [];
  165. const isSearchPage = window.location.pathname === '/search' || window.location.pathname.startsWith('/search/');
  166. if (isSearchPage) {
  167. document.querySelectorAll('.search-title').forEach(titleDiv => {
  168. if (titleDiv.querySelector('.icon-wrapper')) return;
  169. const link = titleDiv.querySelector('a');
  170. if (!link) return;
  171. const href = link.getAttribute('href');
  172. if (!href) return;
  173. const username = href.split('/').filter(Boolean)[0];
  174. const wrapper = document.createElement('div');
  175. wrapper.className = 'icon-wrapper';
  176. titleDiv.appendChild(wrapper);
  177. tasks.push(createIcon(username, wrapper, false));
  178. });
  179. } else {
  180. const repoNav = document.querySelector('#repository-container-header');
  181. document.querySelectorAll('h3:not(.search-title)').forEach(h3 => {
  182. if (h3.closest('#readme') || h3.closest('article')) return;
  183. if (repoNav && !h3.closest('#repository-container-header')) return;
  184. if (h3.querySelector('.icon-wrapper')) return;
  185. const link = h3.querySelector('a');
  186. if (!link) return;
  187. const href = link.getAttribute('href');
  188. if (!href || !href.startsWith('/')) return;
  189. const username = href.split('/').filter(Boolean)[0];
  190. const wrapper = document.createElement('div');
  191. wrapper.className = 'icon-wrapper';
  192. h3.appendChild(wrapper);
  193. tasks.push(createIcon(username, wrapper, false));
  194. });
  195.  
  196. document.querySelectorAll('.f6.color-fg-muted.mb-1').forEach(forkInfo => {
  197. if (forkInfo.querySelector('.icon-wrapper')) return;
  198. const link = forkInfo.querySelector('a.Link--muted');
  199. if (!link || !link.href.includes('/')) return;
  200. const username = link.getAttribute('href').split('/').filter(Boolean)[0];
  201. const wrapper = document.createElement('div');
  202. wrapper.className = 'icon-wrapper';
  203. link.insertAdjacentElement('afterend', wrapper);
  204. tasks.push(createIcon(username, wrapper, true));
  205. });
  206. }
  207.  
  208. await Promise.all(tasks);
  209. }
  210.  
  211. addGitHubIcons();
  212.  
  213. const observer = new MutationObserver(mutations => {
  214. if (mutations.some(m => m.addedNodes.length)) addGitHubIcons();
  215. });
  216.  
  217. observer.observe(document.body, { childList: true, subtree: true });
  218.  
  219. const pushState = history.pushState;
  220. const replaceState = history.replaceState;
  221. history.pushState = function() {
  222. pushState.apply(history, arguments);
  223. addGitHubIcons();
  224. };
  225.  
  226. history.replaceState = function() {
  227. replaceState.apply(history, arguments);
  228. addGitHubIcons();
  229. };
  230.  
  231. window.addEventListener('popstate', addGitHubIcons);
  232. })();