GitHub file list beautifier

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

当前为 2020-06-24 提交的版本,查看 最新版本

  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 3.0.8
  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 styleQueue = [];
  35. const {sheet} = document.head.appendChild(Object.assign(document.createElement('style'), {
  36. textContent: /*language=CSS*/ `
  37. .${IMG_CLS} {
  38. max-width: ${config.iconSize}px;
  39. max-height: ${config.iconSize}px;
  40. width: auto;
  41. height: auto;
  42. margin: auto;
  43. position: absolute;
  44. top: 0;
  45. left: 0;
  46. right: 0;
  47. bottom: 0;
  48. }
  49. .wOxxOm-image-cell {
  50. position: relative;
  51. padding: 0;
  52. margin: 0 10px 0 -6px;
  53. min-width: ${config.iconSize + 4}px;
  54. line-height: inherit;
  55. }
  56. .wOxxOm-image-cell svg {
  57. display: none;
  58. }
  59. a[file-type=":folder"] {
  60. font-weight: bold;
  61. }
  62. `.replace(/;/g, '!important;'),
  63. }));
  64.  
  65. const filetypes = {};
  66. const ME = Symbol(GM_info.script.name);
  67. const ob = new MutationObserver(() => {
  68. if (document.getElementById('files')) {
  69. ob.disconnect();
  70. start();
  71. }
  72. });
  73.  
  74. let lumaBias, lumaFix, lumaAmp;
  75.  
  76. start();
  77.  
  78. function start() {
  79. beautify();
  80. ob.observe(document, {subtree: true, childList: true});
  81. }
  82.  
  83. // postpone observing until the parser can breathe after the initial burst of activity during page load
  84. function beautify() {
  85. const el = document.getElementById('files');
  86. const table = el && el.parentElement.querySelector('.js-navigation-container');
  87. if (!table)
  88. return;
  89. for (const a of table.getElementsByClassName('js-navigation-open')) {
  90. if (!a.hasAttribute('href') || ME in a)
  91. continue;
  92. a[ME] = true;
  93. const row = a.closest('.js-navigation-item');
  94. if (row && row.querySelector('.octicon-file-directory')) {
  95. a.setAttribute('file-type', ':folder');
  96. continue;
  97. }
  98.  
  99. const ext = a.href.match(/\.(\w+)$|$/)[1] || ':empty';
  100. a.setAttribute('file-type', ext);
  101. if (!filetypes[ext])
  102. addFileTypeStyle(ext);
  103.  
  104. if (/^(png|jpe?g|bmp|gif|cur|ico)$/.test(ext)) {
  105. const m = a.href.match(/github\.com\/(.+?\/)blob\/(.*)$/);
  106. const icon = row.querySelector('.octicon');
  107. if (!m || !icon)
  108. continue;
  109. const next = icon.nextElementSibling;
  110. if (next && next.classList.contains(IMG_CLS))
  111. continue;
  112. icon.insertAdjacentElement('afterend', Object.assign(
  113. document.createElement('img'), {
  114. className: IMG_CLS,
  115. src: `https://raw.githubusercontent.com/${m[1]}${m[2]}`,
  116. }));
  117. icon.parentElement.classList.add('wOxxOm-image-cell');
  118. }
  119. }
  120. }
  121.  
  122. function addFileTypeStyle(type) {
  123. filetypes[type] = true;
  124. if (!styleQueue.length)
  125. requestAnimationFrame(commitStyleQueue);
  126. styleQueue.push(type);
  127. }
  128.  
  129. function commitStyleQueue() {
  130. if (!lumaAmp) initLumaScale();
  131. const seed2 = config.colorSeed2;
  132. const seed3 = config.colorSeed3;
  133. for (const type of styleQueue) {
  134. const hash = calcSimpleHash(type);
  135. const H = hash % 360;
  136. const Hq = H / 60;
  137. const S = hash * seed2 % 50 + 50 | 0;
  138. const redFix = (Hq < 1 ? 1 - Hq : Hq > 4 ? (Hq - 4) / 2 : 0);
  139. const blueFix = (Hq < 3 || Hq > 5 ? 0 : Hq < 4 ? Hq - 3 : 5 - Hq) * 3;
  140. const L = hash * seed3 % lumaAmp + lumaBias + (redFix + blueFix) * lumaFix * S / 100 | 0;
  141. sheet.insertRule(/*language=CSS*/ `
  142. a[file-type="${type}"]:not(#foo) {
  143. color: hsl(${H},${S}%,${L}%) !important;
  144. }
  145. `);
  146. }
  147. styleQueue.length = 0;
  148. }
  149.  
  150. function calcSimpleHash(text) {
  151. let hash = 0;
  152. for (let i = 0, len = text.length; i < len; i++)
  153. hash = ((hash << 5) - hash) + text.charCodeAt(i);
  154. return Math.abs(hash * config.colorSeed1 | 0);
  155. }
  156.  
  157. function initLumaScale() {
  158. const [, r, g, b] = getComputedStyle(document.body).backgroundColor.split(/[^\d.]+/).map(parseFloat);
  159. const isDark = (r * .2126 + g * .7152 + b * .0722) < 128;
  160. [lumaBias, lumaAmp, lumaFix] = isDark ? [30, 50, 12] : [25, 15, 0];
  161. }