AtCoderVirtualTimer

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

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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