atcoder-tasks-page-colorize-during-contests

atcoder-tasks-page-colorizer と同様の色付けを,コンテスト中にも行えるようにします.

As of 2021-08-09. See the latest version.

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 or Violentmonkey 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-tasks-page-colorize-during-contests
// @namespace    iilj
// @version      2021.8.0
// @description  atcoder-tasks-page-colorizer と同様の色付けを,コンテスト中にも行えるようにします.
// @author       iilj
// @license      MIT
// @supportURL   https://github.com/iilj/atcoder-tasks-page-colorize-during-contests/issues
// @match        https://atcoder.jp/contests/*/tasks
// @grant        none
// ==/UserScript==
const fetchJson = async (url) => {
    const res = await fetch(url);
    if (!res.ok) {
        throw new Error(res.statusText);
    }
    const obj = (await res.json());
    return obj;
};
const fetchContestStandings = async (contestSlug) => {
    const url = `https://atcoder.jp/contests/${contestSlug}/standings/json`;
    return await fetchJson(url);
};

const getCurrentScores = async (contestSlug) => {
    const problemId2Info = new Map();
    const res = await fetch(`https://atcoder.jp/contests/${contestSlug}/score`);
    const scoreHtml = await res.text();
    const parser = new DOMParser();
    const doc = parser.parseFromString(scoreHtml, 'text/html');
    doc.querySelectorAll('#main-div tbody tr').forEach((tableRow) => {
        const anchor1 = tableRow.querySelector('td:nth-child(1) a');
        if (anchor1 === null)
            throw new Error('問題リンクが見つかりませんでした');
        const problemId = anchor1.href.split('/').pop();
        if (problemId === undefined)
            throw new Error('問題IDが見つかりませんでした');
        const td3 = tableRow.querySelector('td:nth-child(3)');
        if (td3 === null || td3.textContent === null)
            throw new Error('スコアが不明な行があります');
        const score = Number(td3.textContent);
        const td4 = tableRow.querySelector('td:nth-child(4)');
        if (td4 === null || td4.textContent === null)
            throw new Error('提出日時が不明な行があります');
        const datetimeString = td4.textContent;
        // console.log(problemId, score, datetimeString);
        problemId2Info.set(problemId, [score, datetimeString]);
    });
    return problemId2Info;
};

class TaskListManager {
    constructor(mainContainer, contestSlug) {
        this.mainContainer = mainContainer;
        this.contestSlug = contestSlug;
        // ヘッダ挿入
        const headInsertPt = mainContainer.querySelector('thead th:last-child');
        if (headInsertPt === null)
            throw new Error('ヘッダ挿入ポイントが見つかりませんでした');
        headInsertPt.insertAdjacentHTML('beforebegin', '<th width="10%" class="text-center">得点</th><th class="text-center">提出日時</th>');
        // 問題一覧テーブルから,行・セル・問題IDを取り出してリストに収める
        this.rows = [];
        const rowElementss = this.mainContainer.querySelectorAll('#main-div tbody tr');
        rowElementss.forEach((rowElement) => {
            const anchor2 = rowElement.querySelector('td:nth-child(2) a');
            if (anchor2 === null)
                throw new Error('問題リンクが見つかりませんでした');
            const problemId = anchor2.href.split('/').pop();
            if (problemId === undefined)
                throw new Error('問題IDが見つかりませんでした');
            const tdInsertPt = rowElement.querySelector('td:last-child');
            if (tdInsertPt === null)
                throw new Error('td が見つかりませんでした');
            const scoreCell = document.createElement('td');
            const datetimeCell = document.createElement('td');
            scoreCell.classList.add('text-center');
            datetimeCell.classList.add('text-center');
            tdInsertPt.insertAdjacentElement('beforebegin', scoreCell);
            tdInsertPt.insertAdjacentElement('beforebegin', datetimeCell);
            scoreCell.textContent = '-';
            datetimeCell.textContent = '-';
            this.rows.push([problemId, rowElement, scoreCell, datetimeCell]);
        });
    }
    /** 「自分の得点状況」ページの情報からテーブルを更新する */
    async updateByScorePage() {
        this.problemId2Info = await getCurrentScores(this.contestSlug);
        this.rows.forEach(([problemId, rowElement, scoreCell, datetimeCell]) => {
            if (this.problemId2Info === undefined)
                return;
            if (this.problemId2Info.has(problemId)) {
                const [score, datetimeString] = this.problemId2Info.get(problemId);
                scoreCell.textContent = `${score}`;
                datetimeCell.textContent = datetimeString;
                if (datetimeString !== '-') {
                    rowElement.classList.add(score > 0 ? 'success' : 'danger');
                }
            }
            else {
                throw new Error(`スコア情報がありません:${problemId}`);
            }
        });
    }
    /** 順位表情報からテーブルを更新する */
    async updateByStandings() {
        // 一部常設コンテストは順位表情報が提供されておらず 404 が返ってくる
        let standings;
        try {
            standings = await fetchContestStandings(this.contestSlug);
        }
        catch (_a) {
            console.warn('atcoder-tasks-page-colorize-during-contests: このコンテストは順位表が提供されていません');
            return;
        }
        const userStandingsEntry = standings.StandingsData.find((_standingsEntry) => _standingsEntry.UserScreenName == userScreenName);
        if (userStandingsEntry === undefined)
            return;
        this.rows.forEach(([problemId, rowElement, scoreCell, datetimeCell]) => {
            if (!(problemId in userStandingsEntry.TaskResults))
                return;
            const taskResultEntry = userStandingsEntry.TaskResults[problemId];
            const dt = startTime.clone().add(taskResultEntry.Elapsed / 1000000000, 's');
            // console.log(dt.format());
            if (this.problemId2Info === undefined)
                throw new Error('先に updateByScorePage() を呼んでください');
            const [score] = this.problemId2Info.get(problemId);
            const scoreFromStandings = taskResultEntry.Score / 100;
            if (scoreFromStandings >= score) {
                scoreCell.textContent = `${scoreFromStandings}`;
                datetimeCell.textContent = `${dt.format('YYYY/MM/DD HH:mm:ss')}`;
            }
            if (taskResultEntry.Status === 1) {
                if (rowElement.classList.contains('danger'))
                    rowElement.classList.remove('danger');
                rowElement.classList.add('success');
            }
            else {
                if (rowElement.classList.contains('success'))
                    rowElement.classList.remove('success');
                rowElement.classList.add('danger');
            }
        });
    }
}

void (async () => {
    // 終了後のコンテストに対する処理は以下のスクリプトに譲る:
    // https://greatest.deepsurf.us/ja/scripts/380404-atcoder-tasks-page-colorizer
    if (moment() >= endTime)
        return;
    const mainContainer = document.getElementById('main-container');
    if (mainContainer === null)
        throw new Error('コンテナが見つかりませんでした');
    const taskListManager = new TaskListManager(mainContainer, contestScreenName);
    await taskListManager.updateByScorePage();
    console.log('atcoder-tasks-page-colorize-during-contests: updateByScorePage() ended');
    await taskListManager.updateByStandings();
    console.log('atcoder-tasks-page-colorize-during-contests: updateByStandings() ended');
})();