Greasy Fork

4chan Image Browser

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

目前为 2014-09-14 提交的版本。查看 最新版本

// ==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
// ==/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 = 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);