AtCoder ResultsPage Tweaks

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

À partir de 2021-03-28. Voir la dernière version.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==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();
});