QCoderの問題一覧でAC済み問題を緑色にする
// ==UserScript==
// @name QCoder AC Problem List Highlighter
// @namespace https://qcoder.jp/
// @version 1.0.0
// @description QCoderの問題一覧でAC済み問題を緑色にする
// @match https://qcoder.jp/*
// @match https://www.qcoder.jp/*
// @grant none
// @license MIT
// ==/UserScript==
(() => {
'use strict';
const m = location.pathname.match(/\/(?:ja|en)\/contests\/([^/]+)/);
if (!m) return;
const contestId = m[1];
const isProblemList =
/^\/(?:ja|en)\/contests\/[^/]+\/?$/.test(location.pathname);
if (!isProblemList) return;
const GREEN = '#dff6dd';
const BORDER = '#2eaf4a';
const norm = s => (s || '').replace(/\s+/g, ' ').trim();
async function fetchAcceptedLabels() {
const res = await fetch(`/api/contests/${contestId}/submissions/me`, {
credentials: 'include',
});
if (!res.ok) return new Set();
const submissions = await res.json();
return new Set(
submissions
.filter(s => s.submissionStatusCode === 'AC')
.map(s => s.problemLabel)
);
}
function getProblemLabelFromRow(tr) {
const firstCell = tr.querySelector('td');
if (!firstCell) return null;
const text = norm(firstCell.innerText || firstCell.textContent);
const m = text.match(/^([A-Z]+\d+)/);
return m ? m[1] : null;
}
function paint(acLabels) {
document.querySelectorAll('tr').forEach(tr => {
const label = getProblemLabelFromRow(tr);
if (!label || !acLabels.has(label)) return;
tr.style.backgroundColor = GREEN;
tr.style.borderLeft = `8px solid ${BORDER}`;
const a = tr.querySelector('a');
if (a && !tr.querySelector('.qcoder-ac-badge')) {
a.insertAdjacentHTML(
'afterend',
`<span class="qcoder-ac-badge" style="margin-left:8px;color:${BORDER};font-weight:bold;">AC</span>`
);
}
});
}
fetchAcceptedLabels().then(paint);
})();