AtCoderVirtualTimer

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

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

You will need to install an extension such as Tampermonkey to install this script.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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