Greasy Fork镜像 支持简体中文。

ADT→ABC Converter Button

ADTの問題URLを検知してABCで開く

目前為 2025-03-28 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name ADT→ABC Converter Button
  3. // @namespace http://mogobon.github.io/
  4. // @version 1.1
  5. // @description ADTの問題URLを検知してABCで開く
  6. // @author もごぼん
  7. // @match https://atcoder.jp/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. // スタイルを追加する関数
  15. function addStyles() {
  16. const style = document.createElement('style');
  17. style.textContent = `
  18. .adt-converter-button {
  19. position: fixed;
  20. top: 80px;
  21. right: 20px;
  22. background-color: #4CAF50;
  23. color: white;
  24. font-weight: bold;
  25. font-size: 14px;
  26. border: none;
  27. border-radius: 8px;
  28. padding: 10px 15px;
  29. cursor: pointer;
  30. box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
  31. z-index: 9999;
  32. transition: all 0.3s ease;
  33. /* デフォルトで浮き上がった状態 */
  34. transform: translateY(-3px);
  35. }
  36.  
  37. .adt-converter-button:hover {
  38. /* ホバー時に沈む */
  39. background-color: #3c9040;
  40. transform: translateY(0);
  41. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  42. }
  43.  
  44. .adt-converter-button:active {
  45. /* クリック時さらに沈む */
  46. transform: translateY(1px);
  47. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
  48. }
  49.  
  50. .adt-converter-notification {
  51. position: fixed;
  52. top: 130px;
  53. right: 20px;
  54. background: #4CAF50;
  55. color: white;
  56. padding: 10px 15px;
  57. border-radius: 8px;
  58. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  59. z-index: 10000;
  60. font-size: 14px;
  61. animation: adtFadeIn 0.3s, adtFadeOut 0.3s 1.7s forwards;
  62. }
  63.  
  64. @keyframes adtFadeIn {
  65. from { opacity: 0; transform: translateY(-20px); }
  66. to { opacity: 1; transform: translateY(0); }
  67. }
  68.  
  69. @keyframes adtFadeOut {
  70. from { opacity: 1; transform: translateY(0); }
  71. to { opacity: 0; transform: translateY(-20px); }
  72. }
  73.  
  74. @media (max-width: 768px) {
  75. .adt-converter-button {
  76. top: 70px;
  77. right: 10px;
  78. font-size: 12px;
  79. padding: 8px 12px;
  80. }
  81.  
  82. .adt-converter-notification {
  83. top: 120px;
  84. right: 10px;
  85. font-size: 12px;
  86. max-width: 80%;
  87. }
  88. }
  89. `;
  90. document.head.appendChild(style);
  91. }
  92.  
  93. // URL変換ロジック
  94. function convertUrl(adtUrl) {
  95. const parts = adtUrl.split("/tasks/", 2);
  96. if (parts.length < 2) return adtUrl;
  97. const [prefix, taskPart] = parts;
  98.  
  99. // 問題一覧ページの場合はそのまま返す
  100. if (!taskPart || taskPart === "") return adtUrl;
  101.  
  102. const abcId = taskPart.split("_", 1)[0];
  103. return `https://atcoder.jp/contests/${abcId}/tasks/${taskPart}`;
  104. }
  105.  
  106. // 通知を表示する関数
  107. function showNotification(message) {
  108. // 既存の通知を削除
  109. const existingNotifications = document.querySelectorAll('.adt-converter-notification');
  110. existingNotifications.forEach(notif => {
  111. if (document.body.contains(notif)) {
  112. document.body.removeChild(notif);
  113. }
  114. });
  115.  
  116. const notif = document.createElement('div');
  117. notif.className = 'adt-converter-notification';
  118. notif.textContent = message;
  119. document.body.appendChild(notif);
  120.  
  121. // 自動的に削除
  122. setTimeout(() => {
  123. if (document.body.contains(notif)) {
  124. document.body.removeChild(notif);
  125. }
  126. }, 2000);
  127. }
  128.  
  129. // AtCoder公式サイトで開く
  130. function openInAtCoder() {
  131. try {
  132. const currentUrl = window.location.href;
  133. const convertedUrl = convertUrl(currentUrl);
  134.  
  135. // URLが変換されなかった場合
  136. if (convertedUrl === currentUrl) {
  137. showNotification('このページは変換できません');
  138. return;
  139. }
  140.  
  141. // 新しいタブで開く
  142. window.open(convertedUrl, '_blank');
  143. showNotification('AtCoder公式サイトで開きました!');
  144. } catch (error) {
  145. showNotification('エラー: ' + error.message);
  146. }
  147. }
  148.  
  149. // ボタンを作成して追加
  150. function addButton() {
  151. // 既存のボタンを確認(重複防止)
  152. if (document.querySelector('.adt-converter-button')) {
  153. return;
  154. }
  155.  
  156. const button = document.createElement('button');
  157. button.className = 'adt-converter-button';
  158. button.textContent = 'ABCで開く';
  159. button.title = 'この問題をAtCoder公式サイトで開く';
  160. button.addEventListener('click', openInAtCoder);
  161.  
  162. document.body.appendChild(button);
  163. }
  164.  
  165. // URLがADTの個別問題URLかどうかを判定する関数
  166. function isAdtProblemUrl() {
  167. const url = window.location.href.toLowerCase();
  168.  
  169. // 基本的にはADTのURLを含む
  170. const isAdtUrl = (url.includes('atcoder-tools') || url.includes('adt')) && url.includes('tasks');
  171.  
  172. // 問題一覧ページは除外する(/tasks で終わるか、/tasks/ で終わる場合)
  173. const isProblemListPage = url.match(/\/tasks\/?$/);
  174.  
  175. // 問題一覧ページでなく、ADTのURLを含む場合のみtrue
  176. return isAdtUrl && !isProblemListPage;
  177. }
  178.  
  179. // ページ初期化
  180. function init() {
  181. // 個別問題のURLの場合のみボタンを表示
  182. if (isAdtProblemUrl()) {
  183. addStyles();
  184. addButton();
  185. }
  186. }
  187.  
  188. // ページロード完了時に実行
  189. if (document.readyState === 'complete') {
  190. init();
  191. } else {
  192. window.addEventListener('load', init);
  193. }
  194.  
  195. // ページ変更を監視(SPAサイト対応)
  196. let lastUrl = location.href;
  197. new MutationObserver(() => {
  198. const url = location.href;
  199. if (url !== lastUrl) {
  200. lastUrl = url;
  201. setTimeout(() => {
  202. // 既存のボタンを削除
  203. const existingButton = document.querySelector('.adt-converter-button');
  204. if (existingButton) {
  205. existingButton.remove();
  206. }
  207.  
  208. // 個別問題のURLの場合のみボタンを再表示
  209. if (isAdtProblemUrl()) {
  210. addButton();
  211. }
  212. }, 300);
  213. }
  214. }).observe(document, {subtree: true, childList: true});
  215. })();

QingJ © 2025

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