TweetDeck lightboxes

Shows an inline lightbox for images instead of opening a new tab/etc.

// ==UserScript==
// @name         TweetDeck lightboxes
// @namespace    https://yal.cc/
// @version      1.0
// @description  Shows an inline lightbox for images instead of opening a new tab/etc.
// @author       YellowAfterlife
// @match        https://tweetdeck.twitter.com/*
// @grant        none
// ==/UserScript==
/* jshint eqnull:true */
/* jshint esversion:6 */
(function() {
    'use strict';
	let css = document.createElement("style");
	css.type = "text/css";	css.innerHTML = `
	.prf-header .imgxis-badge {
		position: absolute;
		left: 4px;
		top: 4px;
		width: 16px;
		height: 16px;
		border-radius: 50%;
		background: rgba(255, 255, 255, 0.7);
		box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
	}
	.imgxis-panner {
		background: rgba(41,47,51,0.9);
		position: absolute;
		left: 0; width: 100%;
		top: 0; height: 100%;
		z-index: 350;
	}
	.imgxis-panner, .imgxis-panner img {
		cursor: move;
	}
	.imgxis-panner.zoomed, .imgxis-panner.zoomed img {
		-ms-interpolation-mode: nearest-neighbor;
		image-rendering: optimizeSpeed;
		image-rendering: -moz-crisp-edges;
		image-rendering: -webkit-optimize-contrast;
		image-rendering: -o-crisp-edges;
		image-rendering: pixelated;
	}
	.imgxis-panner img, .imgxis-panner video {
		position: absolute;
		transform-origin: top left !important;
		margin: 0;
		background: none !important;
	}
	.imgxis-panner::after {
		content: attr(zoom);
		color: white;
		display: inline-block;
		padding: 1px 2px;
		background: rgba(0, 0, 0, 0.4);
		position: absolute; top: 0; left: 0;
	}
	.imgxis-panner.odd-zoom::after {
		color: #ffe040;
	}
	.imgxis-panner iframe {
		position: absolute;
		top: 0; bottom: 0;
		left: 50px;
		width: calc(100% - 100px);
		height: 100%;
		height: 100vh;
		border: 0;
	}
	`;
	document.body.appendChild(css);
	//
	let img0 = document.createElement("img"); // original
	let img0failed = false;
	img0.onerror = (_) => { img0failed = true };
	//
	let img1 = document.createElement("img"); // full-sized
	let img1failed = false;
	img1.onerror = (_) => { img1failed = true };
	//
	let video = document.createElement("video");
	video.loop = true;
	video.controls = true;
	video.autoplay = true;
	let videoLoaded = false;
	video.oncanplay = (_) => { videoLoaded = true };
	let isVideo = false;
	//
	let iframe = document.createElement("iframe");
	iframe.setAttribute
	//
	let panner = document.createElement("div");
	panner.className = "imgxis-panner";
	panner.appendChild(img0);
	panner.appendChild(img1);
	panner.appendChild(video);
	panner.appendChild(iframe);
	let panX = 0, panY = 0, panZ = 0, panM = 1;
	let panWidth = 0, panHeight = 0;
	let panIdle = false; // whether nothing happened to it yet
	let zoomed = false;
	//
	function panUpdate() {
		let pz = (panM >= 1);
		if (pz != zoomed) {
			zoomed = pz;
			let cl = panner.classList;
			if (pz) cl.add("zoomed"); else cl.remove("zoomed");
		}
		panner.setAttribute("zoom", `${panM*100|0}%`);
		let tf = `matrix(${panM},0,0,${panM},${-panX|0},${-panY|0})`;
		img0.style.transform = tf;
		img1.style.transform = tf;
		video.style.transform = tf;
	}
	//
	function panWheel(e) {
		panIdle = false;
		panner.classList.remove("odd-zoom");
		let d = e.deltaY;
		d = (d < 0 ? 1 : d > 0 ? -1 : 0) * 0.5;
		let zx = e.pageX, zy = e.pageY;
		let prev = panM;
		if (Math.abs(panZ - Math.round(panZ * 2) / 2) > 0.001) {
			panZ = d > 0 ? Math.ceil(panZ * 2) / 2 : Math.floor(panZ * 2) / 2;
		} else panZ = Math.round((panZ + d) * 2) / 2;
		panM = Math.pow(2, panZ);
		let f = panM / prev;
		panX = (zx + panX) * f - zx;
		panY = (zy + panY) * f - zy;
		panUpdate();
	}
	var mouseX = 0, mouseY = 0, mouseDown = false;
	function panMove(e) {
		let lastX = mouseX; mouseX = e.pageX;
		let lastY = mouseY; mouseY = e.pageY;
		if (mouseDown) {
			panX -= (mouseX - lastX);
			panY -= (mouseY - lastY);
			panUpdate();
		}
	}
	function panPress(e) {
		panIdle = false;
		panMove(e);
		if (e.target == panner) {
			e.preventDefault();
			setTimeout(() => panHide(), 1);
		} else if (e.which != 3) {
			e.preventDefault();
			mouseDown = true;
		}
	}
	function panRelease(e) {
		panMove(e);
		mouseDown = false;
	}
	function panKeyDown(e) {
		if (e.keyCode == 27/* ESC */) {
			e.preventDefault();
			e.stopPropagation();
			panHide();
			return false;
		}
	}
	panner.addEventListener("mousemove", panMove);
	panner.addEventListener("mousedown", panPress);
	panner.addEventListener("mouseup", panRelease);
	panner.addEventListener("wheel", panWheel);
	//
	function panFit(lw, lh) {
		let iw = window.innerWidth, ih = window.innerHeight;
		panZ = 0;
		if (lw < iw && lh < ih) {
			// zoom in (up to 800%)
			for (let k = 0; k < 3; k++) {
				if (lw * 2 < iw && lh * 2 < ih) {
					panZ += 1; lw *= 2; lh *= 2;
				}
			}
		} else {
			while (lw > iw || lh > ih) { // zoom out until fits
				panZ -= 1; lw /= 2; lh /= 2;
			}
		}
		panM = Math.pow(2, panZ);
		panX = -(iw - lw) / 2;
		panY = -(ih - lh) / 2;
		console.log(iw, ih, lw, lh, panX, panY, panM);
	}
	//
	let panCheckInt2 = null;
	function panCheck2() {
		if (isVideo) {
			let lw = video.offsetWidth, lh = video.offsetHeight;
			if (!videoLoaded) return;
			clearInterval(panCheckInt2); panCheckInt2 = null;
			panFit(lw, lh);
			console.log(lw, lh, panX, panY);
		} else {
			let lw = img1.width, lh = img1.height;
			if (lw <= 0 || lh <= 0) return;
			clearInterval(panCheckInt2); panCheckInt2 = null;
			//
			if (img1failed) return;
			img1.style.visibility = "";
			if (/*panIdle*/true) { // it makes sense to rescale to original if idle, but looks odd
				panZ -= Math.log2(Math.max(lw / img0.width, lh / img0.height));
				panM = Math.pow(2, panZ);
				if (Math.abs((panZ * 2) % 1) > 0.001) {
					panner.classList.add("odd-zoom");
				}
			} else panFit(lw, lh);
			img0.width = lw;
			img0.height = lh;
		}
		panUpdate();
	}
	//
	let panCheckInt = null;
	function panCheck() {
		let lw = img0.width, lh = img0.height;
		if (lw <= 0 || lh <= 0) return;
		if (img0failed) return;
		//console.log(lw, lh, img0failed);
		clearInterval(panCheckInt); panCheckInt = null;
		panFit(lw, lh);
		img0.style.visibility = "";
		panUpdate();
		if (img1.src) {
			panCheckInt2 = setInterval(panCheck2, 25);
		}
	}
	//
	var panTickInt = null;
	function panTick() {
		let lastWidth = panWidth; panWidth = window.innerWidth;
		let lastHeight = panHeight; panHeight = window.innerHeight;
		if (panWidth != lastWidth || panHeight != lastHeight) {
			panX -= (panWidth - lastWidth) / 2;
			panY -= (panHeight - lastHeight) / 2;
			panUpdate();
		}
	}
	function panShow(url, orig, mode) {
		isVideo = mode == 1;
		img1.style.display = img0.style.display = (mode == 0 ? "" : "none");
		video.style.display = mode == 1 ? "" : "none";
		iframe.style.display = mode == 2 ? "" : "none";
		if (mode == 2) {
			iframe.src = url;
		} else if (mode == 1) {
			video.src = url;
			videoLoaded = false;
		} else {
			img0.removeAttribute("width");
			img0.removeAttribute("height");
			img1.src = url; img0failed = false;
			img0.src = orig; img1failed = false;
			img1.style.visibility = "hidden";
			img0.style.visibility = "hidden";
		}
		document.querySelector(".application").appendChild(panner);
		document.addEventListener("keydown", panKeyDown);
		panWidth = window.innerWidth;
		panHeight = window.innerHeight;
		if (mode == 2) return;
		panTickInt = setInterval(panTick, 100);
		if (isVideo) {
			panCheckInt2 = setInterval(panCheck2, 25);
		} else {
			panCheckInt = setInterval(panCheck, 25);
		}
	}
	function panHide() {
		video.src = "";
		img0.src = "";
		img1.src = "";
		iframe.src = "";
		panner.parentElement.removeChild(panner);
		document.removeEventListener("keydown", panKeyDown);
		clearInterval(panTickInt); panTickInt = null;
		if (panCheckInt != null) { clearInterval(panCheckInt); panCheckInt = null; }
		if (panCheckInt2 != null) { clearInterval(panCheckInt2); panCheckInt2 = null; }
	}
	//
	function panGetShow(url, orig, mode) {
		if (mode == null) mode = 0;
		return (e) => {
			e.preventDefault();
			e.stopPropagation();
			panShow(url, orig, mode);
		};
	}
	//
	function getBackgroundUrl(el) {
		let url = el.style.backgroundImage;
		if (url == null) return url;
		return url.slice(4, -1).replace(/"/g, "");
	}
	setInterval(() => {
		// pictures:
		for (let query of [
			`.js-media-preview-container:not(.is-video):not(.is-gif) .js-media-image-link:not(.imgxis-link)`,
			`.media-image-container .js-media-image-link:not(.imgxis-link)`,
		]) for (let el of document.querySelectorAll(query)) {
			el.classList.add("imgxis-link");
			let url, orig;
			if (/(?:.jpg|.png|.jpeg|.gif)$/g.test(el.href)) {
				url = el.href;
				orig = el.getAttribute("data-original-url") || (url + ":small");
			} else {
				let img = el.querySelector("img");
				if (img == null) {
					orig = getBackgroundUrl(el);
					if (orig == null) continue;
				} else {
					orig = img.src;
				}
				url = orig.replace(/(?:\:small|\:large|\?format=.+)$/g, ":orig");
			}
			el.addEventListener("click", panGetShow(url, orig));
		}
		// profile backgrounds:
		for (let el of document.querySelectorAll(`.prf-header:not(.imgxis-link`)) {
			el.classList.add("imgxis-link");
			let orig = getBackgroundUrl(el);
			if (orig == null) continue;
			let url = orig.replace(/\/web$/g, "/1500x500");
			let a = document.createElement("a");
			a.className = "imgxis-badge";
			a.href = "#";
			a.title = "View profile background";
			a.addEventListener("click", panGetShow(url, orig));
			el.appendChild(a);
		}
		// avatars:
		for (let el of document.querySelectorAll(`.prf-img img.avatar:not(.imgxis-link)`)) {
			el.classList.add("imgxis-link");
			let orig = el.src;
			let url = orig.replace(/(?:_bigger|_normal)\./g, ".");
			el.addEventListener("click", panGetShow(url, orig));
		}
		// gifs:
		for (let el of document.querySelectorAll(`.media-item-gif:not(.imgxis-link)`)) {
			el.classList.add("imgxis-link");
			let url = el.src;
			el.parentElement.addEventListener("click", panGetShow(url, url, 1));
		}
		// videos:
		for (let el of document.querySelectorAll(`.media-preview-container.is-video:not(.imgxis-link)`)) {
			el.classList.add("imgxis-link");
			let link = el.querySelector("a");
			let par = el.parentElement;
			let url = link && link.href;
			if (url) url = url.replace("https://www.", "https://");
			if (!url) {
				//
			} else if (url.startsWith("https://t.co") || url.startsWith("https://twitter.com")) {
				url = null;
			} else if (url.startsWith("https://youtube.com/")) {
				var mt = /v=([\w-]+)/.exec(url);
				if (mt) url = `https://www.youtube.com/embed/${mt[1]}?autoplay=1`;
			}
			if (!url) while (par) {
				if (par.tagName == "ARTICLE" || par.classList.contains("quoted-tweet")) {
					url = par.getAttribute("data-tweet-id");
					if (url) url = `https://twitter.com/i/videos/tweet/${url}?auto_buffer=1&autoplay=1`;
					break;
				} else par = par.parentElement;
			}
			if (url) el.parentElement.addEventListener("click", panGetShow(url, url, 2));
		}
	}, 250);
})();