Gelbooru Image Viewer

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

  1. // ==UserScript==
  2. // @id gelbooru-slide
  3. // @name Gelbooru Image Viewer
  4. // @version 1.9.9.9
  5. // @namespace intermission
  6. // @author intermission
  7. // @license WTFPL; http://www.wtfpl.net/about/
  8. // @description Adds a fullscreen image view option when you click on images and various other neat features
  9. // @match *://gelbooru.com/*
  10. // @match *://rule34.xxx/*
  11. // @match *://e621.net/*
  12. // @match *://*.booru.org/*
  13. // @match *://*.paheal.net/*
  14. // @match *://yande.re/post*
  15. // @match *://lolibooru.moe/*
  16. // @match *://konachan.com/*
  17. // @match *://atfbooru.ninja/*
  18. // @match *://safebooru.org/*
  19. // @match *://hypnohub.net/*
  20. // @match *://tbib.org/*
  21. // @match *://booru.splatoon.ink/*
  22. // @match *://*.sankakucomplex.com/*
  23. // @exclude http://www.sankakucomplex.com/*
  24. // @exclude https://www.sankakucomplex.com/*
  25. // @run-at document-start
  26. // @grant GM_registerMenuCommand
  27. // @grant GM_xmlhttpRequest
  28. // @grant GM_info
  29. // ==/UserScript==
  30.  
  31. /* This program is free software. It comes without any warranty, to the extent
  32. * permitted by applicable law. You can redistribute it and/or modify it under
  33. * the terms of the Do What The Fuck You Want To Public License, Version 2, as
  34. * published by Sam Hocevar. See http://www.wtfpl.net/ for more details. */
  35.  
  36. /* global GM_registerMenuCommand, GM_xmlhttpRequest, GM_info, unsafeWindow */
  37.  
  38. "use strict";
  39.  
  40. /**
  41. * DEBUG
  42. */
  43. const WIREFRAME = 0; // 0 - off, 1 - on during image viewer, 2 - always on
  44. const SAFE_DEBUG = false;
  45.  
  46. /**
  47. * BASIC CONSTANTS
  48. */
  49. const d = document;
  50. const w = window;
  51. const stor = localStorage;
  52. const uW = typeof unsafeWindow !== "undefined" ? unsafeWindow : w;
  53. const domain = location.hostname.match(/[^.]+\.[^.]+$/)[0];
  54. const scriptInfo = GM_info;
  55. const ns = "gelbooru-slide";
  56. const fullImage = stor[ns] === "true";
  57. const passive = { passive: true };
  58. const once = { once: true };
  59. const svg = '<svg xmlns="http://www.w3.org/2000/svg" ';
  60. const httpOk = [200, 302, 304];
  61.  
  62. /**
  63. * `site` OBJECT CONSTRUCTION
  64. * site: {
  65. * name: hostname of current site
  66. * inject: string if a site needs a script to be injected to work this userJS
  67. * [name]: dynamic property, hostname of current site w/o TLD, returns true
  68. * }
  69. */
  70. const site = (() => {
  71. const info = scriptInfo.scriptMetaStr;
  72. const regex = /match.*?[/.](\w+\.\w+)\//g;
  73. const ret = Object.setPrototypeOf({ name: "", inject: "" }, null);
  74. let line, name;
  75. while (line = regex.exec(info)) { // eslint-disable-line no-cond-assign
  76. if (line[1] === domain) {
  77. ret[name = domain.split(".")[0]] = true;
  78. ret.name = name;
  79. break;
  80. }
  81. }
  82. const qS = `document.querySelector("#post-list > .content`;
  83. const iB = "insertBefore";
  84. switch(name) {
  85. case "hypnohub":
  86. ret.inject += `Object.defineProperties(window.PostModeMenu, {\n "post_mouseover": {\n value() {}\n },\n "post_mouseout": {\n value() {}\n }\n});`;
  87. break;
  88. case "sankakucomplex":
  89. 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})`;
  90. }
  91. return ret;
  92. })();
  93.  
  94. /**
  95. * VARIABLE FOR CSS
  96. * paheal's thumbnails are 200x200 instead of 180x180 (every other booru)
  97. */
  98. const paheal = site.paheal ? 20 : 0;
  99.  
  100. /**
  101. * SVG CONSTANTS
  102. */
  103. const SVG = {
  104. 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>`,
  105. 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>`,
  106. 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="'),
  107. 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='),
  108. 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>`,
  109. 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>`
  110. };
  111.  
  112. /**
  113. * GLOBAL VARIABLE INDICATING WHETHER WE ARE IN SLIDESHOW MODE OR NOT
  114. */
  115. let slideshow;
  116.  
  117. /**
  118. * NAMESPACES
  119. */
  120. let Pos, Menu, Btn, Main, Prog, Hover;
  121.  
  122. /**
  123. * SET AN INITIAL VALUE ON FIRST RUN
  124. */
  125. if (!stor[ns]) stor[ns] = "false";
  126.  
  127. /**
  128. * PAHEAL AND BOORU.ORG DON'T USE SAMPLES
  129. */
  130. switch(site.name) {
  131. case "splatoon": case "booru": case "paheal":
  132. break;
  133. default:
  134. GM_registerMenuCommand(`Current image mode: ${fullImage ? "Always original size" : "Sample only"}`, () => { stor[ns] = fullImage ? "false" : "true"; location.reload(); });
  135. break;
  136. }
  137.  
  138. /**
  139. * JQUERY INSPIRED UTILITIES
  140. * details won't be documented, just look at the code
  141. */
  142. const $$ = (a, b = d) => Array.prototype.slice.call(b.querySelectorAll(a));
  143. const $ = (a, b = d) => b.querySelector(a);
  144.  
  145. $.keys = Object.keys;
  146.  
  147. $.extend = function(obj, props) {
  148. const arr = $.keys(props);
  149. for (let i = 0, key, len = arr.length; i < len; ++i) {
  150. obj[key = arr[i]] = props[key];
  151. }
  152. return obj;
  153. };
  154.  
  155. $.extend($, {
  156. cache(id, b) {
  157. if (site.sankakucomplex) return "loading";
  158. const val = fullImage ? "original" : "sample";
  159. let ret, obj, temp;
  160. if (!b) {
  161. const json = $.safe(JSON.parse, null, stor[ns + id]);
  162. if (json !== $.safe.error) ret = json[val];
  163. ret = ret || "loading";
  164. }
  165. else {
  166. const json = $.safe(JSON.parse, null, stor[ns + id]);
  167. if (json !== $.safe.error) temp = json;
  168. obj = temp || {};
  169. obj[val] = b;
  170. stor[ns + id] = JSON.stringify(obj);
  171. ret = b;
  172. }
  173. return ret;
  174. },
  175. base: a => a.match(Main.r[5]),
  176. id: (a, b = d) => b.getElementById(a),
  177. current: src => $(`a[data-id] > img[src*="${$.base(src || Main.el.dataset.src)}"]`).parentNode,
  178. _find(method, el, a) {
  179. el = el[`${method ? "next" : "previous"}ElementSibling`];
  180. a = $("a[data-full]", el);
  181. return [el, a];
  182. },
  183. find(el, method) {
  184. const { _find, safe, safe: { error } } = $;
  185. let a, search;
  186. el = el.parentNode;
  187. do {
  188. if ((search = safe(_find, null, method, el, a)) === error) return false;
  189. else ({ 0: el, 1: a } = search);
  190. if (a) return a;
  191. } while(1); // eslint-disable-line no-constant-condition
  192. },
  193. preload() {
  194. if (site.sankakucomplex) return;
  195. const curr = $.current();
  196. Main.req($.find(curr, true));
  197. Main.req($.find(curr, false));
  198. },
  199. keyDown(e) {
  200. let move, warn;
  201. if (slideshow) return;
  202. if (e.ctrlKey && Main.el.style.objectFit !== "none")
  203. Main.el.style.objectFit = "none";
  204. if (e.warn)
  205. move = warn = true;
  206. else {
  207. switch(e.keyCode) {
  208. case 32: case 39: // Space, →
  209. if (e.preventDefault) e.preventDefault();
  210. move = true;
  211. break;
  212. case 37: // ←
  213. move = false;
  214. break;
  215. case 38: // ↑
  216. return e.event ? Menu.fn(e.event) : w.location = $.current().href;
  217. case 40: // ↓
  218. e.preventDefault();
  219. return Main.el.click();
  220. }
  221. }
  222. if (typeof move !== "undefined") {
  223. if (!warn && (e = $.find($.current(), move))) {
  224. if (Prog.el) {
  225. Prog.el.classList.remove("progdone");
  226. Prog.el.style.width = 0;
  227. }
  228. Main.slide($("img", e).src);
  229. Pos.fn(move);
  230. $.preload();
  231. }
  232. else if (Hover.nextEl && !Hover.nextEl.classList.contains("loadingu") && move === true)
  233. Hover.next(true);
  234. else if (!$.keyDown.el) {
  235. const el = $.keyDown.el = $.c("div");
  236. el.classList.add("nomoreimages");
  237. const side = move ? "right" : "left";
  238. el.setAttribute("style", `background: linear-gradient(to ${side}, transparent, rgba(255,0,0,.5));${side}: 0;`);
  239. $.add(el);
  240. }
  241. }
  242. },
  243. keyUp() {
  244. if (slideshow) return;
  245. if (Main.el.style.objectFit === "none") {
  246. $.zoom.el = $.rm($.zoom.el);
  247. Main.el.removeAttribute("style");
  248. }
  249. },
  250. zoom(e) {
  251. if (Main.gif.hasAttribute("style")) return;
  252. if (!e.ctrlKey) {
  253. $.zoom.el = $.rm($.zoom.el);
  254. return Main.el.removeAttribute("style");
  255. }
  256. else Main.el.style.objectFit = "none";
  257. if (slideshow) return;
  258. const minY = 200 + paheal, maxX = w.innerWidth, maxY = w.innerHeight,
  259. { clientX: x, clientY: y } = e,
  260. tall = Main.el.naturalHeight > maxY, wide = Main.el.naturalWidth > maxX;
  261. if (!$.zoom.el) {
  262. const el = $.zoom.el = $.c("span");
  263. el.id = "zoom_top";
  264. el.innerHTML = "<span></span>";
  265. $.add(el);
  266. }
  267. if ((wide || tall) && minY < y && maxY >= y && 0 <= x && maxX >= x) {
  268. let xPos = 50, yPos = 50;
  269. if (tall) {
  270. const margin = (maxY - minY) * 0.075;
  271. const height = maxY - minY - margin * 2;
  272. if (y < minY + margin)
  273. yPos = 0;
  274. else if (y > maxY - margin)
  275. yPos = 100;
  276. else
  277. yPos = (y - minY - margin) / height * 100;
  278. }
  279. if (wide) {
  280. const margin = maxX * 0.075;
  281. const width = maxX - margin * 2;
  282. if (x < margin)
  283. xPos = 0;
  284. else if (x > maxX - margin)
  285. xPos = 100;
  286. else
  287. xPos = (x - margin) / width * 100;
  288. }
  289. $.zoom.el.removeAttribute("style");
  290. if (tall && yPos < 10) {
  291. const w = maxX / 40 + 30;
  292. $.zoom.el.style.left = `${x - (w < 55 ? 55 : w)}px`;
  293. void $.zoom.el.offsetWidth; // reflow hack
  294. $.zoom.el.style.animationName = "topglowything";
  295. }
  296. Main.el.style.objectPosition = `${xPos}% ${yPos}%`;
  297. }
  298. else if (!Main.el.style.objectPosition) {
  299. Main.el.style.objectPosition = "50% 50%";
  300. }
  301. },
  302. c: c => d.createElement(c),
  303. r: (() => {
  304. let queue = [];
  305. d.addEventListener("DOMContentLoaded", () => {
  306. for (let i = 0, len = queue.length; i < len; ++i) {
  307. const obj = queue[i], copy = [obj.fn, null];
  308. copy.push.apply(copy, obj.args);
  309. $.safe.apply($, copy);
  310. }
  311. queue = null;
  312. }, once);
  313. return (fn, ...args) => queue ?
  314. queue[queue.length] = { fn, args } :
  315. $.safe(fn, null, ...args);
  316. })(),
  317. rm: el => { if (el) el.parentNode.removeChild(el); },
  318. ins: (el, m, t) => el.insertAdjacentHTML(m, t),
  319. eval(text = "") {
  320. const script = $.c("script");
  321. script.innerHTML = text;
  322. $.add(script, d.documentElement);
  323. },
  324. in: (obj => {
  325. const arr = function inArray(key) {
  326. for (let i = 0, len = this.length; i < len; ++i) {
  327. if (key === this[i]) return true;
  328. }
  329. return false;
  330. };
  331. return (k, o) => o && (typeof o.length === "number" ? arr : obj).call(o, k);
  332. })(Object.prototype.hasOwnProperty),
  333. add(el, to = d.body) {
  334. $.safe(to.appendChild, to, el);
  335. return el;
  336. },
  337. safe: (safeError => {
  338. function safe(fn, context) {
  339. const k = arguments.length, args = [];
  340. {
  341. let i = 1, p = -1;
  342. while (++i < k) args[++p] = arguments[i];
  343. }
  344. try { return fn.apply(context, args); }
  345. catch(e) {
  346. if (SAFE_DEBUG) console.error("$.safe debug:", e);
  347. return safeError;
  348. }
  349. }
  350. safe.error = safeError;
  351. return safe;
  352. })(Symbol()),
  353. _evt() {
  354. if (typeof arguments[0] === "string") {
  355. return d[this](arguments[0], arguments[1], arguments[2]);
  356. }
  357. return arguments[0][this](arguments[1], arguments[2], arguments[3]);
  358. },
  359. on() { return $._evt.apply("addEventListener", arguments); },
  360. off() { return $._evt.apply("removeEventListener", arguments); },
  361. u: void 0
  362. });
  363.  
  364. /**
  365. * IF SOMETHING CHANGES IN HOW WE STORE CACHED URLS THEN MODIFY VERSION HERE
  366. */
  367. {
  368. const version = "1.8.8";
  369. if (stor[ns + "-firstrun"] !== version) {
  370. const r = /^gelbooru-slide./, arr = $.keys(stor), len = arr.length;
  371. for (let i = 0; i < len; ++i) {
  372. const a = arr[i];
  373. r.test(a) && stor.removeItem(a);
  374. }
  375. stor[ns + "-firstrun"] = version;
  376. }
  377. }
  378.  
  379.  
  380. /**
  381. * MODULE FOR POSITIONAL DISPLAY IN BOTTOM LEFT CORNER
  382. */
  383. Pos = {
  384. fn(a) {
  385. let el = Pos.el;
  386. if (a != null) {
  387. switch(typeof a) {
  388. case "string":
  389. $("span", el).innerHTML = ++a;
  390. break;
  391. case "boolean": {
  392. const no = $("span", el);
  393. no.innerHTML = Number(no.textContent) + (a ? 1 : -1); }
  394. break;
  395. case "number":
  396. el.lastElementChild.lastChild.data = ` / ${a}`;
  397. }
  398. el.title = el.textContent;
  399. if (Menu.el) {
  400. const arr = $$("a:not([download])", Menu.el);
  401. let i = arr.length;
  402. while (~--i) arr[i].href = $.current().href;
  403. Menu.download();
  404. }
  405. }
  406. else {
  407. if (Main.el && !el) {
  408. const thumbs = $$(".thumb a[data-full]");
  409. el = $.c("div");
  410. $.ins(el, "beforeend", `<div><span>${thumbs.indexOf($.current()) + 1}</span> / ${thumbs.length}</div>`);
  411. el.className = "posel";
  412. el.title = el.textContent;
  413. Main.el.insertAdjacentElement("afterend", el);
  414. Pos.el = el;
  415. }
  416. else if (el) Pos.el = $.rm(el);
  417. }
  418. }
  419. };
  420.  
  421. /**
  422. * MODULE FOR MIDDLE CLICK MENU
  423. */
  424. Menu = {
  425. fn(e) {
  426. const height = Main.gif.hasAttribute("style") ? 48 : 71,
  427. _l = e.clientX + 1, _t = e.clientY + 1,
  428. left = (_l > w.innerWidth - 139 ? _l - 139 : _l) + "px",
  429. top = (_t > w.innerHeight - height ? _t - height : _t) + "px";
  430. let el = Menu.el;
  431. if (el) {
  432. el.removeAttribute("class");
  433. setTimeout($.safe, 10, el.classList.add, el.classList, "menuel");
  434. }
  435. else {
  436. const href = $.current().href + '" style="margin-bottom: 2px"';
  437. el = $.c("div");
  438. el.id = "menuel";
  439. $.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>`);
  440. $.add(el);
  441. Menu.el = el;
  442. $.on(el.lastElementChild, "click", Menu.copyFilename);
  443. el.classList.add("menuel");
  444. if (Menu.realDl) Menu.download();
  445. }
  446. return $.extend(el.style, { left, top });
  447. },
  448. download() {
  449. const el = Menu.realDl;
  450. el.href = Main.el.src;
  451. el.download = Main.el.src.split("/").pop() + "." + $.current().dataset.full.match(Main.r[2])[1];
  452. },
  453. copyFilename() {
  454. const { copy, realDl } = Menu;
  455. copy.value = realDl.getAttribute("download") || "";
  456. copy.select();
  457. if ($.safe(document.execCommand, document, "copy") === $.safe.error)
  458. console.log("Failed to copy filename: %s", copy.value);
  459. realDl.click();
  460. }
  461. };
  462.  
  463. /**
  464. * MODULE FOR SLIDESHOW BUTTON AND SETTINGS IN BOTTOM RIGHT CORNER
  465. */
  466. Btn = {
  467. fn() {
  468. const sel = "this.previousElementSibling.firstElementChild";
  469. let el = Btn.el;
  470. if (el) {
  471. clearTimeout(Btn.hideTimer);
  472. Btn.clear();
  473. Btn.el = $.rm(Btn.el);
  474. Hover.el.removeAttribute("style");
  475. }
  476. else {
  477. el = $.c("div");
  478. el.setAttribute("style", "opacity: .7;");
  479. el.className = "slideshow";
  480. $.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>`);
  481. Btn.state = true;
  482. el.firstElementChild.onclick = Btn.cb;
  483. $.add(el);
  484. Btn.el = el;
  485. }
  486. },
  487. clear() {
  488. clearTimeout(Number(Btn.el.dataset.timer));
  489. Btn.el.removeAttribute("data-timer");
  490. },
  491. cb: ((thumbs, el, options, orig) => {
  492. const sel = ".thumb a[data-full]",
  493. validate = a => a.type === "number" ? (a.value >= 1 ? a.value : 1) * 1E3 : a.checked,
  494. _fnS = () => { if (el.dataset.timer) el.dataset.timer = setTimeout(_fnT, options[2]); };
  495. const _fnT = () => {
  496. let _el;
  497. if (options[1]) {
  498. if (thumbs.length === 0) thumbs = $$(sel);
  499. thumbs.splice(thumbs.indexOf($.current()), 1);
  500. _el = thumbs[Math.random() * thumbs.length & -1];
  501. }
  502. else _el = $.find($.current(), true);
  503. if (!_el && options[0]) _el = $(sel);
  504. if (!_el) return Btn.cb();
  505. $.on(Main.el, "load", _fnS, once);
  506. Main.slide($("img", _el).src);
  507. };
  508. const cb = () => {
  509. el = Btn.el;
  510. Pos.fn();
  511. slideshow = !!Btn.state;
  512. el.firstElementChild.innerHTML = (Btn.state = !Btn.state) ? SVG.play : SVG.pause;
  513. if (slideshow) {
  514. thumbs = [];
  515. options = $$("div input", el).map(validate);
  516. el.dataset.timer = setTimeout(_fnT, options[2]);
  517. el.style.opacity = 0.4;
  518. Hover.el.setAttribute("style", "display: none !important");
  519. orig = d.body.getAttribute("style");
  520. $.on("mousemove", Btn.hide);
  521. }
  522. else {
  523. Btn.clear();
  524. el.style.opacity = ".7";
  525. el.removeAttribute("data-timer");
  526. Hover.el.removeAttribute("style");
  527. Hover.center($.current().firstElementChild.src);
  528. $.off("mousemove", Btn.hide);
  529. clearTimeout(Btn.hideTimer);
  530. if (orig) d.body.setAttribute("style", orig);
  531. else d.body.removeAttribute("style");
  532. }
  533. };
  534. cb.move = () => {
  535. clearTimeout(Number(Btn.el.dataset.timer));
  536. _fnT();
  537. };
  538. return cb;
  539. })([]),
  540. hide() {
  541. const b = d.body;
  542. b.style.cursor = "";
  543. clearTimeout(Btn.hideTimer);
  544. Btn.hideTimer = setTimeout(() => b.style.cursor = "none", 5E3);
  545. }
  546. };
  547.  
  548. /**
  549. * MODULE FOR GENERAL DOWNLOADING, PROGRESS DISPLAY AND IMAGE HANDLING
  550. */
  551. Prog = {
  552. check: id => Main.el.dataset.id === id,
  553. load(e) {
  554. const {el} = Prog, {id, ext} = e.context;
  555. delete Prog.reqs[id];
  556. if (!el) throw Error("There was an event order issue with GM_xmlhttpRequest");
  557. if (!$.in(e.status, httpOk) || !Main.r[5].test(e.finalUrl))
  558. return Prog.error(e);
  559. else {
  560. const blobUrl = w.URL.createObjectURL(
  561. new Blob([e.response], { type: `image/${ext.replace("jpeg", "jpg")}` })
  562. );
  563. $(`a[data-id="${id}"]`).dataset.blob = blobUrl;
  564. if (Main.el && Prog.check(id)) {
  565. el.classList.add("progdone");
  566. Main.el.src = blobUrl;
  567. if (Menu.el) Menu.download();
  568. }
  569. }
  570. },
  571. progress(e) {
  572. let el = Prog.el;
  573. if (!el) {
  574. Prog.el = el = $.c("span");
  575. el.setAttribute("style", "width:0");
  576. el.classList.add("progress");
  577. $.add(el);
  578. }
  579. if (!e) return el;
  580. const id = e.context && e.context.id;
  581. if (Main.el && Prog.check(id) && el) {
  582. el.classList.remove("progfail");
  583. el.style.width = `${$.in(id, Prog.reqs) ? e.loaded / e.total * 100 : 0}%`;
  584. }
  585. },
  586. error(e) {
  587. const el = Prog.el, id = e.context.id;
  588. if (Main.el && Prog.check(id) && el) _: {
  589. el.classList.add("progfail");
  590. const req = Prog.reqs[id];
  591. if (req == null) break _;
  592. $.safe(req.abort, req);
  593. delete Prog.reqs[id];
  594. }
  595. if (slideshow)
  596. Main.el.dispatchEvent(new Event("load"));
  597. stor.removeItem(ns + id);
  598. if (!site.paheal)
  599. $(`a[data-id="${id}"]`).dataset.full = $.cache(id, "loading");
  600. },
  601. parser: new DOMParser,
  602. helper: class { then(r, e) {
  603. Prog.reqs[this.id] = GM_xmlhttpRequest(Object.assign({}, this.details, {
  604. onload: r,
  605. onerror: e,
  606. onabort: e,
  607. ontimeout: e
  608. }));
  609. } },
  610. async sankaku(url, id, extra) {
  611. const req = new Prog.helper;
  612. req.id = id;
  613. const headers = {
  614. Cookie: document.cookie,
  615. Referer: location.href
  616. };
  617. req.details = {
  618. url,
  619. method: "GET",
  620. headers
  621. };
  622. const res = await req;
  623. extra.dataset.full = headers.Referer = res.finalUrl;
  624. extra.removeAttribute("data-already-loading");
  625. const doc = Prog.parser.parseFromString(res.responseText, "text/html");
  626. const a = doc.querySelector("#image-link");
  627. url = fullImage && a.href ? a.href : a.firstElementChild.src;
  628. return [url, headers];
  629. },
  630. async fn(url, id, extra) {
  631. if ($.in(id, Prog.reqs)) return;
  632. let headers;
  633. if (url.startsWith("//assets2")) {
  634. url = `//gelbooru.com/${url.substr(22)}`;
  635. }
  636. else if (site.sankakucomplex) {
  637. try { ({ 0: url, 1: headers } = await Prog.sankaku(url, id, extra)); }
  638. catch (e) {
  639. Prog.error({ context: { id } });
  640. throw e;
  641. }
  642. }
  643. if (Prog.el) Prog.el.style.width = 0;
  644. Prog.reqs[id] = GM_xmlhttpRequest({
  645. url,
  646. headers,
  647. context: { id, url, ext: url.match(Main.r[2])[1], extra },
  648. method: "GET",
  649. responseType: "arraybuffer",
  650. onload: Prog.load,
  651. onprogress: Prog.progress,
  652. onerror: Prog.error,
  653. onabort: Prog.error,
  654. ontimeout: Prog.error
  655. });
  656. },
  657. reqs: Object.setPrototypeOf({}, null)
  658. };
  659.  
  660. /**
  661. * MODULE FOR IMAGE LIST IN THE TOP 200 or 220 (PAHEAL) PIXELS OF THE VIEWPORT
  662. */
  663. Hover = {
  664. lastInList: {},
  665. init() {
  666. const el = $.c("div");
  667. $.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>`);
  668. el.className = "viewpre";
  669. $.add(el);
  670. Hover.index = -1;
  671. Hover.target = $(".listimage", el);
  672. $.on(Hover.target, "click", Hover.click, passive);
  673. $.on(Hover.target, "dragstart", e => e.preventDefault());
  674. Hover.wrap = Hover.target.parentNode;
  675. if (!site.sankakucomplex) {
  676. $.add(Hover.nextEl = $.c("span"), Hover.target);
  677. Hover.nextEl.className = "next";
  678. $.on(Hover.nextEl, "click", Hover.next, passive);
  679. }
  680. {
  681. const arr = $$("a[data-full]"), len = arr.length;
  682. let i = -1;
  683. while (++i < len) Hover.build(arr[i]);
  684. }
  685. Hover.el = el;
  686. Hover.kinetic();
  687. },
  688. noGears() {
  689. Hover.gears.style.opacity = 0;
  690. setTimeout(() => Hover.gears = $.rm(Hover.gears), 300);
  691. },
  692. next(move) {
  693. if (Hover.nextEl.classList.contains("loadingu")) return;
  694. if (!Hover.gears) {
  695. let el, fn = () => el.style.right = "3vw";
  696. Hover.gears = el = $.c("div");
  697. el.className = "nextgears";
  698. $.ins(el, "beforeend", SVG.next);
  699. $.add(el);
  700. requestAnimationFrame(fn);
  701. }
  702. Hover.nextEl.className = "next loadingu";
  703. Hover.lastInList = {
  704. move,
  705. id: Hover.nextEl.previousElementSibling.dataset.id
  706. };
  707. void Hover.nextEl.offsetWidth; // reflow hack
  708. const timer = setTimeout(() => {
  709. Hover.nextEl = $.rm(Hover.nextEl);
  710. Hover.size();
  711. Hover.noGears();
  712. $.keyDown({warn: true});
  713. }, 2E3);
  714. d.dispatchEvent(new CustomEvent(ns + "-next", { detail: timer }));
  715. },
  716. size() {
  717. const { children } = Hover.target;
  718. let length = children.length;
  719. Hover.target.style.width = length * (180 + paheal) + "px";
  720. if (Pos.el) {
  721. if (children[length - 1].className === "next") --length;
  722. Pos.fn(length);
  723. }
  724. },
  725. build(el) {
  726. const span = $.c("span"), img = $.c("img");
  727. img.src = $("img", el).src;
  728. img.alt = img.dataset.nth = ++Hover.index;
  729. img.title = el.firstElementChild.title;
  730. span.dataset.id = el.dataset.id;
  731. if (el.dataset.gif) img.style.outline = "2px solid lime";
  732. if (el.dataset.res) span.dataset.res = el.dataset.res;
  733. $.add(img, span);
  734. if (Hover.nextEl) Hover.target.insertBefore(span, Hover.nextEl);
  735. else $.add(span, Hover.target);
  736. Hover.size();
  737. if (Hover.lastInList.move === true && Hover.lastInList.id === Main.el.dataset.id) {
  738. $.keyDown({keyCode: 39});
  739. Hover.lastInList.move = false;
  740. }
  741. },
  742. click(e) {
  743. e = e.target.src;
  744. if (!e) return;
  745. if (Hover.prevent)
  746. return Hover.prevent = null;
  747. Main.slide(e);
  748. Hover.center(e);
  749. if (Pos.el) Pos.fn($(`img[src*="${$.base(e)}"]`, Hover.el).dataset.nth);
  750. },
  751. center(src) {
  752. if (!(Hover.wrap || Hover.el)) return;
  753. const base = $.base(src),
  754. img = $(`img[src*="${base}"]`, Hover.el),
  755. pos = img.dataset.nth,
  756. scroll = Hover.wrap,
  757. half = scroll.offsetWidth / 2,
  758. width = 180 + paheal,
  759. dist = width * pos + width / 2,
  760. res = dist - half,
  761. curr = $(".current", Hover.el);
  762. if (curr) curr.removeAttribute("class");
  763. Hover.cancel();
  764. Hover.el.children[1].removeAttribute("style");
  765. scroll.scrollLeft = res > 0 ? res : 0;
  766. img.parentNode.setAttribute("class", "current");
  767. },
  768. kinetic() {
  769. const view = Hover.wrap,
  770. rm = () => Hover.el.classList.remove("showimagelist"),
  771. unset = () => Hover.el.classList.add("showimagelist");
  772. let offset, reference, velocity, frame, timestamp, ticker, amplitude, target, pressed = false;
  773. function scroll(x) {
  774. const max = view.scrollLeftMax;
  775. offset = x > max ? max : x < 0 ? 0 : x;
  776. view.scrollLeft = offset;
  777. if (offset === 0 || offset === max) {
  778. amplitude = 0;
  779. Hover.cancel();
  780. rm();
  781. }
  782. }
  783. function track() {
  784. let now = Date.now(),
  785. elapsed = now - timestamp,
  786. delta = offset - frame,
  787. v = 1000 * delta / (1 + elapsed);
  788. timestamp = now;
  789. frame = offset;
  790. velocity = 0.8 * v + 0.2 * velocity;
  791. }
  792. function autoScroll() {
  793. if (amplitude) {
  794. const elapsed = Date.now() - timestamp, delta = -amplitude * Math.exp(-elapsed / 15E2);
  795. if (delta > 5 || delta < -5) {
  796. unset();
  797. scroll(target + delta);
  798. Hover.kinetID = requestAnimationFrame(autoScroll);
  799. }
  800. else {
  801. rm();
  802. scroll(target);
  803. }
  804. }
  805. }
  806. $.on(view, "mousedown", function tap(e) {
  807. Hover.prevent = !(pressed = true);
  808. unset();
  809. clearInterval(ticker);
  810. velocity = amplitude = 0;
  811. if (e.target === view) return pressed = false;
  812. reference = e.clientX;
  813. offset = view.scrollLeft;
  814. frame = offset;
  815. timestamp = Date.now();
  816. ticker = setInterval(track, 100 / 3);
  817. }, passive);
  818. $.on(d.body, "mousemove", function drag(e) {
  819. if (pressed) {
  820. const x = e.clientX, delta = reference - x;
  821. if (delta > 1 || delta < -1) {
  822. Hover.prevent = true;
  823. reference = x;
  824. scroll(offset + delta);
  825. }
  826. }
  827. }, passive);
  828. $.on(d.body, "mouseup", function release() {
  829. pressed = false;
  830. clearInterval(ticker);
  831. if (velocity > 10 || velocity < -10) {
  832. amplitude = 0.8 * velocity;
  833. target = offset + amplitude & -1;
  834. timestamp = Date.now();
  835. Hover.kinetID = requestAnimationFrame(autoScroll);
  836. }
  837. else rm();
  838. }, passive);
  839. },
  840. cancel() {
  841. $.safe(cancelAnimationFrame, null, Hover.kinetID);
  842. }
  843. };
  844.  
  845. /**
  846. * MODULE FOR THE THINGS THAT GET THE BALL ROLLING
  847. */
  848. Main = {
  849. sel: ".thumb:not(a)",
  850. animationEnd(e) {
  851. switch(e.animationName) {
  852. case "Outlined":
  853. e.target.classList.remove("outlined");
  854. break;
  855. case "nomoreimages":
  856. $.keyDown.el = $.rm(e.target);
  857. break;
  858. case "menuelement":
  859. Menu.el = $.rm(e.target);
  860. break;
  861. case "warn":
  862. $.rm(e.target);
  863. break;
  864. case "progfail": case "progdone":
  865. Prog.el = $.rm(e.target);
  866. }
  867. },
  868. finalizeCss() {
  869. const style = $.extend($.c("style"), { type: "text/css" });
  870. $.add(new Text(Main.css), style);
  871. Object.defineProperty(Main, "css", {
  872. get() {
  873. return style.textContent;
  874. },
  875. set(moreCss) {
  876. moreCss = new Text(style.textContent + moreCss);
  877. {
  878. const arr = style.childNodes;
  879. let i = arr.length;
  880. while (~--i) $.rm(arr[i]);
  881. }
  882. style.appendChild(moreCss);
  883. return style.textContent;
  884. }
  885. });
  886. $.add(style, d.documentElement);
  887. },
  888. async initDomainInfo() {
  889. switch(site.name) {
  890. case "gelbooru":
  891. Main.r[0] = /<a href="?([^">]+)"?[^>]*>Orig/;
  892. Main.r[1] = /src="?([^">]+)"? id="?image"?[^>]*\/>/;
  893. 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}";
  894. break;
  895. case "splatoon": case "booru":
  896. Main.r[0] = Main.r[1] = /<img alt="img" src="([^"]+)/i;
  897. Main.css += "span.thumb {\n float: left\n}\n[data-never-hide] {\n display: inline ! important;\n}";
  898. $.r(() => {
  899. const start = $(".thumb").parentNode;
  900. let el, i = 10;
  901. while (i) {
  902. if ((el = start.nextSibling) && el.id !== "paginator") $.rm(el);
  903. else --i;
  904. }
  905. start.dispatchEvent(new CustomEvent("scroll", { bubbles: true }));
  906. });
  907. break;
  908. case "e621":
  909. Main.css += ".thumb > a[data-id] {\n display: inline-block;\n margin-bottom: -3px\n}";
  910. break;
  911. case "tbib":
  912. Main.r[0] = /<a href="?([^"> ]+)"? [^>]*?>\s*Orig/;
  913. Main.r[1] = /<img[^>]*?src="?([^"> ]+)"? [^>]*?id="?image"?[^>]*>/;
  914. Main.css += "div:not([style*='none;padding:10px 0']) {\n background-color: transparent\n}";
  915. break;
  916. case "sankakucomplex":
  917. Main.sel = "#post-list div.content > div:first-of-type > .thumb";
  918. case "hypnohub":
  919. $.r($.eval, site.inject);
  920. case "yande": case "lolibooru": case "konachan":
  921. Main.r[1] = /id="?image"? [^>]*?src="?([^">]+)"?[^>]*\/>/;
  922. 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}";
  923. break;
  924. case "atfbooru":
  925. Main.sel = "div#posts article[id^='post']";
  926. Main.css += "div > article.post-preview {\n overflow: initial\n}\narticle.post-preview > a {\n overflow: hidden\n}";
  927. break;
  928. case "safebooru":
  929. Main.css += "img[title*=' rating:'][src*='.png'] {\n background-color: rgba(255,255,255,.5)\n}";
  930. }
  931. Main.finalizeCss();
  932. },
  933. click(e) {
  934. if (e.button === 0) {
  935. let target = e.target;
  936. while (target && !target.hasAttribute("data-full")) target = target.parentNode;
  937. e.preventDefault();
  938. e.stopPropagation();
  939. if (target) Main.fn(target);
  940. }
  941. },
  942. process(node, full) {
  943. let a, id, alt;
  944. if (!(typeof node === "object" && node.nodeType === 1)) return;
  945. if (node.tagName === "LI" && String(node.id)[0] === "p" && ~String(node.className).indexOf("creator-id-")) return setTimeout(Main.myImuoto, 0, node);
  946. if (site.gelbooru && node.classList.contains("thumbnail-preview")) node = Main.gelbooruFix(node);
  947. if (node.matches(Main.sel) && (a = node.firstElementChild) && !a.dataset.full) {
  948. alt = $("img[alt]", node);
  949. if (!(alt && (alt = alt.title || alt.alt)))
  950. return;
  951. id = (node.id || a.id || a.children[0].id).match(Main.r[4])[0];
  952. switch(site.name) {
  953. case "gelbooru":
  954. a.setAttribute("href", `/index.php?page=post&s=view&id=${id}`);
  955. break;
  956. case "booru":
  957. case "splatoon":
  958. a.setAttribute("data-never-hide", "");
  959. }
  960. if (~alt.indexOf("animated_gif")) {
  961. a.firstElementChild.style.border = "2px solid lime";
  962. a.dataset.gif = "gif";
  963. }
  964. else if (Main.r[3].test(alt) || $$("img[src*='webm-preview.png']", node).length) return a.style.cursor = "alias";
  965. a.dataset.id = id;
  966. a.dataset.full = typeof full === "string" ? full : site.paheal ? node.children[2].href : site.atfbooru ? node.dataset[fullImage ? "largeFileUrl" : "fileUrl"] : $.cache(id);
  967. node.removeAttribute("onclick");
  968. a.removeAttribute("onclick");
  969. if (Hover.el) Hover.build(a);
  970. if (Hover.gears && Hover.gears.style.opacity !== "0")
  971. Hover.noGears();
  972. if (!Main.attachedClickListener) {
  973. Main.attachedClickListener = true;
  974. $.on(node.parentNode, "click", Main.click);
  975. }
  976. }
  977. },
  978. init() {
  979. if (!(site.sankakucomplex || site.atfbooru) && location.pathname === "/")
  980. return $.r(Main.front);
  981. Main.initDomainInfo();
  982. const observer = new MutationObserver(mutations => {
  983. for (let i = 0, len1 = mutations.length; i < len1; ++i)
  984. for (let j = 0, arr = mutations[i].addedNodes, len2 = arr.length; j < len2; ++j)
  985. Main.process(arr[j]);
  986. });
  987. Main.offObj = [{ detail: true }, Main.off];
  988. Main.onObj = [{ detail: false }, Main.on];
  989. observer.observe(d, {
  990. childList: true,
  991. subtree: true
  992. });
  993. $.on("animationend", Main.animationEnd);
  994. $.on(w, "keypress", e => {
  995. if (e.keyCode === 13) {
  996. if (slideshow)
  997. Btn.cb();
  998. else if (e.target.matches(".thumb > a[data-full]")) {
  999. e.preventDefault();
  1000. Main.fn(e.target);
  1001. }
  1002. }
  1003. else if (slideshow && e.charCode === 32) {
  1004. Btn.cb.move();
  1005. }
  1006. });
  1007. $.on(w, "wheel", e => {
  1008. if (e.ctrlKey)
  1009. e.preventDefault();
  1010. if (Main.el)
  1011. $.keyDown({ keyCode: e.deltaY > 0 ? 39 : e.deltaY < 0 ? 37 : 0 });
  1012. });
  1013. $.r(Main.ready);
  1014. $.r(Hover.init);
  1015. },
  1016. ready() {
  1017. if (WIREFRAME) {
  1018. let debug = $.c("div");
  1019. debug.id = "debug";
  1020. $.add(debug);
  1021. $.ins(debug, "beforeend", SVG.debug);
  1022. 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}`;
  1023. }
  1024. for (let i = 0, arr = [["textarea", "copy"], ["a", "realDl"]]; i < 2; ++i) {
  1025. const [tag, prop] = arr[i];
  1026. Menu[prop] = $.c(tag);
  1027. Menu[prop].classList.add("oFfScReEn");
  1028. $.add(Menu[prop]);
  1029. }
  1030. },
  1031. gelbooruFix(el) {
  1032. const img = el.querySelector("img");
  1033. if (!~String(img.getAttribute("src")).indexOf("/")) {
  1034. img.className = "preview";
  1035. let key = Main.gelbooruKey;
  1036. if (!key) {
  1037. for (let i = 0, arr = Object.entries(img.dataset), len = arr.length; i < len; ++i) {
  1038. const [k, v] = arr[i];
  1039. if (Main.r[5].test(v)) {
  1040. Main.gelbooruKey = key = k;
  1041. break;
  1042. }
  1043. }
  1044. }
  1045. img.setAttribute("src", img.getAttribute(`data-${key}`));
  1046. img.removeAttribute(`data-${key}`);
  1047. }
  1048. const span = el.firstElementChild;
  1049. el.parentNode.replaceChild(span, el);
  1050. return span;
  1051. },
  1052. myImuotoOnReady(root) {
  1053. const arr = root.childNodes;
  1054. let i = arr.length;
  1055. while (~--i) {
  1056. const child = arr[i];
  1057. if (child.nodeType !== 1) root.removeChild(child);
  1058. }
  1059. },
  1060. myImuoto(el) {
  1061. const thumb = $.c("span"), id = el.id.substr(1), img = el.children[0].children[0].children[0], full = el.lastElementChild.href;
  1062. if (!Main.myImuoto.readied) {
  1063. $.r(Main.myImuotoOnReady, el.parentNode);
  1064. Main.myImuoto.readied = true;
  1065. }
  1066. thumb.id = el.id;
  1067. thumb.className = `creator-id-${id} thumb`;
  1068. 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>`;
  1069. el.parentNode.replaceChild(thumb, el);
  1070. return Main.process(thumb, fullImage || full.match(Main.r[2])[1].toLowerCase() === "gif" ? full : null);
  1071. },
  1072. fn(node) {
  1073. const [msg, method] = Main.el ? Main.offObj : Main.onObj;
  1074. d.dispatchEvent(new CustomEvent(ns, msg));
  1075. const _ = $.safe(method, null, node);
  1076. if (_ === $.safe.error)
  1077. console.error(_);
  1078. },
  1079. off(a) {
  1080. const reqs = Prog.reqs;
  1081. for (let i = 0, arr = $.keys(reqs), len = arr.length; i < len; ++i) {
  1082. const _req = reqs[arr[i]];
  1083. $.safe(_req.abort, _req);
  1084. }
  1085. Prog.reqs = Object.create(null);
  1086. for (let i = 0, arr = $$("iframe.proxY"), len = arr.length; i < len; ++i)
  1087. $.rm(arr[i]);
  1088. if (slideshow) Btn.cb();
  1089. slideshow = !(a = $.current());
  1090. Main.el = $.rm(Main.el);
  1091. d.body.classList.remove("sliding");
  1092. a.classList.add("outlined");
  1093. $.off("mousemove", $.zoom);
  1094. $.off("keydown", $.keyDown);
  1095. $.off("keyup", $.keyUp);
  1096. if (a) {
  1097. let correction = 0;
  1098. if (site.sankakucomplex || site.e621 || site.atfbooru) {
  1099. a = a.firstElementChild;
  1100. correction = a.offsetParent.offsetTop;
  1101. }
  1102. w.scrollTo(0, a.offsetTop + correction + a.offsetHeight / 2 - w.innerHeight / 2);
  1103. a.focus();
  1104. Btn.fn(); Pos.fn();
  1105. }
  1106. if (site.sankakucomplex) uW.Sankaku.Pagination.auto_enabled = true;
  1107. for (let i = 0, arr = [[Main, "gif"], [Prog], [Menu], [$.keyDown], [Hover, "gears"]]; i < 5; ++i) {
  1108. const [p, el = "el"] = arr[i];
  1109. p[el] = $.rm(p[el]);
  1110. }
  1111. },
  1112. _on: e => e.button === 1 && $.keyDown({ keyCode: 38, event: e }),
  1113. on(a) {
  1114. if (site.sankakucomplex) uW.Sankaku.Pagination.auto_enabled = false;
  1115. d.body.classList.add("sliding");
  1116. {
  1117. const arr = $$("a.outlined[data-full]");
  1118. let i = arr.length;
  1119. while (~--i) arr[i].classList.remove("outlined");
  1120. }
  1121. $.add($.extend(Main.el = $.c("img"),
  1122. { id: "slide", alt: "Loading...", onclick: Main.fn, onmouseup: Main._on }
  1123. ));
  1124. $.add(Main.gif = $.extend($.c("span"), { innerHTML: SVG.gif, className: "gif" }));
  1125. const _ = $.safe(Main.slide, null, $("img", a).src);
  1126. if (_ === $.safe.error) {
  1127. console.error(_);
  1128. return Main.off();
  1129. }
  1130. $.on("keyup", $.keyUp);
  1131. $.on("keydown", $.keyDown);
  1132. $.on("mousemove", $.zoom);
  1133. Pos.fn(); Btn.fn(); $.preload();
  1134. },
  1135. isGif(match) {
  1136. return match && match[1] ? match[1].toLowerCase() === "gif" : null;
  1137. },
  1138. slide(src) {
  1139. if (!slideshow)
  1140. Main.el.src = src;
  1141. if ($.zoom.el)
  1142. $.zoom.el = $.rm($.zoom.el);
  1143. Hover.center(src);
  1144. Main.el.dataset.src = src;
  1145. Main.gif.removeAttribute("style");
  1146. Main.el.removeAttribute("style");
  1147. const curr = $.current(src), data = curr.dataset.full, id = curr.dataset.id;
  1148. Main.el.dataset.id = id;
  1149. /* dirty hack ahead because GIF doesn't want to play as a blob and doesn't
  1150. * give proper progress info for GM_xmlhttpRequest for whatever reason */
  1151. const isGif = Main.isGif(data.match(Main.r[2]));
  1152. if (data === "loading") Main.req(curr);
  1153. else if (curr.dataset.blob) {
  1154. Main.el.src = curr.dataset.blob;
  1155. if (Menu.el) Menu.download();
  1156. if (Prog.el) Prog.el = $.rm(Prog.el);
  1157. const el = Prog.progress();
  1158. el.style.width = "100%";
  1159. el.classList.add("progdone");
  1160. }
  1161. // hack start
  1162. else if (isGif !== false) {
  1163. if (isGif) {
  1164. Main.el.removeAttribute("src");
  1165. Main.el.src = data;
  1166. Main.gif.setAttribute("style", "display: inline-block");
  1167. }
  1168. else Main.el.dispatchEvent(new Event("load"));
  1169. }
  1170. // hack end
  1171. else Prog.fn(data, id);
  1172. },
  1173. 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 */,
  1174. processHttp(x) { if (!$.in(x.status, httpOk)) throw `HTTP status: ${x.status}`; return x.text(); },
  1175. validateHtml(img, el) {
  1176. return img.match(el.dataset.gif || fullImage ? Main.r[0] : Main.r[1])[1];
  1177. },
  1178. checkPreviewId(img) {
  1179. return ~Main.el.dataset.src.indexOf($.base(img));
  1180. },
  1181. processText(img, node, id) {
  1182. if ((img = $.safe(Main.validateHtml, null, img, node)) === $.safe.error)
  1183. throw "API error";
  1184. node.dataset.full = $.cache(id, img);
  1185. const _ = $.safe(Main.checkPreviewId, null, img);
  1186. if (typeof _ === "number" && _)
  1187. $.safe(Main.slide, null, img);
  1188. $("img", node).style.outline = "";
  1189. node.removeAttribute("data-already-loading");
  1190. },
  1191. getApiInfo(node) {
  1192. switch(domain) {
  1193. case "e621.net":
  1194. return [node.parentNode.id.substr(1), "/post/show.xml?id="];
  1195. case "sankakucomplex.com":
  1196. return [node.dataset.id, "/post/show/"];
  1197. case "yande.re":
  1198. case "lolibooru.moe":
  1199. case "konachan.com":
  1200. case "hypnohub.net":
  1201. return [node.parentNode.id.substr(1), "/post/show/"];
  1202. case "booru.org":
  1203. case "tbib.org":
  1204. case "splatoon.ink":
  1205. case "gelbooru.com":
  1206. return [node.dataset.id, "/index.php?page=post&s=view&id="];
  1207. default:
  1208. return [node.dataset.id, "/index.php?page=dapi&s=post&q=index&id="];
  1209. }
  1210. },
  1211. async req(node) {
  1212. if (!node) return;
  1213. const { dataset } = node;
  1214. if (dataset.alreadyLoading || dataset.full !== "loading") return;
  1215. const { 0: id, 1: api } = Main.getApiInfo(node);
  1216. dataset.alreadyLoading = "true";
  1217. try {
  1218. if (site.sankakucomplex) return await Prog.fn(api + id, id, node);
  1219. const request = await fetch(api + id);
  1220. const text = await Main.processHttp(request);
  1221. Main.processText(text, node, id);
  1222. }
  1223. catch (e) {
  1224. Main.warn();
  1225. $("img", node).style.outline = "6px solid red";
  1226. console.error(`Main.req failure:\n\n${location.origin + api + id} |`, e);
  1227. node.removeAttribute("data-already-loading");
  1228. }
  1229. },
  1230. warn() {
  1231. const warn = $.c("span");
  1232. warn.innerHTML = SVG.warn;
  1233. warn.classList.add("warn");
  1234. if ($$(".sliding>.warn").length === 0) $.add(warn);
  1235. if (slideshow)
  1236. Main.el.dispatchEvent(new Event("load"));
  1237. },
  1238. front() {
  1239. let target, method = "beforeend", el;
  1240. switch(domain) {
  1241. case "e621.net":
  1242. target = "#mascot_artist";
  1243. method = "afterend";
  1244. break;
  1245. case "booru.org":
  1246. case "rule34.xxx":
  1247. case "splatoon.ink":
  1248. target = "#static-index > div:last-child > p:first-child";
  1249. break;
  1250. case "lolibooru.moe":
  1251. target = "#links + *";
  1252. break;
  1253. case "safebooru.org":
  1254. target = "div.space + div > p";
  1255. method = "afterend";
  1256. break;
  1257. case "hypnohub.net":
  1258. target = "form > div";
  1259. break;
  1260. case "tbib.org":
  1261. target = "div.space + div > p";
  1262. method = "afterend";
  1263. break;
  1264. default:
  1265. target = "form:last-of-type + div";
  1266. break;
  1267. }
  1268. for (let i = 0, arr = ["tags", "tags-search"]; i < 2; ++i) {
  1269. if (el = $.id(arr[i])) {
  1270. el.focus();
  1271. break;
  1272. }
  1273. }
  1274. $.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>`);
  1275. },
  1276. 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}`
  1277. };
  1278. Main.init();