您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
ADTの問題URLを検知して対応するABCで開くボタンを追加⇄ADTへ戻るボタンを追加
- // ==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();
- }
- })();