OMC Problems Editor

OnlineMathContest problemsページの問題セルに、得点、分野、いいねしているかどうか表示します。

  1. // ==UserScript==
  2. // @name OMC Problems Editor
  3. // @namespace none
  4. // @version 1.0.0
  5. // @description OnlineMathContest problemsページの問題セルに、得点、分野、いいねしているかどうか表示します。
  6. // @author yuyu
  7. // @match https://onlinemathcontest.com/problems
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (() => {
  13. 'use strict';
  14.  
  15. // ログインチェック
  16. let is_login = false;
  17. let username = '';
  18. const userNameEl = document.getElementById('nav-username');
  19. if (userNameEl && userNameEl.dataset.uid) {
  20. is_login = true;
  21. username = userNameEl.dataset.uid;
  22. }
  23.  
  24. let allProblems = {};
  25. let favoritesSet = new Set();
  26.  
  27. // API 取得
  28. const get_api = (t) => {
  29. const url = `https://onlinemathcontest.com/api/problems/list?type=${t}`;
  30. return fetch(url)
  31. .then(res => res.json())
  32. .then(data => {
  33. const result = {};
  34. if (data && data.contests) {
  35. data.contests.forEach(contest => {
  36. contest.tasks.forEach(task => {
  37. result[task.title] = {
  38. point: task.point,
  39. field: task.field
  40. };
  41. });
  42. });
  43. }
  44. return result;
  45. });
  46. };
  47.  
  48. // favorites取得
  49. const fetchFavorites = (user) => {
  50. const url = `https://onlinemathcontest.com/users/${user}/favorites`;
  51. return fetch(url)
  52. .then(res => {
  53. if (!res.ok) throw new Error('Favorites fetch failed');
  54. return res.text();
  55. })
  56. .then(html => {
  57. const set = new Set();
  58. const re = />([A-Z0-9]+\(\w\))</g;
  59. let match;
  60. while ((match = re.exec(html)) !== null) {
  61. set.add(match[1].trim());
  62. }
  63. return set;
  64. });
  65. };
  66.  
  67. const problemPromises = ['B', 'R', 'E', 'S', 'O', 'V'].map(t => get_api(t));
  68.  
  69. const promises = [Promise.all(problemPromises).then(results => {
  70. // 各API呼び出しの結果を統合
  71. results.forEach(data => {
  72. allProblems = Object.assign(allProblems, data);
  73. });
  74. })];
  75.  
  76. if (is_login) {
  77. promises.push(fetchFavorites(username).then(set => { favoritesSet = set; }));
  78. }
  79.  
  80. // オーバーレイ適用、DOM更新を開始
  81. Promise.all(promises).then(() => {
  82. Overlay();
  83. setupMutation();
  84. });
  85.  
  86. function Overlay() {
  87. document.querySelectorAll('td').forEach(td => {
  88. td.querySelectorAll('.omc-overlay').forEach(el => el.remove());
  89.  
  90. const cellText = td.textContent.trim();
  91. const prob = allProblems[cellText];
  92. if (!prob) return;
  93.  
  94. if (window.getComputedStyle(td).position === 'static') {
  95. td.style.position = 'relative';
  96. }
  97.  
  98. if (prob.point !== undefined) {
  99. const change = document.createElement('span');
  100. change.classList.add('omc-overlay');
  101. change.textContent = prob.point;
  102. change.style.position = 'absolute';
  103. change.style.right = '2px';
  104. change.style.bottom = '2px';
  105. change.style.fontSize = '10px';
  106. change.style.color = 'gray';
  107. change.style.pointerEvents = 'none';
  108. change.style.zIndex = '0';
  109. td.appendChild(change);
  110. }
  111.  
  112. if (prob.field !== undefined) {
  113. const change = document.createElement('span');
  114. change.classList.add('omc-overlay');
  115. change.textContent = prob.field;
  116. change.style.position = 'absolute';
  117. change.style.left = '2px';
  118. change.style.bottom = '2px';
  119. change.style.fontSize = '10px';
  120. change.style.color = 'gray';
  121. change.style.pointerEvents = 'none';
  122. change.style.zIndex = '0';
  123. td.appendChild(change);
  124. }
  125.  
  126. if (favoritesSet.has(cellText)) {
  127. const change = document.createElement('span');
  128. change.classList.add('omc-overlay');
  129. change.textContent = '❤️'; // かわいいハート。らぶ。
  130. change.style.position = 'absolute';
  131. change.style.right = '2px';
  132. change.style.top = '2px';
  133. change.style.fontSize = '10px';
  134. change.style.pointerEvents = 'none';
  135. change.style.zIndex = '0';
  136. td.appendChild(change);
  137. }
  138. });
  139. }
  140.  
  141. function applyOverlays() {
  142. Overlay();
  143. }
  144.  
  145. function setupMutation() {
  146. let Timer = null;
  147. const delay = 500;
  148.  
  149. const CallBack = () => {
  150. if (Timer) clearTimeout(Timer);
  151. Timer = setTimeout(() => {
  152. applyOverlays();
  153. Timer = null;
  154. }, delay);
  155. };
  156.  
  157. const observer = new MutationObserver(CallBack);
  158. observer.observe(document.body, { childList: true, subtree: true });
  159.  
  160. window.addEventListener('scroll', applyOverlays);
  161. window.addEventListener('resize', applyOverlays);
  162. }
  163. })();