AtCoder Problems Color Mod

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

当前为 2020-01-15 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name AtCoder Problems Color Mod
  3. // @namespace iilj
  4. // @version 2020.01.15.3
  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 {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 {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 {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 {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 表示ページで表のセルの色を塗り分ける.
  175. *
  176. * @date 2020-01-15
  177. * @param {string} userId
  178. */
  179. async function processTable(userId) {
  180. const [contestsMap, contestsUrl2Id] = await getContestsMap();
  181. const userResultsMap = await getUserResultsMap(userId);
  182. const contestProblemListsMap = await getContestProblemListMap();
  183.  
  184. document.querySelectorAll('td.table-success, td.table-warning').forEach(td => {
  185. const lnk = td.querySelector('a[href]');
  186. if (lnk.href in userResultsMap) {
  187. const userResult = userResultsMap[lnk.href];
  188. const contest = contestsMap[userResult.contest_id];
  189. if (userResult.epoch_second <= contest.start_epoch_second + contest.duration_second) {
  190. td.classList.add(td.classList.contains('table-success') ? 'apcm-intime' : 'apcm-intime-nonac');
  191. if (userResult.epoch_second < contest.start_epoch_second) {
  192. td.classList.add('apcm-intime-writer');
  193. }
  194. const divTimespan = document.createElement("div");
  195. divTimespan.innerText = formatTimespan(userResult.epoch_second - contest.start_epoch_second);
  196. divTimespan.classList.add('apcm-timespan');
  197. td.insertAdjacentElement('beforeend', divTimespan);
  198. }
  199. } else if (lnk.href in contestsUrl2Id) {
  200. const contestId = contestsUrl2Id[lnk.href];
  201. const contest = contestsMap[contestId];
  202. const contestProblemList = contestProblemListsMap[contestId];
  203. if (contestProblemList.every(problemId => {
  204. const key = getProblemUrl(contestId, problemId);
  205. if (key in userResultsMap) {
  206. const userResult = userResultsMap[key];
  207. return (userResult.epoch_second <= contest.start_epoch_second + contest.duration_second);
  208. }
  209. return false;
  210. })) {
  211. td.classList.add('apcm-intime');
  212. if (contestProblemList.every(problemId => {
  213. const key = getProblemUrl(contestId, problemId);
  214. const userResult = userResultsMap[key];
  215. return (userResult.epoch_second < contest.start_epoch_second);
  216. })) {
  217. td.classList.add('apcm-intime-writer');
  218. }
  219. }
  220. }
  221. });
  222. }
  223.  
  224. /**
  225. * List 表示ページでページ移動の検知に利用する MutationObserver
  226. *
  227. * @type {MutationObserver}
  228. */
  229. let listObserver;
  230.  
  231. /**
  232. * List 表示ページで表の行の色を塗り分ける.
  233. *
  234. * @date 2020-01-15
  235. * @param {string} userId ユーザID
  236. */
  237. async function processList(userId) {
  238. const [contestsMap, contestsUrl2Id] = await getContestsMap();
  239. const userResultsMap = await getUserResultsMap(userId);
  240. const contestProblemListsMap = await getContestProblemListMap();
  241.  
  242. const tbl = document.querySelector('.react-bs-table');
  243. const tableChanged = () => {
  244. tbl.querySelectorAll('tr.table-success, tr.table-warning').forEach(tr => {
  245. const lnk = tr.querySelector('a[href]');
  246. if (lnk.href in userResultsMap) {
  247. const userResult = userResultsMap[lnk.href];
  248. const contest = contestsMap[userResult.contest_id];
  249. if (userResult.epoch_second <= contest.start_epoch_second + contest.duration_second) {
  250. tr.classList.add(tr.classList.contains('table-success') ? 'apcm-intime' : 'apcm-intime-nonac');
  251. if (userResult.epoch_second < contest.start_epoch_second) {
  252. tr.classList.add('apcm-intime-writer');
  253. }
  254. }
  255. }
  256. });
  257. };
  258. listObserver = new MutationObserver(mutations => tableChanged());
  259. tableChanged();
  260. listObserver.observe(tbl, { childList: true, subtree: true });
  261. }
  262.  
  263. /**
  264. * ページ URL が変化した際のルートイベントハンドラ.
  265. *
  266. * @date 2020-01-15
  267. */
  268. const hrefChanged = () => {
  269. if (listObserver) {
  270. listObserver.disconnect();
  271. }
  272. /** @type {RegExpMatchArray} */
  273. let result;
  274. if (result = location.href.match(/^https?:\/\/kenkoooo\.com\/atcoder\/#\/table\/([^/?#]+)/)) {
  275. const userId = result[1];
  276. processTable(userId);
  277. }
  278. else if (result = location.href.match(/^https?:\/\/kenkoooo\.com\/atcoder\/#\/list\/([^/?#]+)/)) {
  279. const userId = result[1];
  280. processList(userId);
  281. }
  282. };
  283.  
  284. let href = location.href;
  285. const observer = new MutationObserver(mutations => {
  286. if (href === location.href) {
  287. return;
  288. }
  289. // href changed
  290. href = location.href;
  291. hrefChanged();
  292. });
  293. observer.observe(document, { childList: true, subtree: true });
  294. hrefChanged();
  295. })();