AtCoder Problems Marker

AtCoder Problems 上に表示される問題にユーザが独自のマーカー(解説ACなど)を付けられるようにします

当前为 2020-06-26 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name AtCoder Problems Marker
  3. // @namespace iilj
  4. // @version 2020.6.26.1
  5. // @description AtCoder Problems 上に表示される問題にユーザが独自のマーカー(解説ACなど)を付けられるようにします
  6. // @author iilj
  7. // @supportURL https://github.com/iilj/AtCoderProblemsExt/issues
  8. // @match https://kenkoooo.com/atcoder/*
  9. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.js
  11. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.ui.position.js
  12. // @resource css_contextmenu https://cdnjs.cloudflare.com/ajax/libs/jquery-contextmenu/2.7.1/jquery.contextMenu.min.css
  13. // @grant GM_addStyle
  14. // @grant GM_getResourceText
  15. // ==/UserScript==
  16.  
  17. /* globals $ */
  18.  
  19. (function () {
  20. 'use strict';
  21.  
  22. GM_addStyle(GM_getResourceText('css_contextmenu'));
  23. GM_addStyle(`
  24. td.table-problem.apm-ac-after-reading-answer {
  25. background-color: #ffff88 !important;
  26. }
  27. `);
  28.  
  29. /**
  30. * key for localStorage to save ac-after-reading-answer state
  31. * @type {string}
  32. */
  33. const localStorageKey = "apm-hashset";
  34. /**
  35. * json string loaded from localStorage, or to save on localStorage
  36. * @type {string}
  37. */
  38. let json;
  39. /**
  40. * hashset to store whether the problem is marked as ac-after-reading-answer or not
  41. * @type {{[key:string]: number}}
  42. */
  43. let hash;
  44. /**
  45. * user id of AtCoder
  46. * @type {string}
  47. */
  48. let userId;
  49.  
  50. /**
  51. * load ac-after-reading-answer state from localStorage
  52. */
  53. const loadHash = () => {
  54. json = localStorage.getItem(localStorageKey);
  55. hash = json ? JSON.parse(json) : {};
  56. }
  57. /**
  58. * save ac-after-reading-answer state to localStorage
  59. */
  60. const saveHash = () => {
  61. json = JSON.stringify(hash);
  62. localStorage.setItem(localStorageKey, json);
  63. }
  64. /**
  65. * url of problem -> key of hash
  66. *
  67. * @param {string} href
  68. * @returns {string | null} key of hash if valid url supplied, otherwise null
  69. */
  70. const href2key = (href) => {
  71. let result
  72. if (result = href.match(/^https?:\/\/atcoder\.jp\/contests\/([^\/]+)\/tasks\/([^\/]+)$/)) {
  73. return `${userId}/${result[1]}/${result[2]}`;
  74. }
  75. return null;
  76. };
  77.  
  78. $.contextMenu({
  79. selector: '.table-problem',
  80. callback: (key, options) => {
  81. switch (key) {
  82. case "ac_after_reading_answer": {
  83. const td = options.$trigger;
  84. const href = td.find('a[href]').attr('href');
  85. const key = href2key(href);
  86. if (key in hash) {
  87. delete hash[key];
  88. } else {
  89. hash[key] = 0;
  90. }
  91. td.toggleClass('apm-ac-after-reading-answer');
  92. saveHash();
  93. break;
  94. }
  95. case "about":
  96. alert("解説ACした問題を Table ページ上でマークできます.マークした結果はブラウザに保存されます.");
  97. }
  98. },
  99. items: {
  100. "ac_after_reading_answer": { name: "解説AC On/Off" },
  101. "sep1": "---------",
  102. "about": { name: "About" }
  103. }
  104. });
  105.  
  106. /**
  107. * Table 表示ページで "Show Accepted" の変更検知に利用する MutationObserver
  108. *
  109. * @type {MutationObserver}
  110. */
  111. let tableObserver;
  112.  
  113. /**
  114. * Table 表示ページで表のセルの色を塗り分ける.
  115. *
  116. * @date 2020-01-27
  117. * @param {string} userId
  118. */
  119. const processTable = () => {
  120. const tableChanged = () => {
  121. if (tableObserver) {
  122. tableObserver.disconnect();
  123. }
  124. document.querySelectorAll('.table-problem').forEach(td => {
  125. const lnk = td.querySelector('a[href]');
  126. if (!lnk) {
  127. return;
  128. }
  129. const key = href2key(lnk.href);
  130. if (!key) {
  131. return;
  132. }
  133. if (key in hash) {
  134. td.classList.add('apm-ac-after-reading-answer');
  135. }
  136. });
  137. if (tableObserver) {
  138. document.querySelectorAll('.react-bs-container-body').forEach(div => {
  139. tableObserver.observe(div, { childList: true, subtree: true });
  140. });
  141. }
  142. };
  143.  
  144. tableObserver = new MutationObserver(mutations => tableChanged());
  145. tableChanged();
  146. document.querySelectorAll('.react-bs-container-body').forEach(div => {
  147. tableObserver.observe(div, { childList: true, subtree: true });
  148. });
  149. }
  150.  
  151. /**
  152. * ページ URL が変化した際のルートイベントハンドラ.
  153. *
  154. * @date 2020-01-27
  155. */
  156. const hrefChanged = () => {
  157. if (tableObserver) {
  158. tableObserver.disconnect();
  159. }
  160.  
  161. /** @type {RegExpMatchArray} */
  162. let result;
  163. if (result = location.href.match(/^https?:\/\/kenkoooo\.com\/atcoder\/#\/table\/([^/?#]+)/)) {
  164. userId = result[1];
  165. processTable();
  166. }
  167. };
  168.  
  169. // main
  170. loadHash();
  171.  
  172. let href = location.href;
  173. const observer = new MutationObserver(mutations => {
  174. if (href === location.href) {
  175. return;
  176. }
  177. // href changed
  178. href = location.href;
  179. hrefChanged();
  180. });
  181. observer.observe(document, { childList: true, subtree: true });
  182. hrefChanged();
  183. })();