MojaCoder のユーザーのリンクに AtCoder のレーティングを紐つけます( AtCoder のユーザー名と同じ場合のみ)
// ==UserScript==
// @name MojaCoder Submission User Colorizer (by AtCoder Rating)
// @namespace https://mojacoder.app/
// @version 0.2
// @description MojaCoder のユーザーのリンクに AtCoder のレーティングを紐つけます( AtCoder のユーザー名と同じ場合のみ)
// @author magurofly
// @license CC0-1.0 Universal
// @match https://mojacoder.app/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=mojacoder.app
// @grant none
// @require https://unpkg.com/lscache/lscache.min.js
// ==/UserScript==
(async function() {
'use strict';
document.head.insertAdjacentHTML("afterbegin", `
<style>
.user-red {color:#FF0000;}
.user-orange {color:#FF8000;}
.user-yellow {color:#C0C000;}
.user-blue {color:#0000FF;}
.user-cyan {color:#00C0C0;}
.user-green {color:#008000;}
.user-brown {color:#804000;}
.user-gray {color:#808080;}
.user-unrated {color:#000000;}
.user-admin {color:#C000C0;}
.crown-gold {color: #fb0;}
.crown-silver {color: #aaa;}
</style>
`);
// もりを (morio_prog) 様の AtCoder Submission User Colorizer を改変して使用しています
const lastUpdateKey = 'user-colorizer-ranking-last-update';
const rankingKey = 'user-colorizer-ranking';
const OUT_OF_RANK = Number.MAX_VALUE; // > 100
const UPDATE_INTERVAL = 3 * 60; // 更新周期 [min]
function getColor(rating) {
if (rating >= 2800) return '#FF0000';
if (rating >= 2400) return '#FF8000';
if (rating >= 2000) return '#C0C000';
if (rating >= 1600) return '#0000FF';
if (rating >= 1200) return '#00C0C0';
if (rating >= 800) return '#008000';
if (rating >= 400) return '#804000';
if (rating > 0) return '#808080';
return '#000000';
}
function getColorClass(rating) {
if (rating >= 2800) return 'user-red';
if (rating >= 2400) return 'user-orange';
if (rating >= 2000) return 'user-yellow';
if (rating >= 1600) return 'user-blue';
if (rating >= 1200) return 'user-cyan';
if (rating >= 800) return 'user-green';
if (rating >= 400) return 'user-brown';
if (rating > 0) return 'user-gray';
return 'user-unrated';
}
function getAchRate(rating) {
const base = Math.floor(rating / 400) * 400;
return ((rating - base) / 400) * 100;
}
function colorize(u, ranking, rating) {
/* */if (ranking <= 1) u.insertAdjacentHTML('beforebegin', '<img style="vertical-align: middle;" src="//img.atcoder.jp/assets/icon/crown_champion.png"> ');
else if (ranking <= 10) u.insertAdjacentHTML('beforebegin', '<img style="vertical-align: middle;" src="//img.atcoder.jp/assets/icon/crown_gold.png"> ');
else if (ranking <= 30) u.insertAdjacentHTML('beforebegin', '<img style="vertical-align: middle;" src="//img.atcoder.jp/assets/icon/crown_silver.png"> ');
else if (ranking <= 100) u.insertAdjacentHTML('beforebegin', '<img style="vertical-align: middle;" src="//img.atcoder.jp/assets/icon/crown_bronze.png"> ');
else if (rating > 0) {
const color = getColor(rating);
const achRate = getAchRate(rating);
u.insertAdjacentHTML('beforebegin', `
<span style="
display: inline-block;
height: 12px;
width: 12px;
vertical-align: center;
border-radius: 50%;
border: solid 1px ${color};
background: -webkit-linear-gradient(
bottom,
${color} 0%,
${color} ${achRate}%,
rgba(255, 255, 255, 0.0) ${achRate}%,
rgba(255, 255, 255, 0.0) 100%);
"></span>
`);
}
u.classList.add(getColorClass(rating));
}
async function getRankingMap() {
const currentTime = new Date().getTime();
const lastUpdateTime = localStorage.getItem(lastUpdateKey);
if (lastUpdateTime && currentTime < Number(lastUpdateTime) + UPDATE_INTERVAL * 60 * 1000) {
return JSON.parse(localStorage.getItem(rankingKey));
} else {
let ranking = {};
const data = await fetch("https://corsproxy.io/?https://atcoder.jp/ranking", { mode: "cors", }).then(response => response.text());
const doc = new DOMParser().parseFromString(data, "text/html");
doc.querySelectorAll(".username > span").forEach((elem, idx) => {
const userName = elem.textContent;
ranking[userName] = idx + 1;
});
localStorage.setItem(lastUpdateKey, currentTime);
localStorage.setItem(rankingKey, JSON.stringify(ranking));
return ranking;
}
}
function getRanking(rankingMap, userName) {
if (userName in rankingMap) return rankingMap[userName];
return OUT_OF_RANK;
}
async function colorizeAll(root) {
for (const u of root.querySelectorAll('a[href*="/users/"]')) {
const userName = u.getAttribute('href').slice(7);
if (encodeURIComponent(u.textContent) !== userName) continue; // URL とユーザー名が一致しない場合も弾く
const lskey = "rating-" + userName;
const ranking = getRanking(rankingMap, userName);
let rating = lscache.get(lskey);
if (rating === null) {
const data = await fetch(`https://corsproxy.io/?https://atcoder.jp/users/${encodeURIComponent(userName)}/history/json`, { mode: 'cors', }).then(response => response.json());
const ratedCount = data.length;
if (ratedCount === 0) {
rating = 0;
} else {
rating = data[ratedCount - 1]["NewRating"];
}
lscache.set(lskey, rating, UPDATE_INTERVAL);
}
colorize(u, ranking, rating);
}
}
lscache.flushExpired();
const rankingMap = await getRankingMap();
colorizeAll(document);
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
for (const addedNode of mutation.addedNodes) {
colorizeAll(addedNode);
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
})();