GitHub Image Preview

A userscript that adds clickable image thumbnails

当前为 2016-05-18 提交的版本,查看 最新版本

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