Greasy Fork is available in English.
Add useful functionalities to Funimation. Keyboard skipping, next episode buttons, and more.
// ==UserScript== // @name Funimation Utilities // @namespace https://theusaf.github.io // @version 1.1 // @description Add useful functionalities to Funimation. Keyboard skipping, next episode buttons, and more. // @author theusaf // @match https://funimation.com/shows/*/* // @match https://www.funimation.com/shows/*/* // @grant none // ==/UserScript== /* global TITLE_DATA */ // Adds an episode list // Note: may need rework to prevent issue where images dont show up (function(){ if(location.pathname.split("/").length < 5){ return; } // data for the program initialization const path = location.pathname, [,,showSlug] = path.split("/"), {titleSlug,seriesId,episodeNum,seasonNum} = TITLE_DATA, apiRequest = new XMLHttpRequest(); // getting all episodes apiRequest.open("GET",`https://${location.host}/api/episodes/?title_id=${seriesId}&season=${seasonNum}&sort=order&sort_direction=ASC`); apiRequest.send(); apiRequest.onload = function(){ // genearting the html const sectionAfter = document.querySelector(".video-information"),// document.querySelector("#comments-section"), section = sectionAfter.parentNode.insertBefore(document.createElement("section"),sectionAfter), data = JSON.parse(apiRequest.responseText); section.id = "episodes-section"; section.innerHTML = `<style> #gallery-container{ display: flex; border-top: 1px solid #ccc; padding-top: 0.5rem; } #episode-gallery{ flex: 1; display: flex; } .episode-thumb{ height: 7rem; } .episode-item{ position: relative; width: 12.5rem; } .gallery-control{ margin: 0.5rem; font-size: 2rem; font-weight: bold; text-align: center; align-self: center; } .episode-num{ display: block; font-weight: bold; } .episode-preview{ position: absolute; background: black; border-radius: 0.25rem; top: 100%; color: white; padding: 0.5rem; z-index: 1; } .episode-preview > span{ font-weight: bold; } </style> <div id="gallery-container"> <div id="previous-gallery" class="gallery-control"><</div> <div id="episode-gallery"></div> <div id="next-gallery" class="gallery-control">></div> </div>`; const [prev,episodes,next] = document.querySelector("#gallery-container").children, {items} = data; function IsVisible(elem){ const box = elem.getBoundingClientRect(), {top,bottom,right,left} = box, winh = window.innerHeight, winw = window.innerWidth, topEdgeInRange = top >= 0 && top <= winh, bottomEdgeInRange = bottom >= 0 && bottom <= winh, leftEdgeInRange = left >= 0 && left <= winw, rightEdgeInRange = right >= 0 && right <= winw, coverScreenHorizontally = left <= 0 && right >= winw, coverScreenVertically = top <= 0 && bottom >= winh, topEdgeInScreen = topEdgeInRange && (leftEdgeInRange || rightEdgeInRange || coverScreenHorizontally), bottomEdgeInScreen = bottomEdgeInRange && (leftEdgeInRange || rightEdgeInRange || coverScreenHorizontally), leftEdgeInScreen = leftEdgeInRange && (topEdgeInRange || bottomEdgeInRange || coverScreenVertically), rightEdgeInScreen = rightEdgeInRange && (topEdgeInRange || bottomEdgeInRange || coverScreenVertically); return topEdgeInScreen && bottomEdgeInScreen && leftEdgeInScreen && rightEdgeInScreen; } let firstindex = +episodeNum - 3; if(firstindex < 0){ firstindex = 0; } let index = 0; for(let episode of items){ const {episodeNum,episodeSlug,runtime,episodeName,titleSlug} = episode.item, {synopsis,thumb} = episode; // generate items const container = document.createElement("div"), preview = document.createElement("div"); container.className = "episode-item"; container.innerHTML = `<a href="/shows/${titleSlug}/${episodeSlug}/${location.search}"> <img class="episode-thumb" src="${thumb}" alt="${episodeNum}: ${episodeName}"> <div class="episode-meta"> <span class="episode-num">Episode ${episodeNum}</span> <span class="episode-name">${episodeName}</span> </div> </a>`; preview.className = "episode-preview"; preview.innerHTML = `<span>${episodeName}</span> <p>${synopsis}</p>`; episodes.append(container); if(index < firstindex){ container.style.display = "none"; index++; } let wait; container.addEventListener("mouseenter",()=>{ wait = setTimeout(()=>{ container.append(preview); },750); }); container.addEventListener("mouseleave",()=>{ clearTimeout(wait); try{preview.remove();}catch(e){} }); } function check(){ const items = document.querySelectorAll(".episode-item"); for(let i = 0;i<items.length;i++){ if(!IsVisible(items[i])){ items[i].style.display = "none"; } } if(!episodes.querySelector(".episode-item:not([style])")){ for(let i = 0;i<items.length;i++){ items[i].removeAttribute("style"); if(i < firstindex){ items[i].style.display = "none"; } } setTimeout(check,500); } } setTimeout(check,500); prev.addEventListener("click",()=>{ const visible = Array.from(episodes.querySelectorAll(".episode-item:not([style])")), all = Array.from(episodes.children); if(visible[0] === all[0]){ return; } const index = all.indexOf(visible[0]); let targetIndex = index - visible.length; visible.reverse(); if(targetIndex < 0){ targetIndex = 0; } for(let i = targetIndex,j=0;i<targetIndex + index;i++,j++){ all[i].removeAttribute("style"); visible[j].style.display = "none"; } }); next.addEventListener("click",()=>{ const visible = Array.from(episodes.querySelectorAll(".episode-item:not([style])")), all = Array.from(episodes.children); if(visible[visible.length - 1] === all[all.length - 1]){ return; } const index = all.indexOf(visible[visible.length - 1]); let targetIndex = index + visible.length; if(targetIndex >= all.length){ targetIndex = all.length - 1; } for(let i = 0;i<targetIndex - index;i++){ visible[i].style.display = "none"; all[index + i + 1].removeAttribute("style"); } }); }; })(); // Adds keyboard functions. function addKeyboardFunctions(){ if(location.pathname.split("/").length < 5){ return; } const playerframe = document.querySelector("#player"), {contentDocument,contentWindow} = playerframe, player = contentDocument.querySelector("#brightcove-player"), video = contentDocument.querySelector("#brightcove-player_html5_api"); if(video === null){ return setTimeout(addKeyboardFunctions,2e3); } const {fp} = contentWindow; contentWindow.addEventListener("keydown",(event)=>{ let activated = true; switch(event.code){ case "KeyL":{ fp.player.currentTime(fp.player.currentTime() + 10); break; } case "ArrowRight":{ fp.player.currentTime(fp.player.currentTime() + 5); break; } case "KeyJ":{ fp.player.currentTime(fp.player.currentTime() - 10); break; } case "ArrowLeft":{ fp.player.currentTime(fp.player.currentTime() - 5); break; } case "KeyK":{ if(fp.player.paused()){ fp.player.play(); }else{ fp.player.pause(); } break; } default:{ activated = false; } } if(/Digit\d/.test(event.code)){ activated = true; const int = +event.code.match(/\d/)[0], time = (int / 10) * video.duration; fp.player.currentTime(time); } if(activated){ event.preventDefault(); } }); }; addKeyboardFunctions();