Gelbooru Image Viewer

Adds a fullscreen image view option when you click on images and various other neat features

// ==UserScript==
// @id             gelbooru-slide
// @name           Gelbooru Image Viewer
// @version        1.9.9.9
// @namespace      intermission
// @author         intermission
// @license        WTFPL; http://www.wtfpl.net/about/
// @description    Adds a fullscreen image view option when you click on images and various other neat features
// @match          *://gelbooru.com/*
// @match          *://rule34.xxx/*
// @match          *://e621.net/*
// @match          *://*.booru.org/*
// @match          *://*.paheal.net/*
// @match          *://yande.re/post*
// @match          *://lolibooru.moe/*
// @match          *://konachan.com/*
// @match          *://atfbooru.ninja/*
// @match          *://safebooru.org/*
// @match          *://hypnohub.net/*
// @match          *://tbib.org/*
// @match          *://booru.splatoon.ink/*
// @match          *://*.sankakucomplex.com/*
// @exclude        http://www.sankakucomplex.com/*
// @exclude        https://www.sankakucomplex.com/*
// @run-at         document-start
// @grant          GM_registerMenuCommand
// @grant          GM_xmlhttpRequest
// @grant          GM_info
// ==/UserScript==

/* This program is free software. It comes without any warranty, to the extent
 * permitted by applicable law. You can redistribute it and/or modify it under
 * the terms of the Do What The Fuck You Want To Public License, Version 2, as
 * published by Sam Hocevar. See http://www.wtfpl.net/ for more details. */

/* global GM_registerMenuCommand, GM_xmlhttpRequest, GM_info, unsafeWindow */

"use strict";

/**
 * DEBUG
 */
const WIREFRAME = 0; // 0 - off, 1 - on during image viewer, 2 - always on
const SAFE_DEBUG = false;

/**
 * BASIC CONSTANTS
 */
const d = document;
const w = window;
const stor = localStorage;
const uW = typeof unsafeWindow !== "undefined" ? unsafeWindow : w;
const domain = location.hostname.match(/[^.]+\.[^.]+$/)[0];
const scriptInfo = GM_info;
const ns = "gelbooru-slide";
const fullImage = stor[ns] === "true";
const passive = { passive: true };
const once = { once: true };
const svg = '<svg xmlns="http://www.w3.org/2000/svg" ';
const httpOk = [200, 302, 304];

/**
 * `site` OBJECT CONSTRUCTION
 * site: {
 *   name: hostname of current site
 *   inject: string if a site needs a script to be injected to work this userJS
 *   [name]: dynamic property, hostname of current site w/o TLD, returns true
 * }
 */
const site = (() => {
  const info = scriptInfo.scriptMetaStr;
  const regex = /match.*?[/.](\w+\.\w+)\//g;
  const ret = Object.setPrototypeOf({ name: "", inject: "" }, null);
  let line, name;
  while (line = regex.exec(info)) { // eslint-disable-line no-cond-assign
    if (line[1] === domain) {
      ret[name = domain.split(".")[0]] = true;
      ret.name = name;
      break;
    }
  }
  const qS = `document.querySelector("#post-list > .content`;
  const iB = "insertBefore";
  switch(name) {
   case "hypnohub":
    ret.inject += `Object.defineProperties(window.PostModeMenu, {\n  "post_mouseover": {\n    value() {}\n  },\n  "post_mouseout": {\n    value() {}\n  }\n});`;
    break;
   case "sankakucomplex":
    ret.inject += `Object.defineProperty(${qS}"), "${iB}", {\n  value: function ${iB}(newEl, pos) {\n    if (newEl.nodeType !== 11)\n      return (pos.nodeType === 3 ? pos.nextElementSibling : pos).insertAdjacentElement("beforebegin", newEl);\n    else {\n      const s = ${qS} > div:first-of-type");\n      for (const t of newEl.firstElementChild.children) {\n        t.classList.remove("blacklisted");\n        t.removeAttribute("style");\n        s.appendChild(t);\n      }\n    }\n    return newEl;\n  }\n})`;
  }
  return ret;
})();

/**
 * VARIABLE FOR CSS
 * paheal's thumbnails are 200x200 instead of 180x180 (every other booru)
 */
const paheal = site.paheal ? 20 : 0;

/**
 * SVG CONSTANTS
 */
const SVG = {
  play: `${svg}width="50" height="50"><rect rx="5" height="48" width="48" y="1" x="1" fill="#fff" /><polygon fill="#000" points="16 12 16 38 36 25" /></svg>`,
  pause: `${svg}width="50" height="50"><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>`,
  next: ((z, a, b, c, r) => `${svg}viewBox="0 0 118 118"><style>.a{fill:none;stroke-linejoin:round;stroke-width:4;stroke:#fff}</style><path d="M78.6 71.8l-7.6-1.6${a}${z}-1-3.7l5.8-5.2a4.3 4.3${z}-4.2-7.3l-7.4 2.4a29.7 29.7${z}-2.7-2.7l2.4-7.4a4.3 4.3${z}-7.3-4.2L51.5 48${a}${z}-3.7-1l-1.6-7.6a4.3 4.3${z}-8.4 0l-1.6 7.6${a}${z}-3.7 1l-5.2-5.8a4.3 4.3${z}-7.3 4.2l2.4 7.4a29.7 29.7${z}-2.7 2.7l-7.4-2.4a4.3 4.3${z}-4.2 7.3L14 66.5${a}${z}-1 3.7l-7.6 1.6a4.3 4.3${z} 0 8.4l7.6 1.6${a}${z} 1 3.7l-5.8 5.2a4.3 4.3${z} 4.2 7.3l7.4-2.4a29.8 29.8${z} 2.7 2.7l-2.4 7.4a4.3 4.3${z} 7.3 4.2l5.2-5.8${a}${z} 3.7 1l1.6 7.6a4.3 4.3${z} 8.4 0l1.6-7.6${a}${z} 3.7-1l5.2 5.8a4.3 4.3${z} 7.3-4.2l-2.4-7.4a29.8 29.8${z} 2.7-2.7l7.4 2.4a4.3 4.3${z} 4.2-7.3L70 85.5${a}${z} 1-3.7l7.6-1.6A4.3 4.3${z} 78.6 71.8zM42 92.5A16.5 16.5 0 1 1 58.5 76 16.5 16.5 0 0 1 42 92.5z${r}0 42 76.051" to="360 42 76.051" dur=3${c}<path d="M113.2 24.5l-6.9-1.6${b}${z}-1.1-3l3.7-5.9a3.6 3.6${z}-5-5L98 12.8${b}${z}-3-1.1l-1.6-6.9a3.6 3.6${z}-7 0l-1.6 6.9a16.9 16.9${z}-2.8 1.2l-6-3.8a3.6 3.6${z}-5 5l3.8 6a16.9 16.9${z}-1.2 2.8l-6.9 1.6a3.6 3.6${z} 0 7l6.9 1.6${b}${z} 1.1 3l-3.7 5.9a3.6 3.6${z} 5 5L82 43.2${b}${z} 3 1.1l1.6 6.9a3.6 3.6${z} 7 0l1.6-6.9a16.9 16.9${z} 2.8-1.2l6 3.8a3.6 3.6${z} 5-5l-3.8-6a16.9 16.9${z} 1.2-2.8l6.9-1.6A3.6 3.6${z} 113.2 24.5z${r}360 89.97 28" to="0 89.97 28" dur=2${c}<circle r=8.4 style="fill:none;stroke-width:4;stroke:#fff" cx=89.97 cy=28></circle></svg>`)(" 0 0 0", "a29.3 29.3", "a20.6 20.6", "s></animateTransform></path>", '" class=a><animateTransform attributeName=transform type=rotate repeatCount=indefinite from="'),
  debug: (l => `<div style="width:calc(100vw - 2px);height:${198+paheal}px;position:absolute;top:0;left:0;border:1px solid cyan"></div><div style="width:calc(100vw - 2px);height:calc(100vh - ${202+paheal}px);position:absolute;bottom:0;border:1px solid red;display:block">${svg}viewBox="0 0 200 200" preserveAspectRatio=none style="width: 100%;height: 100%;"><style>.d{fill:none;stroke-width:1px;stroke:#ff0}</style>${l}d y2=15 x2=200 y1=15></line>${l}d y2=185.5 x2=200 y1=185.5></line>${l}d x2=15 y1=200 x1=15></line>${l}d x2=185 y1=200 x1=185></line></svg></div><div style="width:100vw;height:100vh;position:absolute;top:0;left:0">${svg}viewBox="0 0 200 200" preserveAspectRatio=none style="width: 100%;height: 100%;"><style>.b{fill:none;stroke-width:1px;stroke:#0f0}</style>${l}b y2=100 x2=200 y1=100></line>${l}b y2=200 x2=100 x1=100></line></svg></div>`)('<line vector-effect="non-scaling-stroke" shape-rendering="crispEdges" class='),
  gif: `${svg}viewBox="-10 -3 36 22"><path d="M26 16c0 1.6-1.3 3-3 3H-7c-1.7 0-3-1.4-3-3V0c0-1.7 1.3-3 3-3h30c1.7 0 3 1.3 3 3v16z" opacity=".6"/><path fill="#FFF" d="M22-1H-6c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h28c1.1 0 2-.9 2-2V1c0-1.1-.9-2-2-2zM6.3 13.2H4.9l-.2-1.1c-.4.5-.8.9-1.3 1.1-.5.2-1 .3-1.4.3-.8 0-1.5-.1-2.1-.4s-1.1-.6-1.5-1.1-.7-1-1-1.6C-2.9 9.7-3 9-3 8.3c0-.7.1-1.4.3-2.1.2-.6.5-1.2 1-1.7s.9-.8 1.5-1.1C.5 3.2 1.2 3 1.9 3c.5 0 1 .1 1.5.2.5.2.9.4 1.3.7.4.3.7.7 1 1.1.2.5.4 1 .4 1.5H4c-.1-.5-.3-.9-.7-1.2-.4-.3-.8-.4-1.4-.4-.5 0-.9.1-1.2.3-.4.1-.7.4-.9.7-.2.3-.3.7-.4 1.1s-.1.8-.1 1.3c0 .4 0 .8.1 1.2s.3.8.5 1.1c.2.3.5.6.8.8s.8.3 1.3.3c.7 0 1.3-.2 1.7-.6.4-.4.6-.9.7-1.6H2.1V7.8h4.2v5.4zm4 0H8.1v-10h2.2v10zm8.9-8.1h-4.8v2.3h4.2v1.7h-4.2v4.1h-2.2v-10h7v1.9z"/></svg>`,
  warn: `${svg}viewBox="0 0 1000 1000"><style>path.a{fill:red;stroke-width:8px;stroke:black;}</style><path d="M500 673c-17 0-34 4-48 11-24 12-44 33-55 58-5 14-9 28-9 44 0 62 50 113 112 113 63 0 113-50 113-113C613 723 562 673 500 673zM500 843c-32 0-58-26-58-58 0-32 26-58 58-58 32 0 59 26 59 58C558 817 532 843 500 843z" class="a"/><path d="M285 643c4 3 8 5 12 5l132-138c-57 14-109 47-149 94C272 617 273 634 285 643z" class="a"/><path d="M606 522L565 565c43 13 83 39 112 75 5 7 13 10 21 10 6 0 12-2 17-6 11-9 13-27 3-38C689 568 650 539 606 522z" class="a"/><path d="M500 384c16 0 32 1 48 3l46-48c-30-7-61-10-93-10-137 0-265 61-351 167-10 11-8 29 4 38 5 4 11 7 17 7 8 0 15-4 21-10C267 438 379 384 500 384z" class="a"/><path d="M729 393l-38 40c45 24 86 58 119 98 10 12 27 14 39 4 12-9 13-27 4-38C817 454 776 420 729 393z" class="a"/><path d="M685 244l41-43c-70-28-147-42-226-42-188 0-364 84-484 230-9 12-7 29 4 39 5 4 11 6 17 6 8 0 16-3 21-10 109-133 270-210 442-210C564 214 626 224 685 244z" class="a"/><path d="M984 389c-38-48-83-88-133-121l-38 40c49 32 93 71 130 117 9 11 27 13 38 4C991 418 994 401 984 389z" class="a"/><path d="M907 110c-11-10-28-10-38 1L181 828c-10 11-10 28 1 38 5 5 12 8 19 8 7 0 14-3 20-8L908 148C918 138 918 120 907 110z" class="a"/></svg>`
};

/**
 * GLOBAL VARIABLE INDICATING WHETHER WE ARE IN SLIDESHOW MODE OR NOT
 */
let slideshow;

/**
 * NAMESPACES
 */
let Pos, Menu, Btn, Main, Prog, Hover;

/**
 * SET AN INITIAL VALUE ON FIRST RUN
 */
if (!stor[ns]) stor[ns] = "false";

/**
 * PAHEAL AND BOORU.ORG DON'T USE SAMPLES
 */
switch(site.name) {
 case "splatoon": case "booru": case "paheal":
  break;
 default:
  GM_registerMenuCommand(`Current image mode: ${fullImage ? "Always original size" : "Sample only"}`, () => { stor[ns] = fullImage ? "false" : "true"; location.reload(); });
  break;
}

/**
 * JQUERY INSPIRED UTILITIES
 * details won't be documented, just look at the code
 */
const $$ = (a, b = d) => Array.prototype.slice.call(b.querySelectorAll(a));
const $ = (a, b = d) => b.querySelector(a);

$.keys = Object.keys;

$.extend = function(obj, props) {
  const arr = $.keys(props);
  for (let i = 0, key, len = arr.length; i < len; ++i) {
    obj[key = arr[i]] = props[key];
  }
  return obj;
};

$.extend($, {
  cache(id, b) {
    if (site.sankakucomplex) return "loading";
    const val = fullImage ? "original" : "sample";
    let ret, obj, temp;
    if (!b) {
      const json = $.safe(JSON.parse, null, stor[ns + id]);
      if (json !== $.safe.error) ret = json[val];
      ret = ret || "loading";
    }
    else {
      const json = $.safe(JSON.parse, null, stor[ns + id]);
      if (json !== $.safe.error) temp = json;
      obj = temp || {};
      obj[val] = b;
      stor[ns + id] = JSON.stringify(obj);
      ret = b;
    }
    return ret;
  },
  base: a => a.match(Main.r[5]),
  id: (a, b = d) => b.getElementById(a),
  current: src => $(`a[data-id] > img[src*="${$.base(src || Main.el.dataset.src)}"]`).parentNode,
  _find(method, el, a) {
    el = el[`${method ? "next" : "previous"}ElementSibling`];
    a = $("a[data-full]", el);
    return [el, a];
  },
  find(el, method) {
    const { _find, safe, safe: { error } } = $;
    let a, search;
    el = el.parentNode;
    do {
      if ((search = safe(_find, null, method, el, a)) === error) return false;
      else ({ 0: el, 1: a } = search);
      if (a) return a;
    } while(1); // eslint-disable-line no-constant-condition
  },
  preload() {
    if (site.sankakucomplex) return;
    const curr = $.current();
    Main.req($.find(curr, true));
    Main.req($.find(curr, false));
  },
  keyDown(e) {
    let move, warn;
    if (slideshow) return;
    if (e.ctrlKey && Main.el.style.objectFit !== "none")
      Main.el.style.objectFit = "none";
    if (e.warn)
      move = warn = true;
    else {
      switch(e.keyCode) {
       case 32: case 39: // Space, →
        if (e.preventDefault) e.preventDefault();
        move = true;
        break;
       case 37: // ←
        move = false;
        break;
       case 38: // ↑
        return e.event ? Menu.fn(e.event) : w.location = $.current().href;
       case 40: // ↓
        e.preventDefault();
        return Main.el.click();
      }
    }
    if (typeof move !== "undefined") {
      if (!warn && (e = $.find($.current(), move))) {
        if (Prog.el) {
          Prog.el.classList.remove("progdone");
          Prog.el.style.width = 0;
        }
        Main.slide($("img", e).src);
        Pos.fn(move);
        $.preload();
      }
      else if (Hover.nextEl && !Hover.nextEl.classList.contains("loadingu") && move === true)
        Hover.next(true);
      else if (!$.keyDown.el) {
        const el = $.keyDown.el = $.c("div");
        el.classList.add("nomoreimages");
        const side = move ? "right" : "left";
        el.setAttribute("style", `background: linear-gradient(to ${side}, transparent, rgba(255,0,0,.5));${side}: 0;`);
        $.add(el);
      }
    }
  },
  keyUp() {
    if (slideshow) return;
    if (Main.el.style.objectFit === "none") {
      $.zoom.el = $.rm($.zoom.el);
      Main.el.removeAttribute("style");
    }
  },
  zoom(e) {
    if (Main.gif.hasAttribute("style")) return;
    if (!e.ctrlKey) {
      $.zoom.el = $.rm($.zoom.el);
      return Main.el.removeAttribute("style");
    }
    else Main.el.style.objectFit = "none";
    if (slideshow) return;
    const minY = 200 + paheal, maxX = w.innerWidth, maxY = w.innerHeight,
      { clientX: x, clientY: y } = e,
      tall = Main.el.naturalHeight > maxY, wide = Main.el.naturalWidth > maxX;
    if (!$.zoom.el) {
      const el = $.zoom.el = $.c("span");
      el.id = "zoom_top";
      el.innerHTML = "<span></span>";
      $.add(el);
    }
    if ((wide || tall) && minY < y && maxY >= y && 0 <= x && maxX >= x) {
      let xPos = 50, yPos = 50;
      if (tall) {
        const margin = (maxY - minY) * 0.075;
        const height = maxY - minY - margin * 2;
        if (y < minY + margin)
          yPos = 0;
        else if (y > maxY - margin)
          yPos = 100;
        else
          yPos = (y - minY - margin) / height * 100;
      }
      if (wide) {
        const margin = maxX * 0.075;
        const width = maxX - margin * 2;
        if (x < margin)
          xPos = 0;
        else if (x > maxX - margin)
          xPos = 100;
        else
          xPos = (x - margin) / width * 100;
      }
      $.zoom.el.removeAttribute("style");
      if (tall && yPos < 10) {
        const w = maxX / 40 + 30;
        $.zoom.el.style.left = `${x - (w < 55 ? 55 : w)}px`;
        void $.zoom.el.offsetWidth; // reflow hack
        $.zoom.el.style.animationName = "topglowything";
      }
      Main.el.style.objectPosition = `${xPos}% ${yPos}%`;
    }
    else if (!Main.el.style.objectPosition) {
      Main.el.style.objectPosition = "50% 50%";
    }
  },
  c: c => d.createElement(c),
  r: (() => {
    let queue = [];
    d.addEventListener("DOMContentLoaded", () => {
      for (let i = 0, len = queue.length; i < len; ++i) {
        const obj = queue[i], copy = [obj.fn, null];
        copy.push.apply(copy, obj.args);
        $.safe.apply($, copy);
      }
      queue = null;
    }, once);
    return (fn, ...args) => queue ?
      queue[queue.length] = { fn, args } :
      $.safe(fn, null, ...args);
  })(),
  rm: el => { if (el) el.parentNode.removeChild(el); },
  ins: (el, m, t) => el.insertAdjacentHTML(m, t),
  eval(text = "") {
    const script = $.c("script");
    script.innerHTML = text;
    $.add(script, d.documentElement);
  },
  in: (obj => {
    const arr = function inArray(key) {
      for (let i = 0, len = this.length; i < len; ++i) {
        if (key === this[i]) return true;
      }
      return false;
    };
    return (k, o) => o && (typeof o.length === "number" ? arr : obj).call(o, k);
  })(Object.prototype.hasOwnProperty),
  add(el, to = d.body) {
    $.safe(to.appendChild, to, el);
    return el;
  },
  safe: (safeError => {
    function safe(fn, context) {
      const k = arguments.length, args = [];
      {
        let i = 1, p = -1;
        while (++i < k) args[++p] = arguments[i];
      }
      try { return fn.apply(context, args); }
      catch(e) {
        if (SAFE_DEBUG) console.error("$.safe debug:", e);
        return safeError;
      }
    }
    safe.error = safeError;
    return safe;
  })(Symbol()),
  _evt() {
    if (typeof arguments[0] === "string") {
      return d[this](arguments[0], arguments[1], arguments[2]);
    }
    return arguments[0][this](arguments[1], arguments[2], arguments[3]);
  },
  on() { return $._evt.apply("addEventListener", arguments); },
  off() { return $._evt.apply("removeEventListener", arguments); },
  u: void 0
});

/**
 * IF SOMETHING CHANGES IN HOW WE STORE CACHED URLS THEN MODIFY VERSION HERE
 */
{
  const version = "1.8.8";
  if (stor[ns + "-firstrun"] !== version) {
    const r = /^gelbooru-slide./, arr = $.keys(stor), len = arr.length;
    for (let i = 0; i < len; ++i) {
      const a = arr[i];
      r.test(a) && stor.removeItem(a);
    }
    stor[ns + "-firstrun"] = version;
  }
}


/**
 * MODULE FOR POSITIONAL DISPLAY IN BOTTOM LEFT CORNER
 */
Pos = {
  fn(a) {
    let el = Pos.el;
    if (a != null) {
      switch(typeof a) {
       case "string":
        $("span", el).innerHTML = ++a;
        break;
       case "boolean": {
        const no = $("span", el);
        no.innerHTML = Number(no.textContent) + (a ? 1 : -1); }
        break;
       case "number":
        el.lastElementChild.lastChild.data = ` / ${a}`;
      }
      el.title = el.textContent;
      if (Menu.el) {
        const arr = $$("a:not([download])", Menu.el);
        let i = arr.length;
        while (~--i) arr[i].href = $.current().href;
        Menu.download();
      }
    }
    else {
      if (Main.el && !el) {
        const thumbs = $$(".thumb a[data-full]");
        el = $.c("div");
        $.ins(el, "beforeend", `<div><span>${thumbs.indexOf($.current()) + 1}</span> / ${thumbs.length}</div>`);
        el.className = "posel";
        el.title = el.textContent;
        Main.el.insertAdjacentElement("afterend", el);
        Pos.el = el;
      }
      else if (el) Pos.el = $.rm(el);
    }
  }
};

/**
 * MODULE FOR MIDDLE CLICK MENU
 */
Menu = {
  fn(e) {
    const height = Main.gif.hasAttribute("style") ? 48 : 71,
      _l = e.clientX + 1, _t = e.clientY + 1,
      left = (_l > w.innerWidth - 139 ? _l - 139 : _l) + "px",
      top = (_t > w.innerHeight - height ? _t - height : _t) + "px";
    let el = Menu.el;
    if (el) {
      el.removeAttribute("class");
      setTimeout($.safe, 10, el.classList.add, el.classList, "menuel");
    }
    else {
      const href = $.current().href + '" style="margin-bottom: 2px"';
      el = $.c("div");
      el.id = "menuel";
      $.ins(el, "beforeend", `<a href="${href}>Open in This Tab</a><a href="${href} target="_blank">Open in New Tab</a><a href="javascript:;">Save Image As...</a>`);
      $.add(el);
      Menu.el = el;
      $.on(el.lastElementChild, "click", Menu.copyFilename);
      el.classList.add("menuel");
      if (Menu.realDl) Menu.download();
    }
    return $.extend(el.style, { left, top });
  },
  download() {
    const el = Menu.realDl;
    el.href = Main.el.src;
    el.download = Main.el.src.split("/").pop() + "." + $.current().dataset.full.match(Main.r[2])[1];
  },
  copyFilename() {
    const { copy, realDl } = Menu;
    copy.value = realDl.getAttribute("download") || "";
    copy.select();
    if ($.safe(document.execCommand, document, "copy") === $.safe.error)
      console.log("Failed to copy filename: %s", copy.value);
    realDl.click();
  }
};

/**
 * MODULE FOR SLIDESHOW BUTTON AND SETTINGS IN BOTTOM RIGHT CORNER
 */
Btn = {
  fn() {
    const sel = "this.previousElementSibling.firstElementChild";
    let el = Btn.el;
    if (el) {
      clearTimeout(Btn.hideTimer);
      Btn.clear();
      Btn.el = $.rm(Btn.el);
      Hover.el.removeAttribute("style");
    }
    else {
      el = $.c("div");
      el.setAttribute("style", "opacity: .7;");
      el.className = "slideshow";
      $.ins(el, "beforeend", `<span title="Slideshow">${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:&nbsp;<input type="number" value="5" style="width:100px"></div>`);
      Btn.state = true;
      el.firstElementChild.onclick = Btn.cb;
      $.add(el);
      Btn.el = el;
    }
  },
  clear() {
    clearTimeout(Number(Btn.el.dataset.timer));
    Btn.el.removeAttribute("data-timer");
  },
  cb: ((thumbs, el, options, orig) => {
    const sel = ".thumb a[data-full]",
      validate = a => a.type === "number" ? (a.value >= 1 ? a.value : 1) * 1E3 : a.checked,
      _fnS = () => { if (el.dataset.timer) el.dataset.timer = setTimeout(_fnT, options[2]); };
    const _fnT = () => {
      let _el;
      if (options[1]) {
        if (thumbs.length === 0) thumbs = $$(sel);
        thumbs.splice(thumbs.indexOf($.current()), 1);
        _el = thumbs[Math.random() * thumbs.length & -1];
      }
      else _el = $.find($.current(), true);
      if (!_el && options[0]) _el = $(sel);
      if (!_el) return Btn.cb();
      $.on(Main.el, "load", _fnS, once);
      Main.slide($("img", _el).src);
    };
    const cb = () => {
      el = Btn.el;
      Pos.fn();
      slideshow = !!Btn.state;
      el.firstElementChild.innerHTML = (Btn.state = !Btn.state) ? SVG.play : SVG.pause;
      if (slideshow) {
        thumbs = [];
        options = $$("div input", el).map(validate);
        el.dataset.timer = setTimeout(_fnT, options[2]);
        el.style.opacity = 0.4;
        Hover.el.setAttribute("style", "display: none !important");
        orig = d.body.getAttribute("style");
        $.on("mousemove", Btn.hide);
      }
      else {
        Btn.clear();
        el.style.opacity = ".7";
        el.removeAttribute("data-timer");
        Hover.el.removeAttribute("style");
        Hover.center($.current().firstElementChild.src);
        $.off("mousemove", Btn.hide);
        clearTimeout(Btn.hideTimer);
        if (orig) d.body.setAttribute("style", orig);
        else d.body.removeAttribute("style");
      }
    };
    cb.move = () => {
      clearTimeout(Number(Btn.el.dataset.timer));
      _fnT();
    };
    return cb;
  })([]),
  hide() {
    const b = d.body;
    b.style.cursor = "";
    clearTimeout(Btn.hideTimer);
    Btn.hideTimer = setTimeout(() => b.style.cursor = "none", 5E3);
  }
};

/**
 * MODULE FOR GENERAL DOWNLOADING, PROGRESS DISPLAY AND IMAGE HANDLING
 */
Prog = {
  check: id => Main.el.dataset.id === id,
  load(e) {
    const {el} = Prog, {id, ext} = e.context;
    delete Prog.reqs[id];
    if (!el) throw Error("There was an event order issue with GM_xmlhttpRequest");
    if (!$.in(e.status, httpOk) || !Main.r[5].test(e.finalUrl))
      return Prog.error(e);
    else {
      const blobUrl = w.URL.createObjectURL(
        new Blob([e.response], { type: `image/${ext.replace("jpeg", "jpg")}` })
      );
      $(`a[data-id="${id}"]`).dataset.blob = blobUrl;
      if (Main.el && Prog.check(id)) {
        el.classList.add("progdone");
        Main.el.src = blobUrl;
        if (Menu.el) Menu.download();
      }
    }
  },
  progress(e) {
    let el = Prog.el;
    if (!el) {
      Prog.el = el = $.c("span");
      el.setAttribute("style", "width:0");
      el.classList.add("progress");
      $.add(el);
    }
    if (!e) return el;
    const id = e.context && e.context.id;
    if (Main.el && Prog.check(id) && el) {
      el.classList.remove("progfail");
      el.style.width = `${$.in(id, Prog.reqs) ? e.loaded / e.total * 100 : 0}%`;
    }
  },
  error(e) {
    const el = Prog.el, id = e.context.id;
    if (Main.el && Prog.check(id) && el) _: {
      el.classList.add("progfail");
      const req = Prog.reqs[id];
      if (req == null) break _;
      $.safe(req.abort, req);
      delete Prog.reqs[id];
    }
    if (slideshow)
      Main.el.dispatchEvent(new Event("load"));
    stor.removeItem(ns + id);
    if (!site.paheal)
      $(`a[data-id="${id}"]`).dataset.full = $.cache(id, "loading");
  },
  parser: new DOMParser,
  helper: class { then(r, e) {
    Prog.reqs[this.id] = GM_xmlhttpRequest(Object.assign({}, this.details, {
      onload: r,
      onerror: e,
      onabort: e,
      ontimeout: e
    }));
  } },
  async sankaku(url, id, extra) {
    const req = new Prog.helper;
    req.id = id;
    const headers = {
      Cookie: document.cookie,
      Referer: location.href
    };
    req.details = {
      url,
      method: "GET",
      headers
    };
    const res = await req;
    extra.dataset.full = headers.Referer = res.finalUrl;
    extra.removeAttribute("data-already-loading");
    const doc = Prog.parser.parseFromString(res.responseText, "text/html");
    const a = doc.querySelector("#image-link");
    url = fullImage && a.href ? a.href : a.firstElementChild.src;
    return [url, headers];
  },
  async fn(url, id, extra) {
    if ($.in(id, Prog.reqs)) return;
    let headers;
    if (url.startsWith("//assets2")) {
      url = `//gelbooru.com/${url.substr(22)}`;
    }
    else if (site.sankakucomplex) {
      try { ({ 0: url, 1: headers } = await Prog.sankaku(url, id, extra)); }
      catch (e) {
        Prog.error({ context: { id } });
        throw e;
      }
    }
    if (Prog.el) Prog.el.style.width = 0;
    Prog.reqs[id] = GM_xmlhttpRequest({
      url,
      headers,
      context: { id, url, ext: url.match(Main.r[2])[1], extra },
      method: "GET",
      responseType: "arraybuffer",
      onload: Prog.load,
      onprogress: Prog.progress,
      onerror: Prog.error,
      onabort: Prog.error,
      ontimeout: Prog.error
    });
  },
  reqs: Object.setPrototypeOf({}, null)
};

/**
 * MODULE FOR IMAGE LIST IN THE TOP 200 or 220 (PAHEAL) PIXELS OF THE VIEWPORT
 */
Hover = {
  lastInList: {},
  init() {
    const el = $.c("div");
    $.ins(el, "beforeend", `<div class="layover"></div><div class="tentcon"><div class="wrapthatshit"><div class="listimage"></div></div></div>${svg}style="width: 0; height: 0"><filter id="__dropshadow"><feGaussianBlur in="SourceAlpha" stdDeviation="2"></feGaussianBlur><feOffset result="offsetblur" dx="1" dy="1"></feOffset><feMerge><feMergeNode /><feMergeNode in="SourceGraphic" /></feMerge></filter></svg>`);
    el.className = "viewpre";
    $.add(el);
    Hover.index = -1;
    Hover.target = $(".listimage", el);
    $.on(Hover.target, "click", Hover.click, passive);
    $.on(Hover.target, "dragstart", e => e.preventDefault());
    Hover.wrap = Hover.target.parentNode;
    if (!site.sankakucomplex) {
      $.add(Hover.nextEl = $.c("span"), Hover.target);
      Hover.nextEl.className = "next";
      $.on(Hover.nextEl, "click", Hover.next, passive);
    }
    {
      const arr = $$("a[data-full]"), len = arr.length;
      let i = -1;
      while (++i < len) Hover.build(arr[i]);
    }
    Hover.el = el;
    Hover.kinetic();
  },
  noGears() {
    Hover.gears.style.opacity = 0;
    setTimeout(() => Hover.gears = $.rm(Hover.gears), 300);
  },
  next(move) {
    if (Hover.nextEl.classList.contains("loadingu")) return;
    if (!Hover.gears) {
      let el, fn = () => el.style.right = "3vw";
      Hover.gears = el = $.c("div");
      el.className = "nextgears";
      $.ins(el, "beforeend", SVG.next);
      $.add(el);
      requestAnimationFrame(fn);
    }
    Hover.nextEl.className = "next loadingu";
    Hover.lastInList = {
      move,
      id: Hover.nextEl.previousElementSibling.dataset.id
    };
    void Hover.nextEl.offsetWidth; // reflow hack
    const timer = setTimeout(() => {
      Hover.nextEl = $.rm(Hover.nextEl);
      Hover.size();
      Hover.noGears();
      $.keyDown({warn: true});
    }, 2E3);
    d.dispatchEvent(new CustomEvent(ns + "-next", { detail: timer }));
  },
  size() {
    const { children } = Hover.target;
    let length = children.length;
    Hover.target.style.width = length * (180 + paheal) + "px";
    if (Pos.el) {
      if (children[length - 1].className === "next") --length;
      Pos.fn(length);
    }
  },
  build(el) {
    const span = $.c("span"), img = $.c("img");
    img.src = $("img", el).src;
    img.alt = img.dataset.nth = ++Hover.index;
    img.title = el.firstElementChild.title;
    span.dataset.id = el.dataset.id;
    if (el.dataset.gif) img.style.outline = "2px solid lime";
    if (el.dataset.res) span.dataset.res = el.dataset.res;
    $.add(img, span);
    if (Hover.nextEl) Hover.target.insertBefore(span, Hover.nextEl);
    else $.add(span, Hover.target);
    Hover.size();
    if (Hover.lastInList.move === true && Hover.lastInList.id === Main.el.dataset.id) {
      $.keyDown({keyCode: 39});
      Hover.lastInList.move = false;
    }
  },
  click(e) {
    e = e.target.src;
    if (!e) return;
    if (Hover.prevent)
      return Hover.prevent = null;
    Main.slide(e);
    Hover.center(e);
    if (Pos.el) Pos.fn($(`img[src*="${$.base(e)}"]`, Hover.el).dataset.nth);
  },
  center(src) {
    if (!(Hover.wrap || Hover.el)) return;
    const base = $.base(src),
      img = $(`img[src*="${base}"]`, Hover.el),
      pos = img.dataset.nth,
      scroll = Hover.wrap,
      half = scroll.offsetWidth / 2,
      width = 180 + paheal,
      dist = width * pos + width / 2,
      res = dist - half,
      curr = $(".current", Hover.el);
    if (curr) curr.removeAttribute("class");
    Hover.cancel();
    Hover.el.children[1].removeAttribute("style");
    scroll.scrollLeft = res > 0 ? res : 0;
    img.parentNode.setAttribute("class", "current");
  },
  kinetic() {
    const view = Hover.wrap,
      rm = () => Hover.el.classList.remove("showimagelist"),
      unset = () => Hover.el.classList.add("showimagelist");
    let offset, reference, velocity, frame, timestamp, ticker, amplitude, target, pressed = false;
    function scroll(x) {
      const max = view.scrollLeftMax;
      offset = x > max ? max : x < 0 ? 0 : x;
      view.scrollLeft = offset;
      if (offset === 0 || offset === max) {
        amplitude = 0;
        Hover.cancel();
        rm();
      }
    }
    function track() {
      let now = Date.now(),
        elapsed = now - timestamp,
        delta = offset - frame,
        v = 1000 * delta / (1 + elapsed);
      timestamp = now;
      frame = offset;
      velocity = 0.8 * v + 0.2 * velocity;
    }
    function autoScroll() {
      if (amplitude) {
        const elapsed = Date.now() - timestamp, delta = -amplitude * Math.exp(-elapsed / 15E2);
        if (delta > 5 || delta < -5) {
          unset();
          scroll(target + delta);
          Hover.kinetID = requestAnimationFrame(autoScroll);
        }
        else {
          rm();
          scroll(target);
        }
      }
    }
    $.on(view, "mousedown", function tap(e) {
      Hover.prevent = !(pressed = true);
      unset();
      clearInterval(ticker);
      velocity = amplitude = 0;
      if (e.target === view) return pressed = false;
      reference = e.clientX;
      offset = view.scrollLeft;
      frame = offset;
      timestamp = Date.now();
      ticker = setInterval(track, 100 / 3);
    }, passive);
    $.on(d.body, "mousemove", function drag(e) {
      if (pressed) {
        const x = e.clientX, delta = reference - x;
        if (delta > 1 || delta < -1) {
          Hover.prevent = true;
          reference = x;
          scroll(offset + delta);
        }
      }
    }, passive);
    $.on(d.body, "mouseup", function release() {
      pressed = false;
      clearInterval(ticker);
      if (velocity > 10 || velocity < -10) {
        amplitude = 0.8 * velocity;
        target = offset + amplitude & -1;
        timestamp = Date.now();
        Hover.kinetID = requestAnimationFrame(autoScroll);
      }
      else rm();
    }, passive);
  },
  cancel() {
    $.safe(cancelAnimationFrame, null, Hover.kinetID);
  }
};

/**
 * MODULE FOR THE THINGS THAT GET THE BALL ROLLING
 */
Main = {
  sel: ".thumb:not(a)",
  animationEnd(e) {
    switch(e.animationName) {
     case "Outlined":
      e.target.classList.remove("outlined");
      break;
     case "nomoreimages":
      $.keyDown.el = $.rm(e.target);
      break;
     case "menuelement":
      Menu.el = $.rm(e.target);
      break;
     case "warn":
      $.rm(e.target);
      break;
     case "progfail": case "progdone":
      Prog.el = $.rm(e.target);
    }
  },
  finalizeCss() {
    const style = $.extend($.c("style"), { type: "text/css" });
    $.add(new Text(Main.css), style);
    Object.defineProperty(Main, "css", {
      get() {
        return style.textContent;
      },
      set(moreCss) {
        moreCss = new Text(style.textContent + moreCss);
        {
          const arr = style.childNodes;
          let i = arr.length;
          while (~--i) $.rm(arr[i]);
        }
        style.appendChild(moreCss);
        return style.textContent;
      }
    });
    $.add(style, d.documentElement);
  },
  async initDomainInfo() {
    switch(site.name) {
     case "gelbooru":
      Main.r[0] = /<a href="?([^">]+)"?[^>]*>Orig/;
      Main.r[1] = /src="?([^">]+)"? id="?image"?[^>]*\/>/;
      Main.css += "span.thumb {\n  float: left;\n  display: inline-block;\n  width: 180px;\n  height: 180px;\n  text-align: center\n}\nspan.thumb + center::before {\n  content: '';\n  display: block;\n  clear: both\n}\n.slideshow hr {\n  margin: initial;\nborder: initial;\n  height: initial\n}";
      break;
     case "splatoon": case "booru":
      Main.r[0] = Main.r[1] = /<img alt="img" src="([^"]+)/i;
      Main.css += "span.thumb {\n  float: left\n}\n[data-never-hide] {\n  display: inline ! important;\n}";
      $.r(() => {
        const start = $(".thumb").parentNode;
        let el, i = 10;
        while (i) {
          if ((el = start.nextSibling) && el.id !== "paginator") $.rm(el);
          else --i;
        }
        start.dispatchEvent(new CustomEvent("scroll", { bubbles: true }));
      });
      break;
     case "e621":
      Main.css += ".thumb > a[data-id] {\n  display: inline-block;\n  margin-bottom: -3px\n}";
      break;
     case "tbib":
      Main.r[0] = /<a href="?([^"> ]+)"? [^>]*?>\s*Orig/;
      Main.r[1] = /<img[^>]*?src="?([^"> ]+)"? [^>]*?id="?image"?[^>]*>/;
      Main.css += "div:not([style*='none;padding:10px 0']) {\n  background-color: transparent\n}";
      break;
     case "sankakucomplex":
      Main.sel = "#post-list div.content > div:first-of-type > .thumb";
     case "hypnohub":
      $.r($.eval, site.inject);
     case "yande": case "lolibooru": case "konachan":
      Main.r[1] = /id="?image"? [^>]*?src="?([^">]+)"?[^>]*\/>/;
      Main.css += ".javascript-hide[id] {\n  display: inline-block ! important\n}\nspan.thumb {\n  width: 180px;\n  height: 180px;\n  text-align: center\n}\nspan.thumb, span.thumb a {\n  display: inline-block\n}\nspan.thumb .preview, .listimage span img {\n  max-width: 150px;\n  max-height: 150px;\n  overflow: hidden;\n  display: inline-block\n}";
      break;
     case "atfbooru":
      Main.sel = "div#posts article[id^='post']";
      Main.css += "div > article.post-preview {\n  overflow: initial\n}\narticle.post-preview > a {\n  overflow: hidden\n}";
      break;
     case "safebooru":
      Main.css += "img[title*=' rating:'][src*='.png'] {\n  background-color: rgba(255,255,255,.5)\n}";
    }
    Main.finalizeCss();
  },
  click(e) {
    if (e.button === 0) {
      let target = e.target;
      while (target && !target.hasAttribute("data-full")) target = target.parentNode;
      e.preventDefault();
      e.stopPropagation();
      if (target) Main.fn(target);
    }
  },
  process(node, full) {
    let a, id, alt;
    if (!(typeof node === "object" && node.nodeType === 1)) return;
    if (node.tagName === "LI" && String(node.id)[0] === "p" && ~String(node.className).indexOf("creator-id-")) return setTimeout(Main.myImuoto, 0, node);
    if (site.gelbooru && node.classList.contains("thumbnail-preview")) node = Main.gelbooruFix(node);
    if (node.matches(Main.sel) && (a = node.firstElementChild) && !a.dataset.full) {
      alt = $("img[alt]", node);
      if (!(alt && (alt = alt.title || alt.alt)))
        return;
      id = (node.id || a.id || a.children[0].id).match(Main.r[4])[0];
      switch(site.name) {
       case "gelbooru":
        a.setAttribute("href", `/index.php?page=post&s=view&id=${id}`);
        break;
       case "booru":
       case "splatoon":
        a.setAttribute("data-never-hide", "");
      }
      if (~alt.indexOf("animated_gif")) {
        a.firstElementChild.style.border = "2px solid lime";
        a.dataset.gif = "gif";
      }
      else if (Main.r[3].test(alt) || $$("img[src*='webm-preview.png']", node).length) return a.style.cursor = "alias";
      a.dataset.id = id;
      a.dataset.full = typeof full === "string" ? full : site.paheal ? node.children[2].href : site.atfbooru ? node.dataset[fullImage ? "largeFileUrl" : "fileUrl"] : $.cache(id);
      node.removeAttribute("onclick");
      a.removeAttribute("onclick");
      if (Hover.el) Hover.build(a);
      if (Hover.gears && Hover.gears.style.opacity !== "0")
        Hover.noGears();
      if (!Main.attachedClickListener) {
        Main.attachedClickListener = true;
        $.on(node.parentNode, "click", Main.click);
      }
    }
  },
  init() {
    if (!(site.sankakucomplex || site.atfbooru) && location.pathname === "/")
      return $.r(Main.front);
    Main.initDomainInfo();
    const observer = new MutationObserver(mutations => {
      for (let i = 0, len1 = mutations.length; i < len1; ++i)
      for (let j = 0, arr = mutations[i].addedNodes, len2 = arr.length; j < len2; ++j)
        Main.process(arr[j]);
    });
    Main.offObj = [{ detail: true }, Main.off];
    Main.onObj = [{ detail: false }, Main.on];
    observer.observe(d, {
      childList: true,
      subtree: true
    });
    $.on("animationend", Main.animationEnd);
    $.on(w, "keypress", e => {
      if (e.keyCode === 13) {
        if (slideshow)
          Btn.cb();
        else if (e.target.matches(".thumb > a[data-full]")) {
          e.preventDefault();
          Main.fn(e.target);
        }
      }
      else if (slideshow && e.charCode === 32) {
        Btn.cb.move();
      }
    });
    $.on(w, "wheel", e => {
      if (e.ctrlKey)
        e.preventDefault();
      if (Main.el)
        $.keyDown({ keyCode: e.deltaY > 0 ? 39 : e.deltaY < 0 ? 37 : 0 });
    });
    $.r(Main.ready);
    $.r(Hover.init);
  },
  ready() {
    if (WIREFRAME) {
      let debug = $.c("div");
      debug.id = "debug";
      $.add(debug);
      $.ins(debug, "beforeend", SVG.debug);
      Main.css += `#debug{display:${["none","block"][WIREFRAME-1]};position:fixed;z-index:10;top:0;left:0;width:100vw;height:100vh;pointer-events:none;display:block}.sliding > div#debug{display:block}#debug *{pointer-events:none}#zoom_top{border:1px solid red}`;
    }
    for (let i = 0, arr = [["textarea", "copy"], ["a", "realDl"]]; i < 2; ++i) {
      const [tag, prop] = arr[i];
      Menu[prop] = $.c(tag);
      Menu[prop].classList.add("oFfScReEn");
      $.add(Menu[prop]);
    }
  },
  gelbooruFix(el) {
    const img = el.querySelector("img");
    if (!~String(img.getAttribute("src")).indexOf("/")) {
      img.className = "preview";
      let key = Main.gelbooruKey;
      if (!key) {
        for (let i = 0, arr = Object.entries(img.dataset), len = arr.length; i < len; ++i) {
          const [k, v] = arr[i];
          if (Main.r[5].test(v)) {
            Main.gelbooruKey = key = k;
            break;
          }
        }
      }
      img.setAttribute("src", img.getAttribute(`data-${key}`));
      img.removeAttribute(`data-${key}`);
    }
    const span = el.firstElementChild;
    el.parentNode.replaceChild(span, el);
    return span;
  },
  myImuotoOnReady(root) {
    const arr = root.childNodes;
    let i = arr.length;
    while (~--i) {
      const child = arr[i];
      if (child.nodeType !== 1) root.removeChild(child);
    }
  },
  myImuoto(el) {
    const thumb = $.c("span"), id = el.id.substr(1), img = el.children[0].children[0].children[0], full = el.lastElementChild.href;
    if (!Main.myImuoto.readied) {
      $.r(Main.myImuotoOnReady, el.parentNode);
      Main.myImuoto.readied = true;
    }
    thumb.id = el.id;
    thumb.className = `creator-id-${id} thumb`;
    thumb.innerHTML = `<a id="s${id}" href="/post/show/${id}" data-res="${el.lastElementChild.lastElementChild.textContent.replace(Main.r[6], "\u00A0")}"><img class="preview" src="${img.src}" alt="${img.alt}" title="${img.alt}" /></a>`;
    el.parentNode.replaceChild(thumb, el);
    return Main.process(thumb, fullImage || full.match(Main.r[2])[1].toLowerCase() === "gif" ? full : null);
  },
  fn(node) {
    const [msg, method] = Main.el ? Main.offObj : Main.onObj;
    d.dispatchEvent(new CustomEvent(ns, msg));
    const _ = $.safe(method, null, node);
    if (_ === $.safe.error)
      console.error(_);
  },
  off(a) {
    const reqs = Prog.reqs;
    for (let i = 0, arr = $.keys(reqs), len = arr.length; i < len; ++i) {
      const _req = reqs[arr[i]];
      $.safe(_req.abort, _req);
    }
    Prog.reqs = Object.create(null);
    for (let i = 0, arr = $$("iframe.proxY"), len = arr.length; i < len; ++i)
      $.rm(arr[i]);
    if (slideshow) Btn.cb();
    slideshow = !(a = $.current());
    Main.el = $.rm(Main.el);
    d.body.classList.remove("sliding");
    a.classList.add("outlined");
    $.off("mousemove", $.zoom);
    $.off("keydown", $.keyDown);
    $.off("keyup", $.keyUp);
    if (a) {
      let correction = 0;
      if (site.sankakucomplex || site.e621 || site.atfbooru) {
        a = a.firstElementChild;
        correction = a.offsetParent.offsetTop;
      }
      w.scrollTo(0, a.offsetTop + correction + a.offsetHeight / 2 - w.innerHeight / 2);
      a.focus();
      Btn.fn(); Pos.fn();
    }
    if (site.sankakucomplex) uW.Sankaku.Pagination.auto_enabled = true;
    for (let i = 0, arr = [[Main, "gif"], [Prog], [Menu], [$.keyDown], [Hover, "gears"]]; i < 5; ++i) {
      const [p, el = "el"] = arr[i];
      p[el] = $.rm(p[el]);
    }
  },
  _on: e => e.button === 1 && $.keyDown({ keyCode: 38, event: e }),
  on(a) {
    if (site.sankakucomplex) uW.Sankaku.Pagination.auto_enabled = false;
    d.body.classList.add("sliding");
    {
      const arr = $$("a.outlined[data-full]");
      let i = arr.length;
      while (~--i) arr[i].classList.remove("outlined");
    }
    $.add($.extend(Main.el = $.c("img"),
      { id: "slide", alt: "Loading...", onclick: Main.fn, onmouseup: Main._on }
    ));
    $.add(Main.gif = $.extend($.c("span"), { innerHTML: SVG.gif, className: "gif" }));
    const _ = $.safe(Main.slide, null, $("img", a).src);
    if (_ === $.safe.error) {
      console.error(_);
      return Main.off();
    }
    $.on("keyup", $.keyUp);
    $.on("keydown", $.keyDown);
    $.on("mousemove", $.zoom);
    Pos.fn(); Btn.fn(); $.preload();
  },
  isGif(match) {
    return match && match[1] ? match[1].toLowerCase() === "gif" : null;
  },
  slide(src) {
    if (!slideshow)
      Main.el.src = src;
    if ($.zoom.el)
      $.zoom.el = $.rm($.zoom.el);
    Hover.center(src);
    Main.el.dataset.src = src;
    Main.gif.removeAttribute("style");
    Main.el.removeAttribute("style");
    const curr = $.current(src), data = curr.dataset.full, id = curr.dataset.id;
    Main.el.dataset.id = id;
    /* 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 */
    const isGif = Main.isGif(data.match(Main.r[2]));
    if (data === "loading") Main.req(curr);
    else if (curr.dataset.blob) {
      Main.el.src = curr.dataset.blob;
      if (Menu.el) Menu.download();
      if (Prog.el) Prog.el = $.rm(Prog.el);
      const el = Prog.progress();
      el.style.width = "100%";
      el.classList.add("progdone");
    }
    // hack start
    else if (isGif !== false) {
      if (isGif) {
        Main.el.removeAttribute("src");
        Main.el.src = data;
        Main.gif.setAttribute("style", "display: inline-block");
      }
      else Main.el.dispatchEvent(new Event("load"));
    }
    // hack end
    else Prog.fn(data, id);
  },
  r: [/file_url[=>]"?([^" <]+)"?/i,/* 0 */ /sample_url[=>]"?([^" <]+)"?/i,/* 1 */ /\.(gif|png|jpe?g)/i,/* 2 */ /\b(webm|video|mp4|flash)\b/i,/* 3 */ /\d+/,/* 4 */ /[a-f0-9]{32}/,/* 5 */ / /g]/* 6 */,
  processHttp(x) { if (!$.in(x.status, httpOk)) throw `HTTP status: ${x.status}`; return x.text(); },
  validateHtml(img, el) {
    return img.match(el.dataset.gif || fullImage ? Main.r[0] : Main.r[1])[1];
  },
  checkPreviewId(img) {
    return ~Main.el.dataset.src.indexOf($.base(img));
  },
  processText(img, node, id) {
    if ((img = $.safe(Main.validateHtml, null, img, node)) === $.safe.error)
      throw "API error";
    node.dataset.full = $.cache(id, img);
    const _ = $.safe(Main.checkPreviewId, null, img);
    if (typeof _ === "number" && _)
      $.safe(Main.slide, null, img);
    $("img", node).style.outline = "";
    node.removeAttribute("data-already-loading");
  },
  getApiInfo(node) {
    switch(domain) {
     case "e621.net":
      return [node.parentNode.id.substr(1), "/post/show.xml?id="];
     case "sankakucomplex.com":
      return [node.dataset.id, "/post/show/"];
     case "yande.re":
     case "lolibooru.moe":
     case "konachan.com":
     case "hypnohub.net":
      return [node.parentNode.id.substr(1), "/post/show/"];
     case "booru.org":
     case "tbib.org":
     case "splatoon.ink":
     case "gelbooru.com":
      return [node.dataset.id, "/index.php?page=post&s=view&id="];
     default:
      return [node.dataset.id, "/index.php?page=dapi&s=post&q=index&id="];
    }
  },
  async req(node) {
    if (!node) return;
    const { dataset } = node;
    if (dataset.alreadyLoading || dataset.full !== "loading") return;
    const { 0: id, 1: api } = Main.getApiInfo(node);
    dataset.alreadyLoading = "true";
    try {
      if (site.sankakucomplex) return await Prog.fn(api + id, id, node);
      const request = await fetch(api + id);
      const text = await Main.processHttp(request);
      Main.processText(text, node, id);
    }
    catch (e) {
      Main.warn();
      $("img", node).style.outline = "6px solid red";
      console.error(`Main.req failure:\n\n${location.origin + api + id} |`, e);
      node.removeAttribute("data-already-loading");
    }
  },
  warn() {
    const warn = $.c("span");
    warn.innerHTML = SVG.warn;
    warn.classList.add("warn");
    if ($$(".sliding>.warn").length === 0) $.add(warn);
    if (slideshow)
      Main.el.dispatchEvent(new Event("load"));
  },
  front() {
    let target, method = "beforeend", el;
    switch(domain) {
     case "e621.net":
      target = "#mascot_artist";
      method = "afterend";
      break;
     case "booru.org":
     case "rule34.xxx":
     case "splatoon.ink":
      target = "#static-index > div:last-child > p:first-child";
      break;
     case "lolibooru.moe":
      target = "#links + *";
      break;
     case "safebooru.org":
      target = "div.space + div > p";
      method = "afterend";
      break;
     case "hypnohub.net":
      target = "form > div";
      break;
     case "tbib.org":
      target = "div.space + div > p";
      method = "afterend";
      break;
     default:
      target = "form:last-of-type + div";
      break;
    }
    for (let i = 0, arr = ["tags", "tags-search"]; i < 2; ++i) {
      if (el = $.id(arr[i])) {
        el.focus();
        break;
      }
    }
    $.ins($(target), method, `<br><br>Gelbooru&nbsp;Enhancement:<br><pre style="font-size: 11px;text-align: left;display: inline-block;margin-top: 5px;">- Gelbooru Image Viewer ${scriptInfo.script.version}</pre>`);
  },
  css: `@keyframes Outlined {\n  0% { outline: 6px solid orange }\n  60% { outline: 6px solid orange }\n  100% { outline: 6px solid transparent }\n}\n@keyframes nomoreimages {\n  0% { opacity: 0 }\n  20% { opacity: 1 }\n  100% { opacity: 0 }\n}\n@keyframes menuelement {\n  0% { opacity: 1 }\n  80% { opacity: 1 }\n  100% { opacity: 0 }\n}\n@keyframes progfail {\n  0% { opacity: 1 }\n  80% { opacity: 1 }\n  100% { opacity: 0 }\n}\n@keyframes warn {\n  0% { opacity: 0; bottom: 5vh }\n  25% { opacity: 1; bottom: 10vh }\n  90% { opacity: 1 }\n  100% { opacity: 0 }\n}\n@keyframes topglowything {\n  0% { opacity: 1 }\n  80% { opacity: 1 }\n  100% { opacity: 0 }\n}\nbody.sliding > * {\n  display: none\n}\nbody.sliding * {\n  -webkit-box-sizing: initial;\n  -moz-box-sizing: initial;\n  box-sizing: initial;\n  line-height: initial\n}\nbody.sliding > .warn {\n  z-index: 2;\n  display: inline-block;\n  position: absolute;\n  width: 10vw;\n  left: 45vw;\n  bottom: 10vh;\n  animation-duration: 2s;\n  animation-name: warn;\n  pointer-events: none\n}\n#slide {\n  position: absolute;\n  z-index: 1;\n  width: 100vw;\n  height: 100vh;\n  object-fit: contain;\n  display: inherit;\n  top: 0;\n  left: 0\n}\n.outlined {\n  outline: 6px solid transparent;\n  animation-duration: 4s;\n  animation-name: Outlined\n}\nbody.sliding > div.nextgears {\n  display: block;\n  position: absolute;\n  z-index: 2;\n  width: 10vw;\n  filter: url(#__dropshadow);\n  right: -30vw;\n  top: 50%;\n  transform: translateY(-50%);\n  min-width: 50px;\n  transition: all ease .3s\n}\nbody.sliding > .nomoreimages {\n  z-index: 4;\n  pointer-events: none;\n  display: block;\n  width: 33vw;\n  height: 100vh;\n  top: 0;\n  position: fixed;\n  animation-duration: 1s;\n  animation-name: nomoreimages\n}\nspan.thumb {\n  max-width: 180px;\n  max-height: 180px\n}\n#menuel {\n  z-index: 3;\n  opacity: 1;\n  position: fixed;\n  display: block;\n  padding: 2px;\n  background: black;\n  width: 139px;\n  height: 67px;\n  overflow: hidden;\n  animation-duration: 1s\n}\n.gif[style] ~ #menuel {\n  height: 44px\n}\nbody.sliding > .menuel {\n  animation-name: menuelement\n}\n#menuel:hover {\n  animation-name: keepalive\n}\n#menuel a {\n  background: #fff;\n  color: #006FFA;\n  display: block;\n  font-size: 16px;\n  font-family: verdana, sans-serif;\n  font-weight: unset\n}\n#menuel a:hover {\n  color: #33CFFF ! important\n}\n#menuel a:visited {\n  color: #006FFA\n}\n#zoom_top {\n  position: fixed;\n  top: ${200 + paheal}px;\n  left: 0;\n  pointer-events: none;\n  width: calc(5vw + 60px);\n  height: 60px;\n  min-width: 110px;\n  z-index: 2;\n  overflow: hidden;\n  animation-duration: .6s;\n  opacity: 0\n}\n#zoom_top[style] {\n  display: block\n}\n#zoom_top > span {\n  position: absolute;\n  top: 1px;\n  transform: translateY(-100%);\n  box-shadow: 0 0 30px cyan;\n  background: black;\n  left: 30px;\n  width: 5vw;\n  height: 1.6vw;\n  border-radius: .8vw;\n  min-width: 50px\n}\nbody.sliding > .slideshow {\n  z-index: 2;\n  display: block;\n  position: fixed;\n  bottom: 20px;\n  right: 20px;\n  font-size: 16px;\n  font-family: verdana, sans-serif\n}\nbody.sliding > .progress {\n  z-index: 6;\n  display: block;\n  background-color: rgb(128,200,255);\n  height: 1vh;\n  position: absolute;\n  top: 0;\n  left: 0;\n  box-shadow: 0 .5vh 10px rgba(0,0,0,.7), inset 0 0 .1vh black;\n  transition: ease-in-out .08s width;\n  min-height: 3px;\n  min-width: 5px ! important;\n  max-width: 100vw;\n  pointer-events: none;\n  will-change: width, opacity\n}\nbody.sliding > .progfail {\n  background-color: red;\n  width: 100% ! important;\n  animation-name: progfail;\n  animation-duration: 1.5s\n}\nbody.sliding > .progdone {\n  width: 100% ! important;\n  animation-name: progfail;\n  animation-duration: .6s\n}\n.slideshow:hover:not([data-timer]) > div {\n  background: white;\n  color: black;\n  position: fixed;\n  display: block ! important;\n  bottom: 70px;\n  right: 20px\n}\nbody.sliding > .gif {\n  z-index: 5;\n  pointer-events: none;\n  position: absolute;\n  top: 3vh;\n  right: 2vw;\n  width: 3.5vw;\n  opacity: .7;\n  min-width: 50px;\n  transition: ease .15s margin-top;\n  margin-top: 0;\n  will-change: margin-top\n}\nbody.sliding {\n  padding: 0;\n  overflow: hidden\n}\nbody.sliding > .viewpre {\n  display: block\n}\nbody.sliding > .viewpre.showimagelist > .tentcon {\n  transform: unset\n}\n.viewpre > div {\n  display: inherit;\n  z-index: 5;\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: ${200 + paheal}px\n}\n.viewpre > .tentcon {\n  transform: translateY(-100%);\n  transition: ease .15s transform;\n  background: linear-gradient(to bottom, black, transparent);\n  will-change: transform\n}\n.viewpre:hover > .tentcon {\n  transform: unset\n}\n.viewpre .wrapthatshit, .viewpre .wrapthatshit > .listimage {\n  transform: rotateX(180deg);\n}\n.viewpre.showimagelist ~ .gif, .viewpre:hover ~ .gif {\n  margin-top: ${200 + paheal * 2}px\n}\n.wrapthatshit {\n  height: 100%;\n  width: 100%;\n  overflow-x: auto\n}\n.listimage {\n  height: ${180 + paheal}px;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  user-select: none;\n  margin: 0 auto\n}\n.listimage span {\n  height: ${180 + paheal}px;\n  width: ${180 + paheal}px;\n  text-align: center;\n  display: table-cell ! important;\n  vertical-align: middle\n}\n.listimage span img {\n  cursor: pointer\n}\n.listimage .current {\n  background: linear-gradient(to top, transparent 0%, hsla(204, 100%, 56%, .8) 2%, transparent 30%, transparent 100%)\n}\n.listimage .next::after {\n  content: "LOAD\\ANEXT\\APAGE";\n  font-size: 30px;\n  text-transform: full-width;\n  white-space: pre-wrap;\n  color: white;\n  filter: url(#__dropshadow)\n}\n.listimage .next {\n  cursor: pointer\n}\nbody.sliding > .posel {\n  position: fixed;\n  bottom: 20px;\n  left: 0;\n  display: block;\n  pointer-events: none;\n  z-index: 2;\n  font-size: 16px;\n  font-family: verdana, sans-serif\n}\n.posel > div {\n  position: relative;\n  color: #fff;\n  z-index: 2\n}\n.posel::before {\n  content: attr(title);\n  position: absolute;\n  -webkit-text-stroke: 2px black;\n  left: 0;\n  z-index: 1\n}\n[data-res]:hover::after {\n  content: attr(data-res);\n  color: white;\n  position: absolute;\n  top: 8px;\n  left: 50%;\n  transform: translateX(-50%);\n  padding: 3px 5px;\n  background: rgba(0,0,0,.7);\n  border-radius: 5px;\n  border: 2px black solid;\n  box-shadow: 0 0 2px 1px black;\n  pointer-events: none\n}\n[data-res]:hover {\n  position: relative;\n  display: inline-block\n}\nbody:not(.sliding) > div.viewpre {\n  display: none ! important\n}\nbody > .oFfScReEn {\n  display: block;\n  position: fixed;\n  top: -500px\n}`
};
Main.init();