4chan Image Browser

Opens current thread Images in 4chan into a popup viewer, tested in Tampermonkey

As of 14. 09. 2014. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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.1
// @copyright  2014+, Gyst
// ==/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 = '<table id="'+IMG_TABLE_ID+'"><tr><td></td></tr></table>';
            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 = "&nbsp;";
            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>&#9001;</span>';
            V.leftArrow.id = "previousImageButton";
            V.leftArrow.classList.add("pagingButtons","hidden");

            V.rightArrow = document.createElement("div");
            V.rightArrow.innerHTML = '<span>&#9002;</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.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 = 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(){
                        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);