Sleazy Fork is available in English.

Gelbooru Image Viewer

Adds a fullscreen image view option when you click on images

2016-12-19 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

// ==UserScript==
// @id             gelbooru-slide
// @name           Gelbooru Image Viewer
// @version        1.7
// @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
// ==/UserScript==

(function(){
	"use strict";
	var d = document, w = window, array = a => [].slice.call(a), observer, request, slideEl, slider, slidin, base = a => a.split("/").pop().split(".")[0].split("_").pop(), keyDown, find, current, preload, stor = localStorage, l, ns = "gelbooru-slide", toggle = stor[ns] == "true", cache, notification, pos, posEl, menu, menuEl, button, buttonEl, slideshow;
	
	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();
	});
	
	l = toggle ? ["ul>li>a[style*='font-weight:']", "href"] : ["#image", "src"];
	
	current = src => d.querySelector("img.preview[src*='" + base(src || slideEl.src) + "']").parentNode;
	
	cache = (a, b) => {
		let id = a.href.match(/id=([0-9]+)/)[1], val = toggle ? "original" : "sample";
		if (!b) {
			let ret;
			try { ret = JSON.parse(stor[ns + id])[val] } catch(e) {}
			return ret || "loading";
		} else {
			let obj, temp;
			try { temp = JSON.parse(stor[ns + id]) } catch(e) {}
			obj = temp || {};
			obj[val] = b;
			stor[ns + id] = JSON.stringify(obj);
			return b;
		}
	};
	
	pos = a => {
		if (typeof a === "boolean") {
			let no = posEl.firstElementChild;
			if (a) no.innerHTML = Number(no.innerHTML) + 1;
			else no.innerHTML = Number(no.innerHTML) - 1;
		} else {
			if (slideEl && !posEl) {
				let thumbs = array(d.querySelectorAll`span.thumb a[data-full]`);
				posEl = d.createElement`div`;
				posEl.insertAdjacentHTML("beforeend", "<span>" + (thumbs.indexOf(current()) + 1) + "</span> / " + thumbs.length);
				posEl.setAttribute("style", "position: fixed; bottom: 20px; left: 0; display: block; pointer-events: none;");
				slideEl.insertAdjacentElement("afterend", posEl);
			} else if (posEl) posEl = posEl.remove();
		} return;
	};
	
	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;
	};
	
	menu = e => {
		let _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";
		if (menuEl) {
			menuEl.removeAttribute("class");
			menuEl.style.left = left;
			menuEl.style.top = top;
			setTimeout(() => menuEl.classList.add("menuel"), 10);
		} else {
			let href = current().href;
			menuEl = d.createElement("div");
			menuEl.id = "menuel";
			menuEl.insertAdjacentHTML("beforeend", '<a href="'+href+'" style="margin-bottom: 2px">Open in This Tab</a><a href="'+href+'" target="_blank">Open in New Tab</a>');
			menuEl.style.left = left;
			menuEl.style.top = top;
			d.body.appendChild(menuEl);
			menuEl.classList.add("menuel");
		} return;
	};
	
	preload = function() {
		var curr = current();
		request(find(curr, true));
		return request(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(e.event);
				else w.location = current().href;
				return;
			case 40:
				e.preventDefault();
				return slideEl.click();
		}
		if (typeof move != "undefined") {
			e = find(current(), move);
			if (e) {
				slideEl.slide(e.firstElementChild.src);
				pos(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;
	};
	
	button = () => {
		let clear = () => clearTimeout(Number(buttonEl.dataset.timer) || 0);
		if (buttonEl) {
			clear();
			buttonEl = buttonEl.remove();
		} else {
			let svg_state = true, 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></span>`, 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>`, fn = function() {
	let options = array(buttonEl.querySelectorAll("div input")).map(a => 
		a.type == "number" ? (a.value >= 5 ? a.value : 5) * 1E3 : a.checked
	), _fnS = () => {
		slideEl.removeEventListener("load", _fnS);
		buttonEl.dataset.timer = setTimeout(_fnT, options[2]);
	}, _fnT = () => {
		let el, thumbs;
		if (options[1]) {
			thumbs = array(d.querySelectorAll("span.thumb a[data-full]"));
			thumbs.splice(thumbs.indexOf(current()), 1);
			el = thumbs[Math.random() * thumbs.length >> 0];
		} else el = find(current(), true);
		if (!el && options[0]) el = d.querySelector("span.thumb a[data-full]");
		if (!el) return fn();
		slideEl.addEventListener("load", _fnS);
		slideEl.slide(el.firstElementChild.src);
		preload();
	};
	slideshow = !!svg_state;
	pos();
	buttonEl.firstElementChild.innerHTML = (svg_state = !svg_state) ? svg_play : svg_pause;
	if (slideshow) {
		buttonEl.dataset.timer = setTimeout(_fnT, options[2]);
		buttonEl.style.opacity = ".4";
	} else {
		clear();
		buttonEl.style.opacity = ".7";
		buttonEl.removeAttribute("data-timer");
	}
	return;
};
			buttonEl = d.createElement("div");
			buttonEl.setAttribute("style", 'opacity: .7;');
			buttonEl.className = "slideshow";
			buttonEl.insertAdjacentHTML('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>Shuffle:&nbsp;<input type="checkbox"></label>
<br>Interval: <input type="number" value="5" style="width: 100px">
</div>`);
			buttonEl.firstElementChild.onclick = fn;
			d.body.appendChild(buttonEl);
		}
	};
	
	slider = function(a) {
		d.dispatchEvent(new CustomEvent(ns, {bubbles:true}));
		if (slidin) {
			let center;
			slideshow = slidin = !(a = current());
			slideEl = slideEl.remove();
			button(); pos();
			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);
		} else {
			let fn = function(src) {
				var data, load = () => {
					this.removeAttribute("src");
					this.setAttribute("src", data);
					this.removeEventListener("load", load);
				};
				if (!slideshow) this.src = src;
				data = current(src).dataset.full;
				if (data == "loading") request(current(src));
				else {
					if (/\.gif(?:\?\d+)?$/.test(data))
						this.addEventListener("load", load);
					this.src = data;
				}
			};
			slidin = true;
			d.body.classList.add`sliding`;
			array(d.querySelectorAll`span>a.outlined`).map(a => a.classList.remove`outlined`);
			slideEl = d.createElement`img`;
			slideEl.id = "slide";
			slideEl.alt = "Loading...";
			Object.defineProperty(slideEl, "slide", { value: fn });
			slideEl.onclick = slider;
			slideEl.onmouseup = e => e.button === 1 && keyDown({keyCode:38, event:e});
			d.body.appendChild(slideEl);
			slideEl.slide(a.firstElementChild.src);
			d.addEventListener("keydown", keyDown);
			pos(); button(); preload();
		} return;
	};
	
	request = function(node) {
		if (!node || node.dataset.alreadyLoading || node.dataset.full != "loading") return;
		node.dataset.alreadyLoading = "true";
		return fetch(node.href)
			.then(x => x.text().then(text => {
				let doc = (new DOMParser()).parseFromString(text, "text/html"), img, _base;
				if (img = doc.querySelector(l[0])) {
					_base = base(img[l[1]]);
					node.dataset.full = cache(node, img[l[1]]);
					if (slideEl) {
						if (slideEl.src.indexOf(_base) > -1) slideEl.slide(img[l[1]]);
						else if (slideshow) slideEl.src = img[l[1]];
					}
					return;
				} else {
					node.removeAttribute`data-already-loading`;
					fetch`/intermission.php`.then(() => request(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`;
			});
	};
	
	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 (node.querySelector`img[alt*='webm']`) return;
					a.dataset.full = cache(a);
					a.onclick = e => e.button === 0 && (e.preventDefault(), e.stopPropagation(), slider(e.target.parentNode));
				}
			} catch(e) {} return;
		}
		return mutations.forEach(mutation => array(mutation.addedNodes).forEach(process)); 
	});
	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") menuEl = e.target.remove();
	});
	
	w.addEventListener("keypress", e => {
		if (e.key === "Enter" || e.keyCode === 13) {
			if (e.target.matches`span.thumb>a[data-full]`) {
				e.preventDefault();
				slider(e.target);
			}
			if (slideshow) {
				buttonEl.firstElementChild.click();
			}
		}
		
	});
	
	w.addEventListener("wheel", e => slidin && keyDown({keyCode: e.deltaY > 0 ? 39 : e.deltaY < 0 ? 37 : 0}));
	
	if (stor[ns + "-firstrun"] != "1.5.3") {
		(function(l){
			var a;
			for (a in l)
				if (/^gelbooru-slide./.test(a)) l.removeItem(a);
		}(stor));
		stor[ns + "-firstrun"] = "1.5.3";
	}
	
	{
		let css = d.createElement`style`;
		css.textContent = `
@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
}`;
		return d.head.appendChild(css);
	}
}())