Custom add, deduplicate, sort, shuffle, remove, and optionally set points on the AtCoder Problems contest creation page.
// ==UserScript==
// @name AtCoder Problems Custom Add
// @namespace https://kenkoooo.com/atcoder/
// @version 1.5.0
// @description Custom add, deduplicate, sort, shuffle, remove, and optionally set points on the AtCoder Problems contest creation page.
// @author generated
// @match https://kenkoooo.com/atcoder/*
// @grant none
// @license MIT
// @run-at document-idle
// ==/UserScript==
(() => {
"use strict";
const APP_TITLE = "Custom Add";
const VERSION = "1.5.0";
const PANEL_ID = "apca-panel";
const LEGACY_PANEL_IDS = ["apcb-panel"];
const STATUS_KEY = "apca:last-status";
const FORM_KEY = "apca:contest-form";
const POINTS_KEY = "apca:pending-points";
const POINT_CACHE_KEY = "apca:point-cache";
const PROBLEMS_URL = "https://kenkoooo.com/atcoder/resources/problems.json";
const SEP = "~";
const DEFAULT_MIN_NO = 1;
const DEFAULT_MAX_NO = 9999;
const SERIES = ["abc", "arc", "agc", "awc", "adt", "ahc", "custom"];
const SERIES_PRIORITY = ["abc", "arc", "agc", "awc", "adt", "ahc"];
const INDEXES = [
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "EX",
];
let problemsPromise = null;
let restoreStarted = false;
let pointsApplyStarted = false;
let applyingPoints = false;
let originalDocumentTitle = null;
const $ = (selector, base = document) => base.querySelector(selector);
const $$ = (selector, base = document) => Array.from(base.querySelectorAll(selector));
const isCreatePage = () => /^#\/contest\/create(?:\?|$)/.test(location.hash || "");
const root = () => document.getElementById(PANEL_ID);
const normIndex = (s) => String(s || "").trim().toUpperCase();
const esc = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const sleep = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));
function uniq(items) {
const seen = new Set();
const out = [];
for (const item of items) {
if (!item || seen.has(item)) continue;
seen.add(item);
out.push(item);
}
return out;
}
function updateDocumentTitle() {
if (isCreatePage()) {
if (originalDocumentTitle === null) originalDocumentTitle = document.title;
if (document.title !== APP_TITLE) document.title = APP_TITLE;
} else if (originalDocumentTitle !== null && document.title === APP_TITLE) {
document.title = originalDocumentTitle;
}
}
function show(message, type = "info") {
const el = $("[data-apca-status]", root());
if (!el) return;
el.textContent = message || "";
el.style.display = message ? "block" : "none";
el.className = ({
info: "alert alert-info py-2 my-2",
success: "alert alert-success py-2 my-2",
warning: "alert alert-warning py-2 my-2",
danger: "alert alert-danger py-2 my-2",
}[type] || "alert alert-info py-2 my-2");
}
function setBusy(busy) {
const r = root();
if (!r) return;
r.dataset.busy = busy ? "1" : "0";
const series = $("[data-apca-series]", r)?.value || "abc";
const mode = $("[data-apca-mode]", r)?.value || "random";
const pointMode = $("[data-apca-point-mode]", r)?.value || "unset";
$$("button,input,select", r).forEach((el) => { el.disabled = busy; });
const custom = $("[data-apca-custom]", r);
const count = $("[data-apca-count]", r);
const point = $("[data-apca-point]", r);
if (custom) custom.disabled = busy || series !== "custom";
if (count) count.disabled = busy || mode !== "random";
if (point) point.disabled = busy || pointMode !== "set";
}
async function getProblems() {
if (!problemsPromise) {
problemsPromise = fetch(PROBLEMS_URL, { credentials: "same-origin" })
.then((res) => {
if (!res.ok) throw new Error(`problems.json: HTTP ${res.status}`);
return res.json();
})
.then((json) => {
if (!Array.isArray(json)) throw new Error("Invalid problems.json");
return json.filter((p) => p && typeof p.id === "string" && typeof p.contest_id === "string");
});
}
return problemsPromise;
}
function normalizeContestId(input) {
const raw = String(input || "").trim();
if (!raw) return "";
try {
const url = new URL(raw);
const m = url.pathname.match(/\/contests\/([^/?#]+)/i);
if (m) return m[1].toLowerCase();
} catch (_) {}
const m = raw.match(/(?:contests\/)?([a-zA-Z0-9_-]+)/);
return m ? m[1].toLowerCase() : "";
}
function contestAliases(id) {
const s = String(id || "").toLowerCase();
const out = new Set([s]);
const m = s.match(/^([a-z]+)0*(\d+)$/);
if (m) {
const prefix = m[1];
const n = Number(m[2]);
if (Number.isFinite(n)) {
out.add(`${prefix}${n}`);
out.add(`${prefix}${String(n).padStart(3, "0")}`);
out.add(`${prefix}${String(n).padStart(4, "0")}`);
}
}
return out;
}
function parseOptionalInt(value, name) {
const text = String(value || "").trim();
if (text === "") return null;
const n = Number(text);
if (!Number.isInteger(n) || n < 0) throw new Error(`${name}: integer >= 0`);
return n;
}
function readPointPolicy(required = false) {
const r = root();
const mode = $("[data-apca-point-mode]", r)?.value || "unset";
if (mode === "unset") return { mode: "unset", point: null };
const text = String($("[data-apca-point]", r)?.value || "").trim();
if (text === "") {
if (required) throw new Error("Point: integer >= 0");
return { mode: "unset", point: null };
}
const n = Number(text);
if (!Number.isInteger(n) || n < 0) throw new Error("Point: integer >= 0");
return { mode: "set", point: n };
}
function selectedIndexes() {
const r = root();
if (!r || $("[data-apca-index-all]", r)?.checked) return [];
return $$("[data-apca-index]", r)
.filter((input) => input.checked)
.map((input) => normIndex(input.value))
.filter(Boolean);
}
function onIndexChange(event) {
const r = root();
if (!r) return;
const all = $("[data-apca-index-all]", r);
const indexes = $$("[data-apca-index]", r);
if (event.target?.matches?.("[data-apca-index-all]")) {
if (all.checked) indexes.forEach((input) => { input.checked = false; });
else if (!indexes.some((input) => input.checked)) all.checked = true;
return;
}
if (event.target?.matches?.("[data-apca-index]")) {
all.checked = !indexes.some((input) => input.checked);
}
}
function firstNumber(s) {
const m = String(s || "").match(/(\d+)/);
return m ? Number(m[1]) : null;
}
function matchPrefix(contestId, prefix) {
const id = String(contestId || "").toLowerCase();
const p = String(prefix || "").toLowerCase();
if (!p) return null;
if (p === "adt") return id.startsWith("adt_") || id === "adt" ? { number: firstNumber(id) ?? 0 } : null;
const numbered = id.match(new RegExp(`^${esc(p)}0*(\\d+)$`, "i"));
if (numbered) return { number: Number(numbered[1]) };
if (id === p || id.startsWith(`${p}_`) || id.startsWith(`${p}-`)) {
return { number: firstNumber(id) ?? 0 };
}
return null;
}
function readCondition() {
const r = root();
const series = $("[data-apca-series]", r)?.value || "abc";
const customPrefixes = String($("[data-apca-custom]", r)?.value || "")
.split(/[\s,、]+/)
.map((s) => s.trim().toLowerCase())
.filter(Boolean);
const minNo = parseOptionalInt($("[data-apca-min]", r)?.value, "No. min");
const maxNo = parseOptionalInt($("[data-apca-max]", r)?.value, "No. max");
const mode = $("[data-apca-mode]", r)?.value || "random";
const count = parseOptionalInt($("[data-apca-count]", r)?.value, "Count");
if (!SERIES.includes(series)) throw new Error("Series");
if (series === "custom" && customPrefixes.length === 0) throw new Error("Custom prefix");
if (minNo !== null && maxNo !== null && minNo > maxNo) throw new Error("No. min > No. max");
if (mode === "random" && (!Number.isInteger(count) || count <= 0)) throw new Error("Count: integer >= 1");
return { series, customPrefixes, indexes: selectedIndexes(), minNo, maxNo, mode, count };
}
function isDefaultRange(cond) {
return cond.minNo === DEFAULT_MIN_NO && cond.maxNo === DEFAULT_MAX_NO;
}
function isRangeActive(cond) {
return !isDefaultRange(cond) && (cond.minNo !== null || cond.maxNo !== null);
}
function filterProblems(problems, cond) {
const prefixes = cond.series === "custom" ? cond.customPrefixes : [cond.series];
const indexSet = new Set(cond.indexes);
const rangeActive = isRangeActive(cond);
return problems.filter((problem) => {
let matched = null;
for (const prefix of prefixes) {
matched = matchPrefix(problem.contest_id, prefix);
if (matched) break;
}
if (!matched) return false;
if (rangeActive && cond.minNo !== null && matched.number < cond.minNo) return false;
if (rangeActive && cond.maxNo !== null && matched.number > cond.maxNo) return false;
if (indexSet.size && !indexSet.has(normIndex(problem.problem_index))) return false;
return true;
});
}
function seriesOfContest(contestId) {
const id = String(contestId || "").toLowerCase();
if (id.startsWith("adt_") || id === "adt") return "adt";
const m = id.match(/^([a-z]+)\d+/);
if (m) return m[1];
return id.split(/[_-]/)[0] || "";
}
function originalContestFromProblemId(problemId) {
const id = String(problemId || "").toLowerCase();
const m = id.match(/^([a-z]+)(\d+)[_-]/);
if (!m) return "";
return `${m[1]}${Number(m[2])}`;
}
function indexKey(index) {
const s = normIndex(index);
if (/^[A-Z]$/.test(s)) return [s.charCodeAt(0) - 65, s];
if (s === "EX") return [26, s];
const first = s.match(/[A-Z]/)?.[0];
return [first ? first.charCodeAt(0) - 65 : 999, s];
}
function priorityOfProblem(problem) {
const contestId = String(problem?.contest_id || "").toLowerCase();
const original = originalContestFromProblemId(problem?.id);
const originalMatch = original && contestAliases(original).has(contestId) ? 0 : 1;
const series = seriesOfContest(contestId);
const seriesRank = SERIES_PRIORITY.includes(series) ? SERIES_PRIORITY.indexOf(series) : 50;
const contestRank = contestId || "~";
const indexRank = indexKey(problem?.problem_index || "");
return [originalMatch, seriesRank, contestRank, indexRank[0], indexRank[1]];
}
function comparePriority(a, b) {
const ak = priorityOfProblem(a);
const bk = priorityOfProblem(b);
for (let i = 0; i < Math.min(ak.length, bk.length); i += 1) {
if (typeof ak[i] === "number" && typeof bk[i] === "number") {
if (ak[i] !== bk[i]) return ak[i] - bk[i];
} else {
const c = String(ak[i]).localeCompare(String(bk[i]), undefined, { numeric: true, sensitivity: "base" });
if (c) return c;
}
}
return 0;
}
function bestProblemMap(problems) {
const byId = new Map();
for (const problem of problems) {
if (!problem?.id) continue;
const current = byId.get(problem.id);
if (!current || comparePriority(problem, current) < 0) byId.set(problem.id, problem);
}
return byId;
}
function dedupeProblems(problems, byId = null) {
const seen = new Set();
const out = [];
for (const problem of problems) {
const id = problem?.id;
if (!id || seen.has(id)) continue;
seen.add(id);
out.push(byId?.get(id) || problem);
}
return out;
}
function compareProblem(a, b) {
const c = String(a.contest_id).localeCompare(String(b.contest_id), undefined, { numeric: true, sensitivity: "base" });
if (c) return c;
const ak = indexKey(a.problem_index);
const bk = indexKey(b.problem_index);
if (ak[0] !== bk[0]) return ak[0] - bk[0];
return String(ak[1]).localeCompare(String(bk[1]), undefined, { numeric: true, sensitivity: "base" });
}
function randomFloat() {
const cryptoObj = window.crypto || window.msCrypto;
if (cryptoObj?.getRandomValues) {
const buf = new Uint32Array(1);
cryptoObj.getRandomValues(buf);
return buf[0] / (0xffffffff + 1);
}
return Math.random();
}
function shuffle(items) {
const a = items.slice();
for (let i = a.length - 1; i > 0; i -= 1) {
const j = Math.floor(randomFloat() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
function readHashIds() {
const i = location.hash.indexOf("?");
if (i < 0) return [];
const ids = new URLSearchParams(location.hash.slice(i + 1)).get("problemIds");
return ids ? ids.split(SEP).map((id) => decodeURIComponent(id)).filter(Boolean) : [];
}
function problemRows() {
const rows = $$(`tr[data-rbd-draggable-id]`).filter((el) => !el.closest(`#${PANEL_ID}`));
if (rows.length) return rows;
const out = [];
$$(`[data-rbd-draggable-id]`).forEach((el) => {
if (el.closest(`#${PANEL_ID}`)) return;
const row = el.closest("tr") || el;
if (!row || row.closest(`#${PANEL_ID}`) || out.includes(row)) return;
out.push(row);
});
return out;
}
function readDomIds() {
const draggableIds = problemRows()
.map((el) => el.getAttribute("data-rbd-draggable-id") || el.querySelector("[data-rbd-draggable-id]")?.getAttribute("data-rbd-draggable-id"))
.filter(Boolean);
if (draggableIds.length) return draggableIds;
const ids = [];
$$("a[href*='/tasks/']").forEach((a) => {
if (a.closest(`#${PANEL_ID}`)) return;
try {
const m = new URL(a.href).pathname.match(/\/tasks\/([^/?#]+)/);
if (m) ids.push(m[1]);
} catch (_) {}
});
return ids.filter(Boolean);
}
function rawCurrentIds() {
const dom = readDomIds();
return dom.length ? dom : readHashIds();
}
function currentIds() {
return uniq(rawCurrentIds());
}
function pointColumnIndexForRow(row) {
const table = row.closest("table");
if (!table) return 3;
const headers = Array.from(table.querySelectorAll("thead th"));
const idx = headers.findIndex((th) => String(th.textContent || "").trim().toLowerCase() === "point");
return idx >= 0 ? idx : 3;
}
function pointCellOfRow(row) {
const cells = Array.from(row.children).filter((el) => el.tagName === "TD");
const idx = pointColumnIndexForRow(row);
return cells[idx] || cells[3] || null;
}
function parsePointCellValue(cell) {
if (!cell) return undefined;
const input = cell.querySelector('input[type="number"]');
const text = String(input ? input.value : cell.textContent || "").trim();
if (text === "" || text === "-") return null;
if (/^\d+$/.test(text)) return Number(text);
return undefined;
}
function readCurrentPointMap(includeNull = true) {
const map = new Map();
for (const row of problemRows()) {
const id = row.getAttribute("data-rbd-draggable-id") || row.querySelector("[data-rbd-draggable-id]")?.getAttribute("data-rbd-draggable-id");
const value = parsePointCellValue(pointCellOfRow(row));
if (!id || value === undefined) continue;
if (value !== null || includeNull) map.set(id, value);
}
return map;
}
function mapToPlainObject(map) {
const obj = {};
for (const [id, point] of map || new Map()) {
if (!id) continue;
if (point === null || (Number.isInteger(point) && point >= 0)) obj[id] = point;
}
return obj;
}
function plainObjectToPointMap(obj) {
const map = new Map();
Object.entries(obj || {}).forEach(([id, point]) => {
if (!id) return;
if (point === null || (Number.isInteger(point) && point >= 0)) map.set(id, point);
});
return map;
}
function savePointCache(ids = currentIds(), pointMap = readCurrentPointMap(true)) {
if (!ids.length || !pointMap.size) {
sessionStorage.removeItem(POINT_CACHE_KEY);
return;
}
sessionStorage.setItem(POINT_CACHE_KEY, JSON.stringify({
ids,
points: mapToPlainObject(pointMap),
createdAt: Date.now(),
}));
}
function readPointCache() {
try {
const raw = sessionStorage.getItem(POINT_CACHE_KEY);
if (!raw) return new Map();
const data = JSON.parse(raw);
return plainObjectToPointMap(data.points || {});
} catch (_) {
sessionStorage.removeItem(POINT_CACHE_KEY);
return new Map();
}
}
function pointMapForNavigation(ids, policy = { mode: "preserve", point: null }) {
const existingIds = currentIds();
const domMap = readCurrentPointMap(true);
const cacheMap = readPointCache();
if (domMap.size) savePointCache(existingIds, domMap);
const out = new Map();
for (const id of ids) {
if (!id) continue;
if (policy.mode === "set") {
out.set(id, policy.point);
} else if (policy.mode === "clear") {
out.set(id, null);
} else {
if (domMap.has(id)) {
const value = domMap.get(id);
if (value !== null) out.set(id, value);
} else if (existingIds.length && !domMap.size && cacheMap.has(id)) {
const value = cacheMap.get(id);
if (value !== null) out.set(id, value);
}
}
}
return out;
}
function schedulePoints(pointMap) {
const points = mapToPlainObject(pointMap);
if (Object.keys(points).length === 0) {
sessionStorage.removeItem(POINTS_KEY);
return;
}
sessionStorage.setItem(POINTS_KEY, JSON.stringify({ points, createdAt: Date.now() }));
}
function outside(selector) {
return $$(selector).filter((el) => !el.closest(`#${PANEL_ID}`));
}
function saveForm() {
const selectors = [
'input[placeholder="Contest Title"]',
'textarea[placeholder="Description"]',
'input[placeholder^="Penalty"]',
'input[placeholder="AtCoder ID list separated by space"]',
'input[type="date"]',
'input[type="time"]',
'input[type="datetime-local"]',
"select",
];
const data = {};
selectors.forEach((selector) => { data[selector] = outside(selector).map((el) => el.value ?? ""); });
sessionStorage.setItem(FORM_KEY, JSON.stringify(data));
}
function setValue(el, value) {
if (!el) return;
const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el), "value")?.set;
if (setter) setter.call(el, value);
else el.value = value;
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
}
function restoreFormOnce() {
const raw = sessionStorage.getItem(FORM_KEY);
if (!raw) return true;
if (!outside('input[placeholder="Contest Title"]')[0]) return false;
try {
const data = JSON.parse(raw);
Object.entries(data).forEach(([selector, values]) => {
const elements = outside(selector);
(values || []).forEach((value, i) => setValue(elements[i], value));
});
} finally {
sessionStorage.removeItem(FORM_KEY);
}
return true;
}
function startRestore() {
if (restoreStarted) return;
restoreStarted = true;
let tries = 0;
const timer = setInterval(() => {
tries += 1;
if (restoreFormOnce() || tries >= 40) clearInterval(timer);
}, 250);
}
function navigate(ids, message, pointMap = null, options = {}) {
const nextIds = uniq(ids).filter(Boolean);
const capturePoints = options.capturePoints !== false;
if (capturePoints) savePointCache(currentIds(), readCurrentPointMap(true));
saveForm();
if (message) sessionStorage.setItem(STATUS_KEY, message);
if (pointMap) schedulePoints(pointMap);
else sessionStorage.removeItem(POINTS_KEY);
if (!nextIds.length) {
location.href = `${location.origin}${location.pathname}${location.search}#/contest/create`;
} else {
const encoded = nextIds.map((id) => encodeURIComponent(id)).join(SEP);
location.href = `${location.origin}${location.pathname}${location.search}#/contest/create?problemIds=${encoded}`;
}
setTimeout(() => location.reload(), 50);
}
async function setPointInRow(row, point) {
const cell = pointCellOfRow(row);
if (!cell) return false;
const currentValue = parsePointCellValue(cell);
if (currentValue === point) return true;
cell.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
await sleep(20);
let input = cell.querySelector('input[type="number"]');
if (!input) {
await sleep(80);
input = cell.querySelector('input[type="number"]');
}
if (!input) return false;
setValue(input, point === null ? "" : String(point));
input.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", code: "Enter", bubbles: true }));
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", bubbles: true }));
input.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", bubbles: true }));
input.blur();
await sleep(10);
return true;
}
async function applyPointMapToVisibleRows(pointMap) {
const rows = problemRows();
if (!rows.length) return { done: false, applied: 0, missing: pointMap.size };
const rowsById = new Map();
rows.forEach((row) => {
const id = row.getAttribute("data-rbd-draggable-id") || row.querySelector("[data-rbd-draggable-id]")?.getAttribute("data-rbd-draggable-id");
if (id) rowsById.set(id, row);
});
const missing = Array.from(pointMap.keys()).filter((id) => !rowsById.has(id));
if (missing.length) return { done: false, applied: 0, missing: missing.length };
let applied = 0;
for (const [id, point] of pointMap.entries()) {
const row = rowsById.get(id);
if (!row) continue;
if (await setPointInRow(row, point)) applied += 1;
}
savePointCache(currentIds(), readCurrentPointMap(true));
return { done: true, applied, missing: 0 };
}
async function applyPendingPointsOnce() {
const raw = sessionStorage.getItem(POINTS_KEY);
if (!raw || applyingPoints) return true;
let data;
try {
data = JSON.parse(raw);
} catch (_) {
sessionStorage.removeItem(POINTS_KEY);
return true;
}
const pointMap = plainObjectToPointMap(data.points || {});
if (!pointMap.size) {
sessionStorage.removeItem(POINTS_KEY);
return true;
}
applyingPoints = true;
try {
const result = await applyPointMapToVisibleRows(pointMap);
if (result.done) {
sessionStorage.removeItem(POINTS_KEY);
show(`Points applied: ${result.applied}`, "success");
return true;
}
return false;
} finally {
applyingPoints = false;
}
}
function startPointApply() {
if (pointsApplyStarted) return;
if (!sessionStorage.getItem(POINTS_KEY)) return;
pointsApplyStarted = true;
let tries = 0;
const timer = setInterval(async () => {
tries += 1;
const done = await applyPendingPointsOnce();
if (done || tries >= 100) {
clearInterval(timer);
pointsApplyStarted = false;
if (!done) show("Point setting failed", "warning");
}
}, 250);
}
function syncPointCache() {
if (!isCreatePage()) return;
const ids = currentIds();
const map = readCurrentPointMap(true);
if (ids.length && map.size) savePointCache(ids, map);
}
async function appendProblems(problems, label, allProblems = null) {
const policy = readPointPolicy(false);
const source = allProblems || await getProblems();
const byId = bestProblemMap(source);
const selected = dedupeProblems(problems, byId)
.sort(compareProblem)
.map((p) => p.id)
.filter(Boolean);
if (!selected.length) {
show("No problem to add", "warning");
return;
}
const cur = currentIds();
const next = uniq([...cur, ...selected]);
const added = next.length - cur.length;
if (added <= 0) {
show("Already added", "warning");
return;
}
navigate(next, `${label}: ${added} added`, pointMapForNavigation(next, policy.mode === "set" ? policy : { mode: "preserve", point: null }));
}
async function addContest() {
const id = normalizeContestId($("[data-apca-contest]", root())?.value);
if (!id) {
show("Contest ID required", "warning");
return;
}
setBusy(true);
try {
const problems = await getProblems();
const aliases = contestAliases(id);
const byId = bestProblemMap(problems);
const selected = dedupeProblems(
problems.filter((p) => aliases.has(String(p.contest_id).toLowerCase())),
byId,
).sort(compareProblem);
await appendProblems(selected, id.toUpperCase(), problems);
} catch (e) {
console.error(e);
show(String(e.message || e), "danger");
} finally {
setBusy(false);
}
}
function rangeLabel(cond) {
if (!isRangeActive(cond)) return "";
const left = cond.minNo !== null ? String(cond.minNo) : "";
const right = cond.maxNo !== null ? String(cond.maxNo) : "";
return ` No.${left}-${right}`;
}
function labelOf(cond) {
const series = cond.series === "custom" ? cond.customPrefixes.join(",").toUpperCase() : cond.series.toUpperCase();
const index = cond.indexes.length ? cond.indexes.join(",") : "ALL";
return `${series}-${index}${rangeLabel(cond)}`;
}
async function countCandidates() {
setBusy(true);
try {
const cond = readCondition();
const problems = await getProblems();
const candidates = dedupeProblems(filterProblems(problems, cond), bestProblemMap(problems));
show(`${labelOf(cond)}: ${candidates.length} candidates`, candidates.length ? "success" : "warning");
} catch (e) {
console.error(e);
show(String(e.message || e), "danger");
} finally {
setBusy(false);
}
}
async function addByCondition() {
setBusy(true);
try {
const cond = readCondition();
const problems = await getProblems();
const byId = bestProblemMap(problems);
const candidates = dedupeProblems(filterProblems(problems, cond), byId).sort(compareProblem);
const selected = cond.mode === "random"
? shuffle(candidates).slice(0, Math.min(cond.count, candidates.length)).sort(compareProblem)
: candidates;
await appendProblems(selected, labelOf(cond), problems);
} catch (e) {
console.error(e);
show(String(e.message || e), "danger");
} finally {
setBusy(false);
}
}
function removeDuplicatesCurrent() {
const raw = rawCurrentIds();
if (!raw.length) {
show("No problem", "warning");
return;
}
const ids = uniq(raw);
const removed = raw.length - ids.length;
if (removed <= 0) {
show("No duplicate", "success");
return;
}
navigate(ids, `Duplicates removed: ${removed}`, pointMapForNavigation(ids, { mode: "preserve", point: null }));
}
function removeAllCurrent() {
const ids = currentIds();
if (!ids.length) {
show("No problem", "warning");
return;
}
if (!window.confirm(`Remove all ${ids.length} problems?`)) return;
sessionStorage.removeItem(POINT_CACHE_KEY);
sessionStorage.removeItem(POINTS_KEY);
navigate([], `All removed: ${ids.length}`, null, { capturePoints: false });
}
async function sortCurrent() {
const ids = currentIds();
if (!ids.length) {
show("No problem", "warning");
return;
}
setBusy(true);
try {
const problems = await getProblems();
const byId = bestProblemMap(problems);
const pos = new Map(ids.map((id, i) => [id, i]));
const sorted = ids.slice().sort((a, b) => {
const pa = byId.get(a);
const pb = byId.get(b);
if (pa && pb) return compareProblem(pa, pb);
if (pa) return -1;
if (pb) return 1;
return (pos.get(a) ?? 0) - (pos.get(b) ?? 0);
});
navigate(sorted, `Sorted: ${sorted.length}`, pointMapForNavigation(sorted, { mode: "preserve", point: null }));
} catch (e) {
console.error(e);
show(String(e.message || e), "danger");
} finally {
setBusy(false);
}
}
function shuffleCurrent() {
const ids = currentIds();
if (!ids.length) {
show("No problem", "warning");
return;
}
const shuffled = shuffle(ids);
navigate(shuffled, `Shuffled: ${ids.length}`, pointMapForNavigation(shuffled, { mode: "preserve", point: null }));
}
async function setAllPointsCurrent() {
const ids = currentIds();
if (!ids.length) {
show("No problem", "warning");
return;
}
setBusy(true);
try {
const policy = readPointPolicy(true);
const point = policy.mode === "set" ? policy.point : null;
const pointMap = new Map(ids.map((id) => [id, point]));
const result = await applyPointMapToVisibleRows(pointMap);
if (!result.done) {
show("Point setting failed", "warning");
return;
}
show(`Points applied: ${result.applied}`, "success");
} catch (e) {
console.error(e);
show(String(e.message || e), "danger");
} finally {
setBusy(false);
}
}
function indexCheckbox(value, checked = false) {
const attr = value === "__all" ? "data-apca-index-all" : "data-apca-index";
const text = value === "__all" ? "All" : value;
return `<label class="apca-index-item"><input type="checkbox" ${attr} value="${value}" ${checked ? "checked" : ""}>${text}</label>`;
}
function createPanel() {
const div = document.createElement("div");
div.id = PANEL_ID;
div.dataset.apcaVersion = VERSION;
div.className = "card my-3";
div.innerHTML = `
<style>
#${PANEL_ID} .apca-index-list{display:flex;flex-wrap:wrap;gap:6px 10px;padding:8px;border:1px solid #ced4da;border-radius:.25rem;min-height:38px}
#${PANEL_ID} .apca-index-item{margin:0;white-space:nowrap;font-weight:normal}
#${PANEL_ID} .apca-index-item input{margin-right:3px}
</style>
<div class="card-header"><strong>Custom Add</strong></div>
<div class="card-body">
<div class="form-row align-items-end">
<div class="form-group col-md-7">
<label class="mb-1">Contest ID / URL</label>
<input data-apca-contest class="form-control">
</div>
<div class="form-group col-md-5">
<button data-apca-add-contest class="btn btn-primary btn-block" type="button">Add Contest</button>
</div>
</div>
<hr>
<div class="form-row">
<div class="form-group col-md-2">
<label class="mb-1">Series</label>
<select data-apca-series class="form-control">
<option value="abc" selected>ABC</option>
<option value="arc">ARC</option>
<option value="agc">AGC</option>
<option value="awc">AWC</option>
<option value="adt">ADT</option>
<option value="ahc">AHC</option>
<option value="custom">custom</option>
</select>
</div>
<div class="form-group col-md-2">
<label class="mb-1">custom prefix</label>
<input data-apca-custom class="form-control" disabled>
</div>
<div class="form-group col-md-1">
<label class="mb-1">No. min</label>
<input data-apca-min class="form-control" type="number" min="0" value="${DEFAULT_MIN_NO}">
</div>
<div class="form-group col-md-1">
<label class="mb-1">No. max</label>
<input data-apca-max class="form-control" type="number" min="0" value="${DEFAULT_MAX_NO}">
</div>
<div class="form-group col-md-2">
<label class="mb-1">Selection</label>
<select data-apca-mode class="form-control">
<option value="random" selected>Random</option>
<option value="all">All</option>
</select>
</div>
<div class="form-group col-md-2">
<label class="mb-1">Count</label>
<input data-apca-count class="form-control" type="number" min="1" value="50">
</div>
<div class="form-group col-md-2">
<label class="mb-1">Action</label>
<button data-apca-add-condition class="btn btn-success btn-block" type="button">Add by Condition</button>
</div>
</div>
<div class="form-group">
<label class="mb-1">Problem index</label>
<div data-apca-indexes class="apca-index-list">
${indexCheckbox("__all", true)}${INDEXES.map((v) => indexCheckbox(v)).join("")}
</div>
</div>
<div class="form-row align-items-end">
<div class="form-group col-md-2">
<label class="mb-1">Point mode</label>
<select data-apca-point-mode class="form-control">
<option value="unset" selected>Unset</option>
<option value="set">Set</option>
</select>
</div>
<div class="form-group col-md-2">
<label class="mb-1">Point</label>
<input data-apca-point class="form-control" type="number" min="0" disabled>
</div>
<div class="form-group col-md-2">
<button data-apca-set-points class="btn btn-outline-primary btn-block" type="button">Set All Points</button>
</div>
<div class="form-group col-md-2">
<button data-apca-preview class="btn btn-outline-secondary btn-block" type="button">Count Candidates</button>
</div>
<div class="form-group col-md-2">
<button data-apca-dedupe class="btn btn-outline-primary btn-block" type="button">Remove Duplicates</button>
</div>
<div class="form-group col-md-2">
<button data-apca-remove-all class="btn btn-outline-danger btn-block" type="button">Remove All</button>
</div>
</div>
<div class="form-row align-items-end">
<div class="form-group col-md-6">
<button data-apca-sort class="btn btn-outline-primary btn-block" type="button">Sort Problems</button>
</div>
<div class="form-group col-md-6">
<button data-apca-shuffle class="btn btn-outline-primary btn-block" type="button">Shuffle Problems</button>
</div>
</div>
<div data-apca-status style="display:none"></div>
</div>`;
return div;
}
function bind() {
const r = root();
if (!r || r.dataset.bound === VERSION) return;
r.dataset.bound = VERSION;
$("[data-apca-series]", r)?.addEventListener("change", () => setBusy(false));
$("[data-apca-mode]", r)?.addEventListener("change", () => setBusy(false));
$("[data-apca-point-mode]", r)?.addEventListener("change", () => setBusy(false));
$("[data-apca-indexes]", r)?.addEventListener("change", onIndexChange);
$("[data-apca-add-contest]", r)?.addEventListener("click", addContest);
$("[data-apca-preview]", r)?.addEventListener("click", countCandidates);
$("[data-apca-add-condition]", r)?.addEventListener("click", addByCondition);
$("[data-apca-set-points]", r)?.addEventListener("click", setAllPointsCurrent);
$("[data-apca-dedupe]", r)?.addEventListener("click", removeDuplicatesCurrent);
$("[data-apca-remove-all]", r)?.addEventListener("click", removeAllCurrent);
$("[data-apca-sort]", r)?.addEventListener("click", sortCurrent);
$("[data-apca-shuffle]", r)?.addEventListener("click", shuffleCurrent);
const last = sessionStorage.getItem(STATUS_KEY);
if (last) {
show(last, "success");
sessionStorage.removeItem(STATUS_KEY);
} else {
show("");
}
setBusy(false);
}
function insert() {
updateDocumentTitle();
LEGACY_PANEL_IDS.forEach((id) => document.getElementById(id)?.remove());
if (!isCreatePage()) {
root()?.remove();
return;
}
const currentPanel = root();
if (currentPanel && currentPanel.dataset.apcaVersion !== VERSION) currentPanel.remove();
if (!root()) {
const div = createPanel();
const heading = $$("h1,h2").find((el) => /Create Contest|Contest Create/i.test(el.textContent || ""));
if (heading) (heading.closest(".row") || heading).insertAdjacentElement("afterend", div);
else ($(".container") || $("main") || document.body).prepend(div);
}
bind();
startRestore();
startPointApply();
}
function main() {
insert();
new MutationObserver(insert).observe(document.documentElement, { childList: true, subtree: true });
addEventListener("hashchange", () => {
restoreStarted = false;
pointsApplyStarted = false;
setTimeout(insert, 50);
});
setInterval(insert, 1000);
setInterval(syncPointCache, 1000);
}
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", main, { once: true });
else main();
})();