Gelbooru Paged Gallery

A simplified gallery for viewing gelbooru images in succession.

// ==UserScript==
// @name        Gelbooru Paged Gallery
// @namespace   zixaphir
// @description A simplified gallery for viewing gelbooru images in succession.
// @match       *://*.gelbooru.com/index.php?*
// @version     1
// @grant       none
// ==/UserScript==

/*
#
 * $ object largely based on 4chan X's $, which is largely based on jQuery.
 * non-chainable.
#
 * Copyright (c) 2009-2011 James Campos <james.r.campos@gmail.com>
 * Copyright (c) 2012-2014 Nicolas Stepien <stepien.nicolas@gmail.com>
#
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
#
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
#
 */

(function() {
  "use strict";
  var $, FNLIMIT, LIMIT, PRELOAD, SimpleDict, THRESHOLD, cb, clickThumb, d, err, fer, g, loadGallery, mkImage, mkURL, preload, query, queryImages, setImage, setup, setupImages, updateImages,
    slice = [].slice;

  d = document;

  FNLIMIT = 255;

  PRELOAD = 1;

  THRESHOLD = 3;

  LIMIT = 100;

  g = {
    galleryCSS: "#a-gallery {\n  position: fixed;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  z-index: 30;\n  display: flex;\n  flex-direction: row;\n  background: rgba(0,0,0,0.7);\n}\n.gal-thumbnails {\n  flex-basis: 170px;\n  overflow-y: auto;\n  overflow-x: hidden;\n  display: flex;\n  flex-direction: column;\n  align-items: stretch;\n  text-align: center;\n  background: rgba(0,0,0,.5);\n  border-left: 1px solid #222;\n  order: 3;\n}\n.gal-hide-thumbnails .gal-thumbnails {\n  display: none;\n}\n.gal-thumb img {\n  max-width: 150px;\n  max-height: 150px;\n  height: auto;\n  width: auto;\n}\n.gal-thumb {\n  flex-basis: auto;\n  padding: 3px;\n  line-height: 0;\n  transition: background .2s linear;\n}\n.gal-highlight {\n  background: rgba(0, 190, 255, .8);\n}\n.gal-prev {\n  order: 0;\n  border-right: 1px solid #222;\n}\n.gal-next {\n  order: 2;\n  border-left: 1px solid #222;\n}\n.gal-prev,\n.gal-next {\n  flex-basis: 20px;\n  position: relative;\n  cursor: pointer;\n  opacity: 0.7;\n  background-color: rgba(0, 0, 0, 0.3);\n}\n.gal-prev:hover,\n.gal-next:hover {\n  opacity: 1;\n}\n.gal-prev::after,\n.gal-next::after {\n  position: absolute;\n  top: 50%;\n  transform: translateY(-50%)\n  line-height: 22px;\n  display: inline-block;\n  border-top: 11px solid transparent;\n  border-bottom: 11px solid transparent;\n  content: \"\";\n}\n.gal-prev::after {\n  border-right: 12px solid #fff;\n  right: 5px;\n}\n.gal-next::after {\n  border-left: 12px solid #fff;\n  right: 3px;\n}\n.gal-image {\n  position: relative;\n  order: 1;\n  display: flex;\n  align-items: flex-start;\n  justify-content: space-around;\n  overflow: hidden;\n  flex-grow: 1;\n}\n:root:not(.gal-fit-height):not(.gal-pdf) .gal-image {\n  overflow-y: auto !important;\n}\n:root:not(.gal-fit-width):not(.gal-pdf) .gal-image {\n  overflow-x: auto !important;\n}\n.gal-image a {\n  line-height: 0;\n}\n.gal-image > div {\n  margin: auto;\n  max-width: 100%;\n}\n:root.gal-pdf .gal-image a {\n  width: 100%;\n  height: 100%;\n}\n.gal-image video,\n.gal-image img {\n  max-width: 100%;\n}\n.gal-fit-height .gal-image video,\n.gal-fit-height .gal-image img {\n  max-height: 95vh;\n}\n.gal-image iframe {\n  width: 100%;\n  height: 100%;\n}\n.gal-buttons .menu-button {\n  bottom: 2px;\n  color: #ffffff;\n  text-shadow: 0px 0px 1px #000000;\n}\n.gal-close {\n  font-size: 2em;\n  color: #ffffff;\n  text-shadow: 0px 0px 1px #000000;\n  top: 5px;\n  cursor: pointer;\n}\n.gal-close,\n.gal-info {\n  position: absolute;\n  right: 5px;\n}\n.gal-info {\n  bottom: 5px;\n  background: rgba(0,0,0,0.6) !important;\n}\n.gal-info,\n.gal-ex-info {\n  border-radius: 3px;\n  padding: 1px 5px 2px 5px;\n  color: #ffffff !important;\n}\n.gal-ex-info {\n  display: none;\n  position: absolute;\n  bottom: 0;\n  right: 0;\n  font-size: 12px;\n  font-family: calibri;\n  min-width: 200px;\n  padding-left: 15px;\n  text-indent: -15px;\n  background: rgba(0,0,0,0.8) !important;\n}\n.gal-ex-info p {\n  margin: 3px;\n}\n.gal-info:hover .gal-ex-info {\n  display: block;\n}\n:root:not(.gal-fit-width):not(.gal-pdf) .gal-name {\n  bottom: 23px !important;\n}\n:root:not(.gal-fit-width):not(.gal-pdf) .gal-count {\n  bottom: 44px !important;\n}\n:root.gal-fit-height:not(.gal-pdf):not(.gal-hide-thumbnails) .gal-buttons,\n:root.gal-fit-height:not(.gal-pdf):not(.gal-hide-thumbnails) .gal-name,\n:root.gal-fit-height:not(.gal-pdf):not(.gal-hide-thumbnails) .gal-count {\n  right: 178px !important;\n}\n:root.gal-hide-thumbnails:.gal-fit-height:not(.gal-pdf) .gal-buttons,\n:root.gal-hide-thumbnails:.gal-fit-height:not(.gal-pdf) .gal-name,\n:root.gal-hide-thumbnails:.gal-fit-height:not(.gal-pdf) .gal-count {\n  right: 28px !important;\n}\n.spinner {\n  width: 30px;\n  height: 30px;\n  background-color: #aaa;\n  -webkit-animation: rotateplane 1.2s infinite ease-in-out;\n  animation: rotateplane 1.2s infinite ease-in-out;\n}\n@-webkit-keyframes rotateplane {\n  0% { -webkit-transform: perspective(120px) }\n  50% { -webkit-transform: perspective(120px) rotateY(180deg) }\n  100% { -webkit-transform: perspective(120px) rotateY(180deg)  rotateX(180deg) }\n}\n@keyframes rotateplane {\n  0% {\n    transform: perspective(120px) rotateX(0deg) rotateY(0deg);\n    -webkit-transform: perspective(120px) rotateX(0deg) rotateY(0deg)\n  } 50% {\n    transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg);\n    -webkit-transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg)\n  } 100% {\n    transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);\n    -webkit-transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);\n  }\n}",
    nodes: {}
  };

  (function() {
    var z;
    z = 0;
    return Object.defineProperty(g, "currentImageIndex", {
      set: function(x) {
        return z = Math.min(+g.images.length, Math.max(x, 0));
      },
      get: function() {
        return z;
      }
    });
  })();

  $ = function(query, root) {
    if (!root) {
      root = d.body;
    }
    return root.querySelector(query);
  };

  $.$ = function(query, root) {
    if (!root) {
      root = d.body;
    }
    return slice.call(root.querySelectorAll(query));
  };

  $.asap = function(test, fn) {
    var callback;
    callback = function() {
      var err;
      try {
        return fn();
      } catch (_error) {
        err = _error;
        console.log(err.message);
        return console.log(err.stack);
      }
    };
    if (test()) {
      return callback();
    } else {
      return setTimeout($.asap, 25, test, callback);
    }
  };

  $.addStyle = function(css, id) {
    var style;
    style = $.el('style', {
      textContent: css
    });
    if (id) {
      style.id = id;
    }
    $.asap((function() {
      return d.head;
    }), function() {
      return $.add(d.head, style);
    });
    return style;
  };

  $.on = function(target, events, fun, once) {
    var event, fn, func, j, len1, ref;
    fn = function() {
      var err;
      try {
        return fun.apply(this, arguments);
      } catch (_error) {
        err = _error;
        console.log(err.message);
        return console.log(err.stack);
      }
    };
    func = once ? function() {
      $.off(target, events, func);
      return fn.apply(this, arguments);
    } : fn;
    ref = events.split(' ');
    for (j = 0, len1 = ref.length; j < len1; j++) {
      event = ref[j];
      target.addEventListener(event, func, false);
    }
    return func;
  };

  $.off = function(target, events, fn) {
    var event, j, len1, ref;
    ref = events.split(' ');
    for (j = 0, len1 = ref.length; j < len1; j++) {
      event = ref[j];
      target.removeEventListener(event, fn, false);
    }
    return target;
  };

  $.el = function(type, props) {
    var el, prop;
    el = d.createElement(type);
    for (prop in props) {
      if (props.hasOwnProperty(prop)) {
        el[prop] = props[prop];
      }
    }
    return el;
  };

  $.nodes = function(nodes) {
    var frag, j, len1, node;
    if (!(nodes instanceof Array)) {
      return nodes;
    }
    frag = d.createDocumentFragment();
    for (j = 0, len1 = nodes.length; j < len1; j++) {
      node = nodes[j];
      frag.appendChild(node);
    }
    return frag;
  };

  $.html = function(html) {
    var el;
    el = $.el('div', {
      innerHTML: html
    });
    return $.nodes(slice.call(el.children));
  };

  $.add = function(root, nodes) {
    root.appendChild($.nodes(nodes));
    return root;
  };

  $.replace = function(root, el) {
    return root.parentNode.replaceChild($.nodes(el), root);
  };

  SimpleDict = (function() {
    function SimpleDict() {
      this.keys = [];
    }

    SimpleDict.prototype.push = function(key, data) {
      key = "" + key;
      if (!this[key]) {
        this.keys.push(key);
      }
      this[key] = data;
      return this[key].key = key;
    };

    SimpleDict.prototype.contains = function(obj) {
      return this.indexOf(obj) !== -1;
    };

    SimpleDict.prototype.indexOf = function(obj) {
      var key;
      key = obj.key;
      if (key) {
        if (obj !== this[key]) {
          return -1;
        }
        return this.keys.indexOf(key);
      } else {
        return this.keys.indexOf(obj);
      }
    };

    SimpleDict.prototype.rm = function(key) {
      var i;
      key = "" + key;
      i = this.keys.indexOf(key);
      if (i !== -1) {
        this.keys.splice(i, 1);
        return delete this[key];
      }
    };

    SimpleDict.prototype.first = function() {
      return this[this.keys[0]];
    };

    SimpleDict.prototype.forEach = function(fn) {
      var j, key, len1, ref;
      ref = slice.call(this.keys);
      for (j = 0, len1 = ref.length; j < len1; j++) {
        key = ref[j];
        fn.call(this, this[key]);
      }
    };

    SimpleDict.prototype.forEachKey = function(fn) {
      var j, key, len1, ref;
      ref = slice.call(this.keys);
      for (j = 0, len1 = ref.length; j < len1; j++) {
        key = ref[j];
        fn.call(this, key);
      }
    };

    Object.defineProperty(SimpleDict.prototype, 'length', {
      get: function() {
        return this.keys.length;
      }
    });

    return SimpleDict;

  })();

  preload = function(image) {
    var galLength, i, len, results;
    galLength = g.images.length;
    i = g.currentImageIndex;
    len = Math.min(galLength, i + PRELOAD + 1);
    results = [];
    while (++i < len) {
      results.push($.el('img', {
        src: g.images[g.images.keys[i]].url
      }));
    }
    return results;
  };

  loadGallery = function() {
    var close, count, err, gal, next, nodes, prev, thumbs;
    try {
      g.gallery = gal = $.el('div', {
        id: 'a-gallery',
        innerHTML: "<div class=\"gal-prev\"></div>\n<div class=\"gal-image\">\n  <div>\n    <div class=\"gal-info\">\n      INFO\n      <div class=\"gal-ex-info\">\n      </div>\n    </div>\n    <div class=\"gal-close\">✖</div>\n    <a class=\"current\"></a>\n  </div>\n</div>\n<div class=\"gal-next\"></div>\n<div class=\"gal-thumbnails\"></div>"
      });
      nodes = g.nodes;
      nodes.prev = prev = $('.gal-prev', gal);
      nodes.next = next = $('.gal-next', gal);
      nodes.count = count = $('.gal-count', gal);
      nodes.thumbs = thumbs = $('.gal-thumbnails', gal);
      nodes.close = close = $('.gal-close', gal);
      $.on(close, 'click', cb.hideGallery);
      $.on(prev, 'click', cb.prev);
      $.on(next, 'click', cb.next);
      $.on(d, 'keydown', cb.keybinds);
      cb.hideGallery();
      return d.body.appendChild(gal);
    } catch (_error) {
      err = _error;
      console.log(err.message);
      return console.log(err.stack);
    }
  };

  cb = {
    next: function() {
      ++g.currentImageIndex;
      return cb.updateImage();
    },
    prev: function() {
      --g.currentImageIndex;
      return cb.updateImage();
    },
    updateImage: function() {
      return setImage(g.images[g.images.keys[g.currentImageIndex]]);
    },
    showGallery: function() {
      g.gallery.style.display = 'flex';
      return d.body.style.overflow = 'hidden';
    },
    hideGallery: function() {
      cb.pause();
      g.gallery.style.display = 'none';
      g.currentImageIndex = 0;
      return d.body.style.overflow = 'auto';
    },
    highlight: function(image) {
      var gal, highlight, thumbs;
      if (!image) {
        if (!(image = g.images[g.images.keys[g.currentImageIndex]])) {
          return;
        }
      }
      gal = g.gallery;
      $('.gal-image', gal).scrollTop = 0;
      highlight = $('.gal-highlight', gal);
      if (highlight != null) {
        highlight.classList.remove('gal-highlight');
      }
      highlight = $("[data-id='" + image.id + "']", gal);
      if (highlight != null) {
        highlight.classList.add('gal-highlight');
      }
      thumbs = g.nodes.thumbs;
      return thumbs.scrollTop = highlight.offsetTop + highlight.offsetHeight / 2 - thumbs.clientHeight / 2;
    },
    toggleGallery: function() {
      return cb[g.gallery.style.display === 'block' ? 'hideGallery' : 'showGallery']();
    },
    keybinds: function(e) {
      var fn, key;
      if (!(key = e.keyCode)) {
        return;
      }
      fn = (function() {
        switch (key) {
          case 39:
            return cb.next;
          case 37:
            return cb.prev;
          case 27:
            return cb.hideGallery;
        }
      })();
      if (!fn) {
        return;
      }
      e.stopPropagation();
      e.preventDefault();
      return fn();
    },
    pause: function() {
      var current, el;
      current = $('.gal-image a', g.gallery);
      if (current) {
        el = current.firstElementChild;
      }
      if (el && el.pause) {
        return el.pause();
      }
    }
  };

  fer = function(arr, fn) {
    var item, j, len1;
    for (j = 0, len1 = arr.length; j < len1; j++) {
      item = arr[j];
      fn(item);
    }
  };

  clickThumb = function(e) {
    e.preventDefault();
    return $.asap((function() {
      return g.images.length;
    }), (function(_this) {
      return function() {
        var id, image, queryURL;
        id = _this.id.slice(1);
        image = g.images["" + id];
        cb.showGallery();
        if (!image) {
          queryURL = g.baseURL + "page=dapi&s=post&q=index&id=" + id;
          query("get", queryURL, function() {
            image = mkImage(this.response.childNodes[0].children[0]);
            return setImage(image);
          });
          return;
        }
        return setImage(image);
      };
    })(this));
  };

  setImage = function(image) {
    var a, el, err, gal, i, img, info, j, len1, placeHolder, rating, ratingText, ready, ref, source, tag, tags;
    try {
      gal = g.gallery;
      cb.pause();
      el = $('.gal-image .current', gal);
      g.currentImageIndex = i = g.images.indexOf(image);
      a = $.el('a', {
        href: image.download,
        download: image.filename,
        className: 'current'
      });
      switch (image.type) {
        case "jpg":
        case "jpeg":
        case "gif":
        case "png":
          img = $.el('img', {
            src: image.url,
            alt: image.tags
          });
          ready = function() {
            return img.complete;
          };
          break;
        default:
          img = $.el('video', {
            src: image.url,
            poster: image.thumb,
            autoplay: true,
            loop: true,
            width: image.width,
            height: image.height
          });
          ready = function() {
            return img.readyState > 2;
          };
      }
      if (ready()) {
        $.add(a, img);
        preload();
      } else {
        placeHolder = $.el('div', {
          className: 'spinner'
        });
        $.add(a, placeHolder);
        $.asap(ready, function() {
          if (i !== g.currentImageIndex) {
            return;
          }
          $.replace(placeHolder, img);
          return preload();
        });
      }
      $.replace(el, a);
      a.parentElement.click();
      info = $('.gal-ex-info', gal);
      info.textContent = "ID: " + image.id + "\nScore: " + (image.score || 0) + "\nPosted: " + image.age + "\nWidth: " + image.width + "\nHeight: " + image.height + "\nType: " + (image.type.toUpperCase());
      info.innerHTML = "<p>" + (info.textContent.split('\n').join('</p><p>')) + "</p>";
      tags = $.el('p', {
        textContent: "Tags: "
      });
      ref = image.tags.split(' ');
      for (j = 0, len1 = ref.length; j < len1; j++) {
        tag = ref[j];
        $.add(tags, $.el('a', {
          href: g.baseURL + "page=post&s=list&tags=" + tag,
          textContent: tag,
          target: "_blank"
        }));
        $.add(tags, d.createTextNode(' '));
      }
      $.add(info, tags);
      ratingText = (function() {
        switch (image.rating) {
          case 'e':
            return 'explicit';
          case 's':
            return 'safe';
          default:
            return 'questionable';
        }
      })();
      rating = $.el('p', {
        textContent: "Rating: "
      });
      $.add(rating, $.el('a', {
        href: g.baseURL + "page=post&s=list&tags=rating:" + ratingText,
        textContent: ratingText,
        target: "_blank"
      }));
      $.add(info, rating);
      if (image.source) {
        source = $.el('p', {
          textContent: "Source: "
        });
        $.add(source, $.el('a', {
          href: image.source,
          textContent: image.source,
          target: "_blank"
        }));
        $.add(info, source);
      }
      cb.highlight(image);
      if (i + THRESHOLD > g.images.length) {
        return updateImages();
      }
    } catch (_error) {
      err = _error;
      console.log(err.message);
      return console.log(err.stack);
    }
  };

  updateImages = function() {
    var queryURL;
    queryURL = mkURL(g.images.length / LIMIT);
    return queryImages(queryURL);
  };

  setupImages = function() {
    var err, j, len1, parser, post, ref, response, results;
    try {
      if (this.status !== 200) {
        g.error = true;
        return alert(this.status + ": " + this.statusText);
      }
      parser = new DOMParser();
      response = parser.parseFromString(this.response, 'text/xml');
      ref = response.childNodes[0].children;
      results = [];
      for (j = 0, len1 = ref.length; j < len1; j++) {
        post = ref[j];
        results.push(mkImage(post));
      }
      return results;
    } catch (_error) {
      err = _error;
      console.log(err.message);
      return console.log(err.stack);
    }
  };

  mkImage = function(post) {
    var a, download, extension, filename, image, item, j, len1, p, ref, ref1, tags, thumb, type;
    p = {};
    ref = post.attributes;
    for (j = 0, len1 = ref.length; j < len1; j++) {
      item = ref[j];
      p[item.name] = item.value;
    }
    a = $.el('a', {
      href: p.file_url
    });
    a.host = 'gelbooru.com';
    download = a.href;
    type = download.split('.');
    type = ("" + type[type.length - 1]).toLowerCase();
    extension = "." + type;
    tags = p.tags.split(' ');
    while (true) {
      filename = p.id + " - " + (tags.join(' ').trim());
      tags.pop();
      if (!(filename.length + extension.length > FNLIMIT)) {
        break;
      }
    }
    filename += extension;
    image = {
      thumb: p.preview_url,
      url: p.sample_url,
      rating: p.rating,
      source: p.source,
      width: p.width,
      height: p.height,
      score: p.score,
      tags: (ref1 = p.tags) != null ? ref1.trim() : void 0,
      id: p.id,
      age: p.created_at,
      filename: filename,
      download: download,
      type: type
    };
    thumb = $.el('a', {
      href: "javascript:;",
      className: 'gal-thumb',
      innerHTML: "<img src='" + image.thumb + "'>"
    });
    thumb.setAttribute('data-id', image.id);
    $.on(thumb, 'click', function() {
      g.currentImageIndex = g.images.indexOf(image);
      return setImage(image);
    });
    $.add(g.nodes.thumbs, thumb);
    g.images.push(p.id, image);
    return image;
  };

  query = function(method, URL, callback) {
    var r;
    r = new XMLHttpRequest();
    r.open("get", URL, true);
    $.on(r, "load error abort", callback, true);
    r.send();
    return r;
  };

  queryImages = function(URL) {
    if (g.error) {
      return;
    }
    return query("get", URL, setupImages);
  };

  mkURL = function(pid) {
    var j, key, len1, queryURL, ref;
    if (pid) {
      g.attr.pid = pid;
    }
    queryURL = g.baseURL;
    ref = g.attr.keys;
    for (j = 0, len1 = ref.length; j < len1; j++) {
      key = ref[j];
      queryURL += key + "=" + g.attr[key] + "&";
    }
    return queryURL = queryURL.slice(0, -1);
  };

  setup = function() {
    var attr, j, len1, ref;
    g.images = new SimpleDict();
    g.host = d.location;
    g.attr = new SimpleDict();
    ref = g.host.search.slice(1).split('&');
    for (j = 0, len1 = ref.length; j < len1; j++) {
      attr = ref[j];
      attr = attr.split('=');
      g.attr.push(attr[0].toLowerCase(), attr[1].toLowerCase());
    }
    if (g.attr.s === 'view') {
      return;
    }
    g.baseURL = g.host.protocol + "//" + g.host.hostname + "/index.php?";
    g.attr.push('page', 'dapi');
    g.attr.push('q', 'index');
    g.attr.push('s', 'post');
    g.attr.push('limit', 100);
    if (g.attr.tags === 'all') {
      g.attr.rm('tags');
    }
    if (g.attr.pid) {
      g.attr.pid = ~~(g.attr.pid / 100);
    } else {
      g.attr.push('pid', 0);
    }
    g.queryURL = mkURL();
    $.addStyle(g.galleryCSS);
    return $.asap((function() {
      var ref1;
      return (ref1 = d.readyState) === 'interactive' || ref1 === 'complete';
    }), function() {
      var k, len2, ref1, thumb;
      ref1 = $.$('.thumb a');
      for (k = 0, len2 = ref1.length; k < len2; k++) {
        thumb = ref1[k];
        $.on(thumb, 'click', clickThumb);
      }
      loadGallery();
      return queryImages(g.queryURL);
    });
  };

  try {
    setup();
  } catch (_error) {
    err = _error;
    console.log(err.message);
    console.log(err.stack);
  }

}).call(this);