// ==UserScript== // @name 4chan Image Browser // @namespace IdontKnowWhatToDoWithThis // @description Opens current thread Images in 4chan into a popup viewer, tested in Tampermonkey // @match *://*.4chan.org/*/res/* // @match *://*.4chan.org/*/thread/* // @version 6.0 // @copyright 2014+, Gyst // @downloadURL none // ==/UserScript== /** * Constructor function, the outer function is run immediately to store the *the constants in a closure */ var Viewer = (function(){ var INDEX_KEY = "imageBrowserIndexCookie"; var THREAD_KEY="imageBrowserThreadCookie"; var WIDTH_KEY = "imageBrowserWidthCookie"; //cookieInfo var HEIGHT_KEY = "imageBrowserHeightCookie"; //IDs for important elements var VIEW_ID = "mainView"; var IMG_ID = "mainImg"; var IMG_TABLE_ID = "imageAlignmentTable"; var TOP_LAYER_ID = "viewerTopLayer"; //styles for added elements var STYLE_TEXT='\ div.reply.highlight{z-index:100 !important;position:fixed !important; top:1%;left:1%;}\ body{overflow:hidden !important;}\ #quote-preview{z-index:100;} \ a.quotelink, div.viewerBacklinks a.quotelink{color:#5c5cff !important;}\ a.quotelink:hover, div.viewerBacklinks a:hover{color:red !important;}\ #'+IMG_ID+'{display:block !important; margin:auto;max-width:100%;height:auto;-webkit-user-select: none;cursor:pointer;}\ #'+VIEW_ID+'{\ background-color:rgba(0,0,0,0.9);\ z-index:10; \ position:fixed; \ top:0;left:0;bottom:0;right:0; \ overflow:auto;\ text-align:center;\ -webkit-user-select: none;\ }\ #'+IMG_TABLE_ID+' {width: 100%;height:100%;padding:0;margin:0;border-collapse:collapse;}\ #'+IMG_TABLE_ID+' td {text-align: center; vertical-align: middle; padding:0;margin:0;}\ #'+TOP_LAYER_ID+'{position:fixed;top:0;bottom:0;left:0;right:0;z-index:20;opacity:0;visibility:hidden;transition:all .25s ease;}\ .viewerBlockQuote{color:white;}\ #viewerTextWrapper{max-width:60em;display:inline-block; color:gray;-webkit-user-select: all;}\ .bottomMenuShow{visibility:visible;}\ #viewerBottomMenu{box-shadow: -1px -1px 5px #888888;font-size:20px;padding:5px;background-color:white;position:fixed;bottom:0;right:0;z-index:200;}\ .hideCursor{cursor:none !important;}\ .hidden{visibility:hidden}\ .displayNone{display:none;}\ .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;}\ .pagingButtons:hover{color:#27E3EB;text-shadow: 1px 1px 10px #000}\ #previousImageButton{left:0;text-align:left;}\ #nextImageButton{right:0;text-align:right;}\ @-webkit-keyframes flashAnimation{0%{ text-shadow: none;}100%{text-shadow: 0px 0px 5px lightblue;}}\ .flash{-webkit-animation: flashAnimation .5s alternate infinite linear;}\ '; //the real constructor return function(){ //for holding img srcs and a pointer for traversing this.postData = []; this.linkIndex = 0; //set up the div and image for the popup this.mainView = null; this.mainImg = null; this.innerTD = null; this.topLayer = null; this.customStyle = null; this.textWrapper = null; this.leftArrow = null; this.rightArrow = null; this.bottomMenu = null; this.canPreload = false; this.shouldFitImage = false; this.mouseTimer = null; this.lastMousePos = {x: 0, y: 0}; //keycode object. Better than remembering what each code does. this.keys = {38: 'up', 40: 'down', 37: 'left', 39: 'right', 27: 'esc', 86:'v'}; this.open = function() { var V = window._4ChanImageViewer; // === Start constructing the viewer === // console.log("Building 4chan Image Viewer"); var currentThreadId = document.getElementsByClassName('thread')[0].id; //check if its the last thread opened, if so, remember where the index was. if(V.getPersistentValue(THREAD_KEY) === currentThreadId){ V.linkIndex = parseInt(V.getPersistentValue(INDEX_KEY)); }else{ V.linkIndex = 0; V.setPersistentValue(INDEX_KEY,0); } //set thread id V.setPersistentValue(THREAD_KEY,currentThreadId); //reset post array V.postData.length = 0; //add keybinding listener //Yeah, so, unsafeWindow is used here instead because at least in Tampermonkey //the safe window can fail to remove event listeners. unsafeWindow.addEventListener('keydown',V.arrowKeyListener,false); unsafeWindow.addEventListener('mousemove',V.menuWatcher,false); //grab postContainers var posts = document.getElementById('delform').getElementsByClassName('postContainer'); //get image links and post messages from posts var plength = posts.length; for(var i = 0; i < plength; ++i){ var file = posts[i].getElementsByClassName('file')[0]; if(file){ var currentLink = file.getElementsByClassName('fileThumb')[0].href; if(!currentLink){continue;} var type = V.getElementType(currentLink); var currentPostBlock = posts[i].getElementsByClassName('postMessage')[0]; var currentPostBacklinks = posts[i].getElementsByClassName('backlink')[0]; var blockQuote = document.createElement('blockQuote'); var backlinks = document.createElement('div'); if(currentPostBlock){ blockQuote.className = currentPostBlock.className + ' viewerBlockQuote'; blockQuote.innerHTML = currentPostBlock.innerHTML; V.add4chanListenersToLinks(blockQuote.getElementsByClassName('quotelink')); } if(currentPostBacklinks){ backlinks.className = currentPostBacklinks.className + ' viewerBacklinks'; backlinks.innerHTML = currentPostBacklinks.innerHTML; V.add4chanListenersToLinks(backlinks.getElementsByClassName('quotelink')); } V.postData.push({'imgSrc':currentLink,'type':type,'mBlock':blockQuote,'backlinks':backlinks}); } } //build wrapper V.mainView = document.createElement('div'); V.mainView.id = VIEW_ID; V.mainView.addEventListener('click',V.confirmExit, false); document.body.appendChild(V.mainView); //set up table for centering the content. Seriously, the alternatives are worse. V.mainView.innerHTML = '
'; V.innerTD = V.mainView.getElementsByTagName('td')[0]; //build image tag V.mainImg = document.createElement(V.postData[V.linkIndex].type); V.mainImg.src = V.postData[V.linkIndex].imgSrc; V.mainImg.id = IMG_ID; V.mainImg.classList.add("hideCursor"); V.mainImg.autoplay = true; V.mainImg.controls = false; V.mainImg.loop = true; V.innerTD.appendChild(V.mainImg); V.mainImg.addEventListener('click',V.clickImg,false); V.mainImg.onload = function(){ if(V.shouldFitImage){ V.fitHeightToScreen();} }; //start preloading to next image index V.canPreload = true; window.setTimeout(function(){V.runImagePreloading(V.linkIndex);},100); //add quote block/backlinks(first image always has second post quote) V.textWrapper = document.createElement('div'); V.textWrapper.addEventListener('click',V.eventStopper,false); V.textWrapper.id = 'viewerTextWrapper'; V.textWrapper.appendChild(V.postData[V.linkIndex].backlinks); V.textWrapper.appendChild(V.postData[V.linkIndex].mBlock); V.innerTD.appendChild(V.textWrapper); //build top layer V.topLayer = document.createElement('div'); V.topLayer.innerHTML = " "; V.topLayer.id=TOP_LAYER_ID; document.body.appendChild(V.topLayer); //build custom style tag V.customStyle = document.createElement('style'); V.customStyle.innerHTML = STYLE_TEXT; document.body.appendChild(V.customStyle); //build bottom menu var formHtml = '\ |\ \ '; V.bottomMenu = document.createElement('form'); V.bottomMenu.id = "viewerBottomMenu"; V.bottomMenu.className = 'hidden'; V.bottomMenu.innerHTML = formHtml; document.body.appendChild(V.bottomMenu); V.bottomMenu.addEventListener('click',V.menuClickHandler,false); V.menuInit(); //build arrow buttons V.leftArrow = document.createElement("div"); V.leftArrow.innerHTML = ''; V.leftArrow.id = "previousImageButton"; V.leftArrow.classList.add("pagingButtons","hidden"); V.rightArrow = document.createElement("div"); V.rightArrow.innerHTML = ''; V.rightArrow.id = "nextImageButton"; V.rightArrow.classList.add("pagingButtons","hidden"); V.leftArrow.addEventListener('click',function(event){event.stopImmediatePropagation();V.previousImg();},false); V.rightArrow.addEventListener('click',function(event){event.stopImmediatePropagation();V.nextImg();},false); V.mainView.appendChild(V.leftArrow); V.mainView.appendChild(V.rightArrow); //some fixes for weird behaviors V.innerTD.style.outline = '0'; V.innerTD.tabIndex = 1; V.innerTD.focus(); }; this.menuInit = function(){ var V = window._4ChanImageViewer; var menuControls = V.bottomMenu.getElementsByTagName('input'); for(var i = 0; i < menuControls.length; ++i){ var input = menuControls[i]; var cookieValue = V.getPersistentValue(input.id); if(cookieValue === 'true'){ input.checked = true; }else if(cookieValue === 'false'){ input.checked = false; } input.parentElement.classList.toggle('flash',input.checked); switch(input.id){ case WIDTH_KEY: V.setFitToScreenWidth(input.checked); break; case HEIGHT_KEY: V.setFitToScreenHeight(input.checked); break; } } }; this.menuClickHandler = function(){ var V = window._4ChanImageViewer; var menuControls = V.bottomMenu.getElementsByTagName('input'); for(var i = 0; i < menuControls.length; ++i){ var input = menuControls[i]; switch(input.id){ case WIDTH_KEY: V.setFitToScreenWidth(input.checked); break; case HEIGHT_KEY: V.setFitToScreenHeight(input.checked); break; } input.parentElement.classList.toggle('flash',input.checked); V.setPersistentValue(input.id,input.checked); } }; this.windowClick = function(event){ var V = window._4ChanImageViewer; event.preventDefault(); event.stopImmediatePropagation(); V.nextImg(); }; this.add4chanListenersToLinks = function(linkCollection){ for(var i = 0; i < linkCollection.length; ++i){ //These are the functions that 4chan uses linkCollection[i].addEventListener("mouseover", Main.onThreadMouseOver, false); linkCollection[i].addEventListener("mouseout", Main.onThreadMouseOut, false); } }; /* Event function for determining behavior of viewer keypresses */ this.arrowKeyListener = function(evt){ var V = window._4ChanImageViewer; switch(V.keys[evt.keyCode]){ case 'right': V.nextImg(); break; case 'left': V.previousImg(); break; case 'esc': V.remove(); break; } }; /* preloads images starting with the index provided */ this.runImagePreloading = function(index){ var V = window._4ChanImageViewer; if(index < V.postData.length){ if(V.canPreload){ if(V.postData[index].type === 'VIDEO'){ V.runImagePreloading(index+1); }else{ var newImage = document.createElement(V.postData[index].type); var loadFunc = function(){V.runImagePreloading(index+1);}; switch(V.postData[index].type){ case 'VIDEO': newImage.oncanplaythrough = loadFunc; break; case 'IMG': newImage.onload = loadFunc; break; } newImage.onerror = function(){ V.runImagePreloading(index+1); }; newImage.src = V.postData[index].imgSrc; } } } }; /* Sets the img and message to the next one in the list*/ this.nextImg = function () { var V = window._4ChanImageViewer; if (V.linkIndex === V.postData.length - 1) { V.topLayer.style.background = 'linear-gradient(to right,rgba(0,0,0,0) 90%,rgba(125,185,232,1) 100%)'; V.topLayer.style.opacity = '.5'; V.topLayer.style.visibility = "visible"; setTimeout(function () { V.topLayer.style.opacity = '0'; setTimeout(function () { V.topLayer.style.visibility = "hidden"; }, 200); }, 500); return; } else { V.changeData(1); } }; /* Sets the img and message to the previous one in the list*/ this.previousImg = function () { var V = window._4ChanImageViewer; if (V.linkIndex === 0) { V.topLayer.style.background = 'linear-gradient(to left,rgba(0,0,0,0) 90%,rgba(125,185,232,1) 100%)'; V.topLayer.style.opacity = '.5'; V.topLayer.style.visibility = "visible"; setTimeout(function () { V.topLayer.style.opacity = '0'; setTimeout(function () { V.topLayer.style.visibility = "hidden"; }, 200); }, 500); return; } else { V.changeData(-1); } }; this.changeData = function(delta){ var V = window._4ChanImageViewer; V.linkIndex = V.linkIndex + delta; if(V.postData[V.linkIndex].type !== V.mainImg.tagName){ V.mainImg = replaceElement(V.mainImg,V.postData[V.linkIndex].type); } console.log('Opening: "' + V.postData[V.linkIndex].imgSrc +'" at index ' + V.linkIndex); V.mainImg.src = V.postData[V.linkIndex].imgSrc; V.textWrapper.replaceChild(V.postData[V.linkIndex].backlinks,V.postData[V.linkIndex - delta].backlinks); V.textWrapper.replaceChild(V.postData[V.linkIndex].mBlock,V.postData[V.linkIndex - delta].mBlock); V.mainView.scrollTop = 0; V.setPersistentValue(INDEX_KEY,V.linkIndex); }; this.getElementType = function(src){ if(src.match(/\.(?:(?:webm)|(?:ogg)|(?:mp4))$/)){ return 'VIDEO'; }else{ return 'IMG'; } }; this.replaceElement = function(element,newType){ var V = window._4ChanImageViewer; var newElement = document.createElement(newType); newElement.className = element.className; newElement.id = element.id; newElement.style = element.style; newElement.autoplay = element.autoplay; newElement.controls = element.controls; newElement.loop = element.loop; newElement.addEventListener('click',V.clickImg,false); newElement.onload = function(){ if(V.shouldFitImage){ V.fitHeightToScreen();} }; element.parentElement.insertBefore(newElement,element); element.parentElement.removeChild(element); return newElement; }; /* Function for handling click image events*/ this.clickImg = function(event){ var V = window._4ChanImageViewer; event.stopPropagation(); V.nextImg(); }; this.eventStopper = function(event){ if(event.target.nodeName !== 'A'){ event.stopPropagation(); } }; this.confirmExit = function(){ var V = window._4ChanImageViewer; if(window.confirm('Exit Viewer?')){ V.remove(); } }; /* Removes the view and cleans up handlers*/ this.remove = function(){ var V = window._4ChanImageViewer; unsafeWindow.removeEventListener('keydown',V.arrowKeyListener,false); unsafeWindow.removeEventListener('mousemove',V.menuWatcher,false); document.body.removeEventListener('click',V.windowClick,true); document.body.removeChild(V.topLayer); document.body.removeChild(V.mainView); document.body.removeChild(V.customStyle); document.body.removeChild(V.bottomMenu); document.body.style.overflow="auto"; V.canPreload = false; window.setTimeout(function(){ delete window._4ChanImageViewer; },10); }; /*Mouse-move Handler that watches for when menus should appear and mouse behavior*/ this.menuWatcher = function(event){ var V = window._4ChanImageViewer; var height_offset = window.innerHeight - V.bottomMenu.offsetHeight; var width_offset = window.innerWidth - V.bottomMenu.offsetWidth; var center = window.innerHeight / 2; var halfArrow = V.leftArrow.offsetHeight / 2; if(event.clientX >= width_offset && event.clientY >= height_offset){ V.bottomMenu.className='bottomMenuShow'; }else if(V.bottomMenu.className==='bottomMenuShow'){ V.bottomMenu.className ='hidden'; } if((event.clientX <= (100) || event.clientX >= (window.innerWidth-100)) && (event.clientY <= (center + halfArrow) && event.clientY >= (center - halfArrow))){ V.rightArrow.classList.remove('hidden'); V.leftArrow.classList.remove('hidden'); }else{ V.rightArrow.classList.add('hidden'); V.leftArrow.classList.add('hidden'); } //avoids chrome treating mouseclicks as mousemoves if(event.clientX !== V.lastMousePos.x && event.clientY !== V.lastMousePos.y){ //mouse click moves to next image when invisible V.mainImg.classList.remove('hideCursor'); window.clearTimeout(V.mouseTimer); document.body.removeEventListener('click',V.windowClick,true); document.body.classList.remove('hideCursor'); if(event.target.id === V.mainImg.id){ //hide cursor if it stops, show if it moves V.mouseTimer = window.setTimeout(function(){ V.mainImg.classList.add('hideCursor'); document.body.classList.add('hideCursor'); document.body.addEventListener('click',V.windowClick,true); }, 200); } } V.lastMousePos.x = event.clientX; V.lastMousePos.y = event.clientY; }; /*Stores a key value pair as a cookie*/ this.setPersistentValue = function(key, value){ document.cookie = key + '='+value; }; /* Retrieves a cookie value via its key*/ this.getPersistentValue = function(key){ var cookieMatch = document.cookie.match(new RegExp(key+'\\s*=\\s*([^;]+)')); if(cookieMatch){ return cookieMatch[1]; }else{ return null; } }; this.setFitToScreenHeight = function(shouldFitImage){ var V = window._4ChanImageViewer; if(shouldFitImage){ V.fitHeightToScreen(); }else{ V.mainImg.style.maxHeight = ''; } }; this.setFitToScreenWidth = function(shouldFitImage){ var V = window._4ChanImageViewer; V.mainImg.style.maxWidth = shouldFitImage ? '100%' : 'none'; }; /* Fits image to screen height*/ this.fitHeightToScreen = function(){ var V = window._4ChanImageViewer; //sets the changeable properties to the image's real size var height = V.mainImg.naturalHeight; V.mainImg.style.maxHeight = height + 'px'; //actually tests if it is too high including padding var heightDiff = (V.mainImg.clientHeight > height)? V.mainImg.clientHeight - V.mainView.clientHeight: height - V.mainView.clientHeight; if(heightDiff > 0){ V.mainImg.style.maxHeight = (height - heightDiff) + 'px'; }else{ V.mainImg.style.maxHeight = height + 'px'; } }; };//end return function })(); //Build the open button var openBttn = document.createElement('button'); openBttn.style.position = 'fixed'; openBttn.style.bottom = '0'; openBttn.style.right = '0'; openBttn.innerHTML = "Open Viewer"; openBttn.addEventListener('click',function(){ //make the viewer and put it on the window so we can clean it up later window._4ChanImageViewer = new Viewer(); window._4ChanImageViewer.open(); },false); document.body.appendChild(openBttn);