AtCoderVirtualTimer

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

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==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();