ADT⇄ABC Converter Button

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

您需要先安装一个扩展,例如 篡改猴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.5
// @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);
    }
    // ページがADTかどうかを判定
    function isAdtPage() {
        const url = window.location.href.toLowerCase();
        // 基本的にはADTのURLを含む
        const isAdtUrl = (url.includes('adt')) && url.includes('tasks');
        // 問題一覧ページは除外する(/tasks で終わるか、/tasks/ で終わる場合)
        const isProblemListPage = url.match(/\/tasks\/?$/);

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

    }

    // ページがABCかどうかを判定
    function isAbcPage() {
        const href = location.href;
        const abcRegex = /^https:\/\/atcoder\.jp\/contests\/abc\d{3}\/tasks\/abc\d{3}_[a-z]/;
        return abcRegex.test(href);
    }


     // ページに応じてボタンを追加
    function AddButton() {
        const config = getConfig();

        if (isAdtPage()) {
            addAbcButton();
        } else if (isAbcPage()) {
            addAdtButton();
        }
    }
   // 現在のコンテストが進行中かどうかを判定する関数
    function isActiveContest() {
        try {
            // 残り時間のテキストがあるかどうかで判定
            const pageContent = document.body.textContent || '';
            return pageContent.includes('残り時間');
        } catch (error) {
            console.error('コンテスト判定エラー:', error);
            return false;
        }
    }
     // 初期化
    function init() {
        // 現在の設定を取得
        const config = getConfig();
        addStyles();
        AddButton();


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

    // DOMが読み込まれたら初期化
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();