ADT⇄ABC Converter Button

ADTの問題URLを検知して対応するABCで開くボタンを追加⇄ADTへ戻るボタンを追加

当前为 2025-03-28 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         ADT⇄ABC Converter Button
// @namespace    http://mogobon.github.io/
// @version      1.3
// @description  ADTの問題URLを検知して対応するABCで開くボタンを追加⇄ADTへ戻るボタンを追加
// @author       もごぼん
// @match        https://*/*
// @match        https://atcoder.jp/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=atcoder.jp
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 設定キーの定義
    const CONFIG_KEY = "adt-converter-config";

    // デフォルト設定
    const DEFAULT_CONFIG = {
        showDuringContest: false  // コンテスト中も表示する(デフォルトはOFF)
    };

    // 設定を取得する関数
    function getConfig() {
        const val = GM_getValue(CONFIG_KEY, "{}");
        let config;
        try {
            config = JSON.parse(val);
        } catch {
            console.warn("無効な設定が見つかりました", val);
            config = {};
        }
        return { ...DEFAULT_CONFIG, ...config };
    }

    // 設定を保存する関数
    function saveConfig(config) {
        GM_setValue(CONFIG_KEY, JSON.stringify(config));
    }

    // スタイルを追加する関数
    function addStyles() {
        const style = document.createElement('style');
        style.textContent = `
            /* ホバーエリア(ボタンの表示トリガー) */
            .adt-hover-area {
                position: fixed;
                top: 0;
                right: 0;
                width: 40px;
                height: 140px;
                z-index: 9998;
            }

            /* ボタン共通スタイル */
            .adt-button {
                position: fixed;
                right: -105px; /* 初期状態ではより右側に配置 */
                background-color: rgba(0, 0, 0, 0.7);
                color: white;
                font-weight: bold;
                font-size: 16px;
                border: none;
                border-radius: 8px 0 0 8px;
                padding: 12px 18px;
                cursor: pointer;
                box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
                z-index: 9999;
                transition: all 0.3s;
                display: flex;
                align-items: center;
                justify-content: center;
                opacity: 0.9;
                min-width: 100px;
                /* テキスト選択を防止 */
                user-select: none;
                -webkit-user-select: none;
                -moz-user-select: none;
                -ms-user-select: none;
            }

            /* ABCで開くボタン (緑) */
            .adt-converter-button {
                top: 80px;
                background-color: #4CAF50;
                transform: translateY(-3px);
                border-left: 5px solid #2E7D32; /* 左端だけ濃い緑のボーダー */
            }

            /* ホバー時にボタンを表示 */
            .adt-hover-area:hover ~ .adt-button,
            .adt-button:hover {
                right: 0; /* ホバー時に画面端にくっつける */
            }

            .adt-converter-button:hover {
                background-color: #3c9040;
                transform: translateY(0);
                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
                opacity: 1;
            }

            .adt-converter-button:active {
                transform: translateY(1px);
                box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
            }

            /* ADTに戻るボタン (青) */
            .adt-back-button {
                top: 80px;
                background-color: #2196F3;
                transform: translateY(-3px);
                border-left: 5px solid #0D47A1; /* 左端だけ濃い青のボーダー */
            }

            .adt-back-button:hover {
                background-color: #1976D2;
                transform: translateY(0);
                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
                opacity: 1;
            }

            .adt-back-button:active {
                transform: translateY(1px);
                box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
            }

            /* 通知スタイル */
            .adt-notification {
                position: fixed;
                bottom: 20px;
                right: 20px;
                background: #4CAF50;
                color: white;
                padding: 12px 20px;
                border-radius: 8px;
                box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
                z-index: 10001;
                animation: fadeInOut 2s ease;
                pointer-events: none;
            }

            /* アニメーション */
            @keyframes fadeInOut {
                0% { opacity: 0; transform: translateY(20px); }
                20% { opacity: 1; transform: translateY(0); }
                80% { opacity: 1; transform: translateY(0); }
                100% { opacity: 0; transform: translateY(20px); }
            }

            /* モバイル対応 */
            @media (max-width: 480px) {
                .adt-button {
                    font-size: 14px;
                    padding: 10px 15px;
                }

                .adt-notification {
                    bottom: 10px;
                    right: 10px;
                    left: 10px;
                    padding: 10px;
                    width: calc(100% - 40px);
                }

                .adt-hover-area:hover ~ .adt-button,
                .adt-button:hover {
                    right: 0;
                }
            }
        `;
        document.head.appendChild(style);
    }

    // URL変換ロジック
    function convertUrl(adtUrl) {
        const parts = adtUrl.split("/tasks/", 2);
        if (parts.length < 2) return adtUrl;
        const [prefix, taskPart] = parts;

        // 問題一覧ページの場合はそのまま返す
        if (!taskPart || taskPart === "") return adtUrl;

        const abcId = taskPart.split("_", 1)[0];
        return `https://atcoder.jp/contests/${abcId}/tasks/${taskPart}`;
    }

    // AtCoder公式サイトに同じタブで移動
    function moveToAtCoder() {
        try {
            const currentUrl = window.location.href;
            const convertedUrl = convertUrl(currentUrl);

            // URLが変換されなかった場合
            if (convertedUrl === currentUrl) {
                return;
            }

            // 最後に訪問したADTのURLを保存
            GM_setValue('lastAdtUrl', currentUrl);

            // 同じタブで移動
            window.location.href = convertedUrl;
        } catch (error) {
            console.error('URL変換エラー:', error);
        }
    }

    // ADTページへ戻る
    function moveToAdt() {
        try {
            const lastAdtUrl = GM_getValue('lastAdtUrl', '');

            if (!lastAdtUrl) {
                return;
            }

            // ADTに戻るときはリセット
            GM_setValue('lastAdtUrl', '');

            // 同じタブで移動
            window.location.href = lastAdtUrl;
        } catch (error) {
            console.error('ADTページへの移動エラー:', error);
        }
    }

    // すべてのボタンとホバーエリアを削除
    function removeAllButtons() {
        const elements = document.querySelectorAll('.adt-button, .adt-hover-area');
        elements.forEach(element => {
            if (document.body.contains(element)) {
                element.remove();
            }
        });
    }

    // 通知を表示する関数
    function showNotification(message) {
        const notification = document.createElement('div');
        notification.className = 'adt-notification';
        notification.textContent = message;
        document.body.appendChild(notification);

        setTimeout(() => {
            if (document.body.contains(notification)) {
                document.body.removeChild(notification);
            }
        }, 2000);
    }

    // ABCで開くボタンを追加
    function addAbcButton() {
        // 既存のすべてのボタンを削除
        removeAllButtons();

        // ホバーエリア(ボタンを表示するためのトリガー)
        const hoverArea = document.createElement('div');
        hoverArea.className = 'adt-hover-area';
        document.body.appendChild(hoverArea);

        // ボタン
        const button = document.createElement('button');
        button.className = 'adt-button adt-converter-button';
        button.textContent = 'ABCで開く';
        button.title = 'ABCで開く';
        button.addEventListener('click', moveToAtCoder);
        document.body.appendChild(button);
    }

    // ADTに戻るボタンを追加
    function addAdtButton() {
        // 既存のすべてのボタンを削除
        removeAllButtons();

        // ホバーエリア(ボタンを表示するためのトリガー)
        const hoverArea = document.createElement('div');
        hoverArea.className = 'adt-hover-area';
        document.body.appendChild(hoverArea);

        // ボタン
        const button = document.createElement('button');
        button.className = 'adt-button adt-back-button';
        button.textContent = 'ADTに戻る';
        button.title = 'ADTに戻る';
        button.addEventListener('click', moveToAdt);
        document.body.appendChild(button);
    }

    // URLがADTの個別問題URLかどうかを判定する関数
    function isAdtProblemUrl() {
        const url = window.location.href.toLowerCase();

        // 基本的にはADTのURLを含む
        const isAdtUrl = (url.includes('atcoder-tools') || url.includes('adt')) && url.includes('tasks');

        // 問題一覧ページは除外する(/tasks で終わるか、/tasks/ で終わる場合)
        const isProblemListPage = url.match(/\/tasks\/?$/);

        // 問題一覧ページでなく、ADTのURLを含む場合のみtrue
        return isAdtUrl && !isProblemListPage;
    }

    // URLがAtCoder公式の問題ページかどうかを判定する関数
    function isAtcoderProblemPage() {
        const url = window.location.href.toLowerCase();
        return url.includes('atcoder.jp/contests/') && url.includes('/tasks/') && !url.includes('atcoder-tools');
    }

    // 前回のADTページ情報があるかをチェック
    function hasAdtHistory() {
        return GM_getValue('lastAdtUrl', '') !== '';
    }

    // 現在のコンテストが進行中かどうかを判定する関数
    function isActiveContest() {
        try {
            // URLからコンテストIDを取得
            const url = window.location.href;
            const contestMatch = url.match(/\/contests\/([^/]+)/);

            if (!contestMatch) return false;

            const contestId = contestMatch[1];

            // コンテスト開始時間と終了時間の要素を探す
            const timeElements = document.querySelectorAll('time');
            if (timeElements.length < 2) return false;

            // 開始時間と終了時間を取得
            const startTimeStr = timeElements[0].getAttribute('datetime');
            const endTimeStr = timeElements[1].getAttribute('datetime');

            if (!startTimeStr || !endTimeStr) return false;

            const startTime = new Date(startTimeStr);
            const endTime = new Date(endTimeStr);
            const now = new Date();

            // 現在時刻がコンテスト期間内かチェック
            return startTime <= now && now <= endTime;
        } catch (error) {
            console.error('コンテスト判定エラー:', error);
            return false;
        }
    }

    // ページ初期化
    function init() {
        addStyles();

        // 現在の設定を取得
        const config = getConfig();

        // コンテスト中で表示設定がOFFの場合はボタンを表示しない
        if (!config.showDuringContest && isActiveContest()) {
            removeAllButtons();
            return;
        }

        // ADTの個別問題ページの場合
        if (isAdtProblemUrl()) {
            addAbcButton();
        }

        // AtCoder公式の問題ページで、かつ前回のADTページ情報がある場合
        if (isAtcoderProblemPage() && hasAdtHistory()) {
            addAdtButton();
        }
    }

    // ページロード完了時に実行
    if (document.readyState === 'complete') {
        init();
    } else {
        window.addEventListener('load', init);
    }

    // ページ変更を監視(SPAサイト対応)
    let lastUrl = location.href;
    new MutationObserver(() => {
        const url = location.href;
        if (url !== lastUrl) {
            lastUrl = url;
            setTimeout(() => {
                // 現在の設定を取得
                const config = getConfig();

                // コンテスト中で表示設定がOFFの場合はボタンを表示しない
                if (!config.showDuringContest && isActiveContest()) {
                    removeAllButtons();
                    return;
                }

                // 現在のURLに応じて適切なボタンを表示
                if (isAdtProblemUrl()) {
                    addAbcButton();
                } else if (isAtcoderProblemPage() && hasAdtHistory()) {
                    addAdtButton();
                } else {
                    // どちらでもない場合は、すべてのボタンを削除
                    removeAllButtons();
                }
            }, 300);
        }
    }).observe(document, {subtree: true, childList: true});
})();