AtCoder Problems Color Mod

AtCoder Problems のユーザページ上で色の塗り方を細分化します

  1. // ==UserScript==
  2. // @name AtCoder Problems Color Mod
  3. // @namespace iilj
  4. // @version 2020.01.16.1
  5. // @description AtCoder Problems のユーザページ上で色の塗り方を細分化します
  6. // @author iilj
  7. // @supportURL https://github.com/iilj/AtCoderProblemsColorMod/issues
  8. // @match https://kenkoooo.com/atcoder/*
  9. // @grant GM_addStyle
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. 'use strict';
  14.  
  15. GM_addStyle(`
  16. td.apcm-intime {
  17. background-color: #9AD59E;
  18. position: relative;
  19. }
  20. td.apcm-intime-writer {
  21. background-color: #9CF;
  22. }
  23. td.apcm-intime-nonac {
  24. background-color: #F5CD98;
  25. position: relative;
  26. }
  27. div.apcm-timespan {
  28. position: absolute;
  29. right: 0;
  30. bottom: 0;
  31. color: #888;
  32. font-size: x-small;
  33. }
  34.  
  35. /* style for list table */
  36. .react-bs-table .table-striped tbody tr td, .react-bs-table thead th {
  37. font-size: small;
  38. padding: 0.3rem;
  39. line-height: 1.0;
  40. white-space: normal;
  41. }
  42. .react-bs-table .table-striped tbody tr.apcm-intime td {
  43. background-color: #9AD59E;
  44. border-color: #DDD;
  45. }
  46. .react-bs-table .table-striped tbody tr.apcm-intime-writer td {
  47. background-color: #9CF;
  48. }
  49. .react-bs-table .table-striped tbody tr.apcm-intime-nonac td {
  50. background-color: #F5CD98;
  51. }
  52.  
  53. `);
  54.  
  55. /**
  56. * AtCoder コンテストの URL を返す.
  57. *
  58. * @param {string} contestId コンテスト ID
  59. * @returns {string} AtCoder コンテストの URL
  60. */
  61. const getContestUrl = (contestId) => `https://atcoder.jp/contests/${contestId}`;
  62.  
  63. /**
  64. * AtCoder コンテストの問題 URL を返す.
  65. *
  66. * @param {string} contestId コンテスト ID
  67. * @param {string} problemId 問題 ID
  68. * @returns {string} AtCoder コンテストの問題 URL
  69. */
  70. const getProblemUrl = (contestId, problemId) => `${getContestUrl(contestId)}/tasks/${problemId}`;
  71.  
  72. /**
  73. * url string to json object
  74. *
  75. * @date 2020-01-15
  76. * @param {string} uri 取得するリソースのURI
  77. * @returns {Promise<Object[]>} 配列
  78. */
  79. async function getJson(uri) {
  80. const response = await fetch(uri);
  81. /** @type {Object[]} */
  82. const obj = await response.json();
  83. return obj;
  84. }
  85.  
  86. /**
  87. * get contestId->contest map and contestUrl->contestId map
  88. *
  89. * @date 2020-01-15
  90. * @returns {Promise<Object[]>} array [contestId->contest map, contestUrl->contestId map]
  91. */
  92. async function getContestsMap() {
  93. const contests = await getJson('https://kenkoooo.com/atcoder/resources/contests.json');
  94. const contestsMap = contests.reduce((hash, contest) => {
  95. hash[contest.id] = contest;
  96. return hash;
  97. }, {});
  98. const contestsUrl2Id = contests.reduce((hash, contest) => {
  99. hash[getContestUrl(contest.id)] = contest.id;
  100. return hash;
  101. }, {});
  102. return [contestsMap, contestsUrl2Id];
  103. }
  104.  
  105. /**
  106. * return problemUrl->submit map from userId string
  107. *
  108. * @date 2020-01-15
  109. * @param {string} userId
  110. * @returns {Promise<Object>} problemUrl->submit map
  111. */
  112. async function getUserResultsMap(userId) {
  113. const userResults = await getJson(`https://kenkoooo.com/atcoder/atcoder-api/results?user=${userId}`);
  114. const userResultsMap = userResults.reduce((hash, submit) => {
  115. const key = getProblemUrl(submit.contest_id, submit.problem_id);
  116. if (key in hash) {
  117. // ACなら,なるべく昔のACを保持する
  118. if (submit.result == 'AC') {
  119. if (hash[key].result != 'AC') { // AC 済みではないなら,最新の結果で上書き
  120. hash[key] = submit;
  121. } else if (submit.epoch_second < hash[key].epoch_second) {// AC同士なら,なるべく昔のACを保持する
  122. hash[key] = submit;
  123. }
  124. } else {
  125. if (hash[key].result != 'AC' && submit.epoch_second < hash[key].epoch_second) { // ペナ同士なら,なるべく昔の提出を保持する
  126. hash[key] = submit;
  127. }
  128. }
  129. } else {
  130. hash[key] = submit;
  131. }
  132. return hash;
  133. }, {});
  134. return userResultsMap;
  135. }
  136.  
  137. /**
  138. * return contestId->[problemId] map
  139. *
  140. * @date 2020-01-15
  141. * @returns {Promise<Object>} contestId->[problemId] map
  142. */
  143. async function getContestProblemListMap() {
  144. const contestProblem = await getJson('https://kenkoooo.com/atcoder/resources/contest-problem.json');
  145. const contestProblemListsMap = contestProblem.reduce((hash, problem) => {
  146. if (problem.contest_id in hash) {
  147. hash[problem.contest_id].push(problem.problem_id);
  148. } else {
  149. hash[problem.contest_id] = [problem.problem_id];
  150. }
  151. return hash;
  152. }, {});
  153. return contestProblemListsMap;
  154. }
  155.  
  156. /**
  157. * 時間(秒)を表す整数値を mm:ss の形にフォーマットする.
  158. *
  159. * @param {number} sec 時間(秒)を表す整数値
  160. * @returns {string} mm:ss の形にフォーマットされた文字列
  161. */
  162. const formatTimespan = sec => {
  163. let sign;
  164. if (sec >= 0) {
  165. sign = '';
  166. } else {
  167. sign = '-';
  168. sec *= -1;
  169. }
  170. return `${sign}${Math.floor(sec / 60)}:${('0' + (sec % 60)).slice(-2)}`;
  171. }
  172.  
  173. /**
  174. * Table 表示ページで "Show Accepted" の変更検知に利用する MutationObserver
  175. *
  176. * @type {MutationObserver}
  177. */
  178. let tableObserver;
  179.  
  180. /**
  181. * Table 表示ページで表のセルの色を塗り分ける.
  182. *
  183. * @date 2020-01-16
  184. * @param {string} userId
  185. */
  186. async function processTable(userId) {
  187. const [contestsMap, contestsUrl2Id] = await getContestsMap();
  188. const userResultsMap = await getUserResultsMap(userId);
  189. const contestProblemListsMap = await getContestProblemListMap();
  190.  
  191. const tableChanged = () => {
  192. if (tableObserver) {
  193. tableObserver.disconnect();
  194. }
  195. document.querySelectorAll('td.table-success, td.table-warning').forEach(td => {
  196. const lnk = td.querySelector('a[href]');
  197. if (lnk.href in userResultsMap) {
  198. const userResult = userResultsMap[lnk.href];
  199. const contest = contestsMap[userResult.contest_id];
  200. if (userResult.epoch_second <= contest.start_epoch_second + contest.duration_second) {
  201. td.classList.add(td.classList.contains('table-success') ? 'apcm-intime' : 'apcm-intime-nonac');
  202. if (userResult.epoch_second < contest.start_epoch_second) {
  203. td.classList.add('apcm-intime-writer');
  204. }
  205. const divTimespan = document.createElement("div");
  206. divTimespan.innerText = formatTimespan(userResult.epoch_second - contest.start_epoch_second);
  207. divTimespan.classList.add('apcm-timespan');
  208. td.insertAdjacentElement('beforeend', divTimespan);
  209. }
  210. } else if (lnk.href in contestsUrl2Id) {
  211. const contestId = contestsUrl2Id[lnk.href];
  212. const contest = contestsMap[contestId];
  213. const contestProblemList = contestProblemListsMap[contestId];
  214. if (contestProblemList.every(problemId => {
  215. const key = getProblemUrl(contestId, problemId);
  216. if (key in userResultsMap) {
  217. const userResult = userResultsMap[key];
  218. return (userResult.epoch_second <= contest.start_epoch_second + contest.duration_second);
  219. }
  220. return false;
  221. })) {
  222. td.classList.add('apcm-intime');
  223. if (contestProblemList.every(problemId => {
  224. const key = getProblemUrl(contestId, problemId);
  225. const userResult = userResultsMap[key];
  226. return (userResult.epoch_second < contest.start_epoch_second);
  227. })) {
  228. td.classList.add('apcm-intime-writer');
  229. }
  230. }
  231. }
  232. });
  233. if (tableObserver) {
  234. document.querySelectorAll('.react-bs-container-body').forEach(div => {
  235. tableObserver.observe(div, { childList: true, subtree: true });
  236. });
  237. }
  238. };
  239.  
  240. tableObserver = new MutationObserver(mutations => tableChanged());
  241. tableChanged();
  242. document.querySelectorAll('.react-bs-container-body').forEach(div => {
  243. tableObserver.observe(div, { childList: true, subtree: true });
  244. });
  245. }
  246.  
  247. /**
  248. * List 表示ページでページ移動の検知に利用する MutationObserver
  249. *
  250. * @type {MutationObserver}
  251. */
  252. let listObserver;
  253.  
  254. /**
  255. * List 表示ページで表の行の色を塗り分ける.
  256. *
  257. * @date 2020-01-15
  258. * @param {string} userId ユーザID
  259. */
  260. async function processList(userId) {
  261. const [contestsMap, contestsUrl2Id] = await getContestsMap();
  262. const userResultsMap = await getUserResultsMap(userId);
  263. const contestProblemListsMap = await getContestProblemListMap();
  264.  
  265. const tbl = document.querySelector('.react-bs-table');
  266. const tableChanged = () => {
  267. tbl.querySelectorAll('tr.table-success, tr.table-warning').forEach(tr => {
  268. const lnk = tr.querySelector('a[href]');
  269. if (lnk.href in userResultsMap) {
  270. const userResult = userResultsMap[lnk.href];
  271. const contest = contestsMap[userResult.contest_id];
  272. if (userResult.epoch_second <= contest.start_epoch_second + contest.duration_second) {
  273. tr.classList.add(tr.classList.contains('table-success') ? 'apcm-intime' : 'apcm-intime-nonac');
  274. if (userResult.epoch_second < contest.start_epoch_second) {
  275. tr.classList.add('apcm-intime-writer');
  276. }
  277. }
  278. }
  279. });
  280. };
  281. listObserver = new MutationObserver(mutations => tableChanged());
  282. tableChanged();
  283. listObserver.observe(tbl, { childList: true, subtree: true });
  284. }
  285.  
  286. /**
  287. * ページ URL が変化した際のルートイベントハンドラ.
  288. *
  289. * @date 2020-01-15
  290. */
  291. const hrefChanged = () => {
  292. if (tableObserver) {
  293. tableObserver.disconnect();
  294. }
  295. if (listObserver) {
  296. listObserver.disconnect();
  297. }
  298.  
  299. /** @type {RegExpMatchArray} */
  300. let result;
  301. if (result = location.href.match(/^https?:\/\/kenkoooo\.com\/atcoder\/#\/table\/([^/?#]+)/)) {
  302. const userId = result[1];
  303. processTable(userId);
  304. }
  305. else if (result = location.href.match(/^https?:\/\/kenkoooo\.com\/atcoder\/#\/list\/([^/?#]+)/)) {
  306. const userId = result[1];
  307. processList(userId);
  308. }
  309. };
  310.  
  311. let href = location.href;
  312. const observer = new MutationObserver(mutations => {
  313. if (href === location.href) {
  314. return;
  315. }
  316. // href changed
  317. href = location.href;
  318. hrefChanged();
  319. });
  320. observer.observe(document, { childList: true, subtree: true });
  321. hrefChanged();
  322. })();