AtCoder-Favorite-Person-Colors

AtCoderのお気に入り管理のユーザーに色がつきます+レート順ソート(テーブル2つ対応)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         AtCoder-Favorite-Person-Colors
// @namespace    https://ruku.tellpro.net
// @version      2025-06-19-ver3
// @description  AtCoderのお気に入り管理のユーザーに色がつきます+レート順ソート(テーブル2つ対応)
// @author       ruku
// @match        https://atcoder.jp/settings/fav
// @icon         https://www.google.com/s2/favicons?sz=64&domain=atcoder.jp
// @grant        none
// @license      MIT
// ==/UserScript==

const diffToColor = (diff) => {
    if(diff === 0) return "black";
    if(diff <= 399) return "#808080";
    if(diff <= 799) return "#804000";
    if(diff <= 1199) return "#008000";
    if(diff <= 1599) return "#00C0C0";
    if(diff <= 1999) return "#0000FF";
    if(diff <= 2399) return "#C0C000";
    if(diff <= 2799) return "#FF8000";
    return "#FF0000";
};

(function() {
    'use strict';
    const users = document.querySelectorAll("td a[href^='/users/']");
    const userElements = Array.from(users);
    const processed = new Set();
    const userRatingMap = {};

    // --- テーブルごとにトグルボタン追加 ---
    const tables = Array.from(document.querySelectorAll("table"));
    tables.forEach((table, tableIdx) => {
        const tbody = table.querySelector("tbody");
        if (!tbody) return;
        const rows = Array.from(tbody.querySelectorAll("tr"));

        // 元の順序保存
        rows.forEach((tr, i) => tr.setAttribute("data-original-index", i));

        // ボタン作成
        const btn = document.createElement("button");
        btn.textContent = "Sort by Rating: OFF";
        btn.style.margin = "8px";
        btn.style.background = "#eee";
        let sorted = false;

        btn.onclick = function() {
            if (!sorted) {
                // レート順(降順)でソート
                rows.sort((a, b) => {
                    const userA = a.querySelector("a[href^='/users/']")?.textContent.trim();
                    const userB = b.querySelector("a[href^='/users/']")?.textContent.trim();
                    const rateA = userRatingMap[userA] ?? 0;
                    const rateB = userRatingMap[userB] ?? 0;
                    return rateB - rateA;
                });
                btn.textContent = "Sort by Rating: ON";
                btn.style.background = "#cceeff";
                sorted = true;
            } else {
                // 元の順序に戻す
                rows.sort((a, b) => {
                    return Number(a.getAttribute("data-original-index")) - Number(b.getAttribute("data-original-index"));
                });
                btn.textContent = "Sort by Rating: OFF";
                btn.style.background = "#eee";
                sorted = false;
            }
            // 並び替え
            rows.forEach(tr => tbody.appendChild(tr));
        };

        // テーブルの前にボタンを追加
        table.parentNode.insertBefore(btn, table);
    });

    // --- 色付け・レート取得は非同期で ---
    (async function() {
        for(const userElem of userElements){
            const user = userElem.textContent.trim();
            if(!user) continue;
            if(processed.has(user)) continue;
            processed.add(user);

            try {
                const response = await fetch(`https://kenkoooo.com/atcoder/proxy/users/${user}/history/json`);
                if (!response.ok) continue;
                const data = await response.json();
                if (data.length === 0) continue;
                const lastColor = data[data.length - 1].NewRating;
                const color = diffToColor(lastColor);
                userRatingMap[user] = lastColor;

                userElements
                    .filter(el => el.textContent.trim() === user)
                    .forEach(el => {
                        el.style.color = color;
                        // (Rate)を追加
                        if (!el.nextSibling || !el.nextSibling.classList || !el.nextSibling.classList.contains('atcoder-rate-label')) {
                            const rateSpan = document.createElement('span');
                            rateSpan.textContent = ` (${lastColor})`;
                            rateSpan.className = 'atcoder-rate-label';
                            rateSpan.style.color = color;
                            el.parentNode.insertBefore(rateSpan, el.nextSibling);
                        }
                    });
            } catch (e) {
                // ignore
            }
        }
    })();
})();