AtCoder ResultsPage Tweaks

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

目前為 2021-03-28 提交的版本,檢視 最新版本

  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. * - 選択時にフォーカスが飛ばないようにする
  153. */
  154. function initSelectTweaks() {
  155. $panelSubmission.find('#select-task, #select-language, #select-status').each((_, el) => {
  156. watch(el, () => {
  157. // 選択時に自動検索
  158. if (options.autoSearchOnChange) {
  159. const params = { ...baseParams };
  160. Object.keys(params).forEach((key) => {
  161. params[key] = $panelSubmission.find(`[name="${key}"]`).val();
  162. });
  163. params.page = 1;
  164.  
  165. showSearchResult(params);
  166. }
  167.  
  168. // 選択時にフォーカスが飛ばないようにする
  169. if (options.keepSelectFocus) {
  170. el.focus();
  171. }
  172. });
  173. });
  174. }
  175.  
  176. const urlRegExp = new RegExp(location.pathname);
  177. /**
  178. * 検索結果のリンククリック時のページ遷移をなくし、表示を動的に更新する処理に置き換え
  179. */
  180. function initLinks() {
  181. $container.on('click', '.pagination a, .panel-submission a', (event) => {
  182. const { href } = event.target;
  183. if (!urlRegExp.test(href)) {
  184. return;
  185. }
  186.  
  187. event.preventDefault();
  188.  
  189. showSearchResult(parseSubmissionsUrl(href));
  190. });
  191. }
  192.  
  193. function init() {
  194. initSelectTweaks();
  195. if (options.dynamicResult) {
  196. window.addEventListener('popstate', updateSearchResult);
  197. initLinks();
  198. }
  199. }
  200.  
  201. init();
  202. });

QingJ © 2025

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