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-03-04 提交的版本。查看 最新版本

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