Gelbooru Image Viewer

Adds a fullscreen image view option when you click on images

Version au 12/03/2017. Voir la dernière version.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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