// ==UserScript== // @name Aniscripts // @namespace http://tampermonkey.net/ // @version 1.07 // @description Change stuff on Anilist.co // @author hoh // @match https://anilist.co/* // @grant none // @downloadURL none // ==/UserScript== (function(){ scriptVersion = "1.07"; /* Aniscripts, sometimes just "the userscript", is modular and contains of several independet functions The URL matching controller can be found near the bottom of this file. Due to the dynamic nature of how Anilist pages load, these functions are fun on a clock. Functionallity provided by all of these functions are suplemental, so the clock frequenzies are kept slow to not impact performance. */ //a shared style node for all the modules. All classes are prefixed by "hoh" to avoid collisions with native Anilist classes var style = document.createElement('style'); style.type = 'text/css'; //most of these are used by the notification module //The default colour is rgb(var(--color-blue)) provided by Anilist, but rgb(var(--color-green)) is prefered for things related to manga style.innerHTML = ` .hohTime{ position : static; float : right; margin-right : 20px; margin-top : 10px; margin-left: auto; } .hohUnread{ border-right : 8px; border-color: rgba(var(--color-blue)); border-right-style : solid; } .hohNotification{ margin-bottom : 10px; background : rgb(var(--color-foreground)); border-radius : 4px; justify-content: space-between; line-height: 0; min-height: 70px; } .hohNotification *{ line-height: 1.15; } .hohUserImageSmall{ display : inline-block; background-position : 50%; background-repeat : no-repeat; background-size : cover; position : absolute; } .hohUserImage{ height : 72px; width : 72px; display : inline-block; background-position : 50%; background-repeat : no-repeat; background-size : cover; position:absolute; } .hohMediaImage{ height : 70px; margin-right : 5px; } .hohMessageText{ position : absolute; margin-top : 30px; margin-left : 80px; max-width : 330px; } .hohMediaImageContainer{ vertical-align : bottom; margin-left : 400px; display : inline; position: relative; display: inline-block; min-height: 70px; } .hohMediaImageContainer > a{ line-height: 0!important; } span.hohMediaImageContainer{ line-height: 0!important; } .hohCommentsContainer{ margin-top: 5px; } .hohCommentsArea{ margin : 10px; display : none; padding-bottom : 2px; margin-top: 5px; width: 95%; } .hohComments{ float : right; display : none; margin-top: -30px; margin-right: 15px; cursor : pointer; margin-left: 600px; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } .hohCombined .hohComments{ display : none!important; } .hohQuickCom{ padding : 5px; background-color : rgb(var(--color-background)); margin-bottom : 5px; } .hohQuickComName{ margin-right : 15px; color : rgb(var(--color-blue)); } .hohQuickComName::after{ content : ":"; } .hohQuickComContent{ margin-right: 40px; } .hohQuickComLikes{ float : right; display: inline-block; } .hohSpoiler::before{ color : rgb(var(--color-blue)); cursor : pointer; background : rgb(var(--color-background)); border-radius : 3px; content : "Spoiler, click to view"; font-size : 1.3rem; padding : 0 5px; } .hohSpoiler.hohClicked::before{ display : none; } .hohSpoiler > span{ display : none; } .hohMessageText > span > div.time{ display : none; } .hohUnhandledSpecial > div{ margin-top : -20px; } .hohMonospace{ font-family: monospace; } .hohSocialTabActivityCompressedContainer{ min-width:480px; } .hohSocialTabActivityCompressedStatus{ vertical-align: middle; padding-bottom: 7px; } .hohSocialTabActivityCompressedName{ vertical-align: middle; margin-left: 3px; } .hohForumHider{ margin-right: 3px; cursor: pointer; font-family: monospace; } .hohForumHider:hover{ color: rgb(var(--color-blue)); } .hohBackgroundCover{ height: 70px; width: 50px; display: inline-block; background-position: 50%; background-repeat: no-repeat; background-size: cover; margin-top: 1px; margin-bottom: 1px; } #hohDescription{ width: 280px; height: 150px; float: left; color: rgb(var(--color-blue)); } .hohStatsTrigger{ cursor: pointer; border-radius: 3px; color: rgb(var(--color-text-lighter)); display: block; font-size: 1.4rem; margin-bottom: 8px; padding: 5px 10px; } .hohActive{ background: rgba(var(--color-foreground),.8); color: rgb(var(--color-text)); font-weight: 500; } #hohFavCount{ position: absolute; right: 30px; top: 70px; } .hohShamelessLink{ display: block; margin-bottom: 5px; } .hohSlidePlayer{ display: block; position: relative; width: 500px; } .hohSlide{ position: absolute; top: 0px; font-size: 500%; height: 100%; display: flex; align-items: center; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; opacity:0.5; } .hohSlide:hover{ background-color: rgb(0,0,0,0.4); cursor: pointer; opacity:1; } .hohRightSlide{ right: 0px; padding-left: 10px; padding-right: 20px; } .hohLeftSlide{ left: 0px; padding-left: 20px; padding-right: 10px; } .hohBackgroundUserCover{ height: 70px; width: 70px; display: inline-block; background-position: 50%; background-repeat: no-repeat; background-size: cover; margin-top: 1px; margin-bottom: 1px; }; `; document.getElementsByTagName('head')[0].appendChild(style); //Todo: find out how to parse API headers for an accurate result document.APIcallsUsed = 0;//this is NOT a reliable way to figure out how many more calls we can use, just a way to set some limit var pending = {}; var APIcounter = setTimeout(function(){ document.APIcallsUsed = 0; },60*1000);//reset counter every minute function lsTest(){//localStorage is great for not having to fetch the api data every time var test = "test"; try{ localStorage.setItem(test,test); localStorage.removeItem(test); return true; }catch(e) { return false; } } if(lsTest() === true){ var localStorageAvailable = true; var aniscriptsUsed = localStorage.getItem("aniscriptsUsed"); if(aniscriptsUsed === null){ aniscriptsUsed = { keys : [] }; } else{ aniscriptsUsed = JSON.parse(aniscriptsUsed); }; localStorage.setItem("aniscriptsUsed",JSON.stringify(aniscriptsUsed)); } else{ var localStorageAvailable = false; }; var useScripts = {//most modules are turned on by default notifications : true, socialTab : true, favourites : true, forumComments : true, staffPages : true, tagDescriptions : true, completedScore : true, moreStats : true, characterFavouriteCount : true, usefulLinks : false }; if(localStorageAvailable){ var localStorageItem = localStorage.getItem("hohSettings"); if(localStorageItem != null && localStorageItem != ""){ useScriptsSettings = JSON.parse(localStorageItem); for(key in useScriptsSettings){ useScripts[key] = useScriptsSettings[key]; }; }; localStorage.setItem("hohSettings",JSON.stringify(useScripts)); }; try{//looks at the nav var whoAmI = document.getElementById("nav").children[0].children[1].children[1].href.match(/[a-zA-Z0-9-]*\/$/)[0].slice(0,-1); } catch(err){ var whoAmI = ""; };//use later for some scripts Element.prototype.remove = function(){//more comfy way to remove DOM elements this.parentElement.removeChild(this); } NodeList.prototype.remove = HTMLCollection.prototype.remove = function() { for(var i = this.length - 1; i >= 0; i--) { if(this[i] && this[i].parentElement) { this[i].parentElement.removeChild(this[i]); } } } //used for the notification pate comments var badMarkupParser = function(string){//attempt to render some elements, like images and spoilers string = string.replace(/\n/g,"
"); var imageRegexp = /img[0-9]*\((.*?)\)/;//fixme: does not respect percentage width var imageMatches = imageRegexp.exec(string); while(imageMatches){ string = string.replace(imageRegexp,""); imageMatches = imageRegexp.exec(string); }; var youEx = /youtube\((https?\:\/\/www\.youtube\.com\/watch\?v\=(.+?))\)/;//catch subgroup with url and one with the id var videoMatches = youEx.exec(string); while(videoMatches){ string = string.replace(youEx,"youtube " + videoMatches[2] + ""); /* visible: youtube(https://www.youtube.com/watch?v=MzEinM660h4) becomes youtube MzEinM660h4 as a clickable link */ videoMatches = youEx.exec(string); }; var matchPings = /\@([a-zA-Z0-9][a-zA-Z0-9][a-zA-Z0-9]+)/;//at least three characters in user names var pingMatches = matchPings.exec(string); while(pingMatches){ if(whoAmI.toUpperCase() === pingMatches[1].toUpperCase()){ string = string.replace( matchPings, "@" + pingMatches[1] + "" ); } else{ string = string.replace( matchPings, "@" + pingMatches[1] + "" ); }; pingMatches = matchPings.exec(string); }; var matchLinks = /\[(.+?)\]\((.+?)\)/; var linkMatches = matchLinks.exec(string); while(linkMatches){ string = string.replace(matchLinks,"" + linkMatches[1] + ""); linkMatches = matchLinks.exec(string); }; var matchBold = /__(.+?)__/; var boldMatches = matchBold.exec(string); while(boldMatches){ string = string.replace(matchBold,"" + boldMatches[1] + ""); boldMatches = matchBold.exec(string); }; //https://anilist.co/manga/98889/Maikosan-Chi-no-Makanaisan/ var matchMediaLinks = /https?\:\/\/anilist.co\/(anime|manga)\/(\d+?)\/(.*?)\/(\ |\)/; var mediaLinkMatches = matchMediaLinks.exec(string); while(mediaLinkMatches){ string = string.replace( matchMediaLinks, "[" + mediaLinkMatches[1] + "/" + mediaLinkMatches[3] + "]" + mediaLinkMatches[4] ); mediaLinkMatches = matchMediaLinks.exec(string); }; var matchSpoiler = /\~\!(.*?)\!\~/; var spoilerMatches = matchSpoiler.exec(string); while(spoilerMatches){ string = string.replace( matchSpoiler, "" + spoilerMatches[1] + "" ); spoilerMatches = matchSpoiler.exec(string); }; return string; }; var activityCache = {};//reduce API calls even if localStorage is not available. var handleResponse = function(response){//generic handling of API responses return response.json().then(function(json){ return response.ok ? json : Promise.reject(json); }); }; var handleError = function(error){ //alert("Error, check console"); //fixme console.error(error); }; var url = 'https://graphql.anilist.co';//Current Anilist API location var listActivityCall = function(query,variables,callback,vars,cache){ /* query=graphql request vars=just values to pass on to the callback function cache::true use cached data if available cache::false allways fetch new data */ var handleData = function(data){ pending[variables.id] = false; if(localStorageAvailable){ localStorage.setItem(variables.id + "",JSON.stringify(data)); aniscriptsUsed.keys.push(variables.id); if(aniscriptsUsed.keys.length > 1000){//don't hog to much of localStorage for(var i=0;i<10;i++){ localStorage.removeItem(aniscriptsUsed.keys[0]); aniscriptsUsed.keys.shift(); }; }; localStorage.setItem("aniscriptsUsed",JSON.stringify(aniscriptsUsed)); } else{ activityCache[variables.id] = data;//still useful even if we don't have localstorage }; callback(data,vars); }; var options = {//generic headers provided by API examples method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: JSON.stringify({ query: query, variables: variables }) }; if(localStorageAvailable && cache){ var localStorageItem = localStorage.getItem(variables.id + ""); if(!(localStorageItem === null)){ callback(JSON.parse(localStorageItem),vars); console.log("localStorage cache hit"); return; }; } else if(activityCache.hasOwnProperty(variables.id) && cache){ callback(activityCache[variables.id],vars); console.log("cache hit"); return; }; fetch(url,options).then(handleResponse).then(handleData).catch(handleError); ++document.APIcallsUsed; console.log("fetching new data"); }; var generalAPIcall = function(query,variables,callback){//has no cache stuff to worry about var handleData = function(data){ callback(data); }; var options = { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: JSON.stringify({ query: query, variables: variables }) }; fetch(url,options).then(handleResponse).then(handleData).catch(handleError); ++document.APIcallsUsed; console.log("fetching new data"); }; var enhanceSocialTab = function(){ var perform = function(){ if(!document.URL.match(/https:\/\/anilist\.co\/(anime|manga)\/\d*\/[0-9a-zA-Z-]*\/social/)){ return; }; var listOfActs = document.getElementsByClassName("activity-entry"); for(var i=0;i 1) ){ listOfActs[i].marked = true; listOfActs[i].children[0].children[0].children[0].remove();//remove cover image var elements = listOfActs[i].children[0].children[0].children[0].children; elements[2].parentNode.insertBefore(elements[2],elements[0]);//move profile picture to the beginning of the line elements[0].parentNode.parentNode.style.minHeight = "70px"; elements[0].parentNode.classList.add("hohSocialTabActivityCompressedContainer"); elements[0].style.verticalAlign = "bottom"; elements[0].style.marginTop = "0px"; elements[1].classList.add("hohSocialTabActivityCompressedName"); elements[2].classList.add("hohSocialTabActivityCompressedStatus"); listOfActs[i].style.marginBottom = "10px"; }; }; /*add average score to social tab*/ var listOfFollowers = document.getElementsByClassName("follow"); var averageScore = 0; var averageCount = 0; for(var i=0;i 1){ this.parentNode.parentNode.parentNode.children[1].style.display = "none"; }; } else{ this.innerHTML = "[-]"; this.parentNode.parentNode.children[1].style.display = "block"; if(this.parentNode.parentNode.parentNode.children.length > 1){ this.parentNode.parentNode.parentNode.children[1].style.display = "block"; }; }; }; comments[i].children[0].children[0].insertBefore(hider,comments[i].children[0].children[0].children[0]); }; }; }; var tryAgain = function(){//loop the notification script until we leave that page setTimeout(function(){ perform(); if(document.URL.match(/https:\/\/anilist\.co\/forum\/thread\/.*/)){ tryAgain() } else{ activeScripts.forumComments = false; } },100); }; activeScripts.forumComments = true; perform(); tryAgain(); }; var enhanceStaff = function(){//currently doesn't do anything var perform = function(){ if(!document.URL.match(/https:\/\/anilist\.co\/staff\/.*/)){ return; }; var roleCards = document.getElementsByClassName("role-card"); for(var i=0;i"; } else if(data.data.MediaList.score == 2){ suffix = ""; } else if(data.data.MediaList.score == 1){ suffix = ""; }; } else if( data.data.MediaList.user.mediaListOptions.scoreFormat == "POINT_5" ){ suffix = " " + data.data.MediaList.score + ""; } for(var j=0;j= this.parentNode.imageList.length-1){ this.style.display = "none"; }; }; var leftSlide = document.createElement("div"); leftSlide.innerText = "◀"; leftSlide.classList.add("hohLeftSlide"); leftSlide.classList.add("hohSlide"); leftSlide.style.display = "none"; leftSlide.onclick = function(){ this.parentNode.children[2].style.display = "flex"; this.parentNode.indeks--; this.parentNode.children[0].src = this.parentNode.imageList[this.parentNode.indeks]; if(this.parentNode.indeks <= 0){ this.style.display = "none"; }; }; slidePlayer.appendChild(leftSlide); slidePlayer.appendChild(rightSlide); status2[i].appendChild(slidePlayer); status2[i].classList.remove("markdown-spoiler"); }; }; }; }; var tryAgain = function(){//loop the notification script until we leave that page setTimeout(function(){ perform(); if(document.URL.match(/https:\/\/anilist\.co\/(home|user)\/?/)){ tryAgain() } else{ activeScripts.completedScore = false; } },1000); }; activeScripts.completedScore = true; perform(); tryAgain(); }; var enhanceTags = function(){//show tag definition in drop down menu when adding tags var perform = function(){ if(!document.URL.match(/https:\/\/anilist\.co\/(anime|manga)\/.*/)){ return; }; var possibleTagContainers = document.getElementsByClassName("el-select-dropdown__list"); var bestGuess = false; for(var i=0;i 100){//horrible test, but we have not markup to go from. Assumes the tag dropdown is the only one with more than 100 children bestGuess = i; }; }; if(bestGuess == false){ return; }; if(possibleTagContainers[bestGuess].hasOwnProperty("hohMarked")){ return; } else{ possibleTagContainers[bestGuess].hohMarked = true; }; var superBody = document.getElementsByClassName("el-dialog__body")[0]; var descriptionTarget = document.createElement("span"); descriptionTarget.id = "hohDescription"; superBody.insertBefore(descriptionTarget,superBody.children[2]); for(var i=0;i