AtCoder ResultsPage Tweaks

AtCoderの提出結果一覧画面に自動検索機能などを追加します。

Versione datata 28/03/2021. Vedi la nuova versione l'ultima versione.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

  1. // ==UserScript==
  2. // @name AtCoder ResultsPage Tweaks
  3. // @namespace https://github.com/yukuse
  4. // @version 1.0.0
  5. // @description AtCoderの提出結果一覧画面に自動検索機能などを追加します。
  6. // @author yukuse
  7. // @include https://atcoder.jp/contests/*/submissions*
  8. // @grant none
  9. // @license MIT
  10. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js
  11. // @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/locale/ja.min.js
  13. // ==/UserScript==
  14.  
  15. /* global jQuery moment */
  16.  
  17. jQuery(function ($) {
  18. const options = {
  19. // 検索条件変更時に自動検索 on/off
  20. autoSearchOnChange: true,
  21. // 検索結果の動的読み込み on/off
  22. dynamicResult: true,
  23. // 検索条件変更時のフォーカス維持 on/off
  24. keepSelectFocus: true,
  25.  
  26. lang: 'ja',
  27. };
  28.  
  29. // 読み込み元と整合性を取る
  30. // https://wiki.greasespot.net/Third-Party_Libraries
  31. this.$ = this.jQuery = jQuery.noConflict(true);
  32.  
  33. moment.locale(options.lang);
  34.  
  35. const $container = $('#main-container');
  36. const $panelSubmission = $container.find('.panel-submission');
  37.  
  38. /**
  39. * from: js/base.js
  40. */
  41. function fixTime() {
  42. $('time.fixtime').each(function _() {
  43. const date = moment($(this).text());
  44. if ($(this).hasClass('fixtime-second')) {
  45. $(this).text(date.format('YYYY-MM-DD HH:mm:ss'));
  46. } else if ($(this).hasClass('fixtime-short')) {
  47. $(this).text(date.format('M/D(ddd) HH:mm'));
  48. } else {
  49. $(this).text(date.format('YYYY-MM-DD(ddd) HH:mm'));
  50. }
  51. $(this).removeClass('fixtime');
  52. });
  53. }
  54.  
  55. const baseParams = {
  56. 'f.Task': '',
  57. 'f.LanguageName': '',
  58. 'f.Status': '',
  59. 'f.User': '',
  60. page: 1,
  61. };
  62.  
  63. function parseSubmissionsUrl(url) {
  64. const params = { ...baseParams };
  65. if (url) {
  66. Object.keys(params).forEach((key) => {
  67. const regexp = new RegExp(`${key}=([^&]+)`);
  68. const result = url.match(regexp);
  69. if (result) {
  70. [, params[key]] = result;
  71. }
  72. });
  73. }
  74.  
  75. return params;
  76. }
  77.  
  78. /**
  79. * valueの変化を監視する関数
  80. */
  81. const watch = (() => {
  82. const watchers = [];
  83. setInterval(() => {
  84. watchers.forEach((watcher) => {
  85. const { prevValue, el, onChange } = watcher;
  86. if (el.value !== prevValue) {
  87. watcher.prevValue = el.value;
  88. onChange(el.value, prevValue);
  89. }
  90. });
  91. }, 50);
  92.  
  93. return (el, onChange) => {
  94. watchers.push({
  95. prevValue: el.value,
  96. el,
  97. onChange,
  98. });
  99. };
  100. })();
  101.  
  102. /**
  103. * 現在のURLに応じて検索結果表示を更新
  104. * TODO: ジャッジ中表示対応
  105. */
  106. function updateSearchResult() {
  107. const { href } = location;
  108. const params = parseSubmissionsUrl(href);
  109.  
  110. // 検索条件を遷移先の状態にする
  111. // FIXME: ページ遷移によりselectの状態が切り替わったとき、selectの表示が元のままになる
  112. Object.keys(params).forEach((key) => {
  113. $panelSubmission.find(`[name="${key}"]`).val(params[key]).trigger('change');
  114. });
  115.  
  116. const $tmp = $('<div>');
  117. $tmp.load(`${href} #main-container`, '', () => {
  118. const $newTable = $tmp.find('.table-responsive, .panel-body');
  119. // テーブル置換
  120. $panelSubmission.find('.table-responsive, .panel-body').replaceWith($newTable);
  121. // ページネーション置換
  122. if ($newTable.hasClass('table-responsive')) {
  123. $container.find('.pagination').replaceWith($tmp.find('.pagination:first'));
  124. } else {
  125. $container.find('.pagination').empty();
  126. }
  127.  
  128. // 日付を表示
  129. fixTime();
  130. });
  131. }
  132.  
  133. /**
  134. * 検索条件を元にURLを更新し、結果を表示する
  135. */
  136. function showSearchResult(params) {
  137. const paramsStr = Object.keys(params).map((key) => `${key}=${params[key]}`).join('&');
  138. const url = `${location.pathname}?${paramsStr}`;
  139.  
  140. if (options.dynamicResult) {
  141. history.pushState({}, '', url);
  142.  
  143. updateSearchResult();
  144. } else {
  145. location.href = url;
  146. }
  147. }
  148.  
  149. /**
  150. * 選択時にフォーカスが残るようにする
  151. */
  152. function initSelectTweaks() {
  153. $panelSubmission.find('#select-task, #select-language, #select-status').each((_, el) => {
  154. watch(el, () => {
  155. // 選択時に自動検索
  156. if (options.autoSearchOnChange) {
  157. const params = { ...baseParams };
  158. Object.keys(params).forEach((key) => {
  159. params[key] = $panelSubmission.find(`[name="${key}"]`).val();
  160. });
  161. params.page = 1;
  162.  
  163. showSearchResult(params);
  164. }
  165.  
  166. // 選択時にフォーカスが飛ばないようにする
  167. if (options.keepSelectFocus) {
  168. el.focus();
  169. }
  170. });
  171. });
  172. }
  173.  
  174. const urlRegExp = new RegExp(location.pathname);
  175. /**
  176. * 検索結果のリンククリック時のページ遷移をなくし、表示を動的に更新する処理に置き換え
  177. */
  178. function initLinks() {
  179. $container.on('click', '.pagination a, .panel-submission a', (event) => {
  180. const { href } = event.target;
  181. if (!urlRegExp.test(href)) {
  182. return;
  183. }
  184.  
  185. event.preventDefault();
  186.  
  187. showSearchResult(parseSubmissionsUrl(href));
  188. });
  189. }
  190.  
  191. function init() {
  192. initSelectTweaks();
  193. if (options.dynamicResult) {
  194. window.addEventListener('popstate', updateSearchResult);
  195. initLinks();
  196. }
  197. }
  198.  
  199. init();
  200. });