Gelbooru Image Viewer

Adds a fullscreen image view option when you click on images

2017-03-12 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @id             gelbooru-slide
// @name           Gelbooru Image Viewer
// @version        1.8.2
// @namespace      intermission
// @author         intermission
// @license        WTFPL; http://www.wtfpl.net/about/
// @description    Adds a fullscreen image view option when you click on images
// @include        http://gelbooru.com/index.php?*
// @include        https://gelbooru.com/index.php?*
// @run-at         document-start
// @grant          GM_registerMenuCommand
// @grant          GM_xmlhttpRequest
// ==/UserScript==

(function(d, w, stor){
  "use strict";
  var ns = "gelbooru-slide", toggle = stor[ns] == "true", notification, Pos, Menu, Btn, slideshow, $, Main, Prog;

  if (!stor[ns]) stor[ns] = "false";

  GM_registerMenuCommand("Current image mode: " + (toggle ? "Always original size" : "Sample only"), () => {
    stor[ns] = toggle ? "false" : "true";
    return location.reload();
  });

  if (stor[ns + "-firstrun"] != "1.5.3") {
    (function(l){
      var a, r = /^gelbooru-slide./;
      for (a in l)
        if (r.test(a)) l.removeItem(a);
    }(stor));
    stor[ns + "-firstrun"] = "1.5.3";
  }

  $ = function(a, b) {
    return [...(b || d).querySelectorAll(a)];
  };

  $.extend = function(obj, props) {
    var key, val;
    for (key in props) {
      if (!props.hasOwnProperty(key)) continue;
      val = props[key];
      obj[key] = val;
    }
  };

  $.extend($, {
    cache: function(a, b) {
      var id = a.match(Main.r[2])[1], val = toggle ? "original" : "sample", ret, obj, temp;
      if (!b) {
        try { ret = JSON.parse(stor[ns + id])[val]; } catch(e) {}
        ret = ret || "loading";
      } else {
        try { temp = JSON.parse(stor[ns + id]); } catch(e) {}
        obj = temp || {};
        obj[val] = b;
        stor[ns + id] = JSON.stringify(obj);
        ret = b;
      }
      return ret;
    },
    base: a => a.split("/").pop().split(".")[0].split("_").pop(),
    current: src => $("img.preview[src*='" + $.base(src || Main.el.dataset.src) + "']")[0].parentNode,
    find: function(el, method) {
      var a;
      el = el.parentNode;
      do {
        try {
          el = el[(method ? "next" : "previous") + "ElementSibling"];
          a = el.querySelector("a[data-full]");
        } catch(err) {
          return false;
        }
        if (a) break;
        a = false;
      } while(!a);
      return a;
    },
    preload: function() {
      var curr = $.current();
      Main.req($.find(curr, true));
      return Main.req($.find(curr, false));
    },
    keyDown: function(e) {
      var move;
      if (slideshow) return;
      switch(e.keyCode) {
        case 32: case 39:
          move = true;
          break;
        case 37:
          move = false;
          break;
        case 38:
          if (e.event) Menu.fn(e.event);
          else w.location = $.current().href;
          return;
        case 40:
          e.preventDefault();
          return Main.el.click();
      }
      if (typeof move != "undefined") {
        e = $.find($.current(), move);
        if (e) {
          Main.slide(e.firstElementChild.src);
          Pos.fn(move);
          $.preload();
        } else if (!notification) {
          notification = d.createElement("div");
          notification.classList.add("nomoreimages");
          notification.setAttribute("style", "pointer-events: none; background: linear-gradient(to " + (move ? "right" : "left") + ", transparent, rgba(255,0,0,.5));" + (move ? "right" : "left") + ": 0;");
          d.body.insertBefore(notification, d.body.lastElementChild);
        }
      } return;
    }
  });

  Pos = {
    fn: function(a) {
      var no, thumbs, el = Pos.el;
      if (typeof a === "boolean") {
        no = el.firstElementChild;
        no.innerHTML = Number(no.innerHTML) + (a ? 1 : -1);
        if (Menu.el)
          for (let a of $("a", Menu.el)) a.href = $.current().href;
      } else {
        if (Main.el && !el) {
          thumbs = $("span.thumb a[data-full]");
          Pos.el = el = d.createElement("div");
          el.insertAdjacentHTML("beforeend", "<span>" + (thumbs.indexOf($.current()) + 1) + "</span> / " + thumbs.length);
          el.setAttribute("style", "position: fixed; bottom: 20px; left: 0; display: block; pointer-events: none;");
          Main.el.insertAdjacentElement("afterend", el);
        } else if (el) Pos.el = el.remove();
      } return;
    }
  };

  Menu = {
    fn: function(e) {
      var _l = e.clientX + 1, _t = e.clientY + 1,
        left = (_l > w.innerWidth - 139 ? (_l - 139) : _l) + "px",
        top = (_t > w.innerHeight - 48 ? (_t - 48) : _t) + "px",
        href, el = Menu.el;
      if (el) {
        el.removeAttribute("class");
        el.style.left = left;
        el.style.top = top;
        setTimeout(() => el.classList.add("menuel"), 10);
      } else {
        href = $.current().href;
        Menu.el = el = d.createElement("div");
        el.id = "menuel";
        el.insertAdjacentHTML("beforeend", '<a href="'+href+'" style="margin-bottom: 2px">Open in This Tab</a><a href="'+href+'" target="_blank">Open in New Tab</a>');
        el.style.left = left;
        el.style.top = top;
        d.body.appendChild(el);
        el.classList.add("menuel");
      }
      return;
    }
  };

  Btn = {
    fn: function() {
      var sel, el = Btn.el;
      sel = "this.previousElementSibling.firstElementChild";
      if (el) {
        Btn.clear();
        Btn.el = Btn.el.remove();
      } else {
        Btn.el = el = d.createElement("div");
        el.setAttribute("style", 'opacity: .7;');
        el.className = "slideshow";
        el.insertAdjacentHTML('beforeend', '<span title="Slideshow">' + Btn.svg_play + "</span>" + `<div style="display: none;padding: 10px 0">Options<hr><label>Loop:&nbsp;<input type="checkbox" checked></label>&nbsp;<label onclick="${sel}.checked=true;${sel}.disabled=!${sel}.disabled">Shuffle:&nbsp;<input type="checkbox"></label><br>Interval: <input type="number" value="5" style="width: 100px"></div>`);
        Btn.svg_state = true;
        el.firstElementChild.onclick = Btn.cb;
        d.body.appendChild(el);
      }
    },
    clear: () => clearTimeout(Number(Btn.el.dataset.timer) || 0),
    cb: function() {
      var el = Btn.el, sel = "span.thumb a[data-full]",
      options = $("div input", el).map(a => 
        a.type == "number" ? (+a.value >= 1 ? +a.value : 1) * 1E3 : a.checked
      ), _fnS = () => {
        Main.el.removeEventListener("load", _fnS);
        el.dataset.timer = setTimeout(_fnT, options[2]);
      }, _fnT = () => {
        var _el;
        if (thumbs.length === 0) thumbs = $(sel);
        if (options[1]) {
          thumbs.splice(thumbs.indexOf($.current()), 1);
          _el = thumbs[Math.random() * thumbs.length >> 0];
        } else _el = $.find($.current(), true);
        if (!_el && options[0]) _el = $(sel)[0];
        if (!_el) return Btn.cb();
        Main.el.addEventListener("load", _fnS);
        Main.slide(_el.firstElementChild.src);
      }, thumbs = [];
      slideshow = !!Btn.svg_state;
      Pos.fn();
      el.firstElementChild.innerHTML = (Btn.svg_state = !Btn.svg_state) ? Btn.svg_play : Btn.svg_pause;
      if (slideshow) {
        el.dataset.timer = setTimeout(_fnT, options[2]);
        el.style.opacity = ".4";
      } else {
        Btn.clear();
        el.style.opacity = ".7";
        el.removeAttribute("data-timer");
      }
      return;
    },
    svg_play: '<svg width="50" height="50" version="1.1" xmlns="http://www.w3.org/2000/svg"><rect rx="5" height="48" width="48" y="1" x="1" fill="#fff" /><polygon fill="#000" points="16 12 16 38 36 25" /></svg>',
    svg_pause: '<span><svg width="50" height="50" xmlns="http://www.w3.org/2000/svg"><rect fill="#fff" x="1" y="1" width="48" height="48" rx="5" /><rect fill="#000" x="12" y="12" width="10" height="26" /><rect fill="#000" x="28" y="12" width="10" height="26" /></svg>'
  };

  Prog = {
    check: e => Main.el.dataset.src.indexOf($.base(e.finalUrl || e)) > -1,
    load: function(id, e) {
      var blob, type, el = Prog.el, img = Main.el;
      if (!el) throw Error("There was an event order issue with GM_xmlhttpRequest");
      if (img && Prog.check(e)) {
        type = e.responseHeaders.match(/Content-Type: ([a-z\/\+]+)/)[1];
        blob = new Blob([e.response], {type: type});
        img.onload = function() {
          w.URL.revokeObjectURL(img.src);
          img.onload = null;
        };
        el.classList.add("progdone");
        img.src = w.URL.createObjectURL(blob);
      }
      delete Prog.reqs[id];
    },
    progress: function(url, e) {
      var el = Prog.el;
      if (!el) {
        Prog.el = el = d.createElement("span");
        el.setAttribute("style", "width:0");
        el.classList.add("progress");
        d.body.appendChild(el);
      }
      if (e.lengthComputable && Main.el && Prog.check(url) && el) {
        el.classList.remove("progfail");
        el.classList.remove("progdone");
        el.style.width = e.loaded / e.total * 100 + "%";
      }
    },
    error: function(id, e) {
      var el = Prog.el;
      if (Main.el && Prog.check(e) && el) {
        el.classList.add("progfail");
        try { Prog.reqs[id].abort(); }
        catch(err) {}
        delete Prog.reqs[id];
      }
    },
    fn: function(url) {
      var id = (new Date()).getTime().toString(), details, err = Prog.error.bind(Prog, id);
      if (Prog.el) Prog.el.style.width = 0;
      details = {
        method: "GET",
        url: url,
        responseType: "arraybuffer",
        onload: Prog.load.bind(Prog, id),
        onprogress: Prog.progress.bind(Prog, url),
        onerror: err,
        onabort: err,
        ontimeout: err
      };
      Prog.reqs[id] = GM_xmlhttpRequest(details);
    },
    reqs: {}
  };

  Main = {
    init: function() {
      var style = d.createElement("style"), observer;
      function process(node) {
        var a;
        try {
          if (node.matches("span.thumb[id^='s']") && (a = node.firstElementChild) && !a.dataset.full) {
            if ($("img[alt*='webm']", node)[0]) return;
            a.dataset.full = $.cache(a.href);
            a.onclick = e => e.button === 0 && (e.preventDefault(), e.stopPropagation(), Main.fn(e.target.parentNode));
          }
        } catch(e) {} return;
      }
      style.appendChild(d.createTextNode(Main.css));
      d.head.appendChild(style);
      observer = new MutationObserver(mutations => mutations.forEach(mutation => [...mutation.addedNodes].forEach(process)));
      observer.observe(d, {
        childList: true,
        subtree: true
      });
      d.addEventListener("animationend", e => {
        switch(e.animationName) {
          case "Outlined":
            e.target.classList.remove("outlined");
            break;
          case "nomoreimages":
            notification = e.target.remove();
            break;
          case "menuelement":
            Menu.el = e.target.remove();
            break;
          case "progfail": case "progdone":
            Prog.el = e.target.remove();
        }
      });
      w.addEventListener("keypress", e => {
        if (e.key === "Enter" || e.keyCode === 13) {
          if (slideshow) {
            Btn.el.firstElementChild.click();
          } else if (e.target.matches("span.thumb>a[data-full]")) {
            e.preventDefault();
            Main.fn(e.target);
          }
        }
      });
      w.addEventListener("wheel", e => Main.el && $.keyDown({keyCode: e.deltaY > 0 ? 39 : e.deltaY < 0 ? 37 : 0}));
    },
    fn: function(node) {
      d.dispatchEvent(new CustomEvent(ns, {bubbles:true}));
      return Main[!!Main.el ? "off" : "on"](node);
    },
    off: function(a) {
      var center;
      slideshow = !(a = $.current());
      Main.el = Main.el.remove();
      d.body.classList.remove("sliding");
      a.classList.add("outlined");
      d.removeEventListener("keydown", $.keyDown);
      center = a.offsetTop + a.offsetHeight / 2 - w.innerHeight / 2;
      w.scrollTo(0, center < 0 ? 0 : center);
      Pos.fn(); Btn.fn();
      Prog.el = Prog.el.remove();
    },
    on: function(a) {
      var el;
      d.body.classList.add("sliding");
      for (let a of $("span>a.outlined")) a.classList.remove("outlined");
      Main.el = el = d.createElement("img");
      el.id = "slide";
      el.alt = "Loading...";
      el.onclick = Main.fn;
      el.onmouseup = e => e.button === 1 && $.keyDown({keyCode:38, event:e});
      d.body.appendChild(el);
      Main.slide(a.firstElementChild.src);
      d.addEventListener("keydown", $.keyDown);
      Pos.fn(); Btn.fn(); $.preload();
    },
    slide: function(src) {
      var data;
      if (!slideshow) Main.el.src = src;
      Main.el.dataset.src = src;
      data = $.current(src).dataset.full;
      /* dirty hack ahead because GIF doesn't want to play as a blob
       * and doesn't give proper progress info for 
       * GM_xmlhttpRequest for whatever reason */
      if (data == "loading") Main.req($.current(src));
      else if (data.match(Main.r[3])[1].toLowerCase() == "gif") {
        Main.el.removeAttribute("src");
        Main.el.src = data;
      }
      else Prog.fn(data);
    },
    r: [new RegExp((toggle ? "file_url" : "sample_url") + '="?([^"]+)"?', "i"), /com\/images\//i, /id=([0-9]+)/, /\.(gif|png|jpe?g)/i],
    req: function(node) {
      var process;
      if (!node || node.dataset.alreadyLoading || node.dataset.full != "loading") return;
      node.dataset.alreadyLoading = "true";
      process = function(img) {
        img = img.match(Main.r[0])[1].replace(Main.r[1], "com//images/");
        if (!img) throw Error("API error");
        node.dataset.full = $.cache(node.href, img);
        if (Main.el && Main.el.dataset.src.indexOf($.base(img)) > -1) Main.slide(img);
        return node.removeAttribute("data-already-loading");
      };
      return fetch("/index.php?page=dapi&s=post&q=index&id=" + node.id.substr(1))
        .then(x => x.text())
        .then(process)
        .catch(err => {
          console.error("Failed HTTP request\nDo you have an internet connection?\n", err);
          return node.removeAttribute("data-already-loading");
        });
    },
    css: `
@keyframes Outlined {
  0% { outline: 6px solid orange }
  60% { outline: 6px solid orange }
  100% { outline: 6px solid transparent }
}
@keyframes nomoreimages {
  0% { opacity: 0 }
  20% { opacity: 1 }
  100% { opacity: 0 }
}
@keyframes menuelement {
  0% { opacity: 1 }
  80% { opacity: 1 }
  100% { opacity: 0 }
}
@keyframes progfail {
  0% { opacity: 1 }
  60% { opacity: 1 }
  100% { opacity: 0 }
}
body.sliding > *:not(#slide) {
  display: none
}
#slide {
  width: 100vw;
  height: 100vh;
  object-fit: contain
}
.outlined {
  outline: 6px solid transparent;
  animation-duration: 4s;
  animation-name: Outlined
}
.nomoreimages {
  display: block ! important;
  width: 33vw;
  height: 100vh;
  top: 0;
  position: fixed;
  animation-duration: 1s;
  animation-name: nomoreimages
}
span.thumb {
  max-width: 180px;
  max-height: 180px;
}
#menuel {
  opacity: 1;
  position: fixed;
  display: block ! important;
  padding: 2px;
  background: black;
  width: 139px;
  height: 44px;
  animation-duration: 1s
}
.menuel {
  animation-name: menuelement
}
#menuel:hover {
  animation-name: keepalive
}
#menuel a {
  background: #fff;
  display: block
}
.slideshow {
  display: block ! important;
  position: fixed;
  bottom: 20px;
  right: 20px
}
.progress {
  display: block ! important;
  background-color: rgb(128,200,255);
  height: 1vh;
  position: absolute;
  top: 0;
  left: 0;
  box-shadow: 0 .5vh 10px rgba(0,0,0,.7), inset 0 0 .1vh black;
  transition: ease-in-out .08s width;
  min-height: 3px;
  max-width: 100vw;
  pointer-events: none
}
.progfail {
  background-color: red ! important;
  width: 100% ! important;
  animation-name: progfail;
  animation-duration: .6s
}
.progdone {
  width: 100% ! important;
  animation-name: progfail;
  animation-duration: .6s
}
.slideshow:hover:not([data-timer]) > div {
  background: white;
  color: black;
  position: fixed;
  display: block ! important;
  bottom: 70px;
  right: 20px
}`
  };

  Main.init();

}(document, window, localStorage));