GitHub Code Language Icons

Replaces GitHub's boring round code language icons with Material Design Icons.

目前為 2025-03-17 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name GitHub Code Language Icons
  3. // @description Replaces GitHub's boring round code language icons with Material Design Icons.
  4. // @icon https://github.githubassets.com/favicons/favicon-dark.svg
  5. // @version 1.9
  6. // @author afkarxyz
  7. // @namespace https://github.com/afkarxyz/misc-scripts/
  8. // @supportURL https://github.com/afkarxyz/misc-scripts/issues
  9. // @license MIT
  10. // @match https://github.com/*
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. const CONFIG = {
  19. case_01: true, // The average of all icons
  20. case_02: true, // Organization's Repositories (https://github.com/orgs/yt-dlp/repositories)
  21. case_03: true, // Languages
  22. case_04: true, // Other Languages
  23. case_05: true, // Filter by (https://github.com/search?q=spotify&type=code)
  24. case_06: true, // More languages... (https://github.com/search?q=spotify&type=repositories)
  25. case_07: true // Search's Languages (https://github.com/search?q=spotify&type=repositories)
  26. };
  27.  
  28. const BASE_URL = 'https://raw.githubusercontent.com/afkarxyz/misc-scripts/refs/heads/main/';
  29. const ICON_BASE_URL = `${BASE_URL}icons/`;
  30.  
  31. let languageMappings = {};
  32.  
  33. async function fetchLanguageMappings() {
  34. const cacheKey = 'githubLanguageRemapCache';
  35. const currentTime = Date.now();
  36. const cachedData = JSON.parse(GM_getValue(cacheKey, '{}'));
  37. if (cachedData.timestamp && (currentTime - cachedData.timestamp < 7 * 24 * 60 * 60 * 1000)) {
  38. return cachedData.mappings;
  39. }
  40. try {
  41. const response = await fetch(`${BASE_URL}remap.json`);
  42. const data = await response.json();
  43. GM_setValue(cacheKey, JSON.stringify({
  44. mappings: data.iconRemap,
  45. timestamp: currentTime
  46. }));
  47. return data.iconRemap;
  48. } catch (error) {
  49. console.error('Failed to fetch language mappings:', error);
  50. return cachedData.mappings || {};
  51. }
  52. }
  53.  
  54. function normalizeLanguageName(language) {
  55. const normalizedLanguage = language.toLowerCase();
  56.  
  57. for (const [iconName, languageList] of Object.entries(languageMappings)) {
  58. if (languageList.includes(normalizedLanguage)) {
  59. return iconName;
  60. }
  61. }
  62.  
  63. return normalizedLanguage;
  64. }
  65.  
  66. async function fetchAvailableIcons() {
  67. const cacheKey = 'githubLanguageIconsCache';
  68. const currentTime = Date.now();
  69. const cachedData = JSON.parse(GM_getValue(cacheKey, '{}'));
  70.  
  71. if (cachedData.timestamp && (currentTime - cachedData.timestamp < 7 * 24 * 60 * 60 * 1000)) {
  72. return cachedData.fileTypes;
  73. }
  74.  
  75. try {
  76. const response = await fetch(`${BASE_URL}icons.json`);
  77. const data = await response.json();
  78.  
  79. GM_setValue(cacheKey, JSON.stringify({
  80. fileTypes: data.fileTypes,
  81. timestamp: currentTime
  82. }));
  83.  
  84. return data.fileTypes;
  85. } catch (error) {
  86. console.error('Failed to fetch icon list:', error);
  87. return cachedData.fileTypes || [];
  88. }
  89. }
  90.  
  91. async function replaceLanguageIcons() {
  92. let availableIcons;
  93. try {
  94. availableIcons = await fetchAvailableIcons();
  95. } catch (error) {
  96. console.error('Error getting available icons:', error);
  97. return;
  98. }
  99.  
  100. const processedElements = new Set();
  101.  
  102. async function replaceOtherIcon(targetSvg) {
  103. if (!CONFIG.case_04) return;
  104.  
  105. try {
  106. const response = await fetch(`${ICON_BASE_URL}other.svg`);
  107. const svgText = await response.text();
  108.  
  109. const parser = new DOMParser();
  110. const svgDoc = parser.parseFromString(svgText, 'image/svg+xml');
  111. const newSvg = svgDoc.querySelector('svg');
  112.  
  113. const attributes = targetSvg.attributes;
  114. for (let i = 0; i < attributes.length; i++) {
  115. const attr = attributes[i];
  116. if (attr.name !== 'class') {
  117. newSvg.setAttribute(attr.name, attr.value);
  118. }
  119. }
  120.  
  121. newSvg.setAttribute('width', '16');
  122. newSvg.setAttribute('height', '16');
  123. newSvg.setAttribute('viewBox', '0 0 24 24');
  124.  
  125. const innerGroup = newSvg.querySelector('g') || newSvg.querySelector('path');
  126. if (innerGroup) {
  127. innerGroup.setAttribute('transform', 'scale(0.67)');
  128. }
  129.  
  130. const targetClasses = Array.from(targetSvg.classList);
  131. targetClasses.forEach(className => {
  132. newSvg.classList.add(className);
  133. });
  134.  
  135. newSvg.style.width = '16px';
  136. newSvg.style.height = '16px';
  137. newSvg.style.minWidth = '16px';
  138. newSvg.style.minHeight = '16px';
  139.  
  140. targetSvg.parentNode.replaceChild(newSvg, targetSvg);
  141. } catch (error) {
  142. console.error('Failed to replace Other icon:', error);
  143. }
  144. }
  145.  
  146. function processElement(element) {
  147. if (processedElements.has(element)) return;
  148. processedElements.add(element);
  149.  
  150. let langElement, language;
  151.  
  152. // Case 1: The average of all icons
  153. if (CONFIG.case_01 && element.matches('.repo-language-color')) {
  154. const languageSpan = element.parentElement.querySelector('[itemprop="programmingLanguage"]');
  155.  
  156. if (languageSpan && !languageSpan.dataset.iconProcessed) {
  157. language = normalizeLanguageName(languageSpan.textContent);
  158.  
  159. if (availableIcons.includes(language)) {
  160. const iconImg = document.createElement('img');
  161. iconImg.src = `${ICON_BASE_URL}${language}.svg`;
  162. iconImg.alt = `${language} icon`;
  163. iconImg.width = 16;
  164. iconImg.height = 16;
  165. iconImg.style.marginRight = '2px';
  166. iconImg.style.verticalAlign = 'sub';
  167.  
  168. element.parentElement.insertBefore(iconImg, element);
  169. element.remove();
  170. languageSpan.dataset.iconProcessed = 'true';
  171. }
  172. }
  173. }
  174. // Case 2: Organization's Repositories
  175. else if (CONFIG.case_02 && element.matches('.Box-sc-g0xbh4-0.fCvgBf')) {
  176. const languageSpan = element.querySelector('.prc-Text-Text-0ima0');
  177. if (languageSpan && !languageSpan.dataset.iconProcessed) {
  178. const languageText = languageSpan.textContent.trim();
  179. language = normalizeLanguageName(languageText);
  180. element.innerHTML = '';
  181. const newLanguageSpan = document.createElement('span');
  182. newLanguageSpan.className = 'Box-sc-g0xbh4-0 fVplbS prc-Text-Text-0ima0';
  183. newLanguageSpan.textContent = languageText;
  184. if (availableIcons.includes(language)) {
  185. const iconImg = document.createElement('img');
  186. iconImg.src = `${ICON_BASE_URL}${language}.svg`;
  187. iconImg.alt = `${language} icon`;
  188. iconImg.width = 16;
  189. iconImg.height = 16;
  190. iconImg.style.verticalAlign = 'middle';
  191. element.appendChild(iconImg);
  192. }
  193. element.appendChild(newLanguageSpan);
  194. newLanguageSpan.dataset.iconProcessed = 'true';
  195. }
  196. }
  197. // Case 3: Languages
  198. else if (CONFIG.case_03 && element.matches('.d-inline')) {
  199. langElement = element.querySelector('.text-bold');
  200.  
  201. if (!langElement || langElement.textContent.toLowerCase() === 'other' || element.dataset.iconChecked) return;
  202.  
  203. language = normalizeLanguageName(langElement.textContent);
  204. element.dataset.iconChecked = 'true';
  205.  
  206. const svg = element.querySelector('svg');
  207.  
  208. if (!svg || !availableIcons.includes(language)) return;
  209.  
  210. const img = document.createElement('img');
  211. img.src = `${ICON_BASE_URL}${language}.svg`;
  212. img.width = 16;
  213. img.height = 16;
  214. img.className = 'mr-2';
  215. img.style.verticalAlign = 'middle';
  216.  
  217. svg.parentNode.replaceChild(img, svg);
  218. }
  219. // Case 4: Other Languages
  220. else if (CONFIG.case_04 && element.matches('.octicon-dot-fill')) {
  221. const parentElement = element.closest('.d-inline');
  222. if (parentElement) {
  223. const languageElement = parentElement.querySelector('.text-bold');
  224. if (languageElement && languageElement.textContent.toLowerCase() === 'other' && !element.dataset.iconProcessed) {
  225. element.dataset.iconProcessed = 'true';
  226. replaceOtherIcon(element);
  227. }
  228. }
  229. }
  230. // Case 5: Filter by
  231. else if (CONFIG.case_05 && element.matches('.Box-sc-g0xbh4-0.hjDqIa')) {
  232. const languageSpan = element.nextElementSibling;
  233. if (languageSpan && languageSpan.getAttribute('aria-label') && !languageSpan.dataset.iconProcessed) {
  234. language = normalizeLanguageName(languageSpan.getAttribute('aria-label').replace(' language', ''));
  235.  
  236. if (availableIcons.includes(language)) {
  237. const iconImg = document.createElement('img');
  238. iconImg.src = `${ICON_BASE_URL}${language}.svg`;
  239. iconImg.alt = `${language} icon`;
  240. iconImg.width = 16;
  241. iconImg.height = 16;
  242. iconImg.style.marginRight = '4px';
  243. iconImg.style.verticalAlign = 'middle';
  244.  
  245. element.style.display = 'none';
  246. languageSpan.parentNode.insertBefore(iconImg, languageSpan);
  247. languageSpan.dataset.iconProcessed = 'true';
  248. }
  249. }
  250. }
  251. // Case 6: More languages...
  252. else if (CONFIG.case_06 && element.matches('.ActionListItem-visual.ActionListItem-visual--leading')) {
  253. const languageLabel = element.closest('.ActionListContent')?.querySelector('.ActionListItem-label.text-normal');
  254. if (!languageLabel || !languageLabel.textContent || element.dataset.iconChecked) return;
  255.  
  256. language = normalizeLanguageName(languageLabel.textContent);
  257. element.dataset.iconChecked = 'true';
  258.  
  259. const colorDiv = element.querySelector('div');
  260. if (!colorDiv || !availableIcons.includes(language)) return;
  261.  
  262. const img = document.createElement('img');
  263. img.src = `${ICON_BASE_URL}${language}.svg`;
  264. img.width = 16;
  265. img.height = 16;
  266. img.style.verticalAlign = 'middle';
  267.  
  268. colorDiv.replaceWith(img);
  269. }
  270. // Case 7: Search's Languages
  271. else if (CONFIG.case_07 && element.matches('.prc-ActionList-LeadingVisual-dxXxW, .prc-ActionList-VisualWrap-rfjV-')) {
  272. if (element.dataset.iconChecked) return;
  273. element.dataset.iconChecked = 'true';
  274. const listItem = element.closest('.prc-ActionList-ActionListItem-uq6I7');
  275. if (!listItem) return;
  276. const languageDiv = listItem.querySelector('.Truncate__StyledTruncate-sc-23o1d2-0');
  277. if (!languageDiv || !languageDiv.title) return;
  278. language = normalizeLanguageName(languageDiv.title);
  279. const colorDiv = element.querySelector('.Box-sc-g0xbh4-0');
  280. if (!colorDiv || !availableIcons.includes(language)) return;
  281. const img = document.createElement('img');
  282. img.src = `${ICON_BASE_URL}${language}.svg`;
  283. img.alt = `${language} icon`;
  284. img.width = 16;
  285. img.height = 16;
  286. img.style.verticalAlign = 'middle';
  287. colorDiv.replaceWith(img);
  288. }
  289. }
  290.  
  291. const observer = new MutationObserver((mutations) => {
  292. for (const mutation of mutations) {
  293. for (const node of mutation.addedNodes) {
  294. if (node.nodeType === Node.ELEMENT_NODE) {
  295. if (node.matches('.d-inline, .Box-sc-g0xbh4-0.fCvgBf, .repo-language-color, .Box-sc-g0xbh4-0.hjDqIa, .ActionListItem-visual.ActionListItem-visual--leading, .octicon-dot-fill, .prc-ActionList-LeadingVisual-dxXxW, .prc-ActionList-VisualWrap-rfjV-')) {
  296. processElement(node);
  297. } else {
  298. node.querySelectorAll('.d-inline, .Box-sc-g0xbh4-0.fCvgBf, .repo-language-color, .Box-sc-g0xbh4-0.hjDqIa, .ActionListItem-visual.ActionListItem-visual--leading, .octicon-dot-fill, .prc-ActionList-LeadingVisual-dxXxW, .prc-ActionList-VisualWrap-rfjV-').forEach(processElement);
  299. }
  300. }
  301. }
  302. }
  303. });
  304.  
  305. observer.observe(document.body, {
  306. childList: true,
  307. subtree: true
  308. });
  309.  
  310. document.querySelectorAll('.d-inline, .Box-sc-g0xbh4-0.fCvgBf, .repo-language-color, .Box-sc-g0xbh4-0.hjDqIa, .ActionListItem-visual.ActionListItem-visual--leading, .octicon-dot-fill, .prc-ActionList-LeadingVisual-dxXxW, .prc-ActionList-VisualWrap-rfjV-').forEach(processElement);
  311. }
  312.  
  313. async function init() {
  314. languageMappings = await fetchLanguageMappings();
  315. replaceLanguageIcons();
  316. }
  317.  
  318. if (document.readyState === 'loading') {
  319. document.addEventListener('DOMContentLoaded', init);
  320. } else {
  321. init();
  322. }
  323. })();

QingJ © 2025

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