BetterFollowingList

Tweaks to following lists on media pages

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name BetterFollowingList
// @namespace Morimasa
// @author Morimasa
// @description Tweaks to following lists on media pages
// @match https://anilist.co/*
// @grant none
// @license GPL-3.0-or-later
// @version 0.08
// ==/UserScript==
let apiCalls = 0;
const scoremap = {'smile': 85, 'meh': 60, 'frown': 35} // the same as anilist
const stats = {
  element: null,
  count:0,
  scoreSum:0,
  scoreCount:0
}

const scoreProcess = e => {
  let el = e.querySelector('span') || e.querySelector('svg');
  let light = document.body.classList[0]==='site-theme-dark'?45:38;
  if (el===null) return;
  else if (el.nodeName==='svg'){
    // smiley
    let color = scoremap[el.dataset.icon]>70?120:scoremap[el.dataset.icon]<50?10:60;
    el.childNodes[0].setAttribute('fill', `hsl(${color}, 100%, ${light}%)`)
    
    addScore(0.5, scoremap[el.dataset.icon]*0.5) // 0.5 weight same as hoh script
  }
  else if (el.nodeName==='SPAN'){
    let score = el.innerText.split('/');
    score = score.length==1?parseInt(score)*20-10:parseInt(score[1])==10?parseFloat(score[0])*10:parseInt(score[0]); // convert stars, 10 point and 10 point decimal to 100 point
    el.style.color = `hsl(${score*1.2}, 100%, ${light}%)`;
    if (score>100) console.log('why score is bigger than 100?', el);
    addScore(1, score)
  }
}

const addScore = (count, score) => {
  stats.scoreCount+=count;
  stats.scoreSum+=score;
}

const handler = (data, target, idMap) => {
  if (target===undefined) return;
  data.forEach(e=>{
    target[idMap[e.user.id]].style.gridTemplateColumns='30px 1.3fr .7fr .6fr .2fr .2fr .5fr'; //css is my passion
    const progress = document.createElement('DIV');
    progress.innerText = `${e.progress}/${e.media.chapters||e.media.episodes||'?'}`;
    target[idMap[e.user.id]].insertBefore(progress, target[idMap[e.user.id]].children[2])
    
    let notesEL = document.createElement('span') // notes
    if (e.notes){
      notesEL = createIcon('notes', e.notes, "M256 32C114.6 32 0 125.1 0 240c0 49.6 21.4 95 57 130.7C44.5 421.1 2.7 466 2.2 466.5c-2.2 2.3-2.8 5.7-1.5 8.7S4.8 480 8 480c66.3 0 116-31.8 140.6-51.4 32.7 12.3 69 19.4 107.4 19.4 141.4 0 256-93.1 256-208S397.4 32 256 32z");
    }
    target[idMap[e.user.id]].insertBefore(notesEL, target[idMap[e.user.id]].children[4])
    
    let rewatchEL = document.createElement('span'); // rewatches
    if (e.repeat){
      rewatchEL = createIcon('repeat', e.repeat, "M256.455 8c66.269.119 126.437 26.233 170.859 68.685l35.715-35.715C478.149 25.851 504 36.559 504 57.941V192c0 13.255-10.745 24-24 24H345.941c-21.382 0-32.09-25.851-16.971-40.971l41.75-41.75c-30.864-28.899-70.801-44.907-113.23-45.273-92.398-.798-170.283 73.977-169.484 169.442C88.764 348.009 162.184 424 256 424c41.127 0 79.997-14.678 110.629-41.556 4.743-4.161 11.906-3.908 16.368.553l39.662 39.662c4.872 4.872 4.631 12.815-.482 17.433C378.202 479.813 319.926 504 256 504 119.034 504 8.001 392.967 8 256.002 7.999 119.193 119.646 7.755 256.455 8z");
    }
    target[idMap[e.user.id]].insertBefore(rewatchEL, target[idMap[e.user.id]].children[4])
  })
}

const createIcon = (cssClass, text, d) => {
  let el = document.createElement('span');
  el.className = cssClass;
  el.setAttribute('title', text);
  el.innerHTML = `<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="svg-inline--fa fa-redo-alt fa-w-16"><path fill="currentColor" d="${d}"></path></svg>`;
  return el;
}

const getAPI = (target, elMap) => {
    let user = [];
    for (let u in elMap) user.push(u);
    if (user.length===0) return;
    console.log(`%cBetterFollowingList`, 'color:white;background:#888;padding:4px;border-radius:4px;', `quering ${user.length} users | ${++apiCalls} api calls since reload`)
    const mediaID = window.location.pathname.split("/")[2];
    const query = 'query($u:[Int],$media:Int){Page{mediaList(userId_in:$u,mediaId:$media){progress notes repeat user{id}media{chapters episodes}}}}'
    const vars = {u: user, media: mediaID};
	const options = {
		method: 'POST',
		body: JSON.stringify({query: query, variables: vars}),
		headers: new Headers({
			'Content-Type': 'application/json'
		})
	};
	return fetch('https://graphql.anilist.co/', options)
	.then(res => res.json())
	.then(res => handler(res.data.Page.mediaList, target, elMap))
	.catch(error => console.error(`Error: ${error}`));
}

const createStat = (text, number) => {
  let el = document.createElement('span');
  el.innerText = text;
  el.appendChild(document.createElement('span'))
  el.children[0].innerText = number
  return el
}

const MakeStats = () => {
  if(stats.element) return; // element already injected
  let main = document.createElement('h2');
  let count = createStat('Users: ', stats.count);
  main.append(count);
  
  let avg = createStat('Avg: ', 0);
  avg.style.float='right';
  main.append(avg);
  
  const parent = document.querySelector('.following');
  if (parent!==null){
    parent.prepend(main);
    stats.element = main;
  }

}

let observer = new MutationObserver(() => {
  if (window.location.pathname.match(/\/(anime|manga)\/\d+\/.+\/social/)){
    MakeStats();
    const follows = document.querySelectorAll('.follow');
    let idmap = {};
    follows.forEach((e, i)=>{
    if (!e.dataset.changed){
      const avatarURL = e.querySelector('.avatar').dataset.src;
      if (!avatarURL || avatarURL==="https://s4.anilist.co/file/anilistcdn/user/avatar/large/default.png") return
      const id = avatarURL.split('/').pop().match(/\d+/g)[0];
      idmap[id] = i;
      // process score
      scoreProcess(e);
      // add user count
      ++stats.count;
      // set state
      e.dataset.changed=true;
      }
    })
   if (Object.keys(idmap).length>0){
     getAPI(follows, idmap);
     
     let statsElements = stats.element.querySelectorAll('span>span');
     statsElements[0].innerText = stats.count;
     const avgScore = parseInt(stats.scoreSum/stats.scoreCount)||0;
     if (avgScore>0){
       statsElements[1].style.color=`hsl(${avgScore*1.2}, 100%, 40%)`
       statsElements[1].innerText = `${avgScore}%`;
     }
     else statsElements[1].parentNode.remove(); // no need if no scores

   }
  }
  else {
    // reset
    stats.element=null;
    stats.count=0;
    stats.scoreSum=0;
    stats.scoreCount=0;
  }
});
observer.observe(document.getElementById('app'), {childList: true, subtree: true, attributes: true});