4chan Image Browser

Opens current thread Images in 4chan into a popup viewer

目前为 2014-05-23 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name 4chan Image Browser
  3. // @namespace IdontKnowWhatToDoWithThis
  4. // @description Opens current thread Images in 4chan into a popup viewer
  5. // @match http*://*.4chan.org/*/res/*
  6. // @match http*://*.4chan.org/*/thread/*
  7. // @version 5.1
  8. // @copyright 2013+, Gyst
  9. // ==/UserScript==
  10. /*jshint multistr:true */
  11. /*jshint browser:true */
  12. /*jshint smarttabs:true */
  13. /* jshint -W099 */
  14. /* jshint -W015 */
  15. /*global Main:false */
  16.  
  17.  
  18.  
  19. //cookieInfo
  20. var INDEX_KEY = "imageBrowserIndexCookie";
  21. var THREAD_KEY = "imageBrowserThreadCookie";
  22. var WIDTH_KEY = "imageBrowserWidthCookie";
  23. var HEIGHT_KEY = "imageBrowserHeightCookie";
  24.  
  25. //IDs for important elements
  26. var VIEW_ID = "mainView";
  27. var IMG_ID = "mainImg";
  28. var IMG_TABLE_ID = "imageAlignmentTable";
  29. var TOP_LAYER_ID = "viewerTopLayer";
  30.  
  31.  
  32. //for holding img srcs and a pointer for traversing
  33. var postData = [];
  34. var linkIndex = 0;
  35.  
  36. //set up the div and image for the popup
  37. var mainView;
  38. var mainImg;
  39. var innerTD;
  40. var topLayer;
  41. var customStyle;
  42. var textWrapper;
  43.  
  44. var leftArrow;
  45. var rightArrow;
  46.  
  47.  
  48. var bottomMenu;
  49.  
  50. var canPreload = false;
  51. var shouldFitImage = false;
  52.  
  53.  
  54. var mouseTimer;
  55.  
  56. var lastMousePos = {x: 0, y: 0};
  57.  
  58.  
  59.  
  60.  
  61. //keycode object. Better than remembering what each code does.
  62. var keys = {38: 'up', 40: 'down', 37: 'left', 39: 'right', 27: 'esc', 86:'v'};
  63.  
  64. //styles for added elements
  65.  
  66. var STYLE_TEXT ='\
  67. div.reply.highlight{z-index:100 !important;position:fixed !important; top:1%;left:1%;}\
  68. body{overflow:hidden !important;}\
  69. #quote-preview{z-index:100;} \
  70. a.quotelink, div.viewerBacklinks a.quotelink{color:#5c5cff !important;}\
  71. a.quotelink:hover, div.viewerBacklinks a:hover{color:red !important;}\
  72. #'+IMG_ID+'{display:block !important; margin:auto;max-width:100%;height:auto;-webkit-user-select: none;cursor:pointer;}\
  73. #'+VIEW_ID+'{\
  74. background-color:rgba(0,0,0,0.9);\
  75. z-index:10; \
  76. position:fixed; \
  77. top:0;left:0;bottom:0;right:0; \
  78. overflow:auto;\
  79. text-align:center;\
  80. -webkit-user-select: none;\
  81. }\
  82. #'+IMG_TABLE_ID+' {width: 100%;height:100%;padding:0;margin:0;border-collapse:collapse;}\
  83. #'+IMG_TABLE_ID+' td {text-align: center; vertical-align: middle; padding:0;margin:0;}\
  84. #'+TOP_LAYER_ID+'{position:fixed;top:0;bottom:0;left:0;right:0;z-index:20;opacity:0;visibility:hidden;transition:all .25s ease;}\
  85. .viewerBlockQuote{color:white;}\
  86. #viewerTextWrapper{max-width:60em;display:inline-block; color:gray;-webkit-user-select: all;}\
  87. .bottomMenuShow{visibility:visible;}\
  88. #viewerBottomMenu{box-shadow: -1px -1px 5px #888888;font-size:20px;padding:5px;background-color:white;position:fixed;bottom:0;right:0;z-index:200;}\
  89. .hideCursor{cursor:none !important;}\
  90. .hidden{visibility:hidden}\
  91. .displayNone{display:none;}\
  92. .pagingButtons{font-size:100px;color:white;text-shadow: 1px 1px 10px #27E3EB;z-index: 11;top: 50%;position: absolute;margin-top: -57px;width:100px;cursor:pointer;-webkit-user-select: none;}\
  93. .pagingButtons:hover{color:#27E3EB;text-shadow: 1px 1px 10px #000}\
  94. #previousImageButton{left:0;text-align:left;}\
  95. #nextImageButton{right:0;text-align:right;}\
  96. @-webkit-keyframes flashAnimation{0%{ text-shadow: none;}100%{text-shadow: 0px 0px 5px lightblue;}}\
  97. .flash{-webkit-animation: flashAnimation .5s alternate infinite linear;}\
  98. ';
  99.  
  100. //Build the open button
  101. var openBttn = document.createElement('button');
  102. openBttn.style.position = 'fixed';
  103. openBttn.style.bottom = '0';
  104. openBttn.style.right = '0';
  105. openBttn.innerHTML = "Open Viewer";
  106. openBttn.addEventListener('click',buildPopup, false);
  107. document.body.appendChild(openBttn);
  108.  
  109.  
  110.  
  111. /* Builds the popup and adds it to the page*/
  112. function buildPopup(){
  113. console.log("Building 4chan Image Viewer");
  114.  
  115. var currentThreadId = document.getElementsByClassName('thread')[0].id;
  116.  
  117. //check if its the last thread opened, if so, remember where the index was.
  118. if(getPersistentValue(THREAD_KEY) === currentThreadId){
  119. linkIndex = parseInt(getPersistentValue(INDEX_KEY));
  120. }else{
  121. linkIndex = 0;
  122. setPersistentValue(INDEX_KEY,0);
  123. }
  124. //set thread id
  125. setPersistentValue(THREAD_KEY,currentThreadId);
  126. //reset post array
  127. postData.length=0;
  128.  
  129. //add keybinding listener
  130. window.addEventListener('keydown',arrowKeyListener,false);
  131. window.addEventListener('mousemove',menuWatcher,false);
  132. //grab postContainers
  133. var posts = document.getElementById('delform').getElementsByClassName('postContainer');
  134. //get image links and post messages from posts
  135. var plength = posts.length;
  136. for(var i = 0; i < plength; ++i){
  137. var file = posts[i].getElementsByClassName('file')[0];
  138. if(file){
  139. var currentLink = file.getElementsByClassName('fileThumb')[0].href;
  140. if(!currentLink){continue;}
  141. var type = getElementType(currentLink);
  142. var currentPostBlock = posts[i].getElementsByClassName('postMessage')[0];
  143. var currentPostBacklinks = posts[i].getElementsByClassName('backlink')[0];
  144. var blockQuote = document.createElement('blockQuote');
  145. var backlinks = document.createElement('div');
  146. if(currentPostBlock){
  147. blockQuote.className = currentPostBlock.className + ' viewerBlockQuote';
  148. blockQuote.innerHTML = currentPostBlock.innerHTML;
  149. add4chanListenersToLinks(blockQuote.getElementsByClassName('quotelink'));
  150. }
  151. if(currentPostBacklinks){
  152. backlinks.className = currentPostBacklinks.className + ' viewerBacklinks';
  153. backlinks.innerHTML = currentPostBacklinks.innerHTML;
  154. add4chanListenersToLinks(backlinks.getElementsByClassName('quotelink'));
  155. }
  156. postData.push({'imgSrc':currentLink,'type':type,'mBlock':blockQuote,'backlinks':backlinks});
  157. }
  158. }
  159.  
  160.  
  161. //build wrapper
  162. mainView = document.createElement('div');
  163. mainView.id = VIEW_ID;
  164. mainView.addEventListener('click',confirmExit, false);
  165. document.body.appendChild(mainView);
  166. //set up table for centering the content. Seriously, the alternatives are worse.
  167. mainView.innerHTML = '<table id="'+IMG_TABLE_ID+'"><tr><td></td></tr></table>';
  168. innerTD = mainView.getElementsByTagName('td')[0];
  169.  
  170. //build image tag
  171. mainImg = document.createElement(postData[linkIndex].type);
  172. mainImg.src= postData[linkIndex].imgSrc;
  173. mainImg.id = IMG_ID;
  174. mainImg.classList.add("hideCursor");
  175. mainImg.autoplay = true;
  176. mainImg.controls = false;
  177. mainImg.loop = true;
  178. innerTD.appendChild(mainImg);
  179. mainImg.addEventListener('click',clickImg,false);
  180. mainImg.onload = function(){
  181. if(shouldFitImage){ fitHeightToScreen();}
  182. };
  183.  
  184. //start preloading to next image index
  185. canPreload = true;
  186. setTimeout(function(){runImagePreloading(1);},100);
  187.  
  188. //add quote block/backlinks(first image always has second post quote)
  189. textWrapper = document.createElement('div');
  190. textWrapper.addEventListener('click',eventStopper,false);
  191. textWrapper.id = 'viewerTextWrapper';
  192. textWrapper.appendChild(postData[linkIndex].backlinks);
  193. textWrapper.appendChild(postData[linkIndex].mBlock);
  194. innerTD.appendChild(textWrapper);
  195.  
  196. //build top layer
  197. topLayer = document.createElement('div');
  198. topLayer.innerHTML = "&nbsp;";
  199. topLayer.id=TOP_LAYER_ID;
  200. document.body.appendChild(topLayer);
  201.  
  202. //build custom style tag
  203. customStyle = document.createElement('style');
  204. customStyle.innerHTML = STYLE_TEXT;
  205. document.body.appendChild(customStyle);
  206. //build bottom menu
  207. var formHtml = '<label><input id="'+WIDTH_KEY+'" type="checkbox" checked="checked" />Fit Image to Width</label>\
  208. <span>|</span>\
  209. <label><input id="'+HEIGHT_KEY+'" type="checkbox" />Fit Image to Height</label>\
  210. ';
  211. bottomMenu = document.createElement('form');
  212. bottomMenu.id = "viewerBottomMenu";
  213. bottomMenu.className = 'hidden';
  214. bottomMenu.innerHTML = formHtml;
  215. document.body.appendChild(bottomMenu);
  216. bottomMenu.addEventListener('click',menuClickHandler,false);
  217. menuInit();
  218. //build arrow buttons
  219. leftArrow = document.createElement("div");
  220. leftArrow.innerHTML = '<span>&#9001;</span>';
  221. leftArrow.id = "previousImageButton";
  222. leftArrow.classList.add("pagingButtons","hidden");
  223. rightArrow = document.createElement("div");
  224. rightArrow.innerHTML = '<span>&#9002;</span>';
  225. rightArrow.id = "nextImageButton";
  226. rightArrow.classList.add("pagingButtons","hidden");
  227. leftArrow.addEventListener('click',function(event){event.stopImmediatePropagation();previousImg();},false);
  228. rightArrow.addEventListener('click',function(event){event.stopImmediatePropagation();nextImg();},false);
  229. mainView.appendChild(leftArrow);
  230. mainView.appendChild(rightArrow);
  231. //some fixes for weird behaviors
  232. innerTD.style.outline = '0';
  233. innerTD.tabIndex = 1;
  234. innerTD.focus();
  235.  
  236. }
  237.  
  238. function menuInit(){
  239. var menuControls = bottomMenu.getElementsByTagName('input');
  240. for(var i = 0; i < menuControls.length; ++i){
  241. var input = menuControls[i];
  242. var cookieValue = getPersistentValue(input.id);
  243. if(cookieValue === 'true'){
  244. input.checked = true;
  245. }else if(cookieValue === 'false'){
  246. input.checked = false;
  247. }
  248. input.parentElement.classList.toggle('flash',input.checked);
  249. switch(input.id){
  250. case WIDTH_KEY:
  251. setFitToScreenWidth(input.checked);
  252. break;
  253. case HEIGHT_KEY:
  254. setFitToScreenHeight(input.checked);
  255. break;
  256. }
  257.  
  258. }
  259. }
  260.  
  261.  
  262. function menuClickHandler(){
  263. var menuControls = bottomMenu.getElementsByTagName('input');
  264. for(var i = 0; i < menuControls.length; ++i){
  265. var input = menuControls[i];
  266. switch(input.id){
  267. case WIDTH_KEY:
  268. setFitToScreenWidth(input.checked);
  269. break;
  270. case HEIGHT_KEY:
  271. setFitToScreenHeight(input.checked);
  272. break;
  273. }
  274. input.parentElement.classList.toggle('flash',input.checked);
  275. setPersistentValue(input.id,input.checked);
  276. }
  277.  
  278. }
  279.  
  280. function windowClick(event){
  281. event.preventDefault();
  282. event.stopImmediatePropagation();
  283. nextImg();
  284. }
  285.  
  286. function add4chanListenersToLinks(linkCollection){
  287. for(var i = 0; i < linkCollection.length; ++i){
  288. //These are the functions that 4chan uses
  289. linkCollection[i].addEventListener("mouseover", Main.onThreadMouseOver, false);
  290. linkCollection[i].addEventListener("mouseout", Main.onThreadMouseOut, false);
  291.  
  292. }
  293. }
  294.  
  295.  
  296.  
  297. /* Event function for determining behavior of viewer keypresses */
  298. function arrowKeyListener(evt){
  299.  
  300. switch(keys[evt.keyCode]){
  301. case 'right':
  302. nextImg();
  303. break;
  304. case 'left':
  305. previousImg();
  306. break;
  307.  
  308. case 'esc':
  309. clearDiv();
  310. break;
  311. }
  312.  
  313. }
  314.  
  315.  
  316. /* preloads images starting with the index provided */
  317. function runImagePreloading(index){
  318.  
  319. if(index < postData.length){
  320.  
  321. if(canPreload){
  322. var newImage = document.createElement(postData[index].type);
  323. //load the next image after this one loads
  324. newImage.onload = function(){
  325. runImagePreloading(index+1);
  326. };
  327. newImage.onerror = function(){
  328. runImagePreloading(index+1);
  329. };
  330. newImage.src = postData[index].imgSrc;
  331. }
  332.  
  333. }
  334. }
  335.  
  336.  
  337.  
  338. /* Sets the img and message to the next one in the list*/
  339. function nextImg(){
  340.  
  341. if(linkIndex === postData.length - 1){
  342. topLayer.style.background = 'linear-gradient(to right,rgba(0,0,0,0) 90%,rgba(125,185,232,1) 100%)';
  343. topLayer.style.opacity = '.5';
  344. topLayer.style.visibility = "visible";
  345.  
  346. setTimeout(function(){
  347. topLayer.style.opacity = '0';
  348. setTimeout(function(){topLayer.style.visibility = "hidden";},200);
  349. }, 500);
  350. return;
  351. }
  352. else{
  353. changeData(1);
  354. }
  355. }
  356.  
  357. /* Sets the img and message to the previous one in the list*/
  358. function previousImg(){
  359.  
  360. if(linkIndex === 0){
  361. topLayer.style.background = 'linear-gradient(to left,rgba(0,0,0,0) 90%,rgba(125,185,232,1) 100%)';
  362. topLayer.style.opacity = '.5';
  363. topLayer.style.visibility = "visible";
  364.  
  365. setTimeout(function(){
  366. topLayer.style.opacity = '0';
  367. setTimeout(function(){topLayer.style.visibility = "hidden";},200);
  368. }, 500);
  369.  
  370. return;
  371. }
  372. else{
  373. changeData(-1);
  374.  
  375. }
  376. }
  377.  
  378. function changeData(delta){
  379. linkIndex = linkIndex + delta;
  380.  
  381. if(postData[linkIndex].type !== mainImg.tagName){
  382. mainImg = replaceElement(mainImg,postData[linkIndex].type);
  383. }
  384. mainImg.src = postData[linkIndex].imgSrc;
  385.  
  386. textWrapper.replaceChild(postData[linkIndex].backlinks,postData[linkIndex - delta].backlinks);
  387. textWrapper.replaceChild(postData[linkIndex].mBlock,postData[linkIndex - delta].mBlock);
  388.  
  389. mainView.scrollTop = 0;
  390.  
  391. setPersistentValue(INDEX_KEY,linkIndex);
  392. }
  393.  
  394. function getElementType(src){
  395. if(src.match(/\.(?:(?:webm)|(?:ogg)|(?:mp4))$/)){
  396. return 'VIDEO';
  397. }else{
  398. return 'IMG';
  399. }
  400. }
  401.  
  402. function replaceElement(element,newType){
  403. var newElement = document.createElement(newType);
  404. newElement.className = element.className;
  405. newElement.id = element.id;
  406. newElement.style = element.style;
  407. newElement.autoplay = element.autoplay;
  408. newElement.controls = element.controls;
  409. newElement.loop = element.loop;
  410. newElement.addEventListener('click',clickImg,false);
  411. newElement.onload = function(){
  412. if(shouldFitImage){ fitHeightToScreen();}
  413. };
  414. element.parentElement.insertBefore(newElement,element);
  415. element.parentElement.removeChild(element);
  416. return newElement;
  417. }
  418.  
  419.  
  420.  
  421. /* Function for handling click image events*/
  422. function clickImg(event){
  423. event.stopPropagation();
  424. nextImg();
  425.  
  426. }
  427.  
  428. function eventStopper(event){
  429. if(event.target.nodeName !== 'A'){
  430. event.stopPropagation();
  431. }
  432. }
  433.  
  434. function confirmExit(){
  435. if(window.confirm('Exit Viewer?')){
  436. clearDiv();
  437. }
  438.  
  439. }
  440.  
  441. /* Removes the popup and other things added by the build method*/
  442. function clearDiv(){
  443. window.removeEventListener('keydown',arrowKeyListener);
  444. window.removeEventListener('mousemove',menuWatcher);
  445. document.body.removeChild(topLayer);
  446. document.body.removeChild(mainView);
  447. document.body.removeChild(customStyle);
  448. document.body.removeChild(bottomMenu);
  449. document.body.style.overflow="auto";
  450. canPreload = false;
  451.  
  452. }
  453.  
  454.  
  455.  
  456.  
  457. /*Mouse-move Handler that watches for when menus should appear and mouse behavior*/
  458. function menuWatcher(event) {
  459.  
  460. var height_offset = window.innerHeight - bottomMenu.offsetHeight;
  461. var width_offset = window.innerWidth - bottomMenu.offsetWidth;
  462. var center = window.innerHeight / 2;
  463. var halfArrow = leftArrow.offsetHeight / 2;
  464. if(event.clientX >= width_offset && event.clientY >= height_offset){
  465. bottomMenu.className='bottomMenuShow';
  466. }else if(bottomMenu.className==='bottomMenuShow'){
  467. bottomMenu.className ='hidden';
  468. }
  469. if((event.clientX <= (100) || event.clientX >= (window.innerWidth-100)) &&
  470. (event.clientY <= (center + halfArrow) && event.clientY >= (center - halfArrow))){
  471. rightArrow.classList.remove('hidden');
  472. leftArrow.classList.remove('hidden');
  473. }else{
  474. rightArrow.classList.add('hidden');
  475. leftArrow.classList.add('hidden');
  476. }
  477.  
  478. //avoids chrome treating mouseclicks as mousemoves
  479. if(event.clientX !== lastMousePos.x && event.clientY !== lastMousePos.y){
  480. //mouse click moves to next image when invisible
  481. mainImg.classList.remove('hideCursor');
  482. window.clearTimeout(mouseTimer);
  483. document.body.removeEventListener('click',windowClick,true);
  484. document.body.classList.remove('hideCursor');
  485. if(event.target.id === mainImg.id){
  486. //hide cursor if it stops, show if it moves
  487. mouseTimer = window.setTimeout(function(){
  488. mainImg.classList.add('hideCursor');
  489. document.body.classList.add('hideCursor');
  490. document.body.addEventListener('click',windowClick,true);
  491. }, 200);
  492. }
  493.  
  494.  
  495. }
  496.  
  497. lastMousePos.x = event.clientX;
  498. lastMousePos.y = event.clientY;
  499. }
  500.  
  501. /*Stores a key value pair as a cookie*/
  502. function setPersistentValue(key, value){
  503.  
  504. document.cookie = key + '='+value;
  505.  
  506. }
  507.  
  508. /* Retrieves a cookie value via its key*/
  509. function getPersistentValue(key){
  510. var cookieMatch = document.cookie.match(new RegExp(key+'\\s*=\\s*([^;]+)'));
  511. if(cookieMatch){
  512. return cookieMatch[1];
  513. }else{
  514. return null;
  515. }
  516.  
  517.  
  518. }
  519.  
  520.  
  521. function setFitToScreenHeight(shouldFitImage){
  522. if(shouldFitImage){
  523. fitHeightToScreen();
  524. }else{
  525. mainImg.style.maxHeight = '';
  526. }
  527. }
  528. function setFitToScreenWidth(shouldFitImage){
  529.  
  530. mainImg.style.maxWidth = shouldFitImage ? '100%' : 'none';
  531.  
  532. }
  533.  
  534.  
  535. /* Fits image to screen height*/
  536. function fitHeightToScreen(){
  537.  
  538. //sets the changeable properties to the image's real size
  539. var height = mainImg.naturalHeight;
  540. mainImg.style.maxHeight = height + 'px';
  541.  
  542. //actually tests if it is too high including padding
  543. var heightDiff = (mainImg.clientHeight > height)?
  544. mainImg.clientHeight - mainView.clientHeight:
  545. height - mainView.clientHeight;
  546.  
  547. if(heightDiff > 0){
  548. mainImg.style.maxHeight = (height - heightDiff) + 'px';
  549. }else{
  550. mainImg.style.maxHeight = height + 'px';
  551. }
  552. }