GitHub file list beautifier

Adds colors to files by type, displays small images in place of file-type icons in a repository source tree

  1. // ==UserScript==
  2. // @name GitHub file list beautifier
  3. // @description Adds colors to files by type, displays small images in place of file-type icons in a repository source tree
  4. // @license MIT License
  5. //
  6. // @version 4.0.1
  7. //
  8. // @match https://github.com/*
  9. //
  10. // @grant none
  11. // @run-at document-start
  12. //
  13. // @author wOxxOm
  14. // @namespace wOxxOm.scripts
  15. // @icon https://octodex.github.com/images/murakamicat.png
  16. // ==/UserScript==
  17.  
  18. 'use strict';
  19.  
  20. let savedConfig = {};
  21. try {
  22. savedConfig = JSON.parse(localStorage.FileListBeautifier) || {};
  23. } catch (e) {}
  24.  
  25. const config = Object.assign({},
  26. ...Object.entries({
  27. iconSize: 24,
  28. colorSeed1: 13,
  29. colorSeed2: 1299721,
  30. colorSeed3: 179426453,
  31. }).map(([k, v]) => ({[k]: +savedConfig[k] || v})));
  32.  
  33. const IMG_CLS = 'wOxxOm-image-icon';
  34. const rxImages = /^(png|jpe?g|bmp|gif|cur|ico|svg)$/i;
  35. const styleQueue = [];
  36. const {sheet} = document.documentElement.appendChild($create('style', {
  37. textContent: /*language=CSS*/ `
  38. .${IMG_CLS} {
  39. width: ${config.iconSize}px;
  40. height: ${config.iconSize}px;
  41. object-fit: scale-down;
  42. margin: 0 -4px;
  43. }
  44. a[file-type=":folder"] {
  45. font-weight: bold;
  46. }
  47. `.replace(/;/g, '!important;'),
  48. }));
  49.  
  50. const filetypes = {};
  51. const ME = Symbol(GM_info.script.name);
  52. const ob = new MutationObserver(start);
  53.  
  54. let lumaBias, lumaFix, lumaAmp;
  55.  
  56. requestAnimationFrame(start);
  57.  
  58. function start() {
  59. beautify();
  60. ob.observe(document, {subtree: true, childList: true});
  61. }
  62.  
  63. function beautify() {
  64. for (const el of document.querySelectorAll('.react-directory-truncate, .js-navigation-open')) {
  65. if (ME in el)
  66. continue;
  67. el[ME] = true;
  68. const isOld = el.tagName === 'A';
  69. const a = isOld ? el : el.getElementsByTagName('a')[0];
  70. const url = a && a.href;
  71. if (!url)
  72. continue;
  73. const icon = el.closest(isOld ? '.js-navigation-item' : 'td').querySelector('svg');
  74. if (icon.classList.contains(isOld ? 'octicon-file-directory-fill' : 'icon-directory')) {
  75. a.setAttribute('file-type', ':folder');
  76. continue;
  77. }
  78. const ext = url.match(/\.(\w+)$|$/)[1] || ':empty';
  79. a.setAttribute('file-type', ext);
  80. if (!filetypes[ext])
  81. addFileTypeStyle(ext);
  82. if (rxImages.test(ext)) {
  83. const m = url.match(/github\.com\/(.+?\/)blob\/(.*)$/);
  84. const next = icon.nextElementSibling;
  85. if (!m || next && next[ME])
  86. continue;
  87. icon.replaceWith($create('img', {
  88. [ME]: true,
  89. className: IMG_CLS,
  90. src: `https://raw.githubusercontent.com/${m[1]}${m[2]}`,
  91. }));
  92. }
  93. }
  94. }
  95.  
  96. function addFileTypeStyle(type) {
  97. filetypes[type] = true;
  98. if (!styleQueue.length)
  99. requestAnimationFrame(commitStyleQueue);
  100. styleQueue.push(type);
  101. }
  102.  
  103. function commitStyleQueue() {
  104. if (!lumaAmp) initLumaScale();
  105. const seed2 = config.colorSeed2;
  106. const seed3 = config.colorSeed3;
  107. for (const type of styleQueue) {
  108. const hash = calcSimpleHash(type);
  109. const H = hash % 360;
  110. const Hq = H / 60;
  111. const S = hash * seed2 % 50 + 50 | 0;
  112. const redFix = (Hq < 1 ? 1 - Hq : Hq > 4 ? (Hq - 4) / 2 : 0);
  113. const blueFix = (Hq < 3 || Hq > 5 ? 0 : Hq < 4 ? Hq - 3 : 5 - Hq) * 3;
  114. const L = hash * seed3 % lumaAmp + lumaBias + (redFix + blueFix) * lumaFix * S / 100 | 0;
  115. sheet.insertRule(/*language=CSS*/ `
  116. a[file-type="${type}"]:not(#foo) {
  117. color: hsl(${H},${S}%,${L}%) !important;
  118. }
  119. `);
  120. }
  121. styleQueue.length = 0;
  122. }
  123.  
  124. function calcSimpleHash(text) {
  125. let hash = 0;
  126. for (let i = 0, len = text.length; i < len; i++)
  127. hash = ((hash << 5) - hash) + text.charCodeAt(i);
  128. return Math.abs(hash * config.colorSeed1 | 0);
  129. }
  130.  
  131. function initLumaScale() {
  132. const [, r, g, b] = getComputedStyle(document.body).backgroundColor.split(/[^\d.]+/).map(parseFloat);
  133. const isDark = (r * .2126 + g * .7152 + b * .0722) < 128;
  134. [lumaBias, lumaAmp, lumaFix] = isDark ? [30, 50, 12] : [25, 15, 0];
  135. }
  136.  
  137. function $create(tag, props) {
  138. return Object.assign(document.createElement(tag), props);
  139. }