GitHub Image Preview

A userscript that adds clickable image thumbnails

目前為 2016-05-18 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name          GitHub Image Preview
// @version       1.0.2
// @description   A userscript that adds clickable image thumbnails
// @license       https://creativecommons.org/licenses/by-sa/4.0/
// @namespace     http://github.com/Mottie
// @include       https://github.com/*
// @run-at        document-idle
// @grant         GM_addStyle
// @grant         GM_getValue
// @grant         GM_setValue
// @grant         GM_xmlhttpRequest
// @author        Rob Garrison
// ==/UserScript==
/* global GM_addStyle, GM_getValue, GM_setValue, GM_xmlhttpRequest */
/*jshint unused:true */
(function() {
  "use strict";

  GM_addStyle([
    "table.files tr.ghip-image-previews, table.files.ghip-show-previews tbody tr.js-navigation-item { display:none; }",
    "table.files.ghip-show-previews tr.ghip-image-previews { display:table-row; }",
    "table.files.ghip-show-previews .ghip-non-image { height:80px; margin-top:15px; opacity:.8; }",
    "table.files.ghip-show-previews .image { position:relative; overflow:hidden; text-align:center; }",
    ".ghip-image-previews .image { padding:10px; }",
    "table.files.ghip-tiled .image { width:21.9%; }",
    "table.files.ghip-tiled .image .border-wrap img, .ghip-image-previews .border-wrap svg { max-height:130px; }",
    "table.files.ghip-fullw .image { width:97%; height:auto; }",
    "table.files.ghip-tiled .image:hover img:not(.ghip-non-image) { zoom:3; }",
    ".ghip-image-previews .border-wrap img, .ghip-image-previews .border-wrap svg { max-width:95%; }",
    ".ghip-image-previews .border-wrap h4 { overflow:hidden; white-space:nowrap; text-overflow:ellipsis; margin-bottom:5px; }",
    ".btn.ghip-tiled > *, .btn.ghip-fullw > *, .ghip-image-previews iframe { pointer-events:none; }",
    ".image .ghip-file-type { font-size:18px; margin-top:10px; }",
    // override GitHub-Dark styles
    "table.files img[src*='octocat-spinner'], img[src='/images/spinner.gif'] { width:auto !important; height:auto !important; }"
  ].join(""));

  var busy = false,

  // supported img types
  imgExt = /(png|jpg|jpeg|gif|tif|tiff|bmp|webp)$/,
  svgExt = /svg$/,

  tiled = [
    "<svg class='octicon' xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 16 16'>",
      "<path d='M0 0h7v7H0zM9 9h7v7H9zM9 0h7v7H9zM0 9h7v7H0z'/>",
    "</svg>"
  ].join(""),

  fullWidth = [
    "<svg class='octicon' xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 16 16'>",
      "<path d='M0 0h16v7H0zM0 9h16v7H0z'/>",
    "</svg>"
  ].join(""),

  imgTemplate = [
    "<a href='${url}' class='exploregrid-item image js-navigation-open' rel='nofollow'>",
      "<span class='border-wrap'>${image}</span>",
    "</a>"
  ].join(""),

  addToggles = function() {
    if (document.querySelector(".gh-img-preview")) { return; }
    busy = true;
    var div = document.createElement("div"),
      btn = " btn btn-sm tooltipped tooltipped-n' aria-label='Show ";
    div.className = "btn-group right gh-img-preview";
    div.innerHTML = [
      "<div class='ghip-tiled" + btn + "tiled files with image preview'>" + tiled + "</div>",
      "<div class='ghip-fullw" + btn + "full width files with image preview'>" + fullWidth + "</div>"
    ].join("");
    document.querySelector(".file-navigation").appendChild(div);

    div.querySelector(".ghip-tiled").addEventListener("click", function() {
      openView("tiled");
    });
    div.querySelector(".ghip-fullw").addEventListener("click", function() {
      openView("fullw");
    });
    busy = false;
  },

  setInitState = function() {
    var view = GM_getValue("gh-image-preview");
    if (view) {
      openView(view);
    }
  },

  openView = function(name) {
    var el = document.querySelector(".ghip-" + name);
    if (el) {
      el.classList.toggle("selected");
      if (el.classList.contains("selected")) {
        GM_setValue("gh-image-preview", name);
        showPreview(name);
      } else {
        GM_setValue("gh-image-preview", "");
        showList();
      }
    }
  },

  showPreview = function(size) {
    buildPreviews();
    var table = document.querySelector("table.files"),
      btn1 = "ghip-" + size,
      btn2 = "ghip-" + (size === "fullw" ? "tiled" : "fullw");
    table.classList.add("ghip-show-previews");
    table.classList.add(btn1);
    document.querySelector(".btn." + btn1).classList.add("selected");
    table.classList.remove(btn2);
    document.querySelector(".btn." + btn2).classList.remove("selected");
  },

  showList = function() {
    var table = document.querySelector("table.files");
    table.classList.remove("ghip-show-previews");
    table.classList.remove("ghip-tiled");
    table.classList.remove("ghip-fullw");
    document.querySelector(".btn.ghip-tiled").classList.remove("selected");
    document.querySelector(".btn.ghip-fullw").classList.remove("selected");
  },

  buildPreviews = function() {
    busy = true;
    var template, url, temp, ext,
      indx = 0,
      row = document.createElement("tr"),
      imgs = "<td colspan='4' class='ghip-content'>",
      table = document.querySelector("table.files tbody:last-child"),
      files = document.querySelectorAll("tr.js-navigation-item"),
      len = files.length;
    row.className = "ghip-image-previews";
    if (document.querySelector(".ghip-image-previews")) {
      temp = document.querySelector(".ghip-image-previews");
      temp.parentNode.removeChild(temp);
    }
    if (table) {
      for (indx = 0; indx < len; indx++) {
        temp = files[indx].querySelector("td.content a");
        template = temp ? "<h4>" + temp.textContent.trim() + "</h4>" : "";
        url = temp ? temp.href : "";
        if (imgExt.test(url)) {
          // *** image preview ***
          template += "<img src='" + url + "?raw=true'/>";
          imgs += imgTemplate.replace("${url}", url).replace("${image}", template);
        } else if (svgExt.test(url)) {
          // *** svg preview ***
          // loaded & encoded because GitHub sets content-type headers as a string
          temp = url.substring(url.lastIndexOf("/") + 1, url.length);
          template += "<img data-svg-holder='" + temp + "' alt='" + temp + "' />";
          imgs += updateTemplate(url, template);
          getSVG(url + "?raw=true");
        } else {
          // *** non-images (file/folder icons) ***
          temp = files[indx].querySelector("td.icon svg");
          if (temp) {
            ext = temp.classList.contains("octicon-file-directory");
            // add xmlns otherwise the svg won't work inside an img
            // GitHub doesn't include this attribute on any svg octicons
            temp = temp.outerHTML.replace("<svg", "<svg xmlns='http://www.w3.org/2000/svg'");
            // include "leaflet-tile-container" to invert icon for GitHub-Dark
            template += "<span class='leaflet-tile-container'>" +
              "<img class='ghip-non-image' src='data:image/svg+xml;base64," + window.btoa(temp) + "'/>" +
              "</span>" +
              (ext ? "" : "<h4 class='ghip-file-type'>" +
                url.substring(url.lastIndexOf(".") + 1, url.length).toUpperCase() + "</h4>");
            imgs += updateTemplate(url, template);
          } else if (files[indx].classList.contains("up-tree")) {
            // Up tree link
            temp = files[indx].querySelector("td:nth-child(2) a");
            url = temp ? temp.href : "";
            if (url) {
              imgs += updateTemplate(url, "<h4>&middot;&middot</h4>");
            }
          }
        }
      }
      row.innerHTML = imgs + "</td>";
      table.appendChild(row);
    }
    busy = false;
  },

  updateTemplate = function(url, img) {
    return imgTemplate
      .replace("${url}", url)
      .replace("${image}", img);
  },

  getSVG = function(url) {
    GM_xmlhttpRequest({
      method: "GET",
      url: url,
      onload : function(response) {
        busy = true;
        var encoded,
          url = response.finalUrl,
          file = url.substring(url.lastIndexOf("/") + 1, url.length),
          target = document.querySelector("[data-svg-holder='" + file+ "']");
        if (target) {
          encoded = window.btoa(response.responseText);
          target.src = "data:image/svg+xml;base64," + encoded;
        }
        busy = false;
      }
    });
  },

  init = function() {
    if (document.querySelector("table.files")) {
      addToggles();
      setInitState();
    }
  },

  // timer needed for file list to update?
  timer,

  // DOM targets - to detect GitHub dynamic ajax page loading
  targets = document.querySelectorAll([
    "#js-repo-pjax-container",
    ".context-loader-container",
    "[data-pjax-container]"
  ].join(","));

  Array.prototype.forEach.call(targets, function(target) {
    new MutationObserver(function(mutations) {
      mutations.forEach(function(mutation) {
        // preform checks before adding code wrap to minimize function calls
        if (!busy && mutation.target === target) {
          clearTimeout(timer);
          timer = setTimeout(init, 200);
        }
      });
    }).observe(target, {
      childList: true,
      subtree: true
    });
  });

  init();

})();