GitHub Image Preview

A userscript that adds clickable image thumbnails

当前为 2019-09-23 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Image Preview
  3. // @version 1.2.4
  4. // @description A userscript that adds clickable image thumbnails
  5. // @license MIT
  6. // @author Rob Garrison
  7. // @namespace https://github.com/Mottie
  8. // @include https://github.com/*
  9. // @run-at document-idle
  10. // @grant GM_addStyle
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_xmlhttpRequest
  14. // @connect github.com
  15. // @connect githubusercontent.com
  16. // @require https://gf.qytechs.cn/scripts/28721-mutations/code/mutations.js?version=666427
  17. // @icon https://github.githubassets.com/pinned-octocat.svg
  18. // ==/UserScript==
  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 img.error { border:5px solid red;
  43. border-radius:32px; }
  44. .ghip-image-previews .border-wrap h4 { white-space:nowrap;
  45. text-overflow:ellipsis; margin-bottom:5px; }
  46. .ghip-image-previews .border-wrap h4.ghip-file-name { overflow:hidden; }
  47. .btn.ghip-tiled > *, .btn.ghip-fullw > *, .ghip-image-previews iframe {
  48. pointer-events:none; vertical-align:baseline; }
  49. .image .ghip-file-type { font-size:30px; top:-1.8em; position:relative;
  50. z-index:2; }
  51. .ghip-content span.exploregrid-item .ghip-file-name { cursor:default; }
  52. /* override GitHub-Dark styles */
  53. table.files img[src*='octocat-spinner'], img[src='/images/spinner.gif'] {
  54. width:auto !important; height:auto !important; }
  55. table.files td .simplified-path { color:#888 !important; }
  56. `);
  57.  
  58. // supported img types
  59. const imgExt = /(png|jpg|jpeg|gif|tif|tiff|bmp|webp)$/i;
  60. const svgExt = /svg$/i;
  61. const spinner = "https://github.githubassets.com/images/spinners/octocat-spinner-32.gif";
  62.  
  63. const folderIconClasses = `
  64. .octicon-file-directory,
  65. .octicon-file-symlink-directory,
  66. .octicon-file-submodule`;
  67.  
  68. const tiled = `
  69. <svg class="octicon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16">
  70. <path d="M0 0h7v7H0zM9 9h7v7H9zM9 0h7v7H9zM0 9h7v7H0z"/>
  71. </svg>`;
  72.  
  73. const fullWidth = `
  74. <svg class="octicon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16">
  75. <path d="M0 0h16v7H0zM0 9h16v7H0z"/>
  76. </svg>`;
  77.  
  78. const imgTemplate = [
  79. // not using backticks here
  80. "<a href='${url}' class='exploregrid-item image m-3 float-left js-navigation-open' rel='nofollow'>",
  81. "<span class='border-wrap'>${image}</span>",
  82. "</a>"
  83. ].join("");
  84.  
  85. const spanTemplate = [
  86. "<span class='exploregrid-item image m-3 float-left'>",
  87. "<span class='border-wrap'>${image}</span>",
  88. "</span>"
  89. ].join("");
  90.  
  91. function addToggles() {
  92. if ($(".gh-img-preview")) {
  93. return;
  94. }
  95. const div = document.createElement("div");
  96. const btn = `btn btn-sm BtnGroup-item tooltipped tooltipped-n" aria-label="Show`;
  97. div.className = "BtnGroup float-right gh-img-preview";
  98. div.innerHTML = `
  99. <button type="button" class="ghip-tiled ${btn} tiled files with image preview">${tiled}</button>
  100. <button type="button" class="ghip-fullw ${btn} full width files with image preview">${fullWidth}</button>
  101. `;
  102. $(".file-navigation").appendChild(div);
  103.  
  104. $(".ghip-tiled", div).addEventListener("click", event => {
  105. openView("tiled", event);
  106. });
  107. $(".ghip-fullw", div).addEventListener("click", event => {
  108. openView("fullw", event);
  109. });
  110. }
  111.  
  112. function setInitState() {
  113. const state = GM_getValue("gh-image-preview");
  114. if (state) {
  115. openView(state);
  116. }
  117. }
  118.  
  119. function openView(name, event) {
  120. const el = $(".ghip-" + name);
  121. if (el) {
  122. if (event) {
  123. el.classList.toggle("selected");
  124. if (!el.classList.contains("selected")) {
  125. return showList();
  126. }
  127. }
  128. showPreview(name);
  129. }
  130. }
  131.  
  132. function showPreview(name) {
  133. buildPreviews();
  134. const table = $("table.files");
  135. const selected = "ghip-" + name;
  136. const notSelected = "ghip-" + (name === "fullw" ? "tiled" : "fullw");
  137. table.classList.add("ghip-show-previews", selected);
  138. $(".btn." + selected).classList.add("selected");
  139. table.classList.remove(notSelected);
  140. $(".btn." + notSelected).classList.remove("selected");
  141. GM_setValue("gh-image-preview", name);
  142. }
  143.  
  144. function showList() {
  145. $("table.files").classList.remove(
  146. "ghip-show-previews", "ghip-tiled", "ghip-fullw"
  147. );
  148. $(".btn.ghip-tiled").classList.remove("selected");
  149. $(".btn.ghip-fullw").classList.remove("selected");
  150. GM_setValue("gh-image-preview", "");
  151. }
  152.  
  153. function buildPreviews() {
  154. let template, url, temp, noExt, fileName;
  155. let imgs = "<td colspan='4' class='ghip-content'>";
  156. let indx = 0;
  157. const row = document.createElement("tr");
  158. const table = $("table.files tbody:last-child");
  159. const files = $$("tr.js-navigation-item");
  160. const len = files.length;
  161. row.className = "ghip-image-previews";
  162. if ($(".ghip-image-previews")) {
  163. temp = $(".ghip-image-previews");
  164. temp.parentNode.removeChild(temp);
  165. }
  166. if (table) {
  167. for (indx = 0; indx < len; indx++) {
  168. // not every submodule includes a link; reference examples from
  169. // see https://github.com/electron/electron/tree/v1.1.1/vendor
  170. temp = $("td.content a", files[indx]) ||
  171. $("td.content span span", files[indx]);
  172. // use innerHTML because some links include path - see "third_party/lss"
  173. fileName = temp ? temp.innerHTML.trim() : "";
  174. // temp = temp && $("a", temp);
  175. url = temp && temp.nodeName === "A" ? temp.href : "";
  176. // add link color
  177. template = `<h4 class="ghip-file-name ${
  178. (url ? " text-blue" : "")}" title="${fileName}">
  179. ${fileName}
  180. </h4>`;
  181. if (imgExt.test(url)) {
  182. // *** image preview ***
  183. template += "<img src='" + url + "?raw=true'/>";
  184. imgs += imgTemplate
  185. .replace("${url}", url)
  186. .replace("${image}", template);
  187. } else if (svgExt.test(url)) {
  188. // *** svg preview ***
  189. // loaded & encoded because GitHub sets content-type headers as
  190. // a string
  191. temp = url.substring(url.lastIndexOf("/") + 1, url.length);
  192. template += `<img data-svg-holder="${temp}" data-svg-url="${url}" alt="${temp}" src="${spinner}" />`;
  193. imgs += updateTemplate(url, template);
  194. } else {
  195. // *** non-images (file/folder icons) ***
  196. temp = $("td.icon svg", files[indx]);
  197. if (temp) {
  198. // non-files svg class: "directory", "submodule" or "symlink"
  199. // add "ghip-folder" class for file-filters userscript
  200. noExt = temp.matches(folderIconClasses) ? " ghip-folder" : "";
  201. // add xmlns otherwise the svg won't work inside an img
  202. // GitHub doesn't include this attribute on any svg octicons
  203. temp = temp.outerHTML
  204. .replace("<svg", "<svg xmlns='http://www.w3.org/2000/svg'");
  205. // include "leaflet-tile-container" to invert icon for GitHub-Dark
  206. template += `<span class="leaflet-tile-container${noExt}">` +
  207. `<img class="ghip-non-image" src="data:image/svg+xml;base64,` +
  208. window.btoa(temp) + `"/></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 ghip-up-tree'>&middot;&middot;</h4>"
  233. ) : "";
  234. }
  235. }
  236. }
  237. row.innerHTML = imgs + "</td>";
  238. table.appendChild(row);
  239. lazyLoadSVGs();
  240. }
  241. }
  242.  
  243. function updateTemplate(url, img, tmpl) {
  244. return (tmpl || imgTemplate)
  245. .replace("${url}", url)
  246. .replace("${image}", img);
  247. }
  248.  
  249. function lazyLoadSVGs() {
  250. const imgs = $$("[data-svg-holder]");
  251. if (imgs.length && "IntersectionObserver" in window) {
  252. let imgObserver = new IntersectionObserver(entries => {
  253. entries.forEach(entry => {
  254. if (entry.isIntersecting) {
  255. const img = entry.target;
  256. setTimeout(() => {
  257. const bounds = img.getBoundingClientRect();
  258. // Don't load all svgs when the user scrolls down the page really
  259. // fast
  260. if (bounds.top <= window.innerHeight && bounds.bottom >= 0) {
  261. getSVG(imgObserver, img);
  262. }
  263. }, 300);
  264. }
  265. });
  266. });
  267. imgs.forEach(function(img) {
  268. imgObserver.observe(img);
  269. });
  270. }
  271. }
  272.  
  273. function getSVG(observer, img) {
  274. GM_xmlhttpRequest({
  275. method: "GET",
  276. url: img.dataset.svgUrl + "?raw=true",
  277. onload: response => {
  278. const url = response.finalUrl,
  279. file = url.substring(url.lastIndexOf("/") + 1, url.length),
  280. target = $("[data-svg-holder='" + file + "']"),
  281. resp = response.responseText,
  282. // Loading too many images at once makes GitHub returns a "You have triggered
  283. // an abuse detection mechanism" message
  284. abuse = resp.includes("abuse detection");
  285. if (target && !abuse) {
  286. const encoded = window.btoa(response.responseText);
  287. target.src = "data:image/svg+xml;base64," + encoded;
  288. target.title = "";
  289. target.classList.remove("error");
  290. observer.unobserve(img);
  291. } else if (abuse) {
  292. img.title = "GitHub is reporting that too many images have been loaded at once, please wait";
  293. img.classList.add("error");
  294. }
  295. }
  296. });
  297. }
  298.  
  299. function $(selector, el) {
  300. return (el || document).querySelector(selector);
  301. }
  302. function $$(selector, el) {
  303. return [...(el || document).querySelectorAll(selector)];
  304. }
  305.  
  306. function init() {
  307. if ($("table.files")) {
  308. addToggles();
  309. setTimeout(setInitState, 0);
  310. }
  311. }
  312.  
  313. document.addEventListener("ghmo:container", init);
  314. init();
  315. })();

QingJ © 2025

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