GitHub Image Preview

A userscript that adds clickable image thumbnails

当前为 2016-12-21 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Image Preview
  3. // @version 1.1.3
  4. // @description A userscript that adds clickable image thumbnails
  5. // @license https://creativecommons.org/licenses/by-sa/4.0/
  6. // @namespace https://github.com/Mottie
  7. // @include https://github.com/*
  8. // @run-at document-idle
  9. // @grant GM_addStyle
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @grant GM_xmlhttpRequest
  13. // @connect github.com
  14. // @connect githubusercontent.com
  15. // @author Rob Garrison
  16. // ==/UserScript==
  17. /* global GM_addStyle, GM_getValue, GM_setValue, GM_xmlhttpRequest */
  18. /* jshint unused:true, esnext:true */
  19. (() => {
  20. "use strict";
  21.  
  22. GM_addStyle(`
  23. table.files tr.ghip-image-previews,
  24. table.files.ghip-show-previews tbody tr.js-navigation-item {
  25. display:none; }
  26. table.files.ghip-show-previews tr.ghip-image-previews { display:table-row; }
  27. table.files.ghip-show-previews .ghip-non-image {
  28. height:80px; margin-top:15px; opacity:.2; }
  29. table.files.ghip-show-previews .image { position:relative; overflow:hidden;
  30. text-align:center; }
  31. .ghip-image-previews .image { padding:10px; }
  32. table.files.ghip-tiled .image { width:22.5%; height:180px;
  33. margin:12px !important; /* GitHub uses !important flags now :( */ }
  34. table.files.ghip-tiled .image .border-wrap img,
  35. .ghip-image-previews .border-wrap svg { max-height:130px; }
  36. table.files.ghip-fullw .image { width:97%; height:auto; }
  37. /* zoom doesn't work in Firefox, but "-moz-transform:scale(3);"
  38. doesn't limit the size of the image, so it overflows */
  39. table.files.ghip-tiled .image:hover img:not(.ghip-non-image) { zoom:3; }
  40. .ghip-image-previews .border-wrap img,
  41. .ghip-image-previews .border-wrap svg { max-width:95%; }
  42. .ghip-image-previews .border-wrap h4 { white-space:nowrap;
  43. text-overflow:ellipsis; margin-bottom:5px; }
  44. .ghip-image-previews .border-wrap h4.ghip-file-name { overflow:hidden; }
  45. .btn.ghip-tiled > *, .btn.ghip-fullw > *, .ghip-image-previews iframe {
  46. pointer-events:none; vertical-align:baseline; }
  47. .image .ghip-file-type { font-size:30px; top:-1.8em; position:relative;
  48. z-index:2; }
  49. .ghip-content span.exploregrid-item .ghip-file-name { cursor:default; }
  50. /* override GitHub-Dark styles */
  51. table.files img[src*='octocat-spinner'], img[src='/images/spinner.gif'] {
  52. width:auto !important; height:auto !important; }
  53. table.files td .simplified-path { color:#888 !important; }
  54. `);
  55.  
  56. // timer needed for file list to update?
  57. let timer,
  58. busy = false;
  59.  
  60. // supported img types
  61. const imgExt = /(png|jpg|jpeg|gif|tif|tiff|bmp|webp)$/i,
  62. svgExt = /svg$/i,
  63.  
  64. tiled = `
  65. <svg class="octicon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16">
  66. <path d="M0 0h7v7H0zM9 9h7v7H9zM9 0h7v7H9zM0 9h7v7H0z"/>
  67. </svg>
  68. `,
  69. fullWidth = `
  70. <svg class="octicon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16">
  71. <path d="M0 0h16v7H0zM0 9h16v7H0z"/>
  72. </svg>
  73. `,
  74. imgTemplate = [
  75. // not using backticks here
  76. "<a href='${url}' class='exploregrid-item image m-3 float-left js-navigation-open' rel='nofollow'>",
  77. "<span class='border-wrap'>${image}</span>",
  78. "</a>"
  79. ].join(""),
  80. spanTemplate = [
  81. "<span class='exploregrid-item image m-3 float-left'>",
  82. "<span class='border-wrap'>${image}</span>",
  83. "</span>"
  84. ].join("");
  85.  
  86. function addToggles() {
  87. if ($(".gh-img-preview")) {
  88. return;
  89. }
  90. busy = true;
  91. const div = document.createElement("div"),
  92. btn = `btn btn-sm BtnGroup-item tooltipped tooltipped-n" aria-label="Show`;
  93. div.className = "BtnGroup float-right gh-img-preview";
  94. div.innerHTML = `
  95. <div class="ghip-tiled ${btn} tiled files with image preview">${tiled}</div>
  96. <div class="ghip-fullw ${btn} full width files with image preview">${fullWidth}</div>
  97. `;
  98. $(".file-navigation").appendChild(div);
  99.  
  100. $(".ghip-tiled", div).addEventListener("click", () => {
  101. openView("tiled");
  102. });
  103. $(".ghip-fullw", div).addEventListener("click", () => {
  104. openView("fullw");
  105. });
  106. busy = false;
  107. }
  108.  
  109. function setInitState() {
  110. const view = GM_getValue("gh-image-preview");
  111. if (view) {
  112. openView(view);
  113. }
  114. }
  115.  
  116. function openView(name) {
  117. const el = $(".ghip-" + name);
  118. if (el) {
  119. el.classList.toggle("selected");
  120. if (el.classList.contains("selected")) {
  121. GM_setValue("gh-image-preview", name);
  122. showPreview(name);
  123. } else {
  124. GM_setValue("gh-image-preview", "");
  125. showList();
  126. }
  127. }
  128. }
  129.  
  130. function showPreview(size) {
  131. buildPreviews();
  132. const table = $("table.files"),
  133. btn1 = "ghip-" + size,
  134. btn2 = "ghip-" + (size === "fullw" ? "tiled" : "fullw");
  135. table.classList.add(...["ghip-show-previews", btn1]);
  136. $(".btn." + btn1).classList.add("selected");
  137. table.classList.remove(btn2);
  138. $(".btn." + btn2).classList.remove("selected");
  139. }
  140.  
  141. function showList() {
  142. $("table.files").classList.remove(...[
  143. "ghip-show-previews",
  144. "ghip-tiled",
  145. "ghip-fullw"
  146. ]);
  147. $(".btn.ghip-tiled").classList.remove("selected");
  148. $(".btn.ghip-fullw").classList.remove("selected");
  149. }
  150.  
  151. function buildPreviews() {
  152. busy = true;
  153. let template, url, temp, noExt,
  154. imgs = "<td colspan='4' class='ghip-content'>",
  155. indx = 0;
  156. const row = document.createElement("tr"),
  157. table = $("table.files tbody:last-child"),
  158. files = $$("tr.js-navigation-item"),
  159. len = files.length;
  160. row.className = "ghip-image-previews";
  161. if ($(".ghip-image-previews")) {
  162. temp = $(".ghip-image-previews");
  163. temp.parentNode.removeChild(temp);
  164. }
  165. if (table) {
  166. for (indx = 0; indx < len; indx++) {
  167. // not every submodule includes a link; reference examples from
  168. // see https://github.com/electron/electron/tree/v1.1.1/vendor
  169. temp = $("td.content a", files[indx]) ||
  170. $("td.content span span", files[indx]);
  171. // use innerHTML because some links include path - see "third_party/lss"
  172. template = temp ? temp.innerHTML.trim() + "</h4>" : "";
  173. // temp = temp && $("a", temp);
  174. url = temp && temp.nodeName === "A" ? temp.href : "";
  175. // add link color
  176. template = "<h4 class='ghip-file-name" + (url ? " text-blue" : "") +
  177. "'>" + template;
  178. if (imgExt.test(url)) {
  179. // *** image preview ***
  180. template += "<img src='" + url + "?raw=true'/>";
  181. imgs += imgTemplate
  182. .replace("${url}", url)
  183. .replace("${image}", template);
  184. } else if (svgExt.test(url)) {
  185. // *** svg preview ***
  186. // loaded & encoded because GitHub sets content-type headers as
  187. // a string
  188. temp = url.substring(url.lastIndexOf("/") + 1, url.length);
  189. template += `<img data-svg-holder="${temp}" alt="${temp}" />`;
  190. imgs += updateTemplate(url, template);
  191. getSVG(url + "?raw=true");
  192. } else {
  193. // *** non-images (file/folder icons) ***
  194. temp = $("td.icon svg", files[indx]);
  195. if (temp) {
  196. // non-files svg class: "octicon-file-directory" or
  197. // "octicon-file-submodule"
  198. noExt = temp.classList.contains("octicon-file-directory") ||
  199. temp.classList.contains("octicon-file-submodule");
  200. // add xmlns otherwise the svg won't work inside an img
  201. // GitHub doesn't include this attribute on any svg octicons
  202. temp = temp.outerHTML
  203. .replace("<svg", "<svg xmlns='http://www.w3.org/2000/svg'");
  204. // include "leaflet-tile-container" to invert icon for GitHub-Dark
  205. template += "<span class='leaflet-tile-container'>" +
  206. "<img class='ghip-non-image' src='data:image/svg+xml;base64," +
  207. window.btoa(temp) + "'/>" +
  208. "</span>";
  209. // get file name + extension
  210. temp = url.substring(url.lastIndexOf("/") + 1, url.length);
  211. // don't include extension for folders, or files with no extension,
  212. // or files starting with a "." (e.g. ".gitignore")
  213. template += (!noExt && temp.indexOf(".") > 0) ?
  214. "<h4 class='ghip-file-type'>" +
  215. temp
  216. .substring(temp.lastIndexOf(".") + 1, temp.length)
  217. .toUpperCase() +
  218. "</h4>" : "";
  219. imgs += url ?
  220. updateTemplate(url, template) :
  221. // empty url; use non-link template
  222. // see "depot_tools @ 4fa73b8" at
  223. // https://github.com/electron/electron/tree/v1.1.1/vendor
  224. updateTemplate(url, template, spanTemplate);
  225. } else if (files[indx].classList.contains("up-tree")) {
  226. // Up tree link
  227. temp = $("td:nth-child(2) a", files[indx]);
  228. url = temp ? temp.href : "";
  229. imgs += url ?
  230. updateTemplate(
  231. url,
  232. "<h4 class='text-blue'>&middot;&middot;</h4>"
  233. ) : "";
  234. }
  235. }
  236. }
  237. row.innerHTML = imgs + "</td>";
  238. table.appendChild(row);
  239. }
  240. busy = false;
  241. }
  242.  
  243. function updateTemplate(url, img, tmpl) {
  244. return (tmpl || imgTemplate)
  245. .replace("${url}", url)
  246. .replace("${image}", img);
  247. }
  248.  
  249. function getSVG(url) {
  250. GM_xmlhttpRequest({
  251. method: "GET",
  252. url,
  253. onload: response => {
  254. busy = true;
  255. let encoded;
  256. const url = response.finalUrl,
  257. file = url.substring(url.lastIndexOf("/") + 1, url.length),
  258. target = $("[data-svg-holder='" + file + "']");
  259. if (target) {
  260. encoded = window.btoa(response.responseText);
  261. target.src = "data:image/svg+xml;base64," + encoded;
  262. }
  263. busy = false;
  264. }
  265. });
  266. }
  267.  
  268. function $(selector, el) {
  269. return (el || document).querySelector(selector);
  270. }
  271. function $$(selector, el) {
  272. return Array.from((el || document).querySelectorAll(selector));
  273. }
  274.  
  275. function init() {
  276. if ($("table.files")) {
  277. addToggles();
  278. setInitState();
  279. }
  280. }
  281.  
  282. // DOM targets - to detect GitHub dynamic ajax page loading
  283. $$(
  284. "#js-repo-pjax-container, .context-loader-container, [data-pjax-container]"
  285. ).forEach(target => {
  286. new MutationObserver(mutations => {
  287. mutations.forEach(mutation => {
  288. // preform checks before adding code wrap to minimize function calls
  289. if (!busy && mutation.target === target) {
  290. clearTimeout(timer);
  291. timer = setTimeout(init, 200);
  292. }
  293. });
  294. }).observe(target, {
  295. childList: true,
  296. subtree: true
  297. });
  298. });
  299.  
  300. init();
  301. })();

QingJ © 2025

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