// ==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 = '
Settings

General

Loop whole thread (L)
Play Automatically (A)
Random (R)
(S)

Webm

Play Webms (W)
Show Controls (C)
Play sound (S)

Gif

Play Gifs (G)
Gif Duration (up/down)

Images

Play Images (I)
Image Duration (Shift + up/down)
'; 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) }; }