AtCoder Submissions Sort Marker

AtCoder の提出一覧の現在のソート列に ▲ または ▼ を表示します。

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         AtCoder Submissions Sort Marker
// @namespace    http://tampermonkey.net/
// @version      2026.07.07
// @description  AtCoder の提出一覧の現在のソート列に ▲ または ▼ を表示します。
// @author       Not_Leonian
// @match        https://atcoder.jp/contests/*/submissions*
// @icon         https://www.google.com/s2/favicons?domain=atcoder.jp
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(() => {
	const CONFIG = {
		defaultOrderBy: "created",
		defaultDesc: true,
		activeClassName: "ac-submissions-sort-active",
		markerClassName: "ac-submissions-sort-marker",
	};

	const PAGE_PATH_RE = /^\/contests\/[^/]+\/submissions(?:\/me)?\/?$/;

	function isSubmissionsPage() {
		return PAGE_PATH_RE.test(location.pathname);
	}

	function parseBooleanParam(value) {
		return /^(?:true|1)$/i.test(value || "");
	}

	function getCurrentSort() {
		const params = new URLSearchParams(location.search);
		const hasOrderBy = params.has("orderBy");
		const orderBy = params.get("orderBy") || CONFIG.defaultOrderBy;

		const desc = params.has("desc")
			? parseBooleanParam(params.get("desc"))
			: !hasOrderBy && orderBy === CONFIG.defaultOrderBy
				? CONFIG.defaultDesc
				: false;

		return { orderBy, desc };
	}

	function hrefOrderBy(anchor) {
		const href = anchor.getAttribute("href");
		if (!href) return null;

		try {
			return new URL(href, location.origin).searchParams.get("orderBy");
		} catch {
			return null;
		}
	}

	function clearExistingMarkers(root = document) {
		root.querySelectorAll(`.${CONFIG.markerClassName}`).forEach((node) => {
			node.remove();
		});
		root.querySelectorAll(`.${CONFIG.activeClassName}`).forEach((node) => {
			node.classList.remove(CONFIG.activeClassName);
			node.removeAttribute("aria-sort");
		});
	}

	function findSubmissionTable() {
		return document.querySelector(".panel-submission table.table");
	}

	function findHeaderCellForOrder(table, orderBy) {
		const firstHeaderRow = table.querySelector("thead tr:first-child");
		if (!firstHeaderRow) return null;

		for (const anchor of firstHeaderRow.querySelectorAll("th a[href]")) {
			if (hrefOrderBy(anchor) === orderBy) {
				return anchor.closest("th");
			}
		}

		return null;
	}

	function applySortMarker() {
		if (!isSubmissionsPage()) return;

		const table = findSubmissionTable();
		if (!table) return;

		clearExistingMarkers(table);

		const { orderBy, desc } = getCurrentSort();
		const headerCell = findHeaderCellForOrder(table, orderBy);
		if (!headerCell) return;

		headerCell.classList.add(CONFIG.activeClassName);
		headerCell.setAttribute("aria-sort", desc ? "descending" : "ascending");

		const marker = document.createElement("span");
		marker.className = CONFIG.markerClassName;
		marker.textContent = desc ? "▼" : "▲";
		marker.setAttribute("aria-hidden", "true");

		headerCell.appendChild(marker);
	}

	function injectStyle() {
		if (document.getElementById("ac-submissions-sort-marker-style")) return;

		const style = document.createElement("style");
		style.id = "ac-submissions-sort-marker-style";
		style.textContent = `
		.panel-submission table thead tr:first-child th.${CONFIG.activeClassName} {
			position: relative;
			padding-right: 1.6em !important;
		}

		.panel-submission table thead tr:first-child th .${CONFIG.markerClassName} {
			position: absolute;
			right: 0.45em;
			top: 50%;
			transform: translateY(-50%);
			font-size: 0.9em;
			line-height: 1;
			color: #333;
			pointer-events: none;
			user-select: none;
		}
	`;
		document.head.appendChild(style);
	}

	function main() {
		injectStyle();
		applySortMarker();
	}

	main();
})();