TweetDeck lightboxes

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

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.

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

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==
// @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);
})();