您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
バーチャルの開始までの時間、残り時間をコンテストと同じように表示します
// ==UserScript== // @name AtCoderVirtualTimer // @namespace ocha98-virtual-timer // @version 0.2 // @description バーチャルの開始までの時間、残り時間をコンテストと同じように表示します // @author Ocha98 // @match https://atcoder.jp/contests/* // @supportURL https://github.com/ocha98/AtCoderVirtualTimer/issues // @source https://github.com/ocha98/AtCoderVirtualTimer // @license MIT // ==/UserScript== class Timer { constructor(targetElementId, startDate, durationMinutes) { this.startDate = startDate; this.endDate = new Date(this.startDate.getTime() + durationMinutes * 60 * 1000); // durationMinutesをミリ秒に変換してendDateを計算 this.targetElement = document.getElementById(targetElementId); this.virtualTimer = this.createVirtualTimer(); this.timeDelta = Cookies.getJSON("timeDelta"); document.body.appendChild(this.virtualTimer); if(typeof this.timeDelta === 'undefined'){ this.timeDelta = 0; } } now() { const date = new Date(); date.setTime(date.getTime() + this.timeDelta); return date; } createVirtualTimer() { const p = document.createElement('p'); p.classList.add("contest-timer") p.id = 'virtual-timer'; p.style.position = 'fixed'; p.style.right = '10px'; p.style.bottom = '0'; p.style.width = '160px'; p.style.height = '80px'; p.style.margin = '0'; p.style.padding = '20px 0'; p.style.backgroundImage = 'url("//img.atcoder.jp/assets/contest/digitalclock.png")'; p.style.textAlign = 'center'; p.style.lineHeight = '20px'; p.style.fontSize = '15px'; p.style.cursor = 'pointer'; p.style.zIndex = '50'; p.style.userSelect = 'none'; p.style.display = 'none'; // 初めから非表示にする return p; } start() { if (this.intervalId) { return; } this.bindClickEvent(); this.targetElement.style.display = 'none'; this.virtualTimer.style.display = 'block'; const updateDisplay = () => { const nowDate = this.now(); if (this.startDate > nowDate) { // バーチャル開始前 const formattedTimeDiff = this.formatTimeDifference(this.startDate, nowDate); this.virtualTimer.innerHTML = `開始まであと<br/>${formattedTimeDiff}`; } else if(nowDate < this.endDate) { // バーチャル中 const formattedTimeDiff = this.formatTimeDifference(this.endDate, nowDate); this.virtualTimer.innerHTML = `残り時間<br/>${formattedTimeDiff}`; } else { // バーチャル終了後 this.targetElement.innerHTML = ""; this.stop(); this.unbindClickEvent(); // イベント破棄 } }; updateDisplay(); this.intervalId = setInterval(updateDisplay, 1000); } stop() { if (this.intervalId) { clearInterval(this.intervalId); this.intervalId = null; } } formatTimeDifference(startDate, nowDate) { const diffInSeconds = (startDate - nowDate) / 1000; const days = Math.floor(diffInSeconds / 86400); const hours = Math.floor((diffInSeconds % 86400) / 3600); const minutes = Math.floor((diffInSeconds % 3600) / 60); const seconds = Math.floor(diffInSeconds % 60); if (days > 0) { return `${days}日と${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; } else { return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; } } unbindClickEvent() { // イベントを破棄するためのメソッド this.targetElement.removeEventListener('click', this.toggleDisplay); this.virtualTimer.removeEventListener('click', this.toggleDisplay); this.virtualTimer.style.display = 'none'; this.targetElement.style.display = 'block'; } bindClickEvent() { // バチャの時計ともとからある時計を切り替える this.toggleDisplay = (e) => { if (e.target === this.targetElement && this.targetElement.style.opacity == 1) { this.targetElement.style.display = 'none'; this.virtualTimer.style.display = 'block'; } else { this.virtualTimer.style.display = 'none'; this.targetElement.style.display = 'block'; } }; this.targetElement.addEventListener('click', this.toggleDisplay); this.virtualTimer.addEventListener('click', this.toggleDisplay); } } async function getVirtualContestPage() { const contestName = window.location.pathname.split('/')[2]; const virtualUrl = `https://atcoder.jp/contests/${contestName}/virtual`; const response = await fetch(virtualUrl); if (!response.ok) { throw new Error(`Failed to fetch ${virtualUrl}. response status: ${response.status} status text: ${response.statusText}`); } const pageContent = await response.text(); const parser = new DOMParser(); return parser.parseFromString(pageContent, "text/html"); } // コンテスト時間を分で返す function getContestDurationMinutes(dom) { const contestDurationElement = dom.querySelectorAll('small.contest-duration a'); // YYYYMMDDTHHMM const strToDate = (str) => { return new Date( parseInt(str.substring(0, 4)), // year parseInt(str.substring(4, 6)) - 1, // month (0-indexed) parseInt(str.substring(6, 8)), // day parseInt(str.substring(9, 11)), // hour parseInt(str.substring(11, 13)) // minute ); }; const startMatch = contestDurationElement[0].href.match(/iso=(\d+T\d+)/); const endMatch = contestDurationElement[1].href.match(/iso=(\d+T\d+)/); const startStr = startMatch[1]; const endStr = endMatch[1]; const startDate = strToDate(startStr); const endDate = strToDate(endStr); const durationMinutes = (endDate.getTime() - startDate.getTime()) / (1000 * 60); // ミリ秒を分に変換 return durationMinutes; } // バチャの開始時刻を取得 function getVirtualStartTime(dom) { const timeElement = dom.querySelector('#main-container time.fixtime-second'); const timeText = timeElement.textContent.trim(); return new Date(timeText); } async function main() { try { const dom = await getVirtualContestPage() const virtualStartDate = getVirtualStartTime(dom); const contestDurationMinutes = getContestDurationMinutes(dom); const timer = new Timer('fixed-server-timer', virtualStartDate, contestDurationMinutes); timer.start(); } catch (error) { console.error(error); } } main();