Picarto image link previewer

Save a click or two by viewing some images directly within Picarto's "leaving" pages. By StevenRoy

  1. // ==UserScript==
  2. // @name Picarto image link previewer
  3. // @namespace http://michrev.com/
  4. // @description Save a click or two by viewing some images directly within Picarto's "leaving" pages. By StevenRoy
  5. // @include https://www.dropbox.com/*
  6. // @include https://picarto.tv/site/referrer*
  7. // @include https://www.picarto.tv/site/referrer*
  8. // @version 1.21
  9. // @grant none
  10. // @supportURL https://greasyfork.org/en/users/934871
  11. // ==/UserScript==
  12.  
  13. "use strict";
  14.  
  15. // _____________ __ __
  16. // / ___________/ / \,-' /
  17. // / /__ ___ / /\,-/ /
  18. // \___ \ / __\ / / / /
  19. //______/ / / / / / / / StevenRoy was here
  20. //_______/ /_/ /_/ /_/ 02025.02.08
  21.  
  22.  
  23.  
  24. // JB: issues/1092 obviously applies here.
  25.  
  26.  
  27.  
  28. // Sensitivity is proportional to the sum of the numbers being compared
  29. // When a is 200, returns true when b is >=197 and <=204
  30. // When a is 1000, returns true when b is >=981 and <=1020
  31. // (Can also be adjusted by changing the 100 constant but I like it where it is now.)
  32. function ratherclose(a,b) { return Math.abs(a-b)*100<(a+b); }
  33.  
  34. var img,mcx,mcy,ww,wh,dw,dh,dx,dy,isl,imgfit=0; // window and image size, size lists etc
  35. var eizs,eisize; // elements for zoom setting and size list
  36.  
  37. function resized() {
  38. if (!img) { return; } // TSNH - throw()? alert()?
  39. var de=document.documentElement;
  40. if (window.innerWidth) { // Preferred in FF because it doesn't shrink when a scrollbar is present.
  41. ww=window.innerWidth;
  42. wh=window.innerHeight;
  43. } else {
  44. if (de && de.clientWidth) { // Also exists in FF; this one excludes scrollbar if present.
  45. ww=de.clientWidth; // (Though that's kinda moot when we're using overflow:hidden)
  46. wh=de.clientHeight;
  47. } else {
  48. return false; // TSNH? Am I forgetting anything?
  49. }
  50. }
  51. // if (mcx===undefined && mcy===undefined) { mcx=ww>>1; mcy=wh>>1; } // until we get mouse coords
  52. var iw=img.w,ih=img.h; // We use these a lot.
  53. // var war=ww/wh; // window aspect ratio (1=square, >1=wide) but what is it good for?
  54. var iar=iw/ih; // image aspect ratio (1920x800 -> 12/5 which is 2.4)
  55. var wscw=wh*iar; // resulting width from scaling window height to image's AR.
  56.  
  57. // There can be three zoom-states: original (iw,ih), fit-x (ww,ww/iar), and fit-y (wh*iar,wh)
  58. // ...And then sort 'em. But also eliminate any that are nearly identical.
  59.  
  60. isl=[iw]; // image size list, start with original image size
  61. var am=ratherclose(ww,wscw); // aspect ratio match between image and window?
  62. if (!ratherclose(iw,ww)) { // but not actual size match.
  63. if (am) {
  64. console.log("Aspect ratio match");
  65. // isl.push(Math.floor(ww+wh*iar+1)>>1); // use average for near-match (This is actually bad.)
  66. // Assuming a very-near-but-not-perfect match: Which size is further from iw? Let's use that one.
  67. isl.push(Math.abs(iw-ww) > Math.abs(iw-wscw) ? ww : wscw);
  68. }
  69. else { isl.push(ww); }
  70. }
  71. if (!am && !ratherclose(iw,wscw)) { isl.push(wscw); }
  72. if (isl.length>1) {
  73. if (imgfit>=isl.length) { imgfit=isl.length-1; } // for those times when a size vanishes
  74. isl.sort((a,b) => a-b);
  75. img.style.cursor=(imgfit==isl.length-1)?"zoom-out":"zoom-in"; // How to affect blank space around img? (But do we want that?)
  76. } else { imgfit=0; img.style.cursor="default"; }
  77.  
  78. dw=isl[imgfit];
  79. dh=(dw==iw)?ih:(dw/iar); // dh=dw/(iw/ih) ... dh/ih=dw/iw
  80. if (eizs) { eizs.textContent=Math.round(100*dw/iw)+" %"; }
  81.  
  82. // panned(); // setting image size is done in here too now. Except it's now part of the animation process.
  83. }
  84.  
  85. const stratio=0.04;
  86. const stmult=1/(1-2*stratio);
  87.  
  88. function slightstretch(r){
  89. var tr=(r-stratio)*stmult; // a straight interpolation; maybe I should try to make it cooler later
  90. if (tr>=1) return 1;
  91. if (tr<0) return 0;
  92. return tr;
  93. }
  94.  
  95. function panned() { // Animated response to Mouse movement with requestAnimationFrame()
  96. if (mcx!==undefined && dw>ww) { dx=slightstretch(mcx/ww)*(ww-dw); } else { dx=(ww-dw)/2; }
  97. if (mcy!==undefined && dh>wh) { dy=slightstretch(mcy/wh)*(wh-dh); } else { dy=(wh-dh)/2; }
  98. var ics=img.style;
  99. ics.width=Math.round(dw)+"px"; // These were animated for awesomer zooming, but FF has problems with that!!!
  100. ics.height=Math.round(dh)+"px";
  101. ics.left=Math.round(dx)+"px"; // always negative when panning, otherwise positive and centered
  102. ics.top=Math.round(dy)+"px";
  103. }
  104.  
  105. function onstopanimate() {} // placeholder
  106. var animating=0;
  107. function animate() {
  108. if (!animating) return onstopanimate();
  109. panned();
  110. // var c=coordstoimg();
  111. requestAnimationFrame(animate); // Uh oh, now we're doing it!
  112. }
  113.  
  114. // ** ** ** EVENTS
  115.  
  116. function shutup(e) {
  117. if (!e) { e=window.event; if (!e) { return false; } }
  118. e.cancelBubble = true;
  119. if (e.stopPropagation) e.stopPropagation(); // For when "return true" just isn't good enough?
  120. e.preventDefault(); // And I mean it!
  121. return e;
  122. }
  123.  
  124. function smother(evt) { shutup(evt); return false; }
  125.  
  126. function mousecoords(e) { // event
  127. if ("pageX" in e && "pageY" in e) {
  128. mcx = e.pageX; mcy = e.pageY;
  129. } else { // We want coords relative to top (origin) of document, this should do it:
  130. mcx = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
  131. mcy = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
  132. }
  133. }
  134.  
  135. function smdown(evt) {
  136. // var e=shutup(evt);
  137. imgfit++;
  138. if (imgfit>=isl.length) { imgfit=0; } // Select next (or first) size
  139. resized();
  140. smmove(evt); // trigger move() after coordinates change
  141. return false; // Does this value do anything?
  142. }
  143.  
  144. function smmove(evt) {
  145. var e=shutup(evt);
  146. mousecoords(e);
  147. // panned();
  148. return false;
  149. }
  150.  
  151. function makezoomableimage(i) {
  152. img=i;
  153. img.w=img.naturalWidth;
  154. img.h=img.naturalHeight;
  155.  
  156. if (eisize) { eisize.innerHTML=img.w+" x "+img.h; }
  157.  
  158. window.onresize=resized; resized();
  159. // i.onmousedown=smdown;
  160. i.onmousedown=smother;
  161. i.onmouseup=smother; // not used!
  162. window.onmousemove=smmove;
  163. // i.onclick=smother; // none of these used either
  164. i.onclick=smdown; // alternative strategy, maybe less good, maybe not?
  165. i.ondblclick=smother;
  166. // i.oncontextmenu=smother;
  167. if (!animating) { animating=1; animate(); } // This starts the maaaaagic!
  168. img.style.position="fixed";
  169. img.style.maxWidth="unset"; // Leave it to Picarto to screw with zooms...
  170. return i;
  171. }
  172.  
  173. const c64pal="000 fff c05 6ef c5d 6c5 42b fe7 c84 641 f79 555 888 cfa a9f ccc";
  174. // That's my c64 palette. I'm going for a balance of authentic and vivid.
  175. var waitindicator=function(){
  176. var waittimer,wc=4,wde,wrefc=0;
  177. return function(){
  178. if(!wde) {
  179. document.body.appendChild(wde=Object.assign(document.createElement("div"),{
  180. innerHTML:'W<span>a</span><span>i</span><span>t</span><span>.</span><span>.</span><span>.</span>',
  181. // id:'loading',
  182. style:"z-index:90999;box-sizing:border-box;font-size:20pt;line-height:18pt;font-family:" /* 'Roboto', */ +"'Andale Mono',monospace;"+
  183. "position:fixed;height:30px;width:160px;top:50%;left:50%;text-align:center;"+
  184. "margin:-15px 0 0 -80px;background:#000;color:#fff"
  185. }));
  186. }
  187. if (wde && !waittimer) { waittimer=setInterval(function(){
  188. if (!wde) { return clearInterval(waittimer); } // never underestimate the capacity of browsers to screw up
  189. wc=(wc+4)&60;
  190. var wcc=wc;
  191. wde.style.color="#"+c64pal.substr(wc,3); // A very specific set of colors...
  192. Array.prototype.forEach.call(wde.children,(e)=>{
  193. wcc=wcc+4&60;
  194. e.style.color="#"+c64pal.substr(wcc,3);
  195. });
  196. },150); }
  197. wrefc++;
  198. return function(){
  199. if (wrefc>0) { wrefc--; }
  200. setTimeout(()=>{
  201. if (wrefc==0 && wde) { wde.parentNode.removeChild(wde); wde=false; }
  202. },250); // in case of sequential busy-states, delay vanish of busy indicator
  203. };
  204. };
  205. }();
  206.  
  207. // So much gutting of previous code because we don't
  208. // need -any- of the canvas functions...
  209. function loadimage(src,cb) {
  210. var wi=waitindicator();
  211. var ix=document.createElement("img");
  212. // ix=new Image();
  213. ix.onload=function() {
  214. wi(); cb(ix); // pass img to callback
  215. }
  216. ix.src=src;
  217. } // that basically replaces an entire image manipulation library!
  218. // (Although replacing <canvas> with <image> does seem to make this perform worse (and often crash) in FF52
  219. // if my awesome animation function is used... I just won't use it then. Problem solved.)
  220.  
  221. function loadtext(fn,cb) {
  222. var req = new XMLHttpRequest();
  223. req.open("GET", fn, true);
  224. req.overrideMimeType("application/json"); // Does this actually -do- anything?
  225. req.onload = function (oEvent) {
  226. var t = req.responseText;
  227. if (t) { cb(t); }
  228. };
  229. req.send(null);
  230. }
  231.  
  232.  
  233.  
  234. // Freaking dummkopfs Picarto seems to like to lazy-load all its actual page content,
  235. // meaning things we want to replace won't exist at this function's execute-time.
  236. // Of course there's a stupid workaround... (and those are often the best kind!)
  237. var retrycount=0;
  238. function itsapicartopage(addr){
  239. var keeplink=document.links ,m;
  240. var mb=document.getElementById("main-container"); // within div#root within body
  241. // console.log(mb,keeplink);
  242. if (mb && mb.children && mb.children.length && keeplink && keeplink[0] && keeplink[0].href==addr) {
  243. keeplink=keeplink[0].parentNode;
  244. mb=mb.childNodes[0];
  245. m=parsedbaddr(addr);
  246. const firstimageloaded=(ix)=>{
  247. mb.innerHTML='';
  248. mb.style="position:fixed; overflow:hidden; min-height:100vh; height:100vh; width:100%; padding:0; margin:0; display:block";
  249. keeplink.style="position:fixed;right:0;bottom:0;margin:20px";
  250. eisize=document.createElement("div");
  251. eizs=document.createElement("span");
  252. mb.appendChild(makezoomableimage(ix)); // this sets the eisize content now
  253. mb.appendChild(keeplink);
  254. var d1=document.createElement("div"); // additional UI element, mainly for balanced aesthetics!
  255. d1.className=keeplink.className; // This may vary!
  256. d1.style="position:fixed;left:0;bottom:0;margin:20px;text-align:center";
  257. keeplink.style.backgroundColor=d1.style.backgroundColor=getComputedStyle(document.body).backgroundColor;
  258. // eisize.innerHTML=img.w+" x "+img.h;
  259. mb.appendChild(d1);
  260. d1.appendChild(eisize);
  261. d1.appendChild(eizs);
  262. };
  263. const showtweettext=(tx)=>{
  264. console.log("showtweettext called");
  265. if (!(tx && tx.text)) return;
  266. var d1=document.createElement("div");
  267. console.log("showtweettext proceeding",d1,tx.text);
  268. d1.className=keeplink.className; // This may vary!
  269. d1.style="position:fixed;left:0;top:0;margin:20px;text-align:left;width:50%";
  270. d1.style.backgroundColor=getComputedStyle(document.body).backgroundColor;
  271. mb.appendChild(d1);
  272. d1.textContent=tx.text;
  273. if (tx.author) {
  274. var a=document.createElement("a");
  275. a.className=keeplink.getElementsByTagName("a")[0].className;
  276. a.style.display="block";
  277. a.href=tx.author.url;
  278. a.textContent=tx.author.name;
  279. d1.insertBefore(a,d1.firstChild);
  280. if (tx.author.avatar_url) {
  281. var i=document.createElement("img");
  282. i.src=tx.author.avatar_url;
  283. i.style="float:left;width:50px";
  284. d1.insertBefore(i,d1.firstChild);
  285. // var br=document.createElement("br");
  286. }
  287. }
  288. };
  289. const maybemultimode=(urls,onloadcb)=>{
  290. if (!(urls && urls.length)) return false; // definitely a redundant check
  291. loadimage(urls[0],ix=>{
  292. firstimageloaded(ix);
  293. if (onloadcb) { onloadcb(); }
  294. if (urls.length>1) { // oh boy here we go with fancy stuff again
  295. var mimg=[ix];
  296. var ci=0; // current image number
  297. var epn=document.createElement("span");
  298. epn.style.display="block";
  299. epn.innerHTML="<a href=#>\u21e6</a><a href=#>\u21e8</a><span>1</span> / "+urls.length;
  300. eizs.parentNode.insertBefore(epn,eizs.parentNode.firstChild);
  301. var a=epn.getElementsByTagName('a');
  302. if (a.length != 2) throw "WTF our <a> elements vanished";
  303. a[0].style.float="left";
  304. a[1].style.float="right";
  305. a[0].onclick=a[1].onclick=function(e){
  306. e=shutup(e);
  307. var ni=ci;
  308. if (e.currentTarget===a[1]) {
  309. ni++; if (ni>urls.length) ni=0;
  310. } else {
  311. if (ni==0) ni=urls.length;
  312. ni--;
  313. } // TODO: This part isn't finished yet.
  314. // makezoomableimage(mimg[ni]) // also sets the eisize content
  315. };
  316. }
  317. });
  318.  
  319. return urls.length;
  320. };
  321. if (m) {
  322. loadimage(m,firstimageloaded);
  323. } else if ((m=addr.match(/\/\/(?:www\.)?(?:x|twitter|fxtwitter|twxtter|fixupx)\.com\/.+\/status\/([0-9]+)\/?$/i))
  324. && m.length) {
  325. var wi=waitindicator();
  326. console.log('Dead bird detected, loading post number:',m[1]);
  327. loadtext('https://api.fxtwitter.com/'+'unknown'+'/status/'+m[1],(tx)=>{
  328. wi();
  329. // console.log(tx);
  330. tx=JSON.parse(tx);
  331. if (!(tx.code==200 && tx.tweet)) throw "Didn't get a valid response from fxtwitter.com";
  332. tx=tx.tweet;
  333. console.log(tx);
  334. m=tx.media && tx.media.all && tx.media.all.length && tx.media.all.filter(e => e.type=="photo");
  335.  
  336. // TODO: when e.type is "video" we can get a thumbnail url, display it as plain image but also create a "play" button
  337. // (and then what?)
  338.  
  339. if (m && m.length) {
  340. maybemultimode(m.map(u=>u.url), ()=>{ showtweettext(tx) } );
  341. } else showtweettext(tx);
  342.  
  343. });
  344. }
  345. } else if (retrycount<20) { retrycount++; console.log("Retrying"); setTimeout(itsapicartopage,500,addr); }
  346. }
  347.  
  348. function parsedbaddr(a){
  349. // console.log("Parsing addr:",a);
  350. a=a.replace(/e=[0-9]+&/,''); // no idea what this does, but I saw it once so now I gotta add code to ignore it.
  351. var m=a.match(/\/\/(?:www\.|dl\.)?dropbox\.com\/(?:e\/)?scl\/fi\/([^/?.]+)\/.+\?(.+)$/i); // where does the e go?
  352. if (m && m.length && m[2]) {
  353. // console.log("2:",m);
  354. var rlkey=false;
  355. m[2].split('&').forEach(n=>{
  356. if (rlkey) return; // found one so stop looking
  357. var a=n.split('=');
  358. if ((a.length==2) && (a[0]=='rlkey')) rlkey=a[1];
  359. });
  360. // TODO: Figure out what size= and size_mode= do, if anything. Are they needed?
  361. if (rlkey) return "https://www.dropbox.com/temp_thumb_from_token/c/"+m[1]+"?rlkey="+rlkey+"&size=1200x1200&size_mode=4";
  362. return false;
  363. }
  364. m=a.match(/\/\/(?:www\.|dl\.)?dropbox\.com\/.+\/[^/?.]*\.(jpg|jpeg|png|gif|webp)(\?dl=0)?$/);
  365. if (m && m.length && m[1]) {
  366. // console.log("1:",m);
  367. if (m[2] == '?dl=0') a=a.replace(/\?dl=0/,'');
  368. else if (m[2]) throw ("TSNH: Weird query in db addr");
  369. return a+'?dl=1'; // Functioning link to the image itself (easy mode)
  370. }
  371. return false;
  372. }
  373.  
  374. var l=top.location.href;
  375. var m=l.match(/\/\/(?:www\.)?picarto\.tv\/.+?go=(http(?:s)?%3A%2F%2F[^&]+)/);
  376. if (m && m.length && m[1]) itsapicartopage(decodeURIComponent(m[1]));
  377.  
  378. // (Also, the Dropbox site is broken in Firefox 52 but we can detect that
  379. // and force images to appear there anyway. This one's for the WinXP users. Shrug.)
  380. else if (m=parsedbaddr(l)) { // If we're on the dropbox site...
  381. var bv=navigator.userAgent.match(/Firefox\/([0-9]+)/); // but using a browser that db WON'T WORK ON
  382. if (bv && bv[1] && (bv[1]-0)<=52) {
  383. var i0=document.createElement("img"); // Just dump the image (sorry, no fancy viewer this time)
  384. i0.src=m;
  385. document.body.appendChild(i0);
  386. }
  387. }
  388.