4chan Image Browser

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

目前為 2014-05-24 提交的版本,檢視 最新版本

// ==UserScript==
// @name       4chan Image Browser
// @namespace  IdontKnowWhatToDoWithThis
// @description Opens current thread Images in 4chan into a popup viewer, tested in Tampermonkey
// @match   http*://*.4chan.org/*/res/*
// @match   http*://*.4chan.org/*/thread/*
// @version 5.2
// @copyright  2013+, Gyst
// ==/UserScript==
/*jshint multistr:true */
/*jshint browser:true */
/*jshint smarttabs:true */
/* jshint -W099 */
/* jshint -W015 */
/*global Main:false */



//cookieInfo
var INDEX_KEY = "imageBrowserIndexCookie";
var THREAD_KEY = "imageBrowserThreadCookie";
var WIDTH_KEY = "imageBrowserWidthCookie";
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";


//for holding img srcs and a pointer for traversing
var postData = [];
var linkIndex = 0;

//set up the div and image for the popup
var mainView;
var mainImg;
var innerTD;
var topLayer;
var customStyle;
var textWrapper;

var leftArrow;
var rightArrow;


var bottomMenu;

var canPreload = false;
var shouldFitImage = false;


var mouseTimer;

var lastMousePos = {x: 0, y: 0};




//keycode object.  Better than remembering what each code does.
var keys = {38: 'up', 40: 'down', 37: 'left', 39: 'right', 27: 'esc', 86:'v'};

//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;}\
    ';

//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',buildPopup, false);
document.body.appendChild(openBttn);



/* Builds the popup and adds it to the page*/
function buildPopup(){
    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(getPersistentValue(THREAD_KEY) === currentThreadId){
        linkIndex = parseInt(getPersistentValue(INDEX_KEY)); 
    }else{
        linkIndex = 0;
        setPersistentValue(INDEX_KEY,0);
    }
    
   //set thread id
    setPersistentValue(THREAD_KEY,currentThreadId);
    
    //reset post array
    postData.length=0;

    
	//add keybinding listener
    window.addEventListener('keydown',arrowKeyListener,false);
    
    window.addEventListener('mousemove',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 = 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;
                add4chanListenersToLinks(blockQuote.getElementsByClassName('quotelink'));
			
			}
			if(currentPostBacklinks){
				backlinks.className = currentPostBacklinks.className + ' viewerBacklinks';
				backlinks.innerHTML = currentPostBacklinks.innerHTML;
				add4chanListenersToLinks(backlinks.getElementsByClassName('quotelink'));
			}
			
			postData.push({'imgSrc':currentLink,'type':type,'mBlock':blockQuote,'backlinks':backlinks});
        }
    }

        

    //build wrapper
    mainView = document.createElement('div');
    mainView.id = VIEW_ID;
    mainView.addEventListener('click',confirmExit, false);
    
    document.body.appendChild(mainView);
    //set up table for centering the content.  Seriously, the alternatives are worse.
    mainView.innerHTML = '<table id="'+IMG_TABLE_ID+'"><tr><td></td></tr></table>';
	innerTD = mainView.getElementsByTagName('td')[0];
    

    
    //build image tag
    mainImg = document.createElement(postData[linkIndex].type);
    mainImg.src= postData[linkIndex].imgSrc;
    mainImg.id = IMG_ID;
	mainImg.classList.add("hideCursor");
    mainImg.autoplay = true;
    mainImg.controls = false;
    mainImg.loop = true;
    
    innerTD.appendChild(mainImg);
    
    mainImg.addEventListener('click',clickImg,false);
	mainImg.onload = function(){
		if(shouldFitImage){ fitHeightToScreen();}
	};

   
   
    //start preloading to next image index
    canPreload = true;
    setTimeout(function(){runImagePreloading(1);},100);
   

    
    //add quote block/backlinks(first image always has second post quote)
    textWrapper = document.createElement('div');
    textWrapper.addEventListener('click',eventStopper,false);
    textWrapper.id = 'viewerTextWrapper';
    textWrapper.appendChild(postData[linkIndex].backlinks);
	textWrapper.appendChild(postData[linkIndex].mBlock);
	innerTD.appendChild(textWrapper);
    

    //build top layer
    topLayer = document.createElement('div');
    topLayer.innerHTML = "&nbsp;";
    topLayer.id=TOP_LAYER_ID;
    
    document.body.appendChild(topLayer);
    

    //build custom style tag
    customStyle = document.createElement('style');
    customStyle.innerHTML = STYLE_TEXT;
    document.body.appendChild(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>\
                    ';
    bottomMenu = document.createElement('form');
    bottomMenu.id = "viewerBottomMenu";
    bottomMenu.className = 'hidden';
    bottomMenu.innerHTML = formHtml;
    document.body.appendChild(bottomMenu);
    bottomMenu.addEventListener('click',menuClickHandler,false);
    menuInit();
    
    //build arrow buttons
    leftArrow = document.createElement("div");
    leftArrow.innerHTML = '<span>&#9001;</span>';
    leftArrow.id = "previousImageButton";
    leftArrow.classList.add("pagingButtons","hidden");
    
    rightArrow = document.createElement("div");
    rightArrow.innerHTML = '<span>&#9002;</span>';
    rightArrow.id = "nextImageButton";
    rightArrow.classList.add("pagingButtons","hidden");
    
    leftArrow.addEventListener('click',function(event){event.stopImmediatePropagation();previousImg();},false);
    rightArrow.addEventListener('click',function(event){event.stopImmediatePropagation();nextImg();},false);
    mainView.appendChild(leftArrow);
    mainView.appendChild(rightArrow);
    
    
    //some fixes for weird behaviors
	innerTD.style.outline = '0';
	innerTD.tabIndex = 1;
    innerTD.focus();
    

}

function menuInit(){
    var menuControls = bottomMenu.getElementsByTagName('input');
    for(var i = 0; i < menuControls.length; ++i){
        var input = menuControls[i];
        var cookieValue = 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:
                setFitToScreenWidth(input.checked);
                break;
                
         case HEIGHT_KEY:  
                setFitToScreenHeight(input.checked);
                break;
        }
        

        
    }
    
}


function menuClickHandler(){
    var menuControls = bottomMenu.getElementsByTagName('input');
    
    
    for(var i = 0; i < menuControls.length; ++i){
        var input = menuControls[i];
        
        switch(input.id){
         case WIDTH_KEY:
                setFitToScreenWidth(input.checked);
                break;
                
         case HEIGHT_KEY:  
                setFitToScreenHeight(input.checked);
                break;
        }
        
        input.parentElement.classList.toggle('flash',input.checked);
        
        setPersistentValue(input.id,input.checked);
        
    }

}

function windowClick(event){
    event.preventDefault();
    event.stopImmediatePropagation();
    nextImg();
    
}

function add4chanListenersToLinks(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 */
function arrowKeyListener(evt){

    switch(keys[evt.keyCode]){
        case 'right':	
            nextImg();
            break;
            
        case 'left':	
            previousImg();
            break;

        case 'esc':		
            clearDiv();
            break;
    }

}


/* preloads images starting with the index provided */
function runImagePreloading(index){

    if(index < postData.length){

        if(canPreload){
            
            var newImage = document.createElement(postData[index].type);
            console.log(newImage);
            
            var loadFunc = function(){runImagePreloading(index+1);};
            switch(postData[index].type){
                case 'VIDEO':
                    newImage.oncanplaythrough = loadFunc;
                break;
                case 'IMG':
                    newImage.onload = loadFunc;
                break;
            }
            newImage.onerror = function(){
                runImagePreloading(index+1);
            };
            
            newImage.src = postData[index].imgSrc;
        
        }
        

    }
}



/* Sets the img and message to the next one in the list*/
function nextImg(){

	if(linkIndex === postData.length - 1){
        topLayer.style.background = 'linear-gradient(to right,rgba(0,0,0,0) 90%,rgba(125,185,232,1) 100%)';
        topLayer.style.opacity = '.5';
		topLayer.style.visibility = "visible";

		setTimeout(function(){
    		topLayer.style.opacity = '0';
			setTimeout(function(){topLayer.style.visibility = "hidden";},200);
		}, 500);
		return;
	}
	else{
		changeData(1);
	}
}

/* Sets the img and message to the previous one in the list*/
function previousImg(){

	if(linkIndex === 0){
		
        topLayer.style.background = 'linear-gradient(to left,rgba(0,0,0,0) 90%,rgba(125,185,232,1) 100%)';
        topLayer.style.opacity = '.5';
		topLayer.style.visibility = "visible";

		setTimeout(function(){
    		topLayer.style.opacity = '0';
			setTimeout(function(){topLayer.style.visibility = "hidden";},200);
		}, 500);

        return;
	}
	else{
        changeData(-1);

	}
}

function changeData(delta){
    linkIndex = linkIndex + delta;

    if(postData[linkIndex].type !== mainImg.tagName){
        mainImg = replaceElement(mainImg,postData[linkIndex].type);   
    }
    
    mainImg.src = postData[linkIndex].imgSrc;

    textWrapper.replaceChild(postData[linkIndex].backlinks,postData[linkIndex - delta].backlinks);
    textWrapper.replaceChild(postData[linkIndex].mBlock,postData[linkIndex - delta].mBlock);

    mainView.scrollTop = 0;

    setPersistentValue(INDEX_KEY,linkIndex);
}

function getElementType(src){
    if(src.match(/\.(?:(?:webm)|(?:ogg)|(?:mp4))$/)){
        return 'VIDEO';
    }else{
        return 'IMG';
    }
}

function replaceElement(element,newType){
    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',clickImg,false);
	newElement.onload = function(){
		if(shouldFitImage){ fitHeightToScreen();}
	};
    element.parentElement.insertBefore(newElement,element);
    element.parentElement.removeChild(element);
    return newElement;
}



/* Function for handling click image events*/
function clickImg(event){
    
    event.stopPropagation();
    nextImg();

}

function eventStopper(event){
    if(event.target.nodeName !== 'A'){
        event.stopPropagation();
    }
   
}

function confirmExit(){
    if(window.confirm('Exit Viewer?')){
        clearDiv();
    }

}

/* Removes the popup and other things added by the build method*/
function clearDiv(){
    window.removeEventListener('keydown',arrowKeyListener);
    window.removeEventListener('mousemove',menuWatcher);
    document.body.removeChild(topLayer);
    document.body.removeChild(mainView);
    document.body.removeChild(customStyle);
    document.body.removeChild(bottomMenu);
    document.body.style.overflow="auto";
    canPreload = false;

}




/*Mouse-move Handler that watches for when menus should appear and mouse behavior*/
function menuWatcher(event) {  

    var height_offset = window.innerHeight - bottomMenu.offsetHeight;
    var width_offset = window.innerWidth - bottomMenu.offsetWidth;
    var center = window.innerHeight / 2;
    var halfArrow = leftArrow.offsetHeight / 2;
    
    if(event.clientX >= width_offset && event.clientY >= height_offset){
    	bottomMenu.className='bottomMenuShow';   
    }else if(bottomMenu.className==='bottomMenuShow'){
    	bottomMenu.className ='hidden';
    }    
	
    if((event.clientX <= (100) || event.clientX >= (window.innerWidth-100)) && 
       (event.clientY <= (center + halfArrow) && event.clientY >= (center - halfArrow))){
        rightArrow.classList.remove('hidden');
        leftArrow.classList.remove('hidden');
    }else{
        rightArrow.classList.add('hidden');
        leftArrow.classList.add('hidden');
    }

	//avoids chrome treating mouseclicks as mousemoves
    if(event.clientX !== lastMousePos.x && event.clientY !== lastMousePos.y){
    	//mouse click moves to next image when invisible
		mainImg.classList.remove('hideCursor');
        
        window.clearTimeout(mouseTimer);
        document.body.removeEventListener('click',windowClick,true);
        document.body.classList.remove('hideCursor');
        if(event.target.id === mainImg.id){
            //hide cursor if it stops, show if it moves
            mouseTimer = window.setTimeout(function(){
                        mainImg.classList.add('hideCursor');
                        document.body.classList.add('hideCursor');
                        document.body.addEventListener('click',windowClick,true);
                        
                    }, 200);
        }

        

    }

    lastMousePos.x = event.clientX;
    lastMousePos.y = event.clientY;
        
}

/*Stores a key value pair as a cookie*/
function setPersistentValue(key, value){

    document.cookie = key + '='+value;

}

/* Retrieves a cookie value via its key*/
function getPersistentValue(key){
    var cookieMatch = document.cookie.match(new RegExp(key+'\\s*=\\s*([^;]+)'));
    if(cookieMatch){
		return cookieMatch[1];
    }else{
    	return null;   
    }


}


function setFitToScreenHeight(shouldFitImage){
	if(shouldFitImage){
        fitHeightToScreen();
    }else{
        mainImg.style.maxHeight = '';
    }
}
function setFitToScreenWidth(shouldFitImage){

    mainImg.style.maxWidth = shouldFitImage ? '100%' : 'none';

}


/* Fits image to screen height*/
function fitHeightToScreen(){

    //sets the changeable properties to the image's real size
    var height = mainImg.naturalHeight;
    mainImg.style.maxHeight = height + 'px';

    //actually tests if it is too high including padding
    var heightDiff = (mainImg.clientHeight > height)?
        mainImg.clientHeight - mainView.clientHeight:
    	height -  mainView.clientHeight;

    if(heightDiff > 0){      
        mainImg.style.maxHeight = (height - heightDiff) + 'px';
    }else{
    	mainImg.style.maxHeight = height + 'px';	
    }
}

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址