atcoder-tasks-page-colorize-during-contests

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

// ==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://greasyfork.org/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');
})();