AtCoder Center Your Standing

Displays the standings page centered around your rank by default in AtCoder contests.

  1. // ==UserScript==
  2. // @name AtCoder Center Your Standing
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.4
  5. // @description Displays the standings page centered around your rank by default in AtCoder contests.
  6. // @author haruomaki
  7. // @match https://atcoder.jp/contests/*/standings
  8. // @match https://atcoder.jp/contests/*/standings/virtual
  9. // @match https://atcoder.jp/contests/*/results
  10. // @grant none
  11. // @license CC0
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16. /* global vueStandings, vueResults, userScreenName */
  17.  
  18. // Configs
  19. // ---------------------------
  20. const default_center = true;
  21. // ---------------------------
  22.  
  23. var first_click = false;
  24. var current_row = null;
  25.  
  26. function exists(name) {
  27. return typeof window[name] !== 'undefined';
  28. }
  29.  
  30. var vue;
  31. if (exists("vueStandings")) {
  32. vue = vueStandings;
  33. } else if (exists("vueResults")) {
  34. vue = vueResults;
  35. } else {
  36. console.error("順位データが読み込めません");
  37. return;
  38. }
  39.  
  40. function getMyStanding() {
  41. return document.querySelector("tr.info");
  42. }
  43.  
  44. function myRank(user_id) {
  45. if (exists("vueStandings")) {
  46. return vue.orderedStandings.findIndex(data => data.UserScreenName == user_id) + 1;
  47. }
  48. if (exists("vueResults")) {
  49. return vue.orderedResults.findIndex(data => data.UserScreenName == user_id) + 1;
  50. }
  51. }
  52.  
  53. function clickActivePagination() {
  54. // ページネーション内のリンク要素を取得
  55. const linkElement = document.querySelector('.pagination .active a')
  56. linkElement.click();
  57. }
  58.  
  59. function center_me() {
  60. if (!first_click) {
  61. first_click = true;
  62. clickActivePagination(); // 一度クリックしておかないと、なぜかページ移動が効かない
  63. }
  64.  
  65. const user_id = userScreenName;
  66. const rank = myRank(user_id); // 存在しなければ0が返る
  67. if (rank == 0) return;
  68.  
  69. const target_page = Math.ceil(rank / vue.perPage);
  70. console.debug("center me:", { user_id, rank, target_page });
  71. vue.page = target_page; // これで遷移
  72. }
  73.  
  74. function remakeClickable() {
  75. if (current_row != null) {
  76. //console.debug("クリック機能削除:", current_row);
  77. current_row.removeAttribute('title');
  78. current_row.removeEventListener('click', center_me);
  79. }
  80. const row = getMyStanding();
  81. if (row == null) return;
  82. //console.debug("クリック可能にします:", row);
  83. row.setAttribute('title', 'Click to center your standing');
  84. row.addEventListener('click', center_me);
  85. current_row = row;
  86. }
  87.  
  88. function main() {
  89. // 自身の行をクリックするとページ移動
  90. remakeClickable();
  91.  
  92. // 自順位の行が移動もしくは生成されるたびにremake
  93. const observer = new MutationObserver(mutations => {
  94. for (const mutation of mutations) {
  95. if (mutation.type === 'childList') {
  96. // 新しいノードが追加された場合
  97. mutation.addedNodes.forEach(node => {
  98. if (node.nodeType === 1 && node.classList.contains('info')) {
  99. console.debug('<tr class="info"> が追加されました');
  100. remakeClickable();
  101. }
  102. });
  103. } else if (mutation.type === 'attributes' && mutation.target.classList.contains('info')) {
  104. // 既存のノードにinfoクラスが付与された場合
  105. console.debug('<tr> に infoクラスが付与されました');
  106. remakeClickable();
  107. }
  108. }
  109. });
  110.  
  111. // 監視を開始する
  112. observer.observe(document.body, { childList: true, attributes: true, attributeFilter: ['class'], subtree: true });
  113.  
  114. if (default_center) center_me();
  115. }
  116.  
  117. // 順位表本体が読み込まれるまで待機
  118. const interval = setInterval(() => {
  119. if (vue.standings || vue.results) {
  120. clearInterval(interval);
  121. main();
  122. }
  123. }, 100);
  124. })();

QingJ © 2025

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