Gelbooru Image Viewer

Adds a fullscreen image view option when you click on images

Versione datata 12/03/2017. Vedi la nuova versione l'ultima versione.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==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));