AtCoder ResultsPage Tweaks

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

Verze ze dne 28. 03. 2021. Zobrazit nejnovější verzi.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

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.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

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