您需要先安装一个扩展,例如 篡改猴、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();
- })();