Show your current contest rank at the bottom-left corner.
// ==UserScript==
// @name AtCoder Rank Overlay
// @name:ja AtCoder Rank Overlay
// @namespace https://github.com/yourname
// @version 1.0.0
// @description Show your current contest rank at the bottom-left corner.
// @description:ja 順位を左下に常時表示する
// @author yourname
// @match https://atcoder.jp/contests/*
// @grant none
// @run-at document-end
// @license MIT
// ==/UserScript==
(() => {
"use strict";
//--------------------------------------------------
// Config
//--------------------------------------------------
const UPDATE_INTERVAL = 30 * 1000; // 30 sec
//--------------------------------------------------
// Utilities
//--------------------------------------------------
function getContestId() {
const m = location.pathname.match(/^\/contests\/([^/]+)/);
return m ? m[1] : null;
}
function createOverlay() {
const box = document.createElement("div");
Object.assign(box.style, {
position: "fixed",
left: "14px",
bottom: "14px",
zIndex: "2147483647",
background: "rgba(0, 0, 0, 0.82)",
color: "#fff",
padding: "10px 14px",
borderRadius: "12px",
fontSize: "14px",
fontWeight: "600",
fontFamily: "system-ui, sans-serif",
boxShadow: "0 4px 18px rgba(0,0,0,0.25)",
backdropFilter: "blur(4px)",
WebkitBackdropFilter: "blur(4px)",
minWidth: "150px",
userSelect: "none",
pointerEvents: "none",
});
box.textContent = "Rank: Loading...";
document.body.appendChild(box);
return box;
}
//--------------------------------------------------
// Get username
//--------------------------------------------------
function getUsername() {
// Header user link
const a = document.querySelector('a[href^="/users/"]');
if (!a) return null;
const href = a.getAttribute("href");
const m = href.match(/^\/users\/([^/]+)/);
return m ? m[1] : null;
}
//--------------------------------------------------
// Fetch standing
//--------------------------------------------------
async function fetchStanding(contestId, username) {
try {
const url = `https://atcoder.jp/contests/${contestId}/standings/json`;
const res = await fetch(url, {
credentials: "same-origin",
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const data = await res.json();
if (!Array.isArray(data.StandingsData)) {
return null;
}
const user = data.StandingsData.find(
(x) => x.UserScreenName === username,
);
if (!user) {
return {
rank: null,
score: null,
penalty: null,
};
}
return {
rank: user.Rank,
score: user.TotalResult?.Score ?? 0,
penalty: user.TotalResult?.Penalty ?? 0,
};
} catch (err) {
console.error("[AtCoder Rank Overlay]", err);
return null;
}
}
//--------------------------------------------------
// Main
//--------------------------------------------------
async function main() {
const contestId = getContestId();
if (!contestId) return;
const username = getUsername();
if (!username) return;
const overlay = createOverlay();
const cacheKey = `atcoder-rank-overlay:${contestId}:${username}`;
//--------------------------------------------------
// Render
//--------------------------------------------------
function render(data, updating = false) {
if (!data || data.rank == null) {
overlay.innerHTML = `
<div>Rank: --</div>
<div style="
margin-top:4px;
font-size:12px;
opacity:0.75;
">
No submission yet
</div>
`;
return;
}
overlay.innerHTML = `
<div style="
font-size:15px;
display:flex;
align-items:center;
gap:8px;
">
<span>Rank: #${data.rank}</span>
<span style="
font-size:11px;
opacity:${updating ? "0.65" : "0"};
width:72px;
display:inline-block;
text-align:left;
transition:opacity 0.15s ease;
flex-shrink:0;
">
Updating...
</span>
</div>
<div style="
margin-top:4px;
font-size:12px;
opacity:0.82;
">
Score: ${(data.score / 100).toFixed(0)}
|
Penalty: ${data.penalty}
</div>
`;
}
//--------------------------------------------------
// Load cache immediately
//--------------------------------------------------
let cachedData = null;
try {
const raw = localStorage.getItem(cacheKey);
if (raw) {
cachedData = JSON.parse(raw);
render(cachedData, true);
} else {
overlay.textContent = "Rank: Loading...";
}
} catch {
overlay.textContent = "Rank: Loading...";
}
//--------------------------------------------------
// Update from server
//--------------------------------------------------
async function update() {
// Loading表示を出さない
if (cachedData) {
render(cachedData, true);
}
const standing = await fetchStanding(contestId, username);
if (!standing) {
if (!cachedData) {
overlay.textContent = "Rank: Failed to load";
}
return;
}
cachedData = standing;
render(standing, false);
//--------------------------------------------------
// Save cache
//--------------------------------------------------
try {
localStorage.setItem(
cacheKey,
JSON.stringify({
...standing,
updatedAt: Date.now(),
}),
);
} catch {}
}
//--------------------------------------------------
// Initial update
//--------------------------------------------------
await update();
//--------------------------------------------------
// Periodic update
//--------------------------------------------------
setInterval(update, UPDATE_INTERVAL);
}
main();
})();