// ==UserScript== // @name Aniscripts // @namespace http://tampermonkey.net/ // @version 1.42 // @description Change stuff on Anilist.co // @author hoh // @match https://anilist.co/* // @grant none // @downloadURL none // ==/UserScript== (function(){ scriptVersion = "1.42"; /* 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: 72px; } .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; line-height: 0; 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; } .hohShare{ position: absolute; right: 12px; top: 30px; cursor: pointer; color: rgb(var(--color-blue-dim)); } .activity-entry{ position: relative; } .hohEmbed{ border-style: solid; border-color: rgb(var(--color-text)); border-width: 1px; padding: 15px; position: relative; } .hohEmbed .avatar{ border-radius: 3px; height: 40px; width: 40px; background-position: 50%; background-repeat: no-repeat; background-size: cover; display: inline-block; } .hohEmbed .name{ display: inline-block; height: 40px; line-height: 40px; vertical-align: top; color: rgb(var(--color-blue)); font-size: 1.4rem; margin-left: 12px !important; } .hohEmbed .time{ color: rgb(var(--color-text-lighter)); font-size: 1.1rem; position: absolute; right: 12px; top: 12px; } .hohRecsLabel{ color: rgb(var(--color-blue)) !important; } .hohRecsItem{ margin-top: 5px; margin-bottom: 10px; } .hohTaglessLinkException{ display: block; } .hohTaglessLinkException::after{ content: ""!important; } .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, reTweet : false, reTweetKiller : false, userRecs : true }; if(localStorageAvailable){ var localStorageItem = localStorage.getItem("hohSettings"); if(localStorageItem != null && localStorageItem != ""){ useScriptsSettings = JSON.parse(localStorageItem); for(key in useScriptsSettings){//this is to keep the default settings if the version in local storage is outdated 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]); } } }; var svgAssets = { likeIcon : "", retweetIcon : "↷", replyIcon : "" }; var queryMediaList = ` query ($name: String!, $listType: MediaType) { MediaListCollection (userName: $name, type: $listType) { lists { entries { ... mediaListEntry } } } } fragment mediaListEntry on MediaList { mediaId status progress repeat notes startedAt { year month day } media { episodes duration nextAiringEpisode { episode } format title { romaji } tags { name } } scoreRaw: score (format: POINT_100) } `; var queryActivity = "query ($id: Int!) { Activity(id: $id) { ... on TextActivity { id userId type replyCount text createdAt user { id name avatar { large } } likes { id name avatar { large } } replies { id text(asHtml: true) createdAt user { id name avatar { large } } likes { id name avatar { large } } } } ... on ListActivity { id userId type status progress replyCount createdAt user { id name avatar { large } } media { coverImage { large } id title { userPreferred } } likes { id name avatar { large } } replies { id text(asHtml: true) createdAt user { id name avatar { large } } likes { id name avatar { large } } } } ... on MessageActivity { id type replyCount createdAt messenger { id name avatar { large } } likes { id name avatar { large } } replies { id text(asHtml: true) createdAt user { id name avatar { large } } likes { id name avatar { large } } } } } }"; 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,variables); }; 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 reTweetCallback = function(data){ var embedList = document.getElementsByClassName("share" + data.data.Activity.id); for(var i=0;i 10){ embed.innerHTML = "Reached the nesting limit"; embed.classList.remove("share" + data.data.Activity.id); return; }; var header = document.createElement("div"); var content = document.createElement("div"); content.innerHTML = data.data.Activity.text; var uselessMatchData = content.innerText.match(/^(!share|RT)\s+(https:\/\/anilist\.co\/activity\/)?(\d+)/); if(content.children[0].innerText.match(/^(!share|RT)/)){ if(content.children[0].innerHTML.match(/
/)){ content.children[0].innerHTML = content.children[0].innerHTML.replace(/^(!share|RT).*?
/,""); } else{ content.children[0].innerHTML = ""; }; }; content.classList.add("activity-markdown"); header.classList.add("header"); var avatar = document.createElement("a"); avatar.classList.add("avatar"); avatar.style.backgroundImage = "url(" + data.data.Activity.user.avatar.large + ")"; avatar.href = "/user/" + data.data.Activity.user.name; var name = document.createElement("a"); name.innerText = data.data.Activity.user.name; name.href = "/user/" + data.data.Activity.user.name; name.classList.add("name"); header.appendChild(avatar); header.appendChild(name); embed.appendChild(header); embed.appendChild(content); var actions = document.createElement("div"); var time = document.createElement("time"); actions.classList.add("time"); var postTime = new Date(data.data.Activity.createdAt*1000); var currentTime = new Date(); actions.appendChild(time); var fakeActions = document.createElement("div"); actions.classList.add("actions"); actions.appendChild(fakeActions); time.dateTime = postTime.toISOString(); if(currentTime - postTime < 1000*60*60*24){ if(postTime.getMinutes() < 10){ time.innerText = postTime.getHours() + ":0" + postTime.getMinutes(); } else{ time.innerText = postTime.getHours() + ":" + postTime.getMinutes(); }; } else{ time.innerText = postTime.toLocaleDateString() + " " + postTime.getHours() + ":" + postTime.getMinutes(); }; embed.appendChild(actions); embed.classList.remove("share" + data.data.Activity.id); if(uselessMatchData){ var embed2 = document.createElement("div"); embed2.innerHTML = "loading..."; embed2.classList.add("share" + uselessMatchData[3]); embed2.hohLevel = embed.hohLevel + 1; embed2.classList.add("hohEmbed"); content.appendChild(embed2); var variables = {id:uselessMatchData[3]}; var query = ` query ($id: Int!) { Activity(id: $id) { ... on TextActivity { id userId type replyCount text(asHtml: true) createdAt user { id name avatar { large } } } } }`; generalAPIcall(query,variables,reTweetCallback); }; }; }; var reTweet = function(){ var perform = function(){ if(!document.URL.match(/https:\/\/anilist\.co\/(home|user|activity)\/?([a-zA-Z0-9-]+)?\/?$/)){ return; }; var listOfActs = document.getElementsByClassName("activity-text"); for(var i=0;i" + this.parentNode.children[0].children[0].children[1].innerText.replace(/\n+/g,"\n>"); setTimeout(function(){ textArea.value = shareValue; },100); window.scrollTo(0,0); }; if(whoAmI == "hoh" && document.URL.match(/https:\/\/anilist\.co\/home\/?/)){ listOfActs[i].appendChild(shareIcon); }; if(true || listOfActs[i].classList.contains("activity-text")){ var textContent = listOfActs[i].children[0].children[0].children[1].children[0].children[0];//fragile var uselessMatchData = textContent.innerText.match(/^(!share|RT)\s+(https:\/\/anilist\.co\/activity\/)?(\d+)/); if(uselessMatchData){ if(useScripts.reTweetKiller){ listOfActs[i].remove(); }; if(textContent.innerHTML.match(/
/)){ textContent.innerHTML = textContent.innerHTML.replace(/^(!share|RT).*?
/,""); } else{ textContent.innerHTML = ""; }; var embed = document.createElement("div"); embed.innerHTML = "loading..."; embed.classList.add("share" + uselessMatchData[3]); embed.classList.add("hohEmbed"); embed.hohLevel = 0; textContent.appendChild(embed); if(textContent.parentNode.children[textContent.parentNode.children.length-1].nodeName == "BLOCKQUOTE"){ textContent.parentNode.children[textContent.parentNode.children.length-1].remove(); }; var variables = {id:uselessMatchData[3]}; var query = ` query ($id: Int!) { Activity(id: $id) { ... on TextActivity { id userId type replyCount text(asHtml: true) createdAt user { id name avatar { large } } } } }`; generalAPIcall(query,variables,reTweetCallback); }; }; }; }; var tryAgain = function(){//loop the notification script until we leave that page setTimeout(function(){ perform(); if(document.URL.match(/https:\/\/anilist\.co\/(home|user|activity)\/?([a-zA-Z0-9-]+)?\/?$/)){///https:\/\/anilist\.co\/home\/?/ tryAgain() } else{ activeScripts.reTweet = false; } },200); }; activeScripts.reTweet = true; perform(); tryAgain(); }; var userRecs = function(){ var perform = function(){ if(!document.URL.match(/https:\/\/anilist\.co\/(anime|manga)\/\d*\/[0-9a-zA-Z-]*\/$/)){ return; }; var pageId = parseInt(document.URL.match(/https:\/\/anilist\.co\/(anime|manga)\/(\d+)/)[2]); if(userRecsList.hasOwnProperty(pageId) || userRecsList.hasOwnProperty(pageId+"")){ /* var listOfFollowers = document.getElementsByClassName("follow"); 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 if(!document.URL.match(/https:\/\/anilist\.co\/staff\/.*/)){ return; }; var filterGroup = document.getElementsByClassName("content"); if(!filterGroup.length){ setTimeout(function(){ enhanceStaff(); },200);//takes some time to load return; }; filterGroup = filterGroup[0]; var favCount = document.createElement("span"); favCount.id = "hohFavCount"; favCount.innerText; filterGroup.appendChild(favCount); var variables = {id: document.URL.match(/\/staff\/(\d+)\//)[1]}; var query = "query($id: Int!){Staff(id: $id){favourites}}"; var favCallback = function(data){ var favButton = document.getElementsByClassName("favourite"); if(data.data.Staff.favourites == 0 && favButton[0].classList.contains("isFavourite")){//safe to assume document.getElementById("hohFavCount").innerText = data.data.Staff.favourites+1; } else{ document.getElementById("hohFavCount").innerText = data.data.Staff.favourites; }; if(favButton.length){ favButton[0].onclick = function(){ var favCount = document.getElementById("hohFavCount"); if(this.classList.contains("isFavourite")){ favCount.innerText = Math.max(parseInt(favCount.innerText)-1,0);//0 or above, just to avoid looking silly } else{ favCount.innerText = parseInt(favCount.innerText)+1; }; }; }; }; generalAPIcall(query,variables,favCallback); }; //todo: link the relevant status post var addCompletedScores = function(){ var perform = function(){ if(!document.URL.match(/https:\/\/anilist\.co\/(home|user|activity)\/?([a-zA-Z0-9-]+)?\/?$/)){ return; }; var status = document.getElementsByClassName("status"); 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