您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
AtCoder Problems のテーブルページ上で問題ごとにコンテスト中のペナルティ数を表示します
// ==UserScript== // @name AtCoder Problems Penalty Counter // @namespace iilj // @version 2020.01.28.2 // @description AtCoder Problems のテーブルページ上で問題ごとにコンテスト中のペナルティ数を表示します // @author iilj // @supportURL https://github.com/iilj/AtCoderProblemsExt/issues // @match https://kenkoooo.com/atcoder/* // @grant GM_addStyle // ==/UserScript== (function () { 'use strict'; GM_addStyle(` span.appc-penalty { color: red; margin-left: .4rem; margin-right: .1rem; } `); /** * AtCoder コンテストの URL を返す. * * @param {string} contestId コンテスト ID * @returns {string} AtCoder コンテストの URL */ const getContestUrl = (contestId) => `https://atcoder.jp/contests/${contestId}`; /** * AtCoder コンテストの問題 URL を返す. * * @param {string} contestId コンテスト ID * @param {string} problemId 問題 ID * @returns {string} AtCoder コンテストの問題 URL */ const getProblemUrl = (contestId, problemId) => `${getContestUrl(contestId)}/tasks/${problemId}`; /** * url string to json object * * @date 2020-01-15 * @param {string} uri 取得するリソースのURI * @returns {Promise<Object[]>} 配列 */ async function getJson(uri) { const response = await fetch(uri); /** @type {Object[]} */ const obj = await response.json(); return obj; } /** * get contestId->contest map and contestUrl->contestId map * * @date 2020-01-15 * @returns {Promise<Object[]>} array [contestId->contest map, contestUrl->contestId map] */ async function getContestsMap() { const contests = await getJson('https://kenkoooo.com/atcoder/resources/contests.json'); const contestsMap = contests.reduce((hash, contest) => { hash[contest.id] = contest; return hash; }, {}); const contestsUrl2Id = contests.reduce((hash, contest) => { hash[getContestUrl(contest.id)] = contest.id; return hash; }, {}); return [contestsMap, contestsUrl2Id]; } /** * return problemUrl->penalty map from userId string * * @date 2020-01-15 * @param {string} userId * @returns {Promise<Object>} problemUrl->penalty map */ async function getUserPenaltyMap(userId, contestsMap) { const userResults = await getJson(`https://kenkoooo.com/atcoder/atcoder-api/results?user=${userId}`); const userPenaltyMap = userResults.reduce((hash, submit) => { const key = getProblemUrl(submit.contest_id, submit.problem_id); const contest = contestsMap[submit.contest_id]; if (!(key in hash)) { hash[key] = 0; } if (submit.epoch_second <= contest.start_epoch_second + contest.duration_second && submit.result != 'AC') { hash[key]++; } return hash; }, {}); return userPenaltyMap; } /** * Table 表示ページで "Show Accepted" の変更検知に利用する MutationObserver * * @type {MutationObserver} */ let tableObserver; /** * Table 表示ページで表のセルの色を塗り分ける. * * @date 2020-01-16 * @param {string} userId */ async function processTable(userId) { const [contestsMap, contestsUrl2Id] = await getContestsMap(); const userPenaltyMap = await getUserPenaltyMap(userId, contestsMap); const tableChanged = () => { if (tableObserver) { tableObserver.disconnect(); } document.querySelectorAll('span.appc-penalty').forEach(spanPenalty => { spanPenalty.innerText = ''; }); document.querySelectorAll('td.table-problem').forEach(td => { const lnk = td.querySelector('a[href]'); if (lnk && lnk.href in userPenaltyMap && userPenaltyMap[lnk.href] > 0) { const userPenalty = userPenaltyMap[lnk.href]; let divTimespan = td.querySelector('.table-problem-timespan'); if (!divTimespan) { divTimespan = document.createElement("div"); divTimespan.classList.add('table-problem-timespan'); td.insertAdjacentElement('beforeend', divTimespan); } let spanPenalty = divTimespan.querySelector('span.appc-penalty'); if (!spanPenalty) { spanPenalty = document.createElement("span"); spanPenalty.classList.add('appc-penalty'); divTimespan.insertAdjacentElement('beforeend', spanPenalty); } spanPenalty.innerText = `(${userPenalty})`; } }); if (tableObserver) { document.querySelectorAll('.react-bs-container-body').forEach(div => { tableObserver.observe(div, { childList: true, subtree: true }); }); } }; tableObserver = new MutationObserver(mutations => tableChanged()); tableChanged(); document.querySelectorAll('.react-bs-container-body').forEach(div => { tableObserver.observe(div, { childList: true, subtree: true }); }); } /** * ページ URL が変化した際のルートイベントハンドラ. * * @date 2020-01-15 */ const hrefChanged = () => { if (tableObserver) { tableObserver.disconnect(); } /** @type {RegExpMatchArray} */ let result; if (result = location.href.match(/^https?:\/\/kenkoooo\.com\/atcoder\/#\/table\/([^/?#]+)/)) { const userId = result[1]; processTable(userId); } }; let href = location.href; const observer = new MutationObserver(mutations => { if (href === location.href) { return; } // href changed href = location.href; hrefChanged(); }); observer.observe(document, { childList: true, subtree: true }); hrefChanged(); })();