AtCoder ResultsPage Tweaks

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

La data de 28-03-2021. Vezi ultima versiune.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         AtCoder ResultsPage Tweaks
// @namespace    https://github.com/yukuse
// @version      1.0.0
// @description  AtCoderの提出結果一覧画面に自動検索機能などを追加します。
// @author       yukuse
// @include      https://atcoder.jp/contests/*/submissions*
// @grant        none
// @license      MIT
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/locale/ja.min.js
// ==/UserScript==

/* global jQuery moment */

jQuery(function ($) {
  const options = {
    // 検索条件変更時に自動検索 on/off
    autoSearchOnChange: true,
    // 検索結果の動的読み込み on/off
    dynamicResult: true,
    // 検索条件変更時のフォーカス維持 on/off
    keepSelectFocus: true,

    lang: 'ja',
  };

  // 読み込み元と整合性を取る
  // https://wiki.greasespot.net/Third-Party_Libraries
  this.$ = this.jQuery = jQuery.noConflict(true);

  moment.locale(options.lang);

  const $container = $('#main-container');
  const $panelSubmission = $container.find('.panel-submission');

  /**
   * from: js/base.js
   */
  function fixTime() {
    $('time.fixtime').each(function _() {
      const date = moment($(this).text());
      if ($(this).hasClass('fixtime-second')) {
        $(this).text(date.format('YYYY-MM-DD HH:mm:ss'));
      } else if ($(this).hasClass('fixtime-short')) {
        $(this).text(date.format('M/D(ddd) HH:mm'));
      } else {
        $(this).text(date.format('YYYY-MM-DD(ddd) HH:mm'));
      }
      $(this).removeClass('fixtime');
    });
  }

  const baseParams = {
    'f.Task': '',
    'f.LanguageName': '',
    'f.Status': '',
    'f.User': '',
    page: 1,
  };

  function parseSubmissionsUrl(url) {
    const params = { ...baseParams };
    if (url) {
      Object.keys(params).forEach((key) => {
        const regexp = new RegExp(`${key}=([^&]+)`);
        const result = url.match(regexp);
        if (result) {
          [, params[key]] = result;
        }
      });
    }

    return params;
  }

  /**
   * valueの変化を監視する関数
   */
  const watch = (() => {
    const watchers = [];
    setInterval(() => {
      watchers.forEach((watcher) => {
        const { prevValue, el, onChange } = watcher;
        if (el.value !== prevValue) {
          watcher.prevValue = el.value;
          onChange(el.value, prevValue);
        }
      });
    }, 50);

    return (el, onChange) => {
      watchers.push({
        prevValue: el.value,
        el,
        onChange,
      });
    };
  })();

  /**
   * 現在のURLに応じて検索結果表示を更新
   * TODO: ジャッジ中表示対応
   */
  function updateSearchResult() {
    const { href } = location;
    const params = parseSubmissionsUrl(href);

    // 検索条件を遷移先の状態にする
    // FIXME: ページ遷移によりselectの状態が切り替わったとき、selectの表示が元のままになる
    Object.keys(params).forEach((key) => {
      $panelSubmission.find(`[name="${key}"]`).val(params[key]).trigger('change');
    });

    const $tmp = $('<div>');
    $tmp.load(`${href} #main-container`, '', () => {
      const $newTable = $tmp.find('.table-responsive, .panel-body');
      // テーブル置換
      $panelSubmission.find('.table-responsive, .panel-body').replaceWith($newTable);
      // ページネーション置換
      if ($newTable.hasClass('table-responsive')) {
        $container.find('.pagination').replaceWith($tmp.find('.pagination:first'));
      } else {
        $container.find('.pagination').empty();
      }

      // 日付を表示
      fixTime();
    });
  }

  /**
   * 検索条件を元にURLを更新し、結果を表示する
   */
  function showSearchResult(params) {
    const paramsStr = Object.keys(params).map((key) => `${key}=${params[key]}`).join('&');
    const url = `${location.pathname}?${paramsStr}`;

    if (options.dynamicResult) {
      history.pushState({}, '', url);

      updateSearchResult();
    } else {
      location.href = url;
    }
  }

  /**
   * 選択時にフォーカスが残るようにする
   */
  function initSelectTweaks() {
    $panelSubmission.find('#select-task, #select-language, #select-status').each((_, el) => {
      watch(el, () => {
        // 選択時に自動検索
        if (options.autoSearchOnChange) {
          const params = { ...baseParams };
          Object.keys(params).forEach((key) => {
            params[key] = $panelSubmission.find(`[name="${key}"]`).val();
          });
          params.page = 1;

          showSearchResult(params);
        }

        // 選択時にフォーカスが飛ばないようにする
        if (options.keepSelectFocus) {
          el.focus();
        }
      });
    });
  }

  const urlRegExp = new RegExp(location.pathname);
  /**
   * 検索結果のリンククリック時のページ遷移をなくし、表示を動的に更新する処理に置き換え
   */
  function initLinks() {
    $container.on('click', '.pagination a, .panel-submission a', (event) => {
      const { href } = event.target;
      if (!urlRegExp.test(href)) {
        return;
      }

      event.preventDefault();

      showSearchResult(parseSubmissionsUrl(href));
    });
  }

  function init() {
    initSelectTweaks();
    if (options.dynamicResult) {
      window.addEventListener('popstate', updateSearchResult);
      initLinks();
    }
  }

  init();
});