atcoder-tasks-page-colorize-during-contests

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

当前为 2021-08-09 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name atcoder-tasks-page-colorize-during-contests
  3. // @namespace iilj
  4. // @version 2021.8.0
  5. // @description atcoder-tasks-page-colorizer と同様の色付けを,コンテスト中にも行えるようにします.
  6. // @author iilj
  7. // @license MIT
  8. // @supportURL https://github.com/iilj/atcoder-tasks-page-colorize-during-contests/issues
  9. // @match https://atcoder.jp/contests/*/tasks
  10. // @grant none
  11. // ==/UserScript==
  12. const fetchJson = async (url) => {
  13. const res = await fetch(url);
  14. if (!res.ok) {
  15. throw new Error(res.statusText);
  16. }
  17. const obj = (await res.json());
  18. return obj;
  19. };
  20. const fetchContestStandings = async (contestSlug) => {
  21. const url = `https://atcoder.jp/contests/${contestSlug}/standings/json`;
  22. return await fetchJson(url);
  23. };
  24.  
  25. const getCurrentScores = async (contestSlug) => {
  26. const problemId2Info = new Map();
  27. const res = await fetch(`https://atcoder.jp/contests/${contestSlug}/score`);
  28. const scoreHtml = await res.text();
  29. const parser = new DOMParser();
  30. const doc = parser.parseFromString(scoreHtml, 'text/html');
  31. doc.querySelectorAll('#main-div tbody tr').forEach((tableRow) => {
  32. const anchor1 = tableRow.querySelector('td:nth-child(1) a');
  33. if (anchor1 === null)
  34. throw new Error('問題リンクが見つかりませんでした');
  35. const problemId = anchor1.href.split('/').pop();
  36. if (problemId === undefined)
  37. throw new Error('問題IDが見つかりませんでした');
  38. const td3 = tableRow.querySelector('td:nth-child(3)');
  39. if (td3 === null || td3.textContent === null)
  40. throw new Error('スコアが不明な行があります');
  41. const score = Number(td3.textContent);
  42. const td4 = tableRow.querySelector('td:nth-child(4)');
  43. if (td4 === null || td4.textContent === null)
  44. throw new Error('提出日時が不明な行があります');
  45. const datetimeString = td4.textContent;
  46. // console.log(problemId, score, datetimeString);
  47. problemId2Info.set(problemId, [score, datetimeString]);
  48. });
  49. return problemId2Info;
  50. };
  51.  
  52. class TaskListManager {
  53. constructor(mainContainer, contestSlug) {
  54. this.mainContainer = mainContainer;
  55. this.contestSlug = contestSlug;
  56. // ヘッダ挿入
  57. const headInsertPt = mainContainer.querySelector('thead th:last-child');
  58. if (headInsertPt === null)
  59. throw new Error('ヘッダ挿入ポイントが見つかりませんでした');
  60. headInsertPt.insertAdjacentHTML('beforebegin', '<th width="10%" class="text-center">得点</th><th class="text-center">提出日時</th>');
  61. // 問題一覧テーブルから,行・セル・問題IDを取り出してリストに収める
  62. this.rows = [];
  63. const rowElementss = this.mainContainer.querySelectorAll('#main-div tbody tr');
  64. rowElementss.forEach((rowElement) => {
  65. const anchor2 = rowElement.querySelector('td:nth-child(2) a');
  66. if (anchor2 === null)
  67. throw new Error('問題リンクが見つかりませんでした');
  68. const problemId = anchor2.href.split('/').pop();
  69. if (problemId === undefined)
  70. throw new Error('問題IDが見つかりませんでした');
  71. const tdInsertPt = rowElement.querySelector('td:last-child');
  72. if (tdInsertPt === null)
  73. throw new Error('td が見つかりませんでした');
  74. const scoreCell = document.createElement('td');
  75. const datetimeCell = document.createElement('td');
  76. scoreCell.classList.add('text-center');
  77. datetimeCell.classList.add('text-center');
  78. tdInsertPt.insertAdjacentElement('beforebegin', scoreCell);
  79. tdInsertPt.insertAdjacentElement('beforebegin', datetimeCell);
  80. scoreCell.textContent = '-';
  81. datetimeCell.textContent = '-';
  82. this.rows.push([problemId, rowElement, scoreCell, datetimeCell]);
  83. });
  84. }
  85. /** 「自分の得点状況」ページの情報からテーブルを更新する */
  86. async updateByScorePage() {
  87. this.problemId2Info = await getCurrentScores(this.contestSlug);
  88. this.rows.forEach(([problemId, rowElement, scoreCell, datetimeCell]) => {
  89. if (this.problemId2Info === undefined)
  90. return;
  91. if (this.problemId2Info.has(problemId)) {
  92. const [score, datetimeString] = this.problemId2Info.get(problemId);
  93. scoreCell.textContent = `${score}`;
  94. datetimeCell.textContent = datetimeString;
  95. if (datetimeString !== '-') {
  96. rowElement.classList.add(score > 0 ? 'success' : 'danger');
  97. }
  98. }
  99. else {
  100. throw new Error(`スコア情報がありません:${problemId}`);
  101. }
  102. });
  103. }
  104. /** 順位表情報からテーブルを更新する */
  105. async updateByStandings() {
  106. // 一部常設コンテストは順位表情報が提供されておらず 404 が返ってくる
  107. let standings;
  108. try {
  109. standings = await fetchContestStandings(this.contestSlug);
  110. }
  111. catch (_a) {
  112. console.warn('atcoder-tasks-page-colorize-during-contests: このコンテストは順位表が提供されていません');
  113. return;
  114. }
  115. const userStandingsEntry = standings.StandingsData.find((_standingsEntry) => _standingsEntry.UserScreenName == userScreenName);
  116. if (userStandingsEntry === undefined)
  117. return;
  118. this.rows.forEach(([problemId, rowElement, scoreCell, datetimeCell]) => {
  119. if (!(problemId in userStandingsEntry.TaskResults))
  120. return;
  121. const taskResultEntry = userStandingsEntry.TaskResults[problemId];
  122. const dt = startTime.clone().add(taskResultEntry.Elapsed / 1000000000, 's');
  123. // console.log(dt.format());
  124. if (this.problemId2Info === undefined)
  125. throw new Error('先に updateByScorePage() を呼んでください');
  126. const [score] = this.problemId2Info.get(problemId);
  127. const scoreFromStandings = taskResultEntry.Score / 100;
  128. if (scoreFromStandings >= score) {
  129. scoreCell.textContent = `${scoreFromStandings}`;
  130. datetimeCell.textContent = `${dt.format('YYYY/MM/DD HH:mm:ss')}`;
  131. }
  132. if (taskResultEntry.Status === 1) {
  133. if (rowElement.classList.contains('danger'))
  134. rowElement.classList.remove('danger');
  135. rowElement.classList.add('success');
  136. }
  137. else {
  138. if (rowElement.classList.contains('success'))
  139. rowElement.classList.remove('success');
  140. rowElement.classList.add('danger');
  141. }
  142. });
  143. }
  144. }
  145.  
  146. void (async () => {
  147. // 終了後のコンテストに対する処理は以下のスクリプトに譲る:
  148. // https://greasyfork.org/ja/scripts/380404-atcoder-tasks-page-colorizer
  149. if (moment() >= endTime)
  150. return;
  151. const mainContainer = document.getElementById('main-container');
  152. if (mainContainer === null)
  153. throw new Error('コンテナが見つかりませんでした');
  154. const taskListManager = new TaskListManager(mainContainer, contestScreenName);
  155. await taskListManager.updateByScorePage();
  156. console.log('atcoder-tasks-page-colorize-during-contests: updateByScorePage() ended');
  157. await taskListManager.updateByStandings();
  158. console.log('atcoder-tasks-page-colorize-during-contests: updateByStandings() ended');
  159. })();