- // ==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.2
- // @copyright 2014+, Gyst
- // @grant unsafeWindow
- // ==/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_BOX_ID = "imageBox";
- var TOP_LAYER_ID = "viewerTopLayer";
- var IMG_WRAPPER_ID = 'mainImgWrapper';
-
- //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_BOX_ID+' {display:flex;align-items:center;justify-content:center;flex-direction: column;min-height:100%;}\
- #'+IMG_WRAPPER_ID+' {width:100%;}\
- #'+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.centerBox = null;
- this.topLayer = null;
- this.customStyle = null;
- this.textWrapper = null;
-
- this.leftArrow = null;
- this.rightArrow = null;
-
- this.bottomMenu = null;
-
- this.canPreload = false;
- this.shouldFitHeight = false;
- //for deferring functions that need image dimensions
- this.mainImgLoaded = 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 flex box for centering content
- V.centerBox = document.createElement('div');
- V.centerBox.id = IMG_BOX_ID;
- V.mainView.appendChild(V.centerBox);
-
- //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;
-
- var imgDiv = document.createElement('div');
- imgDiv.id = IMG_WRAPPER_ID;
- imgDiv.appendChild(V.mainImg);
- V.centerBox.appendChild(imgDiv);
-
- V.mainImg.addEventListener('click',V.clickImg,false);
-
- //set shouldFit Height so image can know about it if it loads before menuInit()
- var isHeight = V.getPersistentValue(HEIGHT_KEY);
- V.shouldFitHeight = isHeight? true : false;
- V.mainImg.onload = function(){
- window._4ChanImageViewer.imageLoadHandler();
- };
-
- //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.centerBox.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 = '<label><input id="'+WIDTH_KEY+'" type="checkbox" checked="checked" />Fit Image to Width</label>\
- <span>|</span>\
- <label><input id="'+HEIGHT_KEY+'" type="checkbox" />Fit Image to Height</label>\
- ';
- 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 = '<span>〈</span>';
- V.leftArrow.id = "previousImageButton";
- V.leftArrow.classList.add("pagingButtons","hidden");
-
- V.rightArrow = document.createElement("div");
- V.rightArrow.innerHTML = '<span>〉</span>';
- 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.centerBox.style.outline = '0';
- V.centerBox.tabIndex = 1;
- V.centerBox.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(V && 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.mainImgLoaded = false;
-
- V.linkIndex = V.linkIndex + delta;
-
- if(V.postData[V.linkIndex].type !== V.mainImg.tagName){
- V.mainImg = V.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(){
- window._4ChanImageViewer.imageLoadHandler();
- };
- 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 + ';expires=Thu, 01 Jan 3000 00:00:00 UTC;domain=.4chan.org;path=/';
- };
-
- /* 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;
- V.shouldFitHeight = shouldFitImage;
- //ignore if image has no height as it is likely not loaded.
- if(shouldFitImage && V.mainImg.naturalHeight){
- V.fitHeightToScreen();
- }else{
- V.mainImg.style.maxHeight = '';
- }
- };
- this.setFitToScreenWidth = function(shouldFitImage){
- var V = window._4ChanImageViewer;
- V.mainImg.style.maxWidth = shouldFitImage ? '100%' : 'none';
- };
-
-
- this.imageLoadHandler = function(){
- var V = window._4ChanImageViewer;
- if(V.shouldFitHeight) {
- V.fitHeightToScreen();
- }
- };
-
- /* 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);
-
-
-
-
-
-
-
-