GitHub Image Preview

A userscript that adds clickable image thumbnails

目前為 2016-06-07 提交的版本,檢視 最新版本

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