MojaCoder Submission User Colorizer (by AtCoder Rating)

MojaCoder のユーザーのリンクに AtCoder のレーティングを紐つけます( AtCoder のユーザー名と同じ場合のみ)

  1. // ==UserScript==
  2. // @name MojaCoder Submission User Colorizer (by AtCoder Rating)
  3. // @namespace https://mojacoder.app/
  4. // @version 0.2
  5. // @description MojaCoder のユーザーのリンクに AtCoder のレーティングを紐つけます( AtCoder のユーザー名と同じ場合のみ)
  6. // @author magurofly
  7. // @license CC0-1.0 Universal
  8. // @match https://mojacoder.app/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=mojacoder.app
  10. // @grant none
  11. // @require https://unpkg.com/lscache/lscache.min.js
  12. // ==/UserScript==
  13.  
  14. (async function() {
  15. 'use strict';
  16.  
  17. document.head.insertAdjacentHTML("afterbegin", `
  18. <style>
  19. .user-red {color:#FF0000;}
  20. .user-orange {color:#FF8000;}
  21. .user-yellow {color:#C0C000;}
  22. .user-blue {color:#0000FF;}
  23. .user-cyan {color:#00C0C0;}
  24. .user-green {color:#008000;}
  25. .user-brown {color:#804000;}
  26. .user-gray {color:#808080;}
  27. .user-unrated {color:#000000;}
  28. .user-admin {color:#C000C0;}
  29.  
  30. .crown-gold {color: #fb0;}
  31. .crown-silver {color: #aaa;}
  32. </style>
  33. `);
  34.  
  35. // もりを (morio_prog) 様の AtCoder Submission User Colorizer を改変して使用しています
  36.  
  37. const lastUpdateKey = 'user-colorizer-ranking-last-update';
  38. const rankingKey = 'user-colorizer-ranking';
  39. const OUT_OF_RANK = Number.MAX_VALUE; // > 100
  40. const UPDATE_INTERVAL = 3 * 60; // 更新周期 [min]
  41.  
  42. function getColor(rating) {
  43. if (rating >= 2800) return '#FF0000';
  44. if (rating >= 2400) return '#FF8000';
  45. if (rating >= 2000) return '#C0C000';
  46. if (rating >= 1600) return '#0000FF';
  47. if (rating >= 1200) return '#00C0C0';
  48. if (rating >= 800) return '#008000';
  49. if (rating >= 400) return '#804000';
  50. if (rating > 0) return '#808080';
  51. return '#000000';
  52. }
  53.  
  54. function getColorClass(rating) {
  55. if (rating >= 2800) return 'user-red';
  56. if (rating >= 2400) return 'user-orange';
  57. if (rating >= 2000) return 'user-yellow';
  58. if (rating >= 1600) return 'user-blue';
  59. if (rating >= 1200) return 'user-cyan';
  60. if (rating >= 800) return 'user-green';
  61. if (rating >= 400) return 'user-brown';
  62. if (rating > 0) return 'user-gray';
  63. return 'user-unrated';
  64. }
  65.  
  66. function getAchRate(rating) {
  67. const base = Math.floor(rating / 400) * 400;
  68. return ((rating - base) / 400) * 100;
  69. }
  70.  
  71. function colorize(u, ranking, rating) {
  72. /* */if (ranking <= 1) u.insertAdjacentHTML('beforebegin', '<img style="vertical-align: middle;" src="//img.atcoder.jp/assets/icon/crown_champion.png">&nbsp;');
  73. else if (ranking <= 10) u.insertAdjacentHTML('beforebegin', '<img style="vertical-align: middle;" src="//img.atcoder.jp/assets/icon/crown_gold.png">&nbsp;');
  74. else if (ranking <= 30) u.insertAdjacentHTML('beforebegin', '<img style="vertical-align: middle;" src="//img.atcoder.jp/assets/icon/crown_silver.png">&nbsp;');
  75. else if (ranking <= 100) u.insertAdjacentHTML('beforebegin', '<img style="vertical-align: middle;" src="//img.atcoder.jp/assets/icon/crown_bronze.png">&nbsp;');
  76. else if (rating > 0) {
  77. const color = getColor(rating);
  78. const achRate = getAchRate(rating);
  79. u.insertAdjacentHTML('beforebegin', `
  80. <span style="
  81. display: inline-block;
  82. height: 12px;
  83. width: 12px;
  84. vertical-align: center;
  85. border-radius: 50%;
  86. border: solid 1px ${color};
  87. background: -webkit-linear-gradient(
  88. bottom,
  89. ${color} 0%,
  90. ${color} ${achRate}%,
  91. rgba(255, 255, 255, 0.0) ${achRate}%,
  92. rgba(255, 255, 255, 0.0) 100%);
  93. "></span>
  94. `);
  95. }
  96. u.classList.add(getColorClass(rating));
  97. }
  98.  
  99. async function getRankingMap() {
  100. const currentTime = new Date().getTime();
  101. const lastUpdateTime = localStorage.getItem(lastUpdateKey);
  102. if (lastUpdateTime && currentTime < Number(lastUpdateTime) + UPDATE_INTERVAL * 60 * 1000) {
  103. return JSON.parse(localStorage.getItem(rankingKey));
  104. } else {
  105. let ranking = {};
  106. const data = await fetch("https://corsproxy.io/?https://atcoder.jp/ranking", { mode: "cors", }).then(response => response.text());
  107. const doc = new DOMParser().parseFromString(data, "text/html");
  108. doc.querySelectorAll(".username > span").forEach((elem, idx) => {
  109. const userName = elem.textContent;
  110. ranking[userName] = idx + 1;
  111. });
  112. localStorage.setItem(lastUpdateKey, currentTime);
  113. localStorage.setItem(rankingKey, JSON.stringify(ranking));
  114. return ranking;
  115. }
  116. }
  117.  
  118. function getRanking(rankingMap, userName) {
  119. if (userName in rankingMap) return rankingMap[userName];
  120. return OUT_OF_RANK;
  121. }
  122.  
  123. async function colorizeAll(root) {
  124. for (const u of root.querySelectorAll('a[href*="/users/"]')) {
  125. const userName = u.getAttribute('href').slice(7);
  126. if (encodeURIComponent(u.textContent) !== userName) continue; // URL とユーザー名が一致しない場合も弾く
  127. const lskey = "rating-" + userName;
  128. const ranking = getRanking(rankingMap, userName);
  129. let rating = lscache.get(lskey);
  130. if (rating === null) {
  131. const data = await fetch(`https://corsproxy.io/?https://atcoder.jp/users/${encodeURIComponent(userName)}/history/json`, { mode: 'cors', }).then(response => response.json());
  132. const ratedCount = data.length;
  133. if (ratedCount === 0) {
  134. rating = 0;
  135. } else {
  136. rating = data[ratedCount - 1]["NewRating"];
  137. }
  138. lscache.set(lskey, rating, UPDATE_INTERVAL);
  139. }
  140. colorize(u, ranking, rating);
  141. }
  142. }
  143.  
  144. lscache.flushExpired();
  145. const rankingMap = await getRankingMap();
  146.  
  147. colorizeAll(document);
  148. const observer = new MutationObserver(mutations => {
  149. for (const mutation of mutations) {
  150. for (const addedNode of mutation.addedNodes) {
  151. colorizeAll(addedNode);
  152. }
  153. }
  154. });
  155. observer.observe(document.body, {
  156. childList: true,
  157. subtree: true,
  158. });
  159. })();