ac-rating-icon

Add icons to the AtCoder standings table according to ratings.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        ac-rating-icon
// @namespace   https://su8ru.dev/
// @version     1.2.0
// @description Add icons to the AtCoder standings table according to ratings.
// @author      subaru <[email protected]>
// @supportURL  https://github.com/su8ru/ac-rating-icon/issues
// @license     MIT
// @match       https://atcoder.jp/*
// @exclude     https://atcoder.jp/*/json
// ==/UserScript==

// ================================================
//   View source code before bundling on GitHub:
//   https://github.com/su8ru/ac-rating-icon
// ================================================
const isElementWithVue = (element) => Object.prototype.hasOwnProperty.call(element, "__vue__");

const isString = (value) => typeof value === "string";
const isNumber = (value) => typeof value === "number";
const isBoolean = (value) => typeof value === "boolean";
const isObject = (value) => typeof value === "object" && value !== null;

const isVueWithUserInfo = (vue) => isObject(vue.u) &&
    isNumber(vue.u.Rating) &&
    isBoolean(vue.u.IsTeam) &&
    isString(vue.u.UserScreenName);

const createIconElement = (iconSvg) => {
    const template = document.createElement("template");
    template.innerHTML = iconSvg;
    return template.content.firstChild;
};

const icons = [
    '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" style="fill:currentColor"/></svg>',
    '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g style="fill:currentColor"><path class="b" transform="rotate(-45 8.002 7.996)" d="M7 1.64h2v12.73H7z"/><circle class="b" cx="3.5" cy="3.5" r="3.5"/><circle class="b" cx="12.5" cy="12.5" r="3.5"/></g></svg>',
    '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g style="fill:currentColor"><path class="b" d="M8 14.04 1.79 3h12.42L8 14.04ZM5.21 5 8 9.96 10.79 5H5.21Z"/><circle class="b" cx="3.5" cy="4" r="3.5"/><circle class="b" cx="12.5" cy="4" r="3.5"/><circle class="b" cx="8" cy="12" r="3.5"/></g></svg>',
    '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g style="fill:currentColor"><path class="b" d="M13.5 13.5h-11v-11h11v11Zm-9-2h7v-7h-7v7Z"/><circle class="b" cx="3.5" cy="3.5" r="3.5"/><circle class="b" cx="3.5" cy="12.5" r="3.5"/><circle class="b" cx="12.5" cy="12.5" r="3.5"/><circle class="b" cx="12.5" cy="3.5" r="3.5"/></g></svg>',
];
const subIcons = [
    '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" style="fill:currentColor"/><circle cx="8" cy="8" r="4" style="fill:#fff"/></svg>',
    '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g style="fill:currentColor"><path class="c" transform="rotate(-45 8.002 7.996)" d="M7 1.64h2v12.73H7z"/><circle class="c" cx="3.5" cy="3.5" r="3.5"/><circle class="c" cx="12.5" cy="12.5" r="3.5"/></g><g style="fill:#fff"><circle class="b" cx="3.5" cy="3.5" r="1.5"/><circle class="b" cx="12.5" cy="12.5" r="1.5"/></g></svg>',
    '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g style="fill:currentColor"><path class="c" d="M8 14.04 1.79 3h12.42L8 14.04ZM5.21 5 8 9.96 10.79 5H5.21Z"/><circle class="c" cx="3.5" cy="4" r="3.5"/><circle class="c" cx="12.5" cy="4" r="3.5"/><circle class="c" cx="8" cy="12" r="3.5"/></g><g style="fill:#fff"><circle class="b" cx="3.5" cy="4" r="1.5"/><circle class="b" cx="12.5" cy="4" r="1.5"/><circle class="b" cx="8" cy="12" r="1.5"/></g></svg>',
    '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g style="fill:currentColor"><path class="c" d="M13.5 13.5h-11v-11h11v11Zm-9-2h7v-7h-7v7Z"/><circle class="c" cx="3.5" cy="3.5" r="3.5"/><circle class="c" cx="3.5" cy="12.5" r="3.5"/><circle class="c" cx="12.5" cy="12.5" r="3.5"/><circle class="c" cx="12.5" cy="3.5" r="3.5"/></g><g style="fill:#fff"><circle class="b" cx="3.5" cy="3.5" r="1.5"/><circle class="b" cx="3.5" cy="12.5" r="1.5"/><circle class="b" cx="12.5" cy="12.5" r="1.5"/><circle class="b" cx="12.5" cy="3.5" r="1.5"/></g></svg>',
];

const ratingToRankSvg = (rating) => {
    if (0 < rating && rating < 400) {
        if (rating < 32)
            return subIcons[0];
        if (rating < 54)
            return subIcons[1];
        if (rating < 89)
            return subIcons[2];
        if (rating < 147)
            return subIcons[3];
        if (rating < 188)
            return icons[0];
        if (rating < 242)
            return icons[1];
        if (rating < 311)
            return icons[2];
        return icons[3];
    }
    return icons[rating > 2800 ? 0 : ((rating % 400) / 100) | 0];
};

const updateStandings = () => {
    Array.from(document.querySelectorAll(".standings-username > a.username")).map((userElement) => {
        var _a;
        if (isElementWithVue(userElement) &&
            isVueWithUserInfo(userElement.__vue__) &&
            !userElement.__vue__.u.IsTeam &&
            !userElement.querySelector("img") &&
            !userElement.querySelector("svg")) {
            const iconElement = createIconElement(ratingToRankSvg(userElement.__vue__.u.Rating));
            // set style
            const colorClassName = (_a = userElement.querySelector("span")) === null || _a === void 0 ? void 0 : _a.className;
            iconElement.setAttribute("class", colorClassName !== null && colorClassName !== void 0 ? colorClassName : "");
            Object.assign(iconElement.style, {
                width: "14px",
                height: "14px",
                verticalAlign: "text-bottom",
                marginRight: "4px",
            });
            // insert
            userElement.insertBefore(iconElement, userElement.querySelector("span"));
        }
    });
};

const updateProfile = () => {
    const _tableElements = document.querySelectorAll("table");
    if (_tableElements.length < 2)
        return;
    const tableElement = _tableElements[1];
    [...Array(2)].map((_, index) => {
        const tdElement = tableElement.tBodies[0].rows[index +
            (tableElement.tBodies[0].rows[0].cells[0].innerText === "順位" ? 1 : 0)].cells[1];
        const spanElement = tdElement.querySelector("span");
        const rating = +spanElement.innerText || 0;
        const iconElement = createIconElement(ratingToRankSvg(rating));
        const colorClassName = spanElement.className;
        iconElement.setAttribute("class", colorClassName);
        Object.assign(iconElement.style, {
            width: "14px",
            height: "14px",
            verticalAlign: "text-bottom",
            marginRight: "4px",
        });
        tdElement.insertBefore(iconElement, spanElement);
        if (index === 0) {
            const updateBigIcon = () => {
                // === config ====
                const checkboxElement = document.getElementById("acri-profile-icon-config");
                if (!checkboxElement)
                    return;
                const showProfileIcon = checkboxElement.checked;
                const svgElement = document.getElementById("acri-profile-big-icon");
                if (!showProfileIcon) {
                    if (svgElement)
                        svgElement.remove();
                    return;
                }
                // ==== create element ====
                if (svgElement)
                    return;
                const canvasElement = document.getElementById("ratingStatus");
                const divElement = canvasElement === null || canvasElement === void 0 ? void 0 : canvasElement.parentElement;
                if (!canvasElement || !divElement)
                    return;
                const bigIconElement = iconElement.cloneNode(true);
                bigIconElement.id = "acri-profile-big-icon";
                Object.assign(bigIconElement.style, {
                    width: "40px",
                    height: "40px",
                    position: "absolute",
                    top: "-6px",
                    left: "40px",
                });
                divElement.style.position = "relative";
                divElement.insertBefore(bigIconElement, canvasElement);
            };
            const buttonGroupElement = document.querySelector(".col-md-9 .btn-text-group");
            if (!buttonGroupElement)
                return;
            buttonGroupElement.insertAdjacentHTML("beforeend", `<span class="divider"></span>
        <input type="checkbox" id="acri-profile-icon-config" checked />
        <label for="acri-profile-icon-config">[ac-rating-icon] Show Profile Icon</label>
        `);
            const checkboxElement = document.getElementById("acri-profile-icon-config");
            if (!checkboxElement)
                return;
            checkboxElement.addEventListener("change", updateBigIcon);
            updateBigIcon();
        }
    });
};

var _a;
const observeTable = () => {
    var _a;
    updateStandings();
    const tableElement = (_a = document.getElementById("standings-tbody")) === null || _a === void 0 ? void 0 : _a.parentElement;
    if (tableElement)
        new MutationObserver(updateStandings).observe(tableElement.tBodies[0], {
            childList: true,
        });
};
if (/standings(\/virtual)?\/?/.test(document.location.href)) {
    const loaded = () => !!document.getElementById("standings-tbody");
    const loadingElement = (_a = document
        .getElementById("vue-standings")) === null || _a === void 0 ? void 0 : _a.getElementsByClassName("loading-show")[0];
    if (loadingElement)
        new MutationObserver(() => {
            if (loaded())
                observeTable();
        }).observe(loadingElement, { attributes: true });
}
if (/users\/([^/]+)\/?/.test(document.location.href)) {
    updateProfile();
}