ADT⇄ABC Converter Button

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

  1. // ==UserScript==
  2. // @name ADT⇄ABC Converter Button
  3. // @namespace http://mogobon.github.io/
  4. // @version 1.4
  5. // @description ADTの問題URLを検知して対応するABCで開くボタンを追加⇄ADTへ戻るボタンを追加
  6. // @author もごぼん
  7. // @match https://*/*
  8. // @match https://atcoder.jp/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=atcoder.jp
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. // 設定キーの定義
  19. const CONFIG_KEY = "adt-converter-config";
  20.  
  21. // デフォルト設定
  22. const DEFAULT_CONFIG = {
  23. showDuringContest: false // コンテスト中も表示する(デフォルトはOFF)
  24. };
  25.  
  26. // 設定を取得する関数
  27. function getConfig() {
  28. const val = GM_getValue(CONFIG_KEY, "{}");
  29. let config;
  30. try {
  31. config = JSON.parse(val);
  32. } catch {
  33. console.warn("無効な設定が見つかりました", val);
  34. config = {};
  35. }
  36. return { ...DEFAULT_CONFIG, ...config };
  37. }
  38.  
  39. // 設定を保存する関数
  40. function saveConfig(config) {
  41. GM_setValue(CONFIG_KEY, JSON.stringify(config));
  42. }
  43.  
  44. // スタイルを追加する関数
  45. function addStyles() {
  46. const style = document.createElement('style');
  47. style.textContent = `
  48. /* ホバーエリア(ボタンの表示トリガー) */
  49. .adt-hover-area {
  50. position: fixed;
  51. top: 0;
  52. right: 0;
  53. width: 40px;
  54. height: 140px;
  55. z-index: 9998;
  56. }
  57.  
  58. /* ボタン共通スタイル */
  59. .adt-button {
  60. position: fixed;
  61. right: -105px; /* 初期状態ではより右側に配置 */
  62. background-color: rgba(0, 0, 0, 0.7);
  63. color: white;
  64. font-weight: bold;
  65. font-size: 16px;
  66. border: none;
  67. border-radius: 8px 0 0 8px;
  68. padding: 12px 18px;
  69. cursor: pointer;
  70. box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
  71. z-index: 9999;
  72. transition: all 0.3s;
  73. display: flex;
  74. align-items: center;
  75. justify-content: center;
  76. opacity: 0.9;
  77. min-width: 100px;
  78. /* テキスト選択を防止 */
  79. user-select: none;
  80. -webkit-user-select: none;
  81. -moz-user-select: none;
  82. -ms-user-select: none;
  83. }
  84.  
  85. /* ABCで開くボタン (緑) */
  86. .adt-converter-button {
  87. top: 80px;
  88. background-color: #4CAF50;
  89. transform: translateY(-3px);
  90. border-left: 5px solid #2E7D32; /* 左端だけ濃い緑のボーダー */
  91. }
  92.  
  93. /* ホバー時にボタンを表示 */
  94. .adt-hover-area:hover ~ .adt-button,
  95. .adt-button:hover {
  96. right: 0; /* ホバー時に画面端にくっつける */
  97. }
  98.  
  99. .adt-converter-button:hover {
  100. background-color: #3c9040;
  101. transform: translateY(0);
  102. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  103. opacity: 1;
  104. }
  105.  
  106. .adt-converter-button:active {
  107. transform: translateY(1px);
  108. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
  109. }
  110.  
  111. /* ADTに戻るボタン (青) */
  112. .adt-back-button {
  113. top: 80px;
  114. background-color: #2196F3;
  115. transform: translateY(-3px);
  116. border-left: 5px solid #0D47A1; /* 左端だけ濃い青のボーダー */
  117. }
  118.  
  119. .adt-back-button:hover {
  120. background-color: #1976D2;
  121. transform: translateY(0);
  122. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  123. opacity: 1;
  124. }
  125.  
  126. .adt-back-button:active {
  127. transform: translateY(1px);
  128. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
  129. }
  130.  
  131. /* 通知スタイル */
  132. .adt-notification {
  133. position: fixed;
  134. bottom: 20px;
  135. right: 20px;
  136. background: #4CAF50;
  137. color: white;
  138. padding: 12px 20px;
  139. border-radius: 8px;
  140. box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  141. z-index: 10001;
  142. animation: fadeInOut 2s ease;
  143. pointer-events: none;
  144. }
  145.  
  146. /* アニメーション */
  147. @keyframes fadeInOut {
  148. 0% { opacity: 0; transform: translateY(20px); }
  149. 20% { opacity: 1; transform: translateY(0); }
  150. 80% { opacity: 1; transform: translateY(0); }
  151. 100% { opacity: 0; transform: translateY(20px); }
  152. }
  153.  
  154. /* モバイル対応 */
  155. @media (max-width: 480px) {
  156. .adt-button {
  157. font-size: 14px;
  158. padding: 10px 15px;
  159. }
  160.  
  161. .adt-notification {
  162. bottom: 10px;
  163. right: 10px;
  164. left: 10px;
  165. padding: 10px;
  166. width: calc(100% - 40px);
  167. }
  168.  
  169. .adt-hover-area:hover ~ .adt-button,
  170. .adt-button:hover {
  171. right: 0;
  172. }
  173. }
  174. `;
  175. document.head.appendChild(style);
  176. }
  177.  
  178. // URL変換ロジック
  179. function convertUrl(adtUrl) {
  180. const parts = adtUrl.split("/tasks/", 2);
  181. if (parts.length < 2) return adtUrl;
  182. const [prefix, taskPart] = parts;
  183.  
  184. // 問題一覧ページの場合はそのまま返す
  185. if (!taskPart || taskPart === "") return adtUrl;
  186.  
  187. const abcId = taskPart.split("_", 1)[0];
  188. return `https://atcoder.jp/contests/${abcId}/tasks/${taskPart}`;
  189. }
  190.  
  191. // AtCoder公式サイトに同じタブで移動
  192. function moveToAtCoder() {
  193. try {
  194. const currentUrl = window.location.href;
  195. const convertedUrl = convertUrl(currentUrl);
  196.  
  197. // URLが変換されなかった場合
  198. if (convertedUrl === currentUrl) {
  199. return;
  200. }
  201.  
  202. // 最後に訪問したADTのURLを保存
  203. GM_setValue('lastAdtUrl', currentUrl);
  204.  
  205. // 同じタブで移動
  206. window.location.href = convertedUrl;
  207. } catch (error) {
  208. console.error('URL変換エラー:', error);
  209. }
  210. }
  211.  
  212. // ADTページへ戻る
  213. function moveToAdt() {
  214. try {
  215. const lastAdtUrl = GM_getValue('lastAdtUrl', '');
  216.  
  217. if (!lastAdtUrl) {
  218. return;
  219. }
  220.  
  221. // ADTに戻るときはリセット
  222. GM_setValue('lastAdtUrl', '');
  223.  
  224. // 同じタブで移動
  225. window.location.href = lastAdtUrl;
  226. } catch (error) {
  227. console.error('ADTページへの移動エラー:', error);
  228. }
  229. }
  230.  
  231. // すべてのボタンとホバーエリアを削除
  232. function removeAllButtons() {
  233. const elements = document.querySelectorAll('.adt-button, .adt-hover-area');
  234. elements.forEach(element => {
  235. if (document.body.contains(element)) {
  236. element.remove();
  237. }
  238. });
  239. }
  240.  
  241. // 通知を表示する関数
  242. function showNotification(message) {
  243. const notification = document.createElement('div');
  244. notification.className = 'adt-notification';
  245. notification.textContent = message;
  246. document.body.appendChild(notification);
  247.  
  248. setTimeout(() => {
  249. if (document.body.contains(notification)) {
  250. document.body.removeChild(notification);
  251. }
  252. }, 2000);
  253. }
  254.  
  255. // ABCで開くボタンを追加
  256. function addAbcButton() {
  257. // 既存のすべてのボタンを削除
  258. removeAllButtons();
  259.  
  260. // ホバーエリア(ボタンを表示するためのトリガー)
  261. const hoverArea = document.createElement('div');
  262. hoverArea.className = 'adt-hover-area';
  263. document.body.appendChild(hoverArea);
  264.  
  265. // ボタン
  266. const button = document.createElement('button');
  267. button.className = 'adt-button adt-converter-button';
  268. button.textContent = 'ABCで開く';
  269. button.title = 'ABCで開く';
  270. button.addEventListener('click', moveToAtCoder);
  271. document.body.appendChild(button);
  272. }
  273.  
  274. // ADTに戻るボタンを追加
  275. function addAdtButton() {
  276. // 既存のすべてのボタンを削除
  277. removeAllButtons();
  278.  
  279. // ホバーエリア(ボタンを表示するためのトリガー)
  280. const hoverArea = document.createElement('div');
  281. hoverArea.className = 'adt-hover-area';
  282. document.body.appendChild(hoverArea);
  283.  
  284. // ボタン
  285. const button = document.createElement('button');
  286. button.className = 'adt-button adt-back-button';
  287. button.textContent = 'ADTに戻る';
  288. button.title = 'ADTに戻る';
  289. button.addEventListener('click', moveToAdt);
  290. document.body.appendChild(button);
  291. }
  292.  
  293. // URLがADTの個別問題URLかどうかを判定する関数
  294. function isAdtProblemUrl() {
  295. const url = window.location.href.toLowerCase();
  296.  
  297. // 基本的にはADTのURLを含む
  298. const isAdtUrl = (url.includes('atcoder-tools') || url.includes('adt')) && url.includes('tasks');
  299.  
  300. // 問題一覧ページは除外する(/tasks で終わるか、/tasks/ で終わる場合)
  301. const isProblemListPage = url.match(/\/tasks\/?$/);
  302.  
  303. // 問題一覧ページでなく、ADTのURLを含む場合のみtrue
  304. return isAdtUrl && !isProblemListPage;
  305. }
  306.  
  307. // URLがAtCoder公式の問題ページかどうかを判定する関数
  308. function isAtcoderProblemPage() {
  309. const url = window.location.href.toLowerCase();
  310. return url.includes('atcoder.jp/contests/') && url.includes('/tasks/') && !url.includes('atcoder-tools');
  311. }
  312.  
  313. // 前回のADTページ情報があるかをチェック
  314. function hasAdtHistory() {
  315. return GM_getValue('lastAdtUrl', '') !== '';
  316. }
  317.  
  318. // 現在のコンテストが進行中かどうかを判定する関数
  319. // 現在のコンテストが進行中かどうかを判定する関数
  320. function isActiveContest() {
  321. try {
  322. // 残り時間のテキストがあるかどうかで判定
  323. const pageContent = document.body.textContent || '';
  324. return pageContent.includes('残り時間');
  325. } catch (error) {
  326. console.error('コンテスト判定エラー:', error);
  327. return false;
  328. }
  329. }
  330.  
  331. // ページ初期化
  332. function init() {
  333. addStyles();
  334.  
  335. // 現在の設定を取得
  336. const config = getConfig();
  337.  
  338. // コンテスト中で表示設定がOFFの場合はボタンを表示しない
  339. if (!config.showDuringContest && isActiveContest()) {
  340. removeAllButtons();
  341. return;
  342. }
  343.  
  344. // ADTの個別問題ページの場合
  345. if (isAdtProblemUrl()) {
  346. addAbcButton();
  347. }
  348.  
  349. // AtCoder公式の問題ページで、かつ前回のADTページ情報がある場合
  350. if (isAtcoderProblemPage() && hasAdtHistory()) {
  351. addAdtButton();
  352. }
  353. }
  354.  
  355. // ページロード完了時に実行
  356. if (document.readyState === 'complete') {
  357. init();
  358. } else {
  359. window.addEventListener('load', init);
  360. }
  361.  
  362. // ページ変更を監視(SPAサイト対応)
  363. let lastUrl = location.href;
  364. new MutationObserver(() => {
  365. const url = location.href;
  366. if (url !== lastUrl) {
  367. lastUrl = url;
  368. setTimeout(() => {
  369. // 現在の設定を取得
  370. const config = getConfig();
  371.  
  372. // コンテスト中で表示設定がOFFの場合はボタンを表示しない
  373. if (!config.showDuringContest && isActiveContest()) {
  374. removeAllButtons();
  375. return;
  376. }
  377.  
  378. // 現在のURLに応じて適切なボタンを表示
  379. if (isAdtProblemUrl()) {
  380. addAbcButton();
  381. } else if (isAtcoderProblemPage() && hasAdtHistory()) {
  382. addAdtButton();
  383. } else {
  384. // どちらでもない場合は、すべてのボタンを削除
  385. removeAllButtons();
  386. }
  387. }, 300);
  388. }
  389. }).observe(document, {subtree: true, childList: true});
  390. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址