// ==UserScript==
// @id gelbooru-slide
// @name Gelbooru Image Viewer
// @version 1.8
// @namespace intermission
// @author intermission
// @license WTFPL; http://www.wtfpl.net/about/
// @description Adds a fullscreen image view option when you click on images
// @include http://gelbooru.com/index.php?*
// @include https://gelbooru.com/index.php?*
// @run-at document-start
// @grant GM_registerMenuCommand
// @grant GM_xmlhttpRequest
// ==/UserScript==
(function(d, w, stor){
"use strict";
var ns = "gelbooru-slide", toggle = stor[ns] == "true", notification, Pos, Menu, Btn, slideshow, $, Main;
if (!stor[ns]) stor[ns] = "false";
GM_registerMenuCommand("Current image mode: " + (toggle ? "Always original size" : "Sample only"), () => {
stor[ns] = toggle ? "false" : "true";
return location.reload();
});
if (stor[ns + "-firstrun"] != "1.5.3") {
(function(l){
var a, r = /^gelbooru-slide./;
for (a in l)
if (r.test(a)) l.removeItem(a);
}(stor));
stor[ns + "-firstrun"] = "1.5.3";
}
$ = function(a, b) {
return [...(b || d).querySelectorAll(a)];
};
$.extend = function(obj, props) {
var key, val;
for (key in props) {
if (!props.hasOwnProperty(key)) continue;
val = props[key];
obj[key] = val;
}
};
$.extend($, {
cache: function(a, b) {
var id = a.match(/id=([0-9]+)/)[1], val = toggle ? "original" : "sample", ret, obj, temp;
if (!b) {
try { ret = JSON.parse(stor[ns + id])[val]; } catch(e) {}
ret = ret || "loading";
} else {
try { temp = JSON.parse(stor[ns + id]); } catch(e) {}
obj = temp || {};
obj[val] = b;
stor[ns + id] = JSON.stringify(obj);
ret = b;
}
return ret;
},
base: a => a.split("/").pop().split(".")[0].split("_").pop(),
current: src => $("img.preview[src*='" + $.base(src || Main.el.src) + "']")[0].parentNode,
find: function(el, method) {
var a;
el = el.parentNode;
do {
try {
el = el[(method ? "next" : "previous") + "ElementSibling"];
a = el.querySelector("a[data-full]");
} catch(err) {
return false;
}
if (a) break;
a = false;
} while(!a);
return a;
},
preload: function() {
var curr = $.current();
Main.req($.find(curr, true));
return Main.req($.find(curr, false));
},
keyDown: function(e) {
var move;
if (slideshow) return;
switch(e.keyCode) {
case 32: case 39:
move = true;
break;
case 37:
move = false;
break;
case 38:
if (e.event) Menu.fn(e.event);
else w.location = $.current().href;
return;
case 40:
e.preventDefault();
return Main.el.click();
}
if (typeof move != "undefined") {
e = $.find($.current(), move);
if (e) {
Main.el.slide(e.firstElementChild.src);
Pos.fn(move);
$.preload();
} else if (!notification) {
notification = d.createElement("div");
notification.classList.add("nomoreimages");
notification.setAttribute("style", "pointer-events: none; background: linear-gradient(to " + (move ? "right" : "left") + ", transparent, rgba(255,0,0,.5));" + (move ? "right" : "left") + ": 0;");
d.body.insertBefore(notification, d.body.lastElementChild);
}
} return;
}
});
Pos = {
fn: function(a) {
var no, thumbs;
if (typeof a === "boolean") {
no = Pos.el.firstElementChild;
if (a) no.innerHTML = Number(no.innerHTML) + 1;
else no.innerHTML = Number(no.innerHTML) - 1;
} else {
if (Main.el && !Pos.el) {
thumbs = $("span.thumb a[data-full]");
Pos.el = d.createElement("div");
Pos.el.insertAdjacentHTML("beforeend", "<span>" + (thumbs.indexOf($.current()) + 1) + "</span> / " + thumbs.length);
Pos.el.setAttribute("style", "position: fixed; bottom: 20px; left: 0; display: block; pointer-events: none;");
Main.el.insertAdjacentElement("afterend", Pos.el);
} else if (Pos.el) Pos.el = Pos.el.remove();
} return;
}
};
Menu = {
fn: function(e) {
var _l = e.clientX + 1, _t = e.clientY + 1,
left = (_l > w.innerWidth - 139 ? (_l - 139) : _l) + "px",
top = (_t > w.innerHeight - 48 ? (_t - 48) : _t) + "px",
href, el = Menu.el;
if (el) {
el.removeAttribute("class");
el.style.left = left;
el.style.top = top;
setTimeout(() => el.classList.add("menuel"), 10);
} else {
href = $.current().href;
Menu.el = el = d.createElement("div");
el.id = "menuel";
el.insertAdjacentHTML("beforeend", '<a href="'+href+'" style="margin-bottom: 2px">Open in This Tab</a><a href="'+href+'" target="_blank">Open in New Tab</a>');
el.style.left = left;
el.style.top = top;
d.body.appendChild(el);
el.classList.add("menuel");
}
return;
}
};
Btn = {
fn: function() {
var sel, el = Btn.el;
sel = "this.previousElementSibling.firstElementChild";
if (el) {
Btn.clear();
Btn.el = Btn.el.remove();
} else {
Btn.el = el = d.createElement("div");
el.setAttribute("style", 'opacity: .7;');
el.className = "slideshow";
el.insertAdjacentHTML('beforeend', '<span title="Slideshow">' + Btn.svg_play + "</span>" + `<div style="display: none;padding: 10px 0">Options<hr><label>Loop: <input type="checkbox" checked></label> <label onclick="${sel}.checked=true;${sel}.disabled=!${sel}.disabled">Shuffle: <input type="checkbox"></label><br>Interval: <input type="number" value="5" style="width: 100px"></div>`);
Btn.svg_state = true;
el.firstElementChild.onclick = Btn.cb;
d.body.appendChild(el);
}
},
clear: () => clearTimeout(Number(Btn.el.dataset.timer) || 0),
cb: function() {
var el = Btn.el, sel = "span.thumb a[data-full]",
options = $("div input", el).map(a =>
a.type == "number" ? (+a.value >= 1 ? +a.value : 1) * 1E3 : a.checked
), _fnS = () => {
Main.el.removeEventListener("load", _fnS);
el.dataset.timer = setTimeout(_fnT, options[2]);
}, _fnT = () => {
var _el;
if (thumbs.length === 0) thumbs = $(sel);
if (options[1]) {
thumbs.splice(thumbs.indexOf($.current()), 1);
_el = thumbs[Math.random() * thumbs.length >> 0];
} else _el = $.find($.current(), true);
if (!_el && options[0]) _el = $(sel)[0];
if (!_el) return Btn.cb();
Main.el.addEventListener("load", _fnS);
Main.el.slide(_el.firstElementChild.src);
}, thumbs = [];
slideshow = !!Btn.svg_state;
Pos.fn();
el.firstElementChild.innerHTML = (Btn.svg_state = !Btn.svg_state) ? Btn.svg_play : Btn.svg_pause;
if (slideshow) {
el.dataset.timer = setTimeout(_fnT, options[2]);
el.style.opacity = ".4";
} else {
Btn.clear();
el.style.opacity = ".7";
el.removeAttribute("data-timer");
}
return;
},
svg_play: '<svg width="50" height="50" version="1.1" xmlns="http://www.w3.org/2000/svg"><rect rx="5" height="48" width="48" y="1" x="1" fill="#fff" /><polygon fill="#000" points="16 12 16 38 36 25" /></svg>',
svg_pause: '<span><svg width="50" height="50" xmlns="http://www.w3.org/2000/svg"><rect fill="#fff" x="1" y="1" width="48" height="48" rx="5" /><rect fill="#000" x="12" y="12" width="10" height="26" /><rect fill="#000" x="28" y="12" width="10" height="26" /></svg>'
};
Main = {
init: function() {
var style = d.createElement("style"), observer = new MutationObserver(function(mutations) {
function process(node) {
var a;
try {
if (node.matches("span.thumb[id^='s']") && (a = node.firstElementChild) && !a.dataset.full) {
if ($("img[alt*='webm']", node)[0]) return;
a.dataset.full = $.cache(a.href);
a.onclick = e => e.button === 0 && (e.preventDefault(), e.stopPropagation(), Main.fn(e.target.parentNode));
}
} catch(e) {} return;
}
return mutations.forEach(mutation => [...mutation.addedNodes].forEach(process));
});
style.appendChild(d.createTextNode(Main.css));
d.head.appendChild(style);
observer.observe(d, {
childList: true,
subtree: true
});
d.addEventListener("animationend", e => {
if (e.animationName == "Outlined") e.target.classList.remove("outlined");
else if (e.animationName == "nomoreimages") notification = e.target.remove();
else if (e.animationName == "menuelement") Menu.el = e.target.remove();
});
w.addEventListener("keypress", e => {
if (e.key === "Enter" || e.keyCode === 13) {
if (slideshow) {
Btn.el.firstElementChild.click();
} else if (e.target.matches("span.thumb>a[data-full]")) {
e.preventDefault();
Main.fn(e.target);
}
}
});
w.addEventListener("wheel", e => Main.el && $.keyDown({keyCode: e.deltaY > 0 ? 39 : e.deltaY < 0 ? 37 : 0}));
},
fn: function(a) {
d.dispatchEvent(new CustomEvent(ns, {bubbles:true}));
if (Main.el) {
let center;
slideshow = !(a = $.current());
Main.el = Main.el.remove();
d.body.classList.remove("sliding");
a.classList.add("outlined");
d.removeEventListener("keydown", $.keyDown);
center = a.offsetTop + a.offsetHeight / 2 - w.innerHeight / 2;
w.scrollTo(0, center < 0 ? 0 : center);
Pos.fn(); Btn.fn();
} else {
let el, fn = function(src) {
var data, load = () => {
el.removeAttribute("src");
el.setAttribute("src", data);
el.onload = null;
};
el.onload = null;
if (!slideshow) el.src = src;
data = $.current(src).dataset.full;
if (data == "loading") Main.req($.current(src));
else {
if (/\.gif(?:\?\d+)?$/.test(data)) el.onload = load;
el.src = data;
}
el.removeAttribute("style"); el.hidden = false;
};
d.body.classList.add("sliding");
$("span>a.outlined").map(a => a.classList.remove("outlined"));
Main.el = el = d.createElement("img");
el.id = "slide";
el.alt = "Loading...";
Object.defineProperty(el, "slide", { value: fn });
el.onclick = Main.fn;
el.onmouseup = e => e.button === 1 && $.keyDown({keyCode:38, event:e});
d.body.appendChild(el);
el.slide(a.firstElementChild.src);
d.addEventListener("keydown", $.keyDown);
Pos.fn(); Btn.fn(); $.preload();
} return;
},
req: function(node) {
if (!node || node.dataset.alreadyLoading || node.dataset.full != "loading") return;
node.dataset.alreadyLoading = "true";
return fetch("/index.php?page=dapi&s=post&q=index&id=" + node.id.substr(1))
.then(x => x.text())
.then(text => {
let img = $("post", (new DOMParser()).parseFromString(text, "text/xml"))[0].getAttribute(toggle ? "file_url" : "sample_url").replace(/com\/images\//i, "com//images/");
if (img) {
node.dataset.full = $.cache(node.href, img);
if (Main.el) {
if (slideshow) Main.el.src = img;
else if (Main.el.src.indexOf($.base(img)) > -1) Main.el.slide(img);
}
return node.removeAttribute("data-already-loading");
} else {
node.removeAttribute("data-already-loading");
fetch("/intermission.php").then(() => Main.req(node));
throw undefined;
}
}).catch(err => {
if (typeof err == "undefined") return;
console.error("Failed HTTP request\nDo you have an internet connection?\n", err);
return node.removeAttribute("data-already-loading");
});
},
css: `
@keyframes Outlined {
0% { outline: 6px solid orange }
60% { outline: 6px solid orange }
100% { outline: 6px solid transparent }
}
@keyframes nomoreimages {
0% { opacity: 0 }
20% { opacity: 1 }
100% { opacity: 0 }
}
@keyframes menuelement {
0% { opacity: 1 }
80% { opacity: 1 }
100% { opacity: 0 }
}
body.sliding > *:not(#slide) {
display: none
}
#slide {
width: 100vw;
height: 100vh;
object-fit: contain
}
.outlined {
outline: 6px solid transparent;
animation-duration: 4s;
animation-name: Outlined
}
.nomoreimages {
display: block ! important;
width: 33vw;
height: 100vh;
top: 0;
position: fixed;
animation-duration: 1s;
animation-name: nomoreimages
}
span.thumb {
max-width: 180px;
max-height: 180px;
}
#menuel {
opacity: 1;
position: fixed;
display: block ! important;
padding: 2px;
background: black;
width: 139px;
height: 44px;
animation-duration: 1s
}
.menuel {
animation-name: menuelement
}
#menuel:hover {
animation-name: keepalive
}
#menuel a {
background: #fff;
display: block
}
.slideshow {
display: block ! important;
position: fixed;
bottom: 20px;
right: 20px
}
.slideshow:hover:not([data-timer]) > div {
background: white;
color: black;
position: fixed;
display: block ! important;
bottom: 70px;
right: 20px
}`
};
Main.init();
}(document, window, localStorage));