// ==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 = " ";
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>〈</span>';
leftArrow.id = "previousImageButton";
leftArrow.classList.add("pagingButtons","hidden");
rightArrow = document.createElement("div");
rightArrow.innerHTML = '<span>〉</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';
}
}