Track up to 5 users with stable UI and no overlap
// ==UserScript==
// @name AtCoder Multi Tracker
// @name:ja AtCoder Multi Tracker
// @description:ja 五人までのユーザーの提出状況を確認できます。
// @license MIT
// @namespace https://github.com/yourname
// @version 1.4.0
// @description Track up to 5 users with stable UI and no overlap
// @match https://atcoder.jp/contests/*
// @grant none
// @run-at document-end
// ==/UserScript==
(() => {
'use strict';
if (window.top !== window.self) return;
const MAX_USERS = 5;
const STORAGE_USERS_KEY = 'acmt:users:v4';
const STORAGE_SNAPSHOT_KEY = (contestId) => `acmt:snapshot:v4:${contestId}`;
const UPDATE_INTERVAL_MS = 30_000;
const BASE_BOTTOM = 14;
const GAP = 10;
const contestId = location.pathname.match(/^\/contests\/([^/]+)/)?.[1];
if (!contestId) return;
const state = {
users: loadUsers(),
snapshot: loadSnapshot(contestId),
open: false,
refreshing: false,
refreshTimer: null,
refreshToken: 0,
};
injectStyles();
const root = document.createElement('div');
root.id = 'acmt-root';
document.body.appendChild(root);
const dock = createDock();
const modal = createSettingsModal();
root.appendChild(dock);
root.appendChild(modal);
const launcher = dock.querySelector('[data-acmt="launcher"]');
const panelBody = dock.querySelector('[data-acmt="panel-body"]');
const panelStatus = dock.querySelector('[data-acmt="panel-status"]');
const launcherLabel = dock.querySelector('[data-acmt="launcher-label"]');
const launcherCount = dock.querySelector('[data-acmt="launcher-count"]');
const modalBackdrop = modal.querySelector('[data-acmt="modal-backdrop"]');
const modalInputs = modal.querySelector('[data-acmt="modal-inputs"]');
const modalTitle = modal.querySelector('[data-acmt="modal-title"]');
launcher.addEventListener('click', () => {
if (state.users.length === 0) {
openSettings();
return;
}
toggleDock();
});
dock.querySelector('[data-acmt="settings-button"]').addEventListener('click', (e) => {
e.stopPropagation();
openSettings();
});
dock.querySelector('[data-acmt="refresh-button"]').addEventListener('click', (e) => {
e.stopPropagation();
void refresh(true);
});
dock.querySelector('[data-acmt="close-button"]').addEventListener('click', (e) => {
e.stopPropagation();
closeDock();
});
modalBackdrop.addEventListener('click', closeSettings);
modal.querySelector('[data-acmt="modal-save"]').addEventListener('click', saveSettingsFromModal);
modal.querySelector('[data-acmt="modal-cancel"]').addEventListener('click', closeSettings);
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
if (modal.classList.contains('open')) {
closeSettings();
return;
}
if (state.open) closeDock();
}
});
window.addEventListener('resize', syncDockPosition, { passive: true });
window.addEventListener('scroll', syncDockPosition, { passive: true });
setInterval(syncDockPosition, 500);
renderLauncher();
renderPanelFromSnapshot(state.snapshot, false);
syncDockPosition();
if (state.users.length > 0) {
setHeaderStatus(state.snapshot ? 'Cached' : 'No cache');
} else {
setHeaderStatus('No users configured');
}
function loadUsers() {
try {
const raw = localStorage.getItem(STORAGE_USERS_KEY);
if (!raw) return [];
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return [];
return parsed
.map(String)
.map((s) => s.trim())
.filter(Boolean)
.slice(0, MAX_USERS);
} catch {
return [];
}
}
function saveUsers() {
try {
localStorage.setItem(STORAGE_USERS_KEY, JSON.stringify(state.users));
} catch {
// ignore
}
}
function loadSnapshot(id) {
try {
const raw = localStorage.getItem(STORAGE_SNAPSHOT_KEY(id));
if (!raw) return null;
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') return null;
return parsed;
} catch {
return null;
}
}
function saveSnapshot(id, snapshot) {
try {
localStorage.setItem(STORAGE_SNAPSHOT_KEY(id), JSON.stringify(snapshot));
} catch {
// ignore
}
}
function injectStyles() {
const style = document.createElement('style');
style.textContent = `
#acmt-root {
position: static;
z-index: 2147483647;
}
#acmt-dock {
position: fixed;
left: 14px;
bottom: 14px;
width: 160px;
max-height: 48px;
overflow: hidden;
border-radius: 999px;
background: rgba(255,255,255,.92);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
box-shadow: 0 8px 24px rgba(0,0,0,.16);
transition:
width .42s cubic-bezier(.2,1.2,.2,1),
max-height .42s cubic-bezier(.2,1.2,.2,1),
bottom .18s ease,
border-radius .42s cubic-bezier(.2,1.2,.2,1),
box-shadow .22s ease,
transform .42s cubic-bezier(.2,1.2,.2,1);
transform-origin: left bottom;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #111;
user-select: none;
z-index: 2147483647;
}
#acmt-dock.open {
width: min(420px, calc(100vw - 28px));
max-height: 70vh;
border-radius: 18px;
box-shadow: 0 18px 48px rgba(0,0,0,.24);
}
#acmt-dock.spring-open {
animation: acmt-spring-open 460ms cubic-bezier(.16,1,.3,1);
}
@keyframes acmt-spring-open {
0% { transform: scale(.985); }
55% { transform: scale(1.012); }
78% { transform: scale(.998); }
100% { transform: scale(1); }
}
.acmt-launcher {
width: 100%;
height: 48px;
border: 0;
background: transparent;
color: inherit;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 0 14px;
box-sizing: border-box;
font-size: 13px;
font-weight: 800;
text-align: left;
}
.acmt-launcher-left {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.acmt-launcher-label {
white-space: nowrap;
}
.acmt-launcher-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 22px;
height: 22px;
padding: 0 7px;
border-radius: 999px;
background: rgba(0,0,0,.06);
color: #333;
font-size: 12px;
font-weight: 800;
}
.acmt-launcher-chevron {
color: rgba(0,0,0,.55);
font-size: 16px;
line-height: 1;
transform: translateY(-1px);
transition: transform .22s ease, opacity .22s ease;
}
#acmt-dock.open .acmt-launcher-chevron {
transform: translateY(-1px) rotate(90deg);
}
.acmt-panel {
opacity: 0;
transform: translateY(12px) scale(.985);
pointer-events: none;
transition:
opacity .22s ease,
transform .22s cubic-bezier(.2,.9,.2,1);
}
#acmt-dock.open .acmt-panel {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: auto;
}
.acmt-panel-shell {
border-top: 1px solid rgba(0,0,0,.06);
}
.acmt-panel-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 12px 12px 10px 14px;
}
.acmt-header-left {
min-width: 0;
}
.acmt-title {
font-size: 14px;
font-weight: 900;
letter-spacing: .01em;
}
.acmt-status {
margin-top: 4px;
font-size: 12px;
color: rgba(0,0,0,.60);
}
.acmt-header-actions {
display: inline-flex;
gap: 8px;
flex-shrink: 0;
}
.acmt-icon-btn {
appearance: none;
border: 1px solid rgba(0,0,0,.08);
background: rgba(255,255,255,.72);
color: #111;
border-radius: 10px;
padding: 7px 10px;
font-size: 12px;
font-weight: 800;
cursor: pointer;
transition: transform .16s ease, background .16s ease, opacity .16s ease;
}
.acmt-icon-btn:hover {
transform: translateY(-1px);
background: rgba(255,255,255,.92);
}
.acmt-body {
overflow: auto;
padding: 0 12px 12px;
min-height: 360px;
}
.acmt-empty {
margin: 0 2px 2px;
padding: 18px 14px;
border-radius: 14px;
border: 1px dashed rgba(0,0,0,.12);
background: rgba(255,255,255,.72);
color: rgba(0,0,0,.64);
font-size: 13px;
line-height: 1.55;
}
.acmt-card {
margin-bottom: 10px;
padding: 12px 12px 11px;
border-radius: 16px;
border: 1px solid rgba(0,0,0,.08);
background: linear-gradient(135deg, rgba(255,255,255,.98) 0%, rgba(255,255,255,.95) 100%);
box-shadow: 0 6px 18px rgba(0,0,0,.06);
min-height: 96px;
}
.acmt-card-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.acmt-user-name {
font-size: 14px;
font-weight: 900;
word-break: break-word;
}
.acmt-badges {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 6px;
}
.acmt-pill {
display: inline-flex;
align-items: center;
padding: 4px 8px;
border-radius: 999px;
border: 1px solid rgba(0,0,0,.08);
background: rgba(0,0,0,.03);
font-size: 12px;
font-weight: 800;
white-space: nowrap;
}
.acmt-meta {
margin-top: 8px;
font-size: 12px;
color: rgba(0,0,0,.64);
min-height: 16px;
}
.acmt-chip-wrap {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 9px;
min-height: 30px;
}
.acmt-chip {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 24px;
padding: 4px 8px;
border-radius: 999px;
border: 1px solid rgba(0,0,0,.08);
background: rgba(255,255,255,.92);
font-size: 12px;
font-weight: 800;
color: #111;
}
.acmt-footer {
margin-top: 2px;
padding-top: 10px;
border-top: 1px solid rgba(0,0,0,.06);
font-size: 12px;
color: rgba(0,0,0,.56);
min-height: 20px;
}
.acmt-skeleton-list {
display: grid;
gap: 10px;
}
.acmt-skeleton-card {
min-height: 96px;
padding: 12px 12px 11px;
border-radius: 16px;
border: 1px solid rgba(0,0,0,.06);
background: linear-gradient(90deg, rgba(0,0,0,.04) 0%, rgba(0,0,0,.08) 50%, rgba(0,0,0,.04) 100%);
background-size: 200% 100%;
animation: acmt-shimmer 1.3s ease-in-out infinite;
}
.acmt-skeleton-line {
height: 12px;
border-radius: 999px;
background: rgba(0,0,0,.08);
}
.acmt-skeleton-title {
width: 48%;
height: 14px;
}
.acmt-skeleton-meta {
width: 72%;
margin-top: 8px;
}
.acmt-skeleton-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
}
.acmt-skeleton-chip {
width: 36px;
height: 24px;
border-radius: 999px;
background: rgba(0,0,0,.07);
}
@keyframes acmt-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
#acmt-settings-modal {
position: fixed;
inset: 0;
opacity: 0;
pointer-events: none;
transition: .2s;
z-index: 2147483647;
}
#acmt-settings-modal.open {
opacity: 1;
pointer-events: auto;
}
.acmt-modal-backdrop {
position: absolute;
inset: 0;
background: rgba(0,0,0,.28);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
.acmt-modal {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%) scale(.98);
width: min(420px, calc(100vw - 28px));
background: #fff;
border-radius: 16px;
padding: 14px;
box-shadow: 0 18px 48px rgba(0,0,0,.22);
transition: transform .22s cubic-bezier(.2,.9,.2,1), opacity .22s ease;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
#acmt-settings-modal.open .acmt-modal {
transform: translateX(-50%) scale(1);
}
.acmt-modal-title {
font-weight: 900;
margin-bottom: 10px;
}
.acmt-inputs {
display: grid;
gap: 10px;
}
.acmt-inputs input {
width: 100%;
box-sizing: border-box;
border-radius: 12px;
border: 1px solid rgba(0,0,0,.10);
background: rgba(255,255,255,.96);
padding: 10px 12px;
font-size: 13px;
outline: none;
}
.acmt-inputs input:focus {
border-color: rgba(0,122,255,.45);
box-shadow: 0 0 0 3px rgba(0,122,255,.10);
}
.acmt-modal-actions {
margin-top: 12px;
display: flex;
justify-content: flex-end;
gap: 8px;
}
.acmt-action-btn {
appearance: none;
border: 1px solid rgba(0,0,0,.10);
border-radius: 12px;
padding: 9px 12px;
font-size: 13px;
font-weight: 800;
cursor: pointer;
background: rgba(255,255,255,.96);
color: #111;
}
.acmt-action-btn.primary {
border-color: rgba(0,122,255,.26);
background: rgba(0,122,255,.96);
color: #fff;
}
`;
document.head.appendChild(style);
}
function createDock() {
const el = document.createElement('div');
el.id = 'acmt-dock';
el.innerHTML = `
<button class="acmt-launcher" data-acmt="launcher" type="button">
<span class="acmt-launcher-left">
<span class="acmt-launcher-label" data-acmt="launcher-label">Tracker</span>
<span class="acmt-launcher-badge" data-acmt="launcher-count">0</span>
</span>
<span class="acmt-launcher-chevron">›</span>
</button>
<div class="acmt-panel" data-acmt="panel">
<div class="acmt-panel-shell">
<div class="acmt-panel-header">
<div class="acmt-header-left">
<div class="acmt-title">AtCoder Tracker</div>
<div class="acmt-status" data-acmt="panel-status">Idle</div>
</div>
<div class="acmt-header-actions">
<button class="acmt-icon-btn" type="button" data-acmt="refresh-button">Refresh</button>
<button class="acmt-icon-btn" type="button" data-acmt="settings-button">Settings</button>
<button class="acmt-icon-btn" type="button" data-acmt="close-button">Close</button>
</div>
</div>
<div class="acmt-body" data-acmt="panel-body"></div>
</div>
</div>
`;
return el;
}
function createSettingsModal() {
const el = document.createElement('div');
el.id = 'acmt-settings-modal';
el.innerHTML = `
<div class="acmt-modal-backdrop" data-acmt="modal-backdrop"></div>
<div class="acmt-modal">
<div class="acmt-modal-title" data-acmt="modal-title">Settings</div>
<div class="acmt-inputs" data-acmt="modal-inputs"></div>
<div class="acmt-modal-actions">
<button class="acmt-action-btn" type="button" data-acmt="modal-cancel">Cancel</button>
<button class="acmt-action-btn primary" type="button" data-acmt="modal-save">Save</button>
</div>
</div>
`;
return el;
}
function setHeaderStatus(text) {
panelStatus.textContent = text;
}
function renderLauncher() {
launcherLabel.textContent = state.users.length > 0 ? 'Tracker' : 'Settings';
launcherCount.textContent = String(state.users.length);
syncDockPosition();
}
function openDock() {
if (state.open) return;
state.open = true;
dock.classList.add('open');
dock.classList.remove('spring-open');
void dock.offsetWidth;
dock.classList.add('spring-open');
window.setTimeout(() => dock.classList.remove('spring-open'), 480);
renderPanelFromSnapshot(state.snapshot, true);
setHeaderStatus(state.snapshot ? 'Updating…' : 'Loading…');
startAutoRefresh();
void refresh(true);
syncDockPosition();
}
function closeDock() {
if (!state.open) return;
state.open = false;
dock.classList.remove('open');
dock.classList.remove('spring-open');
stopAutoRefresh();
syncDockPosition();
}
function toggleDock() {
if (state.open) closeDock();
else openDock();
}
function openSettings() {
modalTitle.textContent = 'Settings';
modalInputs.innerHTML = '';
for (let i = 0; i < MAX_USERS; i++) {
const input = document.createElement('input');
input.type = 'text';
input.autocomplete = 'off';
input.spellcheck = false;
input.placeholder = 'Enter user name';
input.value = state.users[i] || '';
modalInputs.appendChild(input);
}
modal.classList.add('open');
}
function closeSettings() {
modal.classList.remove('open');
}
function saveSettingsFromModal() {
state.users = [...modalInputs.querySelectorAll('input')]
.map((i) => i.value.trim())
.filter(Boolean)
.slice(0, MAX_USERS);
saveUsers();
closeSettings();
renderLauncher();
renderPanelFromSnapshot(state.snapshot, false);
if (state.open) {
setHeaderStatus(state.snapshot ? 'Updating…' : 'Loading…');
void refresh(true);
} else {
setHeaderStatus(state.snapshot ? 'Cached' : 'No cache');
}
}
function startAutoRefresh() {
stopAutoRefresh();
state.refreshTimer = window.setInterval(() => {
if (state.open) void refresh(false);
}, UPDATE_INTERVAL_MS);
}
function stopAutoRefresh() {
if (state.refreshTimer != null) {
clearInterval(state.refreshTimer);
state.refreshTimer = null;
}
}
function syncDockPosition() {
const offset = detectExternalBottomOffset();
dock.style.bottom = `${offset}px`;
}
function detectExternalBottomOffset() {
let maxBottom = BASE_BOTTOM;
const candidates = [...document.querySelectorAll('body *')].filter((el) => {
if (!(el instanceof HTMLElement)) return false;
if (el.closest('#acmt-root')) return false;
const style = getComputedStyle(el);
if (style.position !== 'fixed') return false;
const left = Number.parseFloat(style.left);
const bottom = Number.parseFloat(style.bottom);
if (!Number.isFinite(left) || !Number.isFinite(bottom)) return false;
if (left > 28) return false;
const rect = el.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) return false;
const fromBottom = Math.max(0, window.innerHeight - rect.bottom);
if (fromBottom > 240) return false;
return true;
});
for (const el of candidates) {
const rect = el.getBoundingClientRect();
const bottom = Number.parseFloat(getComputedStyle(el).bottom || '0');
if (!Number.isFinite(bottom)) continue;
maxBottom = Math.max(maxBottom, Math.ceil(bottom + rect.height + GAP));
}
return maxBottom;
}
function formatTime(ts) {
if (!ts) return '—';
try {
return new Intl.DateTimeFormat(undefined, {
hour: '2-digit',
minute: '2-digit',
}).format(new Date(ts));
} catch {
return '—';
}
}
function renderPanelFromSnapshot(snapshot, refreshing) {
panelBody.innerHTML = '';
if (!state.users.length) {
const empty = document.createElement('div');
empty.className = 'acmt-empty';
empty.innerHTML = `
No users are configured.<br>
Open <strong>Settings</strong> and enter up to five AtCoder user names.
`;
panelBody.appendChild(empty);
const footer = document.createElement('div');
footer.className = 'acmt-footer';
footer.textContent = snapshot ? `Updated ${formatTime(snapshot.updatedAt)}` : 'No cached data';
panelBody.appendChild(footer);
return;
}
if (!snapshot) {
const list = document.createElement('div');
list.className = 'acmt-skeleton-list';
for (let i = 0; i < state.users.length; i++) {
list.appendChild(createSkeletonCard());
}
panelBody.appendChild(list);
const footer = document.createElement('div');
footer.className = 'acmt-footer';
footer.textContent = refreshing ? 'Loading…' : 'Waiting for data';
panelBody.appendChild(footer);
return;
}
const usersData = snapshot.users ?? {};
for (const username of state.users) {
const data = usersData[username] ?? null;
panelBody.appendChild(createUserCard(username, data));
}
const footer = document.createElement('div');
footer.className = 'acmt-footer';
footer.textContent = snapshot ? `Updated ${formatTime(snapshot.updatedAt)}` : 'Waiting for data';
panelBody.appendChild(footer);
}
function createSkeletonCard() {
const card = document.createElement('div');
card.className = 'acmt-skeleton-card';
card.innerHTML = `
<div class="acmt-skeleton-line acmt-skeleton-title"></div>
<div class="acmt-skeleton-line acmt-skeleton-meta"></div>
<div class="acmt-skeleton-chips">
<div class="acmt-skeleton-chip"></div>
<div class="acmt-skeleton-chip"></div>
<div class="acmt-skeleton-chip"></div>
</div>
`;
return card;
}
function createUserCard(username, data) {
const card = document.createElement('div');
card.className = 'acmt-card';
const top = document.createElement('div');
top.className = 'acmt-card-top';
const left = document.createElement('div');
left.style.minWidth = '0';
const name = document.createElement('div');
name.className = 'acmt-user-name';
name.textContent = username;
const meta = document.createElement('div');
meta.className = 'acmt-meta';
if (!data) {
meta.textContent = 'Not found in standings yet.';
} else {
const score = data.score != null ? Math.round(data.score / 100) : null;
const penalty = data.penalty ?? null;
const solvedCount = data.solvedAssignments?.length ?? 0;
const taskCount = data.taskCount ?? 0;
meta.textContent =
`Solved ${solvedCount}/${taskCount}` +
(score != null ? ` · Score ${score}` : '') +
(penalty != null ? ` · Penalty ${penalty}` : '');
}
left.appendChild(name);
left.appendChild(meta);
const badges = document.createElement('div');
badges.className = 'acmt-badges';
const rankBadge = document.createElement('span');
rankBadge.className = 'acmt-pill';
rankBadge.textContent = data?.rank != null ? `Rank #${data.rank}` : 'Rank —';
badges.appendChild(rankBadge);
top.appendChild(left);
top.appendChild(badges);
card.appendChild(top);
const chipWrap = document.createElement('div');
chipWrap.className = 'acmt-chip-wrap';
const solvedAssignments = data?.solvedAssignments ?? [];
if (solvedAssignments.length === 0) {
const none = document.createElement('span');
none.className = 'acmt-chip';
none.textContent = 'No AC yet';
none.style.color = '#666';
chipWrap.appendChild(none);
} else {
for (const assignment of solvedAssignments) {
const chip = document.createElement('span');
chip.className = 'acmt-chip';
chip.textContent = assignment;
const task = data.taskByAssignment?.[assignment];
if (task?.TaskName) chip.title = task.TaskName;
chipWrap.appendChild(chip);
}
}
card.appendChild(chipWrap);
return card;
}
async function refresh(forceRender) {
if (state.refreshing) return;
if (!state.users.length) {
renderLauncher();
renderPanelFromSnapshot(state.snapshot, false);
return;
}
const token = ++state.refreshToken;
state.refreshing = true;
if (state.open || forceRender) {
setHeaderStatus(state.snapshot ? 'Updating…' : 'Loading…');
renderPanelFromSnapshot(state.snapshot, true);
}
try {
const res = await fetch(`/contests/${contestId}/standings/json`, {
credentials: 'same-origin',
});
if (!res.ok) throw new Error(`standings/json failed: ${res.status}`);
const json = await res.json();
if (token !== state.refreshToken) return;
const snapshot = buildSnapshot(json, state.users);
snapshot.updatedAt = Date.now();
state.snapshot = snapshot;
saveSnapshot(contestId, snapshot);
renderLauncher();
if (state.open || forceRender) {
renderPanelFromSnapshot(snapshot, false);
setHeaderStatus(`Updated ${formatTime(snapshot.updatedAt)}`);
}
} catch (err) {
console.warn('[AtCoder Multi Tracker]', err);
if (state.open || forceRender) {
if (state.snapshot) {
renderPanelFromSnapshot(state.snapshot, false);
setHeaderStatus('Update failed; showing cached data');
} else {
renderPanelFromSnapshot(null, false);
setHeaderStatus('Failed to load data');
}
}
} finally {
if (token === state.refreshToken) {
state.refreshing = false;
renderLauncher();
}
}
}
function buildSnapshot(json, trackedUsers) {
const taskInfo = Array.isArray(json.TaskInfo) ? json.TaskInfo : [];
const rows = Array.isArray(json.StandingsData) ? json.StandingsData : [];
const byUser = new Map();
for (const row of rows) {
if (row?.UserScreenName) byUser.set(row.UserScreenName, row);
}
const taskByAssignment = Object.fromEntries(
taskInfo.map((t) => [t.Assignment, t])
);
const users = {};
for (const username of trackedUsers) {
const row = byUser.get(username);
if (!row) {
users[username] = {
rank: null,
score: null,
penalty: null,
solvedAssignments: [],
taskCount: taskInfo.length,
taskByAssignment,
};
continue;
}
const taskResults = row.TaskResults ?? {};
const solvedAssignments = [];
for (const task of taskInfo) {
const result = getTaskResult(taskResults, task);
if (isSolvedResult(result)) {
solvedAssignments.push(task.Assignment);
}
}
users[username] = {
rank: row.Rank ?? null,
score: row.TotalResult?.Score ?? null,
penalty: row.TotalResult?.Penalty ?? null,
solvedAssignments,
taskCount: taskInfo.length,
taskByAssignment,
};
}
return {
contestId,
updatedAt: Date.now(),
taskInfo,
users,
};
}
function getTaskResult(taskResults, task) {
return (
taskResults?.[task.TaskScreenName] ??
taskResults?.[task.Assignment] ??
taskResults?.[task.TaskName] ??
null
);
}
function isSolvedResult(result) {
if (!result || typeof result !== 'object') return false;
const score = Number(result.Score ?? result.Point ?? 0);
const status = result.Status;
return (
score > 0 ||
result.IsSolved === true ||
result.IsAccepted === true ||
status === 1 ||
status === 'AC' ||
status === 'Accepted'
);
}
function openSettings() {
modalTitle.textContent = 'Settings';
modalInputs.innerHTML = '';
for (let i = 0; i < MAX_USERS; i++) {
const input = document.createElement('input');
input.type = 'text';
input.autocomplete = 'off';
input.spellcheck = false;
input.placeholder = 'Enter user name';
input.value = state.users[i] || '';
modalInputs.appendChild(input);
}
modal.classList.add('open');
}
function closeSettings() {
modal.classList.remove('open');
}
function saveSettingsFromModal() {
state.users = [...modalInputs.querySelectorAll('input')]
.map((i) => i.value.trim())
.filter(Boolean)
.slice(0, MAX_USERS);
saveUsers();
closeSettings();
renderLauncher();
renderPanelFromSnapshot(state.snapshot, false);
if (state.open) {
setHeaderStatus(state.snapshot ? 'Updating…' : 'Loading…');
void refresh(true);
} else {
setHeaderStatus(state.snapshot ? 'Cached' : 'No cache');
}
}
})();