TweetDeck lightboxes

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

目前为 2019-04-12 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name TweetDeck lightboxes
  3. // @namespace https://yal.cc/
  4. // @version 1.0
  5. // @description Shows an inline lightbox for images instead of opening a new tab/etc.
  6. // @author YellowAfterlife
  7. // @match https://tweetdeck.twitter.com/*
  8. // @grant none
  9. // ==/UserScript==
  10. /* jshint eqnull:true */
  11. /* jshint esversion:6 */
  12. (function() {
  13. 'use strict';
  14. let css = document.createElement("style");
  15. css.type = "text/css"; css.innerHTML = `
  16. .prf-header .imgxis-badge {
  17. position: absolute;
  18. left: 4px;
  19. top: 4px;
  20. width: 16px;
  21. height: 16px;
  22. border-radius: 50%;
  23. background: rgba(255, 255, 255, 0.7);
  24. box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
  25. }
  26. .imgxis-panner {
  27. background: rgba(41,47,51,0.9);
  28. position: absolute;
  29. left: 0; width: 100%;
  30. top: 0; height: 100%;
  31. z-index: 350;
  32. }
  33. .imgxis-panner, .imgxis-panner img {
  34. cursor: move;
  35. }
  36. .imgxis-panner.zoomed, .imgxis-panner.zoomed img {
  37. -ms-interpolation-mode: nearest-neighbor;
  38. image-rendering: optimizeSpeed;
  39. image-rendering: -moz-crisp-edges;
  40. image-rendering: -webkit-optimize-contrast;
  41. image-rendering: -o-crisp-edges;
  42. image-rendering: pixelated;
  43. }
  44. .imgxis-panner img, .imgxis-panner video {
  45. position: absolute;
  46. transform-origin: top left !important;
  47. margin: 0;
  48. background: none !important;
  49. }
  50. .imgxis-panner::after {
  51. content: attr(zoom);
  52. color: white;
  53. display: inline-block;
  54. padding: 1px 2px;
  55. background: rgba(0, 0, 0, 0.4);
  56. position: absolute; top: 0; left: 0;
  57. }
  58. .imgxis-panner.odd-zoom::after {
  59. color: #ffe040;
  60. }
  61. .imgxis-panner iframe {
  62. position: absolute;
  63. top: 0; bottom: 0;
  64. left: 50px;
  65. width: calc(100% - 100px);
  66. height: 100%;
  67. height: 100vh;
  68. border: 0;
  69. }
  70. `;
  71. document.body.appendChild(css);
  72. //
  73. let img0 = document.createElement("img"); // original
  74. let img0failed = false;
  75. img0.onerror = (_) => { img0failed = true };
  76. //
  77. let img1 = document.createElement("img"); // full-sized
  78. let img1failed = false;
  79. img1.onerror = (_) => { img1failed = true };
  80. //
  81. let video = document.createElement("video");
  82. video.loop = true;
  83. video.controls = true;
  84. video.autoplay = true;
  85. let videoLoaded = false;
  86. video.oncanplay = (_) => { videoLoaded = true };
  87. let isVideo = false;
  88. //
  89. let iframe = document.createElement("iframe");
  90. iframe.setAttribute
  91. //
  92. let panner = document.createElement("div");
  93. panner.className = "imgxis-panner";
  94. panner.appendChild(img0);
  95. panner.appendChild(img1);
  96. panner.appendChild(video);
  97. panner.appendChild(iframe);
  98. let panX = 0, panY = 0, panZ = 0, panM = 1;
  99. let panWidth = 0, panHeight = 0;
  100. let panIdle = false; // whether nothing happened to it yet
  101. let zoomed = false;
  102. //
  103. function panUpdate() {
  104. let pz = (panM >= 1);
  105. if (pz != zoomed) {
  106. zoomed = pz;
  107. let cl = panner.classList;
  108. if (pz) cl.add("zoomed"); else cl.remove("zoomed");
  109. }
  110. panner.setAttribute("zoom", `${panM*100|0}%`);
  111. let tf = `matrix(${panM},0,0,${panM},${-panX|0},${-panY|0})`;
  112. img0.style.transform = tf;
  113. img1.style.transform = tf;
  114. video.style.transform = tf;
  115. }
  116. //
  117. function panWheel(e) {
  118. panIdle = false;
  119. panner.classList.remove("odd-zoom");
  120. let d = e.deltaY;
  121. d = (d < 0 ? 1 : d > 0 ? -1 : 0) * 0.5;
  122. let zx = e.pageX, zy = e.pageY;
  123. let prev = panM;
  124. if (Math.abs(panZ - Math.round(panZ * 2) / 2) > 0.001) {
  125. panZ = d > 0 ? Math.ceil(panZ * 2) / 2 : Math.floor(panZ * 2) / 2;
  126. } else panZ = Math.round((panZ + d) * 2) / 2;
  127. panM = Math.pow(2, panZ);
  128. let f = panM / prev;
  129. panX = (zx + panX) * f - zx;
  130. panY = (zy + panY) * f - zy;
  131. panUpdate();
  132. }
  133. var mouseX = 0, mouseY = 0, mouseDown = false;
  134. function panMove(e) {
  135. let lastX = mouseX; mouseX = e.pageX;
  136. let lastY = mouseY; mouseY = e.pageY;
  137. if (mouseDown) {
  138. panX -= (mouseX - lastX);
  139. panY -= (mouseY - lastY);
  140. panUpdate();
  141. }
  142. }
  143. function panPress(e) {
  144. panIdle = false;
  145. panMove(e);
  146. if (e.target == panner) {
  147. e.preventDefault();
  148. setTimeout(() => panHide(), 1);
  149. } else if (e.which != 3) {
  150. e.preventDefault();
  151. mouseDown = true;
  152. }
  153. }
  154. function panRelease(e) {
  155. panMove(e);
  156. mouseDown = false;
  157. }
  158. function panKeyDown(e) {
  159. if (e.keyCode == 27/* ESC */) {
  160. e.preventDefault();
  161. e.stopPropagation();
  162. panHide();
  163. return false;
  164. }
  165. }
  166. panner.addEventListener("mousemove", panMove);
  167. panner.addEventListener("mousedown", panPress);
  168. panner.addEventListener("mouseup", panRelease);
  169. panner.addEventListener("wheel", panWheel);
  170. //
  171. function panFit(lw, lh) {
  172. let iw = window.innerWidth, ih = window.innerHeight;
  173. panZ = 0;
  174. if (lw < iw && lh < ih) {
  175. // zoom in (up to 800%)
  176. for (let k = 0; k < 3; k++) {
  177. if (lw * 2 < iw && lh * 2 < ih) {
  178. panZ += 1; lw *= 2; lh *= 2;
  179. }
  180. }
  181. } else {
  182. while (lw > iw || lh > ih) { // zoom out until fits
  183. panZ -= 1; lw /= 2; lh /= 2;
  184. }
  185. }
  186. panM = Math.pow(2, panZ);
  187. panX = -(iw - lw) / 2;
  188. panY = -(ih - lh) / 2;
  189. console.log(iw, ih, lw, lh, panX, panY, panM);
  190. }
  191. //
  192. let panCheckInt2 = null;
  193. function panCheck2() {
  194. if (isVideo) {
  195. let lw = video.offsetWidth, lh = video.offsetHeight;
  196. if (!videoLoaded) return;
  197. clearInterval(panCheckInt2); panCheckInt2 = null;
  198. panFit(lw, lh);
  199. console.log(lw, lh, panX, panY);
  200. } else {
  201. let lw = img1.width, lh = img1.height;
  202. if (lw <= 0 || lh <= 0) return;
  203. clearInterval(panCheckInt2); panCheckInt2 = null;
  204. //
  205. if (img1failed) return;
  206. img1.style.visibility = "";
  207. if (/*panIdle*/true) { // it makes sense to rescale to original if idle, but looks odd
  208. panZ -= Math.log2(Math.max(lw / img0.width, lh / img0.height));
  209. panM = Math.pow(2, panZ);
  210. if (Math.abs((panZ * 2) % 1) > 0.001) {
  211. panner.classList.add("odd-zoom");
  212. }
  213. } else panFit(lw, lh);
  214. img0.width = lw;
  215. img0.height = lh;
  216. }
  217. panUpdate();
  218. }
  219. //
  220. let panCheckInt = null;
  221. function panCheck() {
  222. let lw = img0.width, lh = img0.height;
  223. if (lw <= 0 || lh <= 0) return;
  224. if (img0failed) return;
  225. //console.log(lw, lh, img0failed);
  226. clearInterval(panCheckInt); panCheckInt = null;
  227. panFit(lw, lh);
  228. img0.style.visibility = "";
  229. panUpdate();
  230. if (img1.src) {
  231. panCheckInt2 = setInterval(panCheck2, 25);
  232. }
  233. }
  234. //
  235. var panTickInt = null;
  236. function panTick() {
  237. let lastWidth = panWidth; panWidth = window.innerWidth;
  238. let lastHeight = panHeight; panHeight = window.innerHeight;
  239. if (panWidth != lastWidth || panHeight != lastHeight) {
  240. panX -= (panWidth - lastWidth) / 2;
  241. panY -= (panHeight - lastHeight) / 2;
  242. panUpdate();
  243. }
  244. }
  245. function panShow(url, orig, mode) {
  246. isVideo = mode == 1;
  247. img1.style.display = img0.style.display = (mode == 0 ? "" : "none");
  248. video.style.display = mode == 1 ? "" : "none";
  249. iframe.style.display = mode == 2 ? "" : "none";
  250. if (mode == 2) {
  251. iframe.src = url;
  252. } else if (mode == 1) {
  253. video.src = url;
  254. videoLoaded = false;
  255. } else {
  256. img0.removeAttribute("width");
  257. img0.removeAttribute("height");
  258. img1.src = url; img0failed = false;
  259. img0.src = orig; img1failed = false;
  260. img1.style.visibility = "hidden";
  261. img0.style.visibility = "hidden";
  262. }
  263. document.querySelector(".application").appendChild(panner);
  264. document.addEventListener("keydown", panKeyDown);
  265. panWidth = window.innerWidth;
  266. panHeight = window.innerHeight;
  267. if (mode == 2) return;
  268. panTickInt = setInterval(panTick, 100);
  269. if (isVideo) {
  270. panCheckInt2 = setInterval(panCheck2, 25);
  271. } else {
  272. panCheckInt = setInterval(panCheck, 25);
  273. }
  274. }
  275. function panHide() {
  276. video.src = "";
  277. img0.src = "";
  278. img1.src = "";
  279. iframe.src = "";
  280. panner.parentElement.removeChild(panner);
  281. document.removeEventListener("keydown", panKeyDown);
  282. clearInterval(panTickInt); panTickInt = null;
  283. if (panCheckInt != null) { clearInterval(panCheckInt); panCheckInt = null; }
  284. if (panCheckInt2 != null) { clearInterval(panCheckInt2); panCheckInt2 = null; }
  285. }
  286. //
  287. function panGetShow(url, orig, mode) {
  288. if (mode == null) mode = 0;
  289. return (e) => {
  290. e.preventDefault();
  291. e.stopPropagation();
  292. panShow(url, orig, mode);
  293. };
  294. }
  295. //
  296. function getBackgroundUrl(el) {
  297. let url = el.style.backgroundImage;
  298. if (url == null) return url;
  299. return url.slice(4, -1).replace(/"/g, "");
  300. }
  301. setInterval(() => {
  302. // pictures:
  303. for (let query of [
  304. `.js-media-preview-container:not(.is-video):not(.is-gif) .js-media-image-link:not(.imgxis-link)`,
  305. `.media-image-container .js-media-image-link:not(.imgxis-link)`,
  306. ]) for (let el of document.querySelectorAll(query)) {
  307. el.classList.add("imgxis-link");
  308. let url, orig;
  309. if (/(?:.jpg|.png|.jpeg|.gif)$/g.test(el.href)) {
  310. url = el.href;
  311. orig = el.getAttribute("data-original-url") || (url + ":small");
  312. } else {
  313. let img = el.querySelector("img");
  314. if (img == null) {
  315. orig = getBackgroundUrl(el);
  316. if (orig == null) continue;
  317. } else {
  318. orig = img.src;
  319. }
  320. url = orig.replace(/(?:\:small|\:large|\?format=.+)$/g, ":orig");
  321. }
  322. el.addEventListener("click", panGetShow(url, orig));
  323. }
  324. // profile backgrounds:
  325. for (let el of document.querySelectorAll(`.prf-header:not(.imgxis-link`)) {
  326. el.classList.add("imgxis-link");
  327. let orig = getBackgroundUrl(el);
  328. if (orig == null) continue;
  329. let url = orig.replace(/\/web$/g, "/1500x500");
  330. let a = document.createElement("a");
  331. a.className = "imgxis-badge";
  332. a.href = "#";
  333. a.title = "View profile background";
  334. a.addEventListener("click", panGetShow(url, orig));
  335. el.appendChild(a);
  336. }
  337. // avatars:
  338. for (let el of document.querySelectorAll(`.prf-img img.avatar:not(.imgxis-link)`)) {
  339. el.classList.add("imgxis-link");
  340. let orig = el.src;
  341. let url = orig.replace(/(?:_bigger|_normal)\./g, ".");
  342. el.addEventListener("click", panGetShow(url, orig));
  343. }
  344. // gifs:
  345. for (let el of document.querySelectorAll(`.media-item-gif:not(.imgxis-link)`)) {
  346. el.classList.add("imgxis-link");
  347. let url = el.src;
  348. el.parentElement.addEventListener("click", panGetShow(url, url, 1));
  349. }
  350. // videos:
  351. for (let el of document.querySelectorAll(`.media-preview-container.is-video:not(.imgxis-link)`)) {
  352. el.classList.add("imgxis-link");
  353. let link = el.querySelector("a");
  354. let par = el.parentElement;
  355. let url = link && link.href;
  356. if (url) url = url.replace("https://www.", "https://");
  357. if (!url) {
  358. //
  359. } else if (url.startsWith("https://t.co") || url.startsWith("https://twitter.com")) {
  360. url = null;
  361. } else if (url.startsWith("https://youtube.com/")) {
  362. var mt = /v=([\w-]+)/.exec(url);
  363. if (mt) url = `https://www.youtube.com/embed/${mt[1]}?autoplay=1`;
  364. }
  365. if (!url) while (par) {
  366. if (par.tagName == "ARTICLE" || par.classList.contains("quoted-tweet")) {
  367. url = par.getAttribute("data-tweet-id");
  368. if (url) url = `https://twitter.com/i/videos/tweet/${url}?auto_buffer=1&autoplay=1`;
  369. break;
  370. } else par = par.parentElement;
  371. }
  372. if (url) el.parentElement.addEventListener("click", panGetShow(url, url, 2));
  373. }
  374. }, 250);
  375. })();