// ==UserScript==
// @name stream4chan
// @namespace http://tampermonkey.net/
// @version 3.0
// @description Click the button to stream all webms in a 4chan thread
// @author Lauchlan105
// @match http://boards.4chan.org/*/thread/*
// @grant none
// @downloadURL none
// ==/UserScript==
//////////////////
// # Settings # //
//////////////////
var settingsArray = [
//Loop whole thread
true,
//Play automatically
true,
//Randomize on startup
false,
//Play Webms
true,
//Show Webm controls
true,
//Play webm sound
true,
//Play Gifs
true,
//Gif duration (Seconds)
3,
//Play Images
true,
//Image duration (Seconds)
3,
//Open Stream4chan on startup
true
];
///////////////////
// # Variables # //
///////////////////
//Placeholder variables
var globalTimeout;
var webm;
var gif;
var png;
var jpg;
var SOT;
var EOT;
var noneSelected;
var allContent;
var usedContent;
var currentContent;
//////////////////////
// # Object Model # //
//////////////////////
//Class constructor for content elements
class Media{
constructor(thumb, source, id){
// local scope variable for object access via video/thumbnail elements
var obj = this;
this.position = 0;
this.id = id === undefined ? "" : id;
this.thumb = document.createElement('img');
this.thumb.src = thumb;
this.thumb.setAttribute('class','sfc-slide-preview');
this.type = mediaType(source);
//Handles deleted files/invalid media
if(this.type === undefined || this.type === null){
//Force null for simpler conditionals
this.type = null;
console.log('Media has been given invalid source');
console.log(' Given Arguments:');
console.log(' thumb: ' + thumb);
console.log(' source: ' + source);
console.log(' id: ' + id);
console.log('');
console.log(' All functions will be assigned placeholders');
this.play = function(){
console.log('This object is not valid and');
console.log(calledFunction + ' cannot be called in this object');
return;
};
this.pause = function(){
console.log('This object is not valid and');
console.log(arguments.callee.caller.toString() + ' cannot be called in this object');
return;
};
this.highlight = function(){
console.log('This object is not valid and');
console.log(arguments.callee.caller.toString() + ' cannot be called in this object');
return;
};
this.unhighlight = function(){
console.log('This object is not valid and');
console.log(arguments.callee.caller.toString() + ' cannot be called in this object');
return;
};
this.select = function(){
console.log('This object is not valid and');
console.log(arguments.callee.caller.toString() + ' cannot be called in this object');
return;
};
this.deselect = function(){
console.log('This object is not valid and');
console.log(arguments.callee.caller.toString() + ' cannot be called in this object');
return;
};
this.resize = function(){
console.log('This object is not valid and');
console.log(arguments.callee.caller.toString() + ' cannot be called in this object');
return;
};
return false;
}
if(this.type === webm){
this.media = document.createElement('video');
this.media.setAttribute("id", "sfc-webm");
this.media.setAttribute("controls","");
this.media.setAttribute("loop","");
this.media.loop = true;
this.media.setAttribute("autoplay","");
this.media.autoplay = false;
this.media.setAttribute("preload","");
this.media.preload = "none";
}else if(this.type === png || this.type === gif || this.type === jpg){
this.media = document.createElement('img');
this.media.setAttribute("id","sfc-img");
}
this.media.setAttribute("class", "sfc-media");
this.media.src = source;
///////////////////
//MEDIA FUNCTIONS//
///////////////////
this.play = function(){
if(obj !== SOT && obj !== EOT && obj !== noneSelected){
if(obj.type == webm){
obj.media.volume = op_playSound.checked ? obj.media.volume : 0;
obj.media.play();
}else if(obj.type == gif){
//Restart gif
obj.media.src = obj.media.src;
}
//Load neighbouring media
var prevMedia = obj.position === 0 ? usedContent[usedContent.length - 1] : usedContent[obj.position - 1];
var nextMedia = obj.position === usedContent.length - 1 ? usedContent[0] : usedContent[obj.position + 1];
if(prevMedia.type === webm){
prevMedia.media.load();
}else if(nextMedia.type === webm){
nextMedia.media.load();
}
}
};
this.pause = function(){
clearTimeout(globalTimeout);
if(obj.type == webm){
obj.media.pause();
}
};
///////////////////////
//THUMBNAIL FUNCTIONS//
///////////////////////
var highlight = function(){
obj.thumb.style.border = "2px solid gainsboro";
/*
//Scroll into view
var pc = document.getElementById('pc' + id.substr(id.lastIndexOf('p') + 1));
pc.scrollIntoView();
//Set container style to mimic focused content
pc.style.background = '#f0e0d6';
pc.style.border = '1px solid #D99F91!important';
*/
};
var unhighlight = function(){
obj.thumb.style.border = "2px solid transparent";
/*
//Remove focused content styles
var pc = document.getElementById('pc' + id.substr(id.lastIndexOf('p') + 1));
pc.style.background = '#F0C0B0!important';
pc.style.border = '1px solid #D9BFB7';
*/
};
///////////////////////////
//MISCELLANEOUS FUNCTIONS//
///////////////////////////
this.select = function(){
//Deselect active content
if(currentContent !== null){
currentContent.deselect();
}
//Set currentContent to this object
currentContent = obj;
//Highlight thumbnail border
highlight();
//Add media to stage
obj.media.controls = op_controls.checked;
el_stage.appendChild(obj.media);
//Play Media
obj.play();
//resize media
obj.resize();
//Update auto playing
updateAutoplay();
//Moves gallery so selected object is centered
//half of thumbnail width
el_internalSlider.style.transform = '';
var middleOfThumb = getPageTopLeft(obj.thumb).left + (obj.thumb.clientWidth/2);
var middleOfWindow = window.innerWidth/2;
var distance = middleOfThumb - middleOfWindow;
var distanceFromMiddle = distance > 0 ? distance*-1 : distance + ((-1*distance)*2);
el_internalSlider.style.transform = 'translateX( ' + distanceFromMiddle + 'px)';
};
this.deselect = function(){
obj.pause();
obj.media.currentTime = 0;
unhighlight();
el_stage.innerHTML = "";
currentContent = null;
};
this.thumb.onclick = function(){
if(obj.thumb.style.border == "2px solid gainsboro"){
obj.deselect();
}else{
obj.select();
}
};
this.resize = function(){
var resizeTimeout = setTimeout(function(){}, 0);
//Recursive resize function
//repeats every {interval} seconds
//if interval is undefined or < 0.01, default to 0.01
function execResize(interval){
//if interval is below 0.01, set to minimum 0.01
if(interval){
if(interval < 0.05){
interval = 0.05;
}
}
if(obj === currentContent){
var setByWidth = true;
//Set to max width
obj.media.style.width = window.innerWidth - (el_stagePrev.clientWidth + el_stageNext.clientWidth) + 'px';
obj.media.style.height = 'auto';
//if media height exceeds the stage height
if(obj.media.clientHeight > el_stage.clientHeight){
//Set to max height instead
console.log('too tall');
obj.media.style.height = el_stage.clientHeight + 'px';
obj.media.style.width = 'auto';
setByWidth = false;
}
if(setByWidth){
//if full width, set height padding
// var difHeight = (el_stage.clientHeight - obj.media.clientHeight)/2;
// var topMarg = (difHeight) - ( difHeight%1 ); //Minus any decimals
// obj.media.style.marginTop = topMarg + 'px';
}else{
//if full height, set width padding
// var difWidth = (el_stage.clientWidth - obj.media.clientWidth)/2;
// var leftMarg = (difWidth) - ( difWidth%1 ); //Minus any decimals
// obj.media.style.marginLeft = leftMarg + 'px';
}
if(interval){
setTimeout(function(){
execResize(interval);
}, interval*1000);
}else{
return;
}
}else{
clearTimeout(resizeTimeout);
}
return;
}
//Continue resizing every 1 second till video ends
execResize();
execResize();
execResize();
execResize();
execResize();
};
this.media.getObj = function(){ return obj; };
this.thumb.getObj = function(){ return obj; };
return true;
}
}
//////////////
// # Main # //
//////////////
(function(){
insertElements();
initVars();
initPlaceholders();
startInteractions();
startEventListeners();
applyDefaulSettings();
//Show and hide gallery to force load thumbnails
//otherwise .select() does not work until gallery is shown
showGallery(true);
showGallery(false);
sfc.style.display = "none"; //Force hide SFC
showSFC(false); //Force styles to be ready for fade in
//If open on startup is selected -> open on startup
if(settingsArray[10]){
showSFC(true);
usedContent[0].select();
}
})();
/////////////////////////////////
// # Initial Setup Functions # //
/////////////////////////////////
//Insert html for buttons and modal
function insertElements(){
//Start Button
var btn1 = '[start]';
var btn2 = '[resume]';
//Add span to nav
var nav = document.getElementsByClassName('navLinks desktop');
for(var i = 0; i < nav.length; i++){
var span = document.createElement('span');
span.innerHTML = btn1 + " " + btn2;
span.className = 'sfc-nav';
span.style.display = nav[i].style.display;
nav[i].parentNode.insertBefore(span, nav[i]);
nav[i].parentNode.insertBefore(document.getElementById('op'), nav[i]);
}
var html = '
';
var css = ' ';
var sfc = document.createElement('div');
sfc.setAttribute('id','sfc');
sfc.innerHTML = html + css;
var target = document.getElementsByClassName('thread');
for(i = 0; i < target.length; i++){
target[i].prepend(sfc);
}
}
//Create and initialize global variables for easy access to HTML elements
function initVars(){
//Custom function to find elements while
//alerting console of errors in case of null || undefined
function getEl(elName){
var temp = document.getElementById(elName);
if(temp === null || temp === undefined){
temp = document.getElementsByClassName(elName)[0];
if(temp === null || temp === undefined){
console.log('### ERROR ###');
console.log('initVars: getEl(\'' + elName +'\') returned... ');
console.log(temp);
}
}
return temp;
}
//Main Page
el_startBtn = getEl('sfc-start');
el_resumeBtn = getEl('sfc-resume');
//Modal
el_sfc = getEl('sfc');
//Stage Area
el_stage = getEl('sfc-stage');
el_stagePrev = getEl('sfc-main-prev');
el_stageNext = getEl('sfc-main-next');
//Utility buttons
el_util = getEl("sfc-utility");
el_galleryBtn = getEl("sfc-main-gallery");
el_settingsBtn = getEl("sfc-main-settings");
//Gallery Area
el_gallery = getEl("sfc-gallery");
el_slider = getEl('sfc-slider');
el_internalSlider = getEl('sfc-slider-internal');
el_sliderPrev = getEl('sfc-gallery-prev');
el_sliderNext = getEl('sfc-gallery-next');
//Settings and option area
el_settings = getEl("sfc-settings-column");
el_settingsExit = getEl("sfc-settings-exit");
op_loopAll = getEl('stream4chan-loopAll');
op_auto = getEl('stream4chan-auto');
op_random = getEl('stream4chan-random');
op_shuffle = getEl('stream4chan-shuffle');
op_webms = getEl('stream4chan-webms');
op_controls = getEl('stream4chan-controls');
op_playSound = getEl('stream4chan-playSound');
op_gifs = getEl('stream4chan-gifs');
op_gif_duration = getEl('stream4chan-gif-duration');
op_imgs = getEl('stream4chan-imgs');
op_img_duration = getEl('stream4chan-img-duration');
}
//Inititialize values to placeholder variables
function initPlaceholders(){
//Type placeholders. Less quotations in code
webm = 'webm';
gif = 'gif';
png = 'png';
jpg ='jpg';
//Start of thread
//Object based placeholder for the beginning of the thread (used when loopAll is unchecked)
SOT = new Media("https://dummyimage.com/1920x1080/000000/ffffff.png","https://dummyimage.com/1920x1080/000000/ffffff.png");
SOT.media.src = ""; //https://dummyimage.com/1920x1080/000000/ffffff.png&text=Start+of+thread";
SOT.thumb.src = ""; //https://dummyimage.com/480x270/000000/ffffff.png&text=Start+of+thread";
SOT.type = "SOT";
//End of thread
//Object based placeholder for the end of the thread (used when loopAll is unchecked)
EOT = new Media("https://dummyimage.com/1920x1080/000000/ffffff.png","https://dummyimage.com/1920x1080/000000/ffffff.png");
EOT.media.src = ""; //https://dummyimage.com/1920x1080/000000/ffffff.png&text=End+of+thread
EOT.thumb.src = ""; //https://dummyimage.com/480x270/000000/ffffff.png&text=End+of+thread
EOT.type = "EOT";
//Object based placeholder for when there is no applicable media found or nothing is selected
noneSelected = new Media("https://dummyimage.com/1920x1080/000000/ffffff.png","https://dummyimage.com/1920x1080/000000/ffffff.png");
noneSelected.media.src = "https://dummyimage.com/1920x1080/000000/ffffff.png&text=No+Media+Selected";
noneSelected.thumb.src = "https://dummyimage.com/480x270/000000/ffffff.png&text=No+Media+Selected";
allContent = getContent();
usedContent = getUsedContent();
currentContent = noneSelected;
}
//////////////////////////////////////////////
// # SFC Control and Animation Functions # //
//////////////////////////////////////////////
//Links functions with page controllers
//eg: making gallery button show/hide gallery
function startInteractions(){
//Apply functionality: click start to show modal and play first media item
el_startBtn.onclick = function(){
showSFC(true);
if(op_auto.checked){
usedContent[0].select();
}
};
el_resumeBtn.onclick = function(){
showSFC(true);
currentContent.play();
updateAutoplay();
};
//Apply functionality: click gallery button to show/hide gallery
el_gallery.style.transform = "translateY(100%)";
el_galleryBtn.onclick = showGallery;
//Apply functionality: click settings button to show settings
//Click exit button to exit settings
el_settingsBtn.onclick = function(){ showSettings(true); };
el_settingsExit.onclick = function(){ showSettings(false); };
}
//Applies default settings
// • Default settings are on line 5
function applyDefaulSettings(){
op_loopAll.checked = settingsArray[0];
op_auto.checked = settingsArray[1];
op_random.checked = settingsArray[2];
op_webms.checked = settingsArray[3];
op_controls.checked = settingsArray[4];
op_playSound.checked = settingsArray[5];
op_gifs.checked = settingsArray[6];
op_gif_duration.value = settingsArray[7];
op_imgs.checked = settingsArray[8];
op_img_duration.value = settingsArray[9];
}
//Toggles showing the modal
function showSFC(bool){
function show(){
document.body.style.overflow = "hidden";
el_sfc.style.display = "block";
setTimeout(function(){
el_sfc.style.opacity = 1;
}, 40);
return true;
}
function hide(){
showGallery(false);
showSettings(false);
currentContent.pause();
el_sfc.style.opacity = 0;
el_sfc.addEventListener("transitionend", function() {
if(el_sfc.style.opacity == 0){
el_sfc.style.display = "none";
el_sfc.removeEventListener("transitionend", function(){}, false);
document.body.style.overflow = "scroll";
}
}, false);
return true;
}
if(bool === true){
show();
}else if (bool === false){
hide();
}else if (isShown(el_sfc)){
show();
}else{
hide();
}
return false;
}
//Toggles showing the gallery
function showGallery(bool){
function show(){
//Sets internal gallery slider to appropriate width
//'if' statements causes this to only fire once
if(el_internalSlider.style.width == ""){
updateGallery();
}
el_gallery.style.transform = "translateY(0px)";
el_util.style.transform = "translateY(-" + el_gallery.clientHeight + "px)";
return true;
}
function hide(){
el_gallery.style.transform = "translateY(100%)";
el_util.style.transform = "translateY(0)";
return true;
}
if(bool === true){
show();
}else if(bool === false){
hide();
}else if(el_gallery.style.transform == "translateY(100%)"){
show();
}else{
hide();
}
return false;
}
//Toggles showing the settings
function showSettings(bool){
function show(){
el_settings.style.display = "flex";
return true;
}
function hide(){
el_settings.style.display = "none";
return true;
}
if(bool === true){
show();
}else if(bool === false){
hide();
}else if(el_settings.style.display == "none" || el_settings.style.display === ""){
show();
}else{
hide();
}
return false;
}
//Parse through el_sfc, el_settings or el_gallery
//Return boolean indicating it's state
function isShown(el){
if(el === el_sfc){
return !(el_sfc.style.display == "none");
}
if(el === el_settings){
return !(el_settings.style.display == "none" || el_settings.style.display === "");
}
if(el === el_gallery){
return !(el_gallery.style.transform == "translateY(100%)");
}
}
/////////////////////////////////////////
// # Media and Media Array Functions # //
/////////////////////////////////////////
//Updates usedContent array, populates gallery and readjusts width
function updateGallery(){
//Update contents of usedContent array
usedContent = getUsedContent();
//Change currentContent to closest valid content
if(currentContent !== noneSelected && currentContent !== null){
if(!canPlay(currentContent)){
var newContent = null;
//If usedContent actually has something in it
if(usedContent.length > 0){
var a = 0;
var b = 0;
//Find currentContent in allContent array
if(currentContent !== noneSelected){
for(var i = 0; i < allContent.length; i++){
if(allContent[i] === currentContent){
a = i;
b = i;
}
}
}
//Begin searching in both directions for playable media
//starting from currentContent
do{
a++;
b--;
//If a is above range set to start
if(a >= allContent.length){
a = 0;
}
//If b is below range set to end
if(b < 0){
b = allContent.length - 1;
}
if(canPlay(allContent[a])){ //if can play a --> play a
newContent = allContent[a];
}else if(canPlay(allContent[b])){ // if can play b --> play b
newContent = allContent[b];
}else if(a == b){ // if circled around to beginning --> noneSelected (no content found)
newContent = noneSelected;
}
}while(newContent === null);
newContent = newContent;
}else{
newContent = noneSelected;
}
newContent.select();
}
}
//Clear contents and width of internalSlider
el_internalSlider.innerHTML = "";
el_internalSlider.style.width = "-1px";
//Add all thumbnails to internalSlider
for(var i = 0; i < usedContent.length; i++){
el_internalSlider.appendChild(usedContent[i].thumb);
el_internalSlider.style.width = (el_internalSlider.offsetWidth + usedContent[i].thumb.offsetWidth) + "px";
}
//Trigger height calculations without changing gallery state
showGallery(isShown(el_gallery));
}
//Play next valid media item
function next(){
if(op_loopAll.checked){
//If at last position play first item
if(currentContent.position === usedContent.length-1){
usedContent[0].select();
}else{
if(usedContent[currentContent.position + 1] !== undefined){
usedContent[currentContent.position + 1].select();
}
}
}else{
//If its not the last item --> play next, else do nothing
if(currentContent.position !== usedContent.length-1){
usedContent[currentContent.position + 1].select();
}
}
}
//Play previous valid media item
function previous(){
if(op_loopAll.checked){
//If at first position play last item
//else play previous
if(currentContent.position === 0){
usedContent[usedContent.length - 1].select();
}else{
if(usedContent[currentContent.position - 1] !== undefined){
usedContent[currentContent.position - 1].select();
}
}
}else{
//If its not the last item --> play next, else do nothing
if(currentContent.position !== 0){
usedContent[currentContent.position - 1].select();
}
}
}
//Returns media type when given source
function mediaType(input){
if(input === undefined){
console.log('Error: mediaType input argument was undefined');
}else if(input === null){
console.log('Error: mediaType input argument was null');
}else{
var temp = input.toString();
temp = temp.substr(temp.lastIndexOf('.') + 1);
if(temp == webm) return webm;
if(temp == gif) return gif;
if(temp == png) return png;
if(temp == jpg) return jpg;
}
//Last Resort
return null;
}
//Returns if current user settings permits the playing of parsed object
function canPlay(mediaObj){
var objType = mediaObj.type;
return (objType == webm && op_webms.checked) || (objType == gif && op_gifs.checked) || ( (objType == png || objType == jpg) && op_imgs.checked ) || (objType == "SOT" && !op_loopAll.checked) || (objType == "EOT" && !op_loopAll.checked);
}
//Applies autoplay based on user settings
function updateAutoplay(){
//Clear timeout to avoid timeout overlaps and
//unwanted function calls
clearTimeout(globalTimeout);
if(currentContent.type == webm){
//Loop media (incase auto is not turned on)
currentContent.media.loop = true;
//If it is turned on, set to false and await end of video
if(op_auto.checked){
currentContent.media.loop = false;
currentContent.media.onended = next;
}
}else if(currentContent.type == gif){
//If auto is checked apply according timeout
if(op_auto.checked){
globalTimeout = setTimeout(next, op_gif_duration.value*1000);
}
}else if(currentContent.type == png || currentContent.type == jpg){
//If auto is checked apply according timeout
if(op_auto.checked){
globalTimeout = setTimeout(next, op_img_duration.value*1000);
}
}
}
//Returns ALL elemnts. Including SOT, EOT and noneSelected
function getContent(){
var temp = [];
var elements = document.getElementsByClassName('fileThumb');
//Pushes 'start of thread' placeholder
temp.push(SOT);
//Loops over all media elements in thread
//and pushes them to temp array
for(var i = 0; i < elements.length; i++){
var vidSrc = elements[i].href;
var imgSrc = elements[i].getElementsByTagName('img')[0].src;
var id = elements[i].parentNode.parentNode.id;
var x = new Media(imgSrc, vidSrc, id);
temp.push(x);
}
//Pushes 'end of thread' placeholder
temp.push(EOT);
return temp;
}
//Returns all media permitted to play by user settings
function getUsedContent(){
var temp = [];
var count = 0;
for(var i = 0; i < allContent.length; i++){
if(canPlay(allContent[i])){
temp.push(allContent[i]);
temp[count].position = count;
count++;
}
}
return temp;
}
///////////////////////
// # Miscellaneous # //
///////////////////////
function startEventListeners(){
window.onresize = function(){
updateGallery();
currentContent.resize();
}
op_loopAll.onchange = updateGallery;
op_controls.onchange = function(){
if(currentContent.type == webm){
currentContent.media.controls = op_controls.checked;
}
};
op_playSound.onchange = function(){
if(currentContent.type == webm){
currentContent.media.volume = op_playSound.checked ? 1 : 0;
}
};
op_webms.onchange = updateGallery;
op_gifs.onchange = updateGallery;
op_imgs.onchange = updateGallery;
op_auto.onchange = updateAutoplay;
el_stagePrev.onclick = previous;
el_stageNext.onclick = next;
document.onkeydown = function(event){
switch(event.keyCode){
//Esc Key
case 27:
if(isShown(el_settings)){
showSettings(false);
}else if(isShown(el_sfc)){
showSFC(false);
}
break;
//Left arrow Key
case 37:
previous();
break;
//Right arrow Key
case 39:
next();
break;
//Up arrow Key
case 38:
if(event.shiftKey){
op_img_duration.value++;
}else{
op_gif_duration.value++;
}
break;
//Down arrow Key
case 40:
if(event.shiftKey){
op_img_duration.value--;
}else{
op_gif_duration.value--;
}
break;
//L Key
case 76:
op_loopAll.checked = !op_loopAll.checked;
op_loopAll.onchange();
break;
//A Key
case 65:
op_auto.checked = !op_auto.checked;
op_auto.onchange();
break;
//R Key
case 82:
op_random.checked = !op_random.checked;
op_random.onchange();
break;
//Q Key
case 81:
//op_shuffle.onclick();
break;
//S Key
case 83:
op_playSound.checked = !op_playSound.checked;
op_playSound.onchange();
break;
//W Key
case 87:
op_webms.checked = !op_webms.checked;
op_webms.onchange();
break;
//C Key
case 67:
op_controls.checked = !op_controls.checked;
op_controls.onchange();
break;
//G Key
case 71:
op_gifs.checked = !op_gifs.checked;
op_gifs.onchange();
break;
//I Key
case 73:
op_imgs.checked = !op_imgs.checked;
op_imgs.onchange();
break;
//Print what was typed into console
default:
var temp = "";
if(event.shiftKey){
temp += "Shift + ";
}
if(event.altKey){
temp += "Alt + ";
}
if(event.ctrlKey){
temp += "Ctrl + ";
}
temp += event.keyCode;
console.log(temp);
}
}
}
function getPageTopLeft(el) {
var rect = el.getBoundingClientRect();
var docEl = document.documentElement;
return {
left: rect.left + (window.pageXOffset || docEl.scrollLeft || 0),
top: rect.top + (window.pageYOffset || docEl.scrollTop || 0)
};
}