Gelbooru Image Viewer

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

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

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

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

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

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

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

(У мене вже є менеджер скриптів, дайте мені встановити його!)

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

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

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

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

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

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

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

// ==UserScript==
// @id             gelbooru-slide
// @name           Gelbooru Image Viewer
// @version        1.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();