AtCoder Submissions Sort Marker

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

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

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

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu betiği yüklemek için bir betik yöneticisi eklentisi yüklemeniz gerekecektir.

(Zaten bir betik yöneticim var, hadi yükleyelim!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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