Greasy Fork is available in English.

AtCoderVirtualTimer

バーチャルの開始までの時間、残り時間をコンテストと同じように表示します

  1. // ==UserScript==
  2. // @name AtCoderVirtualTimer
  3. // @namespace ocha98-virtual-timer
  4. // @version 0.2
  5. // @description バーチャルの開始までの時間、残り時間をコンテストと同じように表示します
  6. // @author Ocha98
  7. // @match https://atcoder.jp/contests/*
  8. // @supportURL https://github.com/ocha98/AtCoderVirtualTimer/issues
  9. // @source https://github.com/ocha98/AtCoderVirtualTimer
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. class Timer {
  14. constructor(targetElementId, startDate, durationMinutes) {
  15. this.startDate = startDate;
  16. this.endDate = new Date(this.startDate.getTime() + durationMinutes * 60 * 1000); // durationMinutesをミリ秒に変換してendDateを計算
  17. this.targetElement = document.getElementById(targetElementId);
  18. this.virtualTimer = this.createVirtualTimer();
  19. this.timeDelta = Cookies.getJSON("timeDelta");
  20. document.body.appendChild(this.virtualTimer);
  21.  
  22. if(typeof this.timeDelta === 'undefined'){
  23. this.timeDelta = 0;
  24. }
  25. }
  26.  
  27. now() {
  28. const date = new Date();
  29. date.setTime(date.getTime() + this.timeDelta);
  30. return date;
  31. }
  32.  
  33. createVirtualTimer() {
  34. const p = document.createElement('p');
  35. p.classList.add("contest-timer")
  36. p.id = 'virtual-timer';
  37. p.style.position = 'fixed';
  38. p.style.right = '10px';
  39. p.style.bottom = '0';
  40. p.style.width = '160px';
  41. p.style.height = '80px';
  42. p.style.margin = '0';
  43. p.style.padding = '20px 0';
  44. p.style.backgroundImage = 'url("//img.atcoder.jp/assets/contest/digitalclock.png")';
  45. p.style.textAlign = 'center';
  46. p.style.lineHeight = '20px';
  47. p.style.fontSize = '15px';
  48. p.style.cursor = 'pointer';
  49. p.style.zIndex = '50';
  50. p.style.userSelect = 'none';
  51. p.style.display = 'none'; // 初めから非表示にする
  52. return p;
  53. }
  54.  
  55. start() {
  56. if (this.intervalId) { return; }
  57. this.bindClickEvent();
  58.  
  59. this.targetElement.style.display = 'none';
  60. this.virtualTimer.style.display = 'block';
  61.  
  62. const updateDisplay = () => {
  63. const nowDate = this.now();
  64. if (this.startDate > nowDate) {
  65. // バーチャル開始前
  66. const formattedTimeDiff = this.formatTimeDifference(this.startDate, nowDate);
  67. this.virtualTimer.innerHTML = `開始まであと<br/>${formattedTimeDiff}`;
  68. } else if(nowDate < this.endDate) {
  69. // バーチャル中
  70. const formattedTimeDiff = this.formatTimeDifference(this.endDate, nowDate);
  71. this.virtualTimer.innerHTML = `残り時間<br/>${formattedTimeDiff}`;
  72. } else {
  73. // バーチャル終了後
  74. this.targetElement.innerHTML = "";
  75. this.stop();
  76. this.unbindClickEvent(); // イベント破棄
  77. }
  78. };
  79.  
  80. updateDisplay();
  81. this.intervalId = setInterval(updateDisplay, 1000);
  82. }
  83.  
  84. stop() {
  85. if (this.intervalId) {
  86. clearInterval(this.intervalId);
  87. this.intervalId = null;
  88. }
  89. }
  90.  
  91. formatTimeDifference(startDate, nowDate) {
  92. const diffInSeconds = (startDate - nowDate) / 1000;
  93. const days = Math.floor(diffInSeconds / 86400);
  94. const hours = Math.floor((diffInSeconds % 86400) / 3600);
  95. const minutes = Math.floor((diffInSeconds % 3600) / 60);
  96. const seconds = Math.floor(diffInSeconds % 60);
  97.  
  98. if (days > 0) {
  99. return `${days}日と${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
  100. } else {
  101. return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
  102. }
  103. }
  104.  
  105. unbindClickEvent() {
  106. // イベントを破棄するためのメソッド
  107. this.targetElement.removeEventListener('click', this.toggleDisplay);
  108. this.virtualTimer.removeEventListener('click', this.toggleDisplay);
  109. this.virtualTimer.style.display = 'none';
  110. this.targetElement.style.display = 'block';
  111. }
  112.  
  113. bindClickEvent() {
  114. // バチャの時計ともとからある時計を切り替える
  115. this.toggleDisplay = (e) => {
  116. if (e.target === this.targetElement && this.targetElement.style.opacity == 1) {
  117. this.targetElement.style.display = 'none';
  118. this.virtualTimer.style.display = 'block';
  119. } else {
  120. this.virtualTimer.style.display = 'none';
  121. this.targetElement.style.display = 'block';
  122. }
  123. };
  124. this.targetElement.addEventListener('click', this.toggleDisplay);
  125. this.virtualTimer.addEventListener('click', this.toggleDisplay);
  126. }
  127. }
  128.  
  129. async function getVirtualContestPage() {
  130. const contestName = window.location.pathname.split('/')[2];
  131. const virtualUrl = `https://atcoder.jp/contests/${contestName}/virtual`;
  132.  
  133. const response = await fetch(virtualUrl);
  134. if (!response.ok) {
  135. throw new Error(`Failed to fetch ${virtualUrl}. response status: ${response.status} status text: ${response.statusText}`);
  136. }
  137.  
  138. const pageContent = await response.text();
  139.  
  140. const parser = new DOMParser();
  141. return parser.parseFromString(pageContent, "text/html");
  142. }
  143.  
  144. // コンテスト時間を分で返す
  145. function getContestDurationMinutes(dom) {
  146. const contestDurationElement = dom.querySelectorAll('small.contest-duration a');
  147. // YYYYMMDDTHHMM
  148. const strToDate = (str) => {
  149. return new Date(
  150. parseInt(str.substring(0, 4)), // year
  151. parseInt(str.substring(4, 6)) - 1, // month (0-indexed)
  152. parseInt(str.substring(6, 8)), // day
  153. parseInt(str.substring(9, 11)), // hour
  154. parseInt(str.substring(11, 13)) // minute
  155. );
  156. };
  157. const startMatch = contestDurationElement[0].href.match(/iso=(\d+T\d+)/);
  158. const endMatch = contestDurationElement[1].href.match(/iso=(\d+T\d+)/);
  159.  
  160. const startStr = startMatch[1];
  161. const endStr = endMatch[1];
  162.  
  163. const startDate = strToDate(startStr);
  164. const endDate = strToDate(endStr);
  165.  
  166. const durationMinutes = (endDate.getTime() - startDate.getTime()) / (1000 * 60); // ミリ秒を分に変換
  167.  
  168. return durationMinutes;
  169. }
  170.  
  171. // バチャの開始時刻を取得
  172. function getVirtualStartTime(dom) {
  173. const timeElement = dom.querySelector('#main-container time.fixtime-second');
  174. const timeText = timeElement.textContent.trim();
  175. return new Date(timeText);
  176. }
  177.  
  178. async function main() {
  179. try {
  180. const dom = await getVirtualContestPage()
  181. const virtualStartDate = getVirtualStartTime(dom);
  182. const contestDurationMinutes = getContestDurationMinutes(dom);
  183.  
  184. const timer = new Timer('fixed-server-timer', virtualStartDate, contestDurationMinutes);
  185. timer.start();
  186. } catch (error) {
  187. console.error(error);
  188. }
  189. }
  190.  
  191. main();