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